@workflow/web-shared 5.0.0-beta.3 → 5.0.0-beta.4

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.
Files changed (70) hide show
  1. package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -1
  2. package/dist/components/new-trace-viewer/components/event-list.js +7 -4
  3. package/dist/components/new-trace-viewer/components/event-list.js.map +1 -1
  4. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts +18 -0
  5. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts.map +1 -0
  6. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js +54 -0
  7. package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js.map +1 -0
  8. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts +20 -0
  9. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts.map +1 -0
  10. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js +81 -0
  11. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js.map +1 -0
  12. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts +25 -0
  13. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts.map +1 -0
  14. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js +101 -0
  15. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js.map +1 -0
  16. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts +20 -0
  17. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts.map +1 -0
  18. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js +114 -0
  19. package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js.map +1 -0
  20. package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -1
  21. package/dist/components/new-trace-viewer/components/split-pane.js +2 -3
  22. package/dist/components/new-trace-viewer/components/split-pane.js.map +1 -1
  23. package/dist/components/new-trace-viewer/components/timeline.d.ts +10 -11
  24. package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
  25. package/dist/components/new-trace-viewer/components/timeline.js +57 -53
  26. package/dist/components/new-trace-viewer/components/timeline.js.map +1 -1
  27. package/dist/components/new-trace-viewer/context.d.ts.map +1 -1
  28. package/dist/components/new-trace-viewer/context.js +5 -7
  29. package/dist/components/new-trace-viewer/context.js.map +1 -1
  30. package/dist/components/new-trace-viewer/detail-panel.d.ts.map +1 -1
  31. package/dist/components/new-trace-viewer/detail-panel.js +2 -1
  32. package/dist/components/new-trace-viewer/detail-panel.js.map +1 -1
  33. package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
  34. package/dist/components/new-trace-viewer/trace-viewer.js +37 -17
  35. package/dist/components/new-trace-viewer/trace-viewer.js.map +1 -1
  36. package/dist/components/new-trace-viewer/utils.d.ts +3 -8
  37. package/dist/components/new-trace-viewer/utils.d.ts.map +1 -1
  38. package/dist/components/new-trace-viewer/utils.js +12 -65
  39. package/dist/components/new-trace-viewer/utils.js.map +1 -1
  40. package/dist/components/trace-viewer/util/timing.d.ts +1 -1
  41. package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
  42. package/dist/components/trace-viewer/util/timing.js +5 -8
  43. package/dist/components/trace-viewer/util/timing.js.map +1 -1
  44. package/dist/components/workflow-traces/trace-span-construction.d.ts +2 -2
  45. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  46. package/dist/components/workflow-traces/trace-span-construction.js +16 -3
  47. package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
  48. package/dist/lib/trace-builder.d.ts.map +1 -1
  49. package/dist/lib/trace-builder.js +2 -1
  50. package/dist/lib/trace-builder.js.map +1 -1
  51. package/dist/lib/utils.d.ts +9 -7
  52. package/dist/lib/utils.d.ts.map +1 -1
  53. package/dist/lib/utils.js +27 -27
  54. package/dist/lib/utils.js.map +1 -1
  55. package/package.json +2 -2
  56. package/src/components/new-trace-viewer/components/event-list.tsx +27 -20
  57. package/src/components/new-trace-viewer/components/middle-truncate/copy-selection.ts +114 -0
  58. package/src/components/new-trace-viewer/components/middle-truncate/middle-truncate.tsx +157 -0
  59. package/src/components/new-trace-viewer/components/middle-truncate/truncate.ts +132 -0
  60. package/src/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.ts +167 -0
  61. package/src/components/new-trace-viewer/components/split-pane.tsx +2 -3
  62. package/src/components/new-trace-viewer/components/timeline.tsx +119 -115
  63. package/src/components/new-trace-viewer/context.tsx +7 -9
  64. package/src/components/new-trace-viewer/detail-panel.tsx +2 -1
  65. package/src/components/new-trace-viewer/trace-viewer.tsx +120 -60
  66. package/src/components/new-trace-viewer/utils.ts +19 -93
  67. package/src/components/trace-viewer/util/timing.ts +5 -9
  68. package/src/components/workflow-traces/trace-span-construction.ts +22 -4
  69. package/src/lib/trace-builder.ts +2 -1
  70. package/src/lib/utils.ts +29 -29
@@ -0,0 +1,114 @@
1
+ import { ELLIPSIS, toGraphemes } from './truncate';
2
+
3
+ interface GetMiddleTruncateCopyTextOptions {
4
+ prefixText: string;
5
+ selectionEnd: number;
6
+ selectionStart: number;
7
+ suffixText: string;
8
+ value: string;
9
+ }
10
+
11
+ interface GetMiddleTruncateCopyTextFromSelectionTextOptions {
12
+ prefixText: string;
13
+ selectionText: string;
14
+ suffixText: string;
15
+ value: string;
16
+ }
17
+
18
+ function getMiddleTruncateCopyText({
19
+ prefixText,
20
+ selectionEnd,
21
+ selectionStart,
22
+ suffixText,
23
+ value,
24
+ }: GetMiddleTruncateCopyTextOptions): string | null {
25
+ const visibleText = prefixText + ELLIPSIS + suffixText;
26
+
27
+ if (
28
+ selectionStart < 0 ||
29
+ selectionEnd > visibleText.length ||
30
+ selectionStart >= selectionEnd
31
+ ) {
32
+ return null;
33
+ }
34
+
35
+ if (selectionStart === 0 && selectionEnd === visibleText.length) {
36
+ return value;
37
+ }
38
+
39
+ const ellipsisStart = prefixText.length;
40
+ const ellipsisEnd = ellipsisStart + ELLIPSIS.length;
41
+
42
+ if (selectionStart > ellipsisStart || selectionEnd < ellipsisEnd) {
43
+ return null;
44
+ }
45
+
46
+ const originalGraphemes = toGraphemes(value);
47
+ const selectedPrefixGraphemeCount = toGraphemes(
48
+ visibleText.slice(0, selectionStart)
49
+ ).length;
50
+ const selectedSuffixGraphemeCount = toGraphemes(
51
+ visibleText.slice(ellipsisEnd, selectionEnd)
52
+ ).length;
53
+ const suffixStart = originalGraphemes.length - toGraphemes(suffixText).length;
54
+
55
+ return originalGraphemes
56
+ .slice(
57
+ selectedPrefixGraphemeCount,
58
+ suffixStart + selectedSuffixGraphemeCount
59
+ )
60
+ .join('');
61
+ }
62
+
63
+ function getMiddleTruncateCopyTextFromSelectionText({
64
+ prefixText,
65
+ selectionText,
66
+ suffixText,
67
+ value,
68
+ }: GetMiddleTruncateCopyTextFromSelectionTextOptions): string | null {
69
+ const visibleText = prefixText + ELLIPSIS + suffixText;
70
+ const trimmedSelectionText = selectionText.trim();
71
+
72
+ if (!trimmedSelectionText) {
73
+ return null;
74
+ }
75
+
76
+ const leading = selectionText.slice(
77
+ 0,
78
+ selectionText.length - selectionText.trimStart().length
79
+ );
80
+ const trailing = selectionText.slice(selectionText.trimEnd().length);
81
+
82
+ if (trimmedSelectionText === visibleText) {
83
+ return leading + value + trailing;
84
+ }
85
+
86
+ if (!trimmedSelectionText.includes(ELLIPSIS)) {
87
+ return null;
88
+ }
89
+
90
+ const selectionStart = visibleText.indexOf(trimmedSelectionText);
91
+ if (selectionStart === -1) {
92
+ return null;
93
+ }
94
+
95
+ const selectionEnd = selectionStart + trimmedSelectionText.length;
96
+ const mappedText = getMiddleTruncateCopyText({
97
+ prefixText,
98
+ selectionEnd,
99
+ selectionStart,
100
+ suffixText,
101
+ value,
102
+ });
103
+
104
+ return mappedText === null ? null : leading + mappedText + trailing;
105
+ }
106
+
107
+ export {
108
+ getMiddleTruncateCopyText,
109
+ getMiddleTruncateCopyTextFromSelectionText,
110
+ };
111
+ export type {
112
+ GetMiddleTruncateCopyTextFromSelectionTextOptions,
113
+ GetMiddleTruncateCopyTextOptions,
114
+ };
@@ -0,0 +1,157 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useRef } from 'react';
4
+ import type { ClipboardEvent, ComponentPropsWithoutRef, JSX } from 'react';
5
+ import { cn } from '../../../../lib/utils';
6
+ import {
7
+ getMiddleTruncateCopyText,
8
+ getMiddleTruncateCopyTextFromSelectionText,
9
+ } from './copy-selection';
10
+ import { ELLIPSIS } from './truncate';
11
+ import { useMiddleTruncate } from './use-middle-truncate';
12
+
13
+ /** Props for the {@link MiddleTruncate} component. */
14
+ interface MiddleTruncateProps
15
+ extends Omit<ComponentPropsWithoutRef<'span'>, 'children'> {
16
+ /** The full text string to display, truncated from the middle when it overflows. */
17
+ value: string;
18
+ }
19
+
20
+ function getRangeOffsets(
21
+ container: HTMLElement,
22
+ range: Range
23
+ ): { end: number; start: number } | null {
24
+ if (
25
+ !container.contains(range.startContainer) ||
26
+ !container.contains(range.endContainer)
27
+ ) {
28
+ return null;
29
+ }
30
+
31
+ const startRange = document.createRange();
32
+ startRange.selectNodeContents(container);
33
+ startRange.setEnd(range.startContainer, range.startOffset);
34
+
35
+ const endRange = document.createRange();
36
+ endRange.selectNodeContents(container);
37
+ endRange.setEnd(range.endContainer, range.endOffset);
38
+
39
+ return {
40
+ end: endRange.toString().length,
41
+ start: startRange.toString().length,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * @gdoc
47
+ *
48
+ * Truncates text from the middle with an ellipsis when it overflows, preserving the beginning and end (e.g. file paths, URLs). Copy behavior restores the full untruncated text to the clipboard.
49
+ * Renders a `<span>` element.
50
+ *
51
+ * Documentation: [Geist Middle Truncate](https://vercel.com/geist/middle-truncate)
52
+ *
53
+ * @param value - Full text string to display.
54
+ */
55
+ function MiddleTruncate({
56
+ value,
57
+ className,
58
+ onCopy: onCopyProp,
59
+ ...props
60
+ }: MiddleTruncateProps): JSX.Element {
61
+ const { ref, measureRef, displayText, isTruncated, prefixText, suffixText } =
62
+ useMiddleTruncate(value);
63
+ const visibleRef = useRef<HTMLSpanElement | null>(null);
64
+
65
+ const handleCopy = useCallback(
66
+ (e: ClipboardEvent<HTMLSpanElement>) => {
67
+ onCopyProp?.(e);
68
+ if (e.defaultPrevented || !isTruncated) return;
69
+
70
+ const selection = window.getSelection();
71
+ if (!selection || selection.rangeCount === 0) return;
72
+ const selectionText = selection.toString();
73
+ if (!selectionText) return;
74
+
75
+ // Only intercept when the selection is fully within this element.
76
+ // If the user selected across elements (e.g. a table row), let
77
+ // the browser's default behavior preserve surrounding context.
78
+ const range = selection.getRangeAt(0);
79
+ const visibleEl = visibleRef.current;
80
+ let copyText: string | null = null;
81
+
82
+ if (
83
+ e.currentTarget.contains(range.startContainer) &&
84
+ e.currentTarget.contains(range.endContainer) &&
85
+ visibleEl
86
+ ) {
87
+ const offsets = getRangeOffsets(visibleEl, range);
88
+ if (offsets) {
89
+ copyText = getMiddleTruncateCopyText({
90
+ prefixText,
91
+ selectionEnd: offsets.end,
92
+ selectionStart: offsets.start,
93
+ suffixText,
94
+ value,
95
+ });
96
+ }
97
+ }
98
+
99
+ copyText ??= getMiddleTruncateCopyTextFromSelectionText({
100
+ prefixText,
101
+ selectionText,
102
+ suffixText,
103
+ value,
104
+ });
105
+
106
+ if (copyText === null) return;
107
+
108
+ e.preventDefault();
109
+ e.clipboardData.setData('text/plain', copyText);
110
+ },
111
+ [onCopyProp, isTruncated, prefixText, suffixText, value]
112
+ );
113
+
114
+ return (
115
+ <span
116
+ title={isTruncated ? value : undefined}
117
+ {...props}
118
+ ref={ref}
119
+ className={cn(
120
+ 'relative inline-grid min-w-0 max-w-full overflow-hidden whitespace-nowrap',
121
+ className
122
+ )}
123
+ onCopy={handleCopy}
124
+ >
125
+ {isTruncated && <span className='sr-only select-none'>{value}</span>}
126
+ <span
127
+ aria-hidden='true'
128
+ className='pointer-events-none col-start-1 row-start-1 invisible select-none whitespace-nowrap'
129
+ >
130
+ {value}
131
+ </span>
132
+ <span
133
+ aria-hidden={isTruncated || undefined}
134
+ className='col-start-1 row-start-1 min-w-0 overflow-hidden'
135
+ ref={visibleRef}
136
+ >
137
+ {isTruncated ? (
138
+ <>
139
+ <span>{prefixText}</span>
140
+ <span>{ELLIPSIS}</span>
141
+ <span>{suffixText}</span>
142
+ </>
143
+ ) : (
144
+ displayText
145
+ )}
146
+ </span>
147
+ <span
148
+ aria-hidden='true'
149
+ className='pointer-events-none absolute left-0 top-0 inline-block invisible select-none whitespace-nowrap'
150
+ ref={measureRef}
151
+ />
152
+ </span>
153
+ );
154
+ }
155
+
156
+ export { MiddleTruncate };
157
+ export type { MiddleTruncateProps };
@@ -0,0 +1,132 @@
1
+ const ELLIPSIS = '...';
2
+ const MIN_START = 3;
3
+ const MIN_END = 3;
4
+ const MIN_KEPT = MIN_START + MIN_END;
5
+ const graphemeSegmenter =
6
+ typeof Intl !== 'undefined' && 'Segmenter' in Intl
7
+ ? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
8
+ : null;
9
+
10
+ type MeasureFn = (text: string) => number;
11
+
12
+ /**
13
+ * Splits a string into grapheme clusters using Intl.Segmenter.
14
+ * Falls back to Array.from for environments without Intl.Segmenter.
15
+ */
16
+ function toGraphemes(text: string): string[] {
17
+ if (graphemeSegmenter) {
18
+ return [...graphemeSegmenter.segment(text)].map((s) => s.segment);
19
+ }
20
+ return Array.from(text);
21
+ }
22
+
23
+ interface TruncateResult {
24
+ prefixText: string;
25
+ prefixGraphemeCount: number;
26
+ suffixText: string;
27
+ suffixGraphemeCount: number;
28
+ text: string;
29
+ truncated: boolean;
30
+ }
31
+
32
+ function buildCandidate(graphemes: string[], kept: number): TruncateResult {
33
+ if (kept <= 0) {
34
+ return {
35
+ prefixText: '',
36
+ prefixGraphemeCount: 0,
37
+ suffixText: '',
38
+ suffixGraphemeCount: 0,
39
+ text: ELLIPSIS,
40
+ truncated: true,
41
+ };
42
+ }
43
+
44
+ const suffixGraphemeCount =
45
+ kept >= MIN_KEPT
46
+ ? Math.max(MIN_END, Math.floor(kept / 2))
47
+ : Math.floor(kept / 2);
48
+ const prefixGraphemeCount = kept - suffixGraphemeCount;
49
+ const prefixText = graphemes.slice(0, prefixGraphemeCount).join('');
50
+ const suffixText =
51
+ suffixGraphemeCount > 0
52
+ ? graphemes.slice(-suffixGraphemeCount).join('')
53
+ : '';
54
+
55
+ return {
56
+ prefixText,
57
+ prefixGraphemeCount,
58
+ suffixText,
59
+ suffixGraphemeCount,
60
+ text: prefixText + ELLIPSIS + suffixText,
61
+ truncated: true,
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Middle-truncates a string so `prefix...suffix` fits within `availableWidth`.
67
+ * Uses binary search over grapheme clusters with an injectable measure function.
68
+ */
69
+ function middleTruncate(
70
+ graphemes: string[],
71
+ availableWidth: number,
72
+ measure: MeasureFn,
73
+ fullWidth?: number
74
+ ): TruncateResult {
75
+ const fullText = graphemes.join('');
76
+ const resolvedFullWidth = fullWidth ?? measure(fullText);
77
+
78
+ if (availableWidth <= 0 || graphemes.length === 0) {
79
+ return {
80
+ prefixText: fullText,
81
+ prefixGraphemeCount: graphemes.length,
82
+ suffixText: '',
83
+ suffixGraphemeCount: 0,
84
+ text: fullText,
85
+ truncated: false,
86
+ };
87
+ }
88
+
89
+ if (resolvedFullWidth <= availableWidth) {
90
+ return {
91
+ prefixText: fullText,
92
+ prefixGraphemeCount: graphemes.length,
93
+ suffixText: '',
94
+ suffixGraphemeCount: 0,
95
+ text: fullText,
96
+ truncated: false,
97
+ };
98
+ }
99
+
100
+ // Binary search for the maximum number of kept graphemes
101
+ let lo = 0;
102
+ let hi = graphemes.length - 1;
103
+ let best = -1;
104
+
105
+ while (lo <= hi) {
106
+ const mid = (lo + hi) >>> 1;
107
+ const candidate = buildCandidate(graphemes, mid);
108
+
109
+ if (measure(candidate.text) <= availableWidth) {
110
+ best = mid;
111
+ lo = mid + 1;
112
+ } else {
113
+ hi = mid - 1;
114
+ }
115
+ }
116
+
117
+ if (best === -1) {
118
+ return {
119
+ prefixText: '',
120
+ prefixGraphemeCount: 0,
121
+ suffixText: '',
122
+ suffixGraphemeCount: 0,
123
+ text: '',
124
+ truncated: true,
125
+ };
126
+ }
127
+
128
+ return buildCandidate(graphemes, best);
129
+ }
130
+
131
+ export { middleTruncate, toGraphemes, ELLIPSIS, MIN_START, MIN_END };
132
+ export type { MeasureFn, TruncateResult };
@@ -0,0 +1,167 @@
1
+ 'use client';
2
+
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from 'react';
11
+ import { middleTruncate, toGraphemes } from './truncate';
12
+
13
+ const useIsomorphicLayoutEffect =
14
+ typeof window === 'undefined' ? useEffect : useLayoutEffect;
15
+
16
+ interface MiddleTruncateState {
17
+ displayText: string;
18
+ isTruncated: boolean;
19
+ prefixGraphemeCount: number;
20
+ prefixText: string;
21
+ suffixGraphemeCount: number;
22
+ suffixText: string;
23
+ }
24
+
25
+ function createFullState(
26
+ value: string,
27
+ graphemes: string[]
28
+ ): MiddleTruncateState {
29
+ return {
30
+ displayText: value,
31
+ isTruncated: false,
32
+ prefixGraphemeCount: graphemes.length,
33
+ prefixText: value,
34
+ suffixGraphemeCount: 0,
35
+ suffixText: '',
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Middle-truncation logic. Returns refs to attach to the container and measurement elements, plus the truncated display text. Recalculates on resize, font loading, and value changes.
41
+ * Powers the `<MiddleTruncate>` (`<span>`) component.
42
+ *
43
+ * Documentation: [Geist Middle Truncate](https://vercel.com/geist/middle-truncate)
44
+ *
45
+ * @param value - Full text string to truncate.
46
+ */
47
+ function useMiddleTruncate(value: string): {
48
+ ref: React.RefObject<HTMLSpanElement | null>;
49
+ measureRef: React.RefObject<HTMLSpanElement | null>;
50
+ displayText: string;
51
+ isTruncated: boolean;
52
+ prefixGraphemeCount: number;
53
+ prefixText: string;
54
+ suffixGraphemeCount: number;
55
+ suffixText: string;
56
+ } {
57
+ const graphemes = useMemo(() => toGraphemes(value), [value]);
58
+ const fullState = useMemo(
59
+ () => createFullState(value, graphemes),
60
+ [graphemes, value]
61
+ );
62
+ const ref = useRef<HTMLSpanElement | null>(null);
63
+ const measureRef = useRef<HTMLSpanElement | null>(null);
64
+ const [state, setState] = useState<MiddleTruncateState>(() => fullState);
65
+ const rafRef = useRef<number>(0);
66
+
67
+ const updateState = useCallback((nextState: MiddleTruncateState) => {
68
+ setState((currentState) => {
69
+ if (
70
+ currentState.displayText === nextState.displayText &&
71
+ currentState.isTruncated === nextState.isTruncated &&
72
+ currentState.prefixText === nextState.prefixText &&
73
+ currentState.prefixGraphemeCount === nextState.prefixGraphemeCount &&
74
+ currentState.suffixText === nextState.suffixText &&
75
+ currentState.suffixGraphemeCount === nextState.suffixGraphemeCount
76
+ ) {
77
+ return currentState;
78
+ }
79
+
80
+ return nextState;
81
+ });
82
+ }, []);
83
+
84
+ const recalculate = useCallback(() => {
85
+ const el = ref.current;
86
+ const measureEl = measureRef.current;
87
+ if (!el || !measureEl) return;
88
+
89
+ const available = el.clientWidth;
90
+
91
+ if (available <= 0) {
92
+ updateState(fullState);
93
+ return;
94
+ }
95
+
96
+ const measure = (text: string): number => {
97
+ measureEl.textContent = text;
98
+ return measureEl.scrollWidth;
99
+ };
100
+
101
+ const fullWidth = measure(value);
102
+
103
+ if (fullWidth <= available) {
104
+ updateState(fullState);
105
+ return;
106
+ }
107
+
108
+ const result = middleTruncate(graphemes, available, measure, fullWidth);
109
+ updateState({
110
+ displayText: result.text,
111
+ isTruncated: result.truncated,
112
+ prefixGraphemeCount: result.prefixGraphemeCount,
113
+ prefixText: result.prefixText,
114
+ suffixGraphemeCount: result.suffixGraphemeCount,
115
+ suffixText: result.suffixText,
116
+ });
117
+ }, [fullState, graphemes, updateState, value]);
118
+
119
+ // Measure on mount and when value changes - before paint
120
+ useIsomorphicLayoutEffect(() => {
121
+ recalculate();
122
+ }, [recalculate]);
123
+
124
+ // ResizeObserver + font loading for ongoing responsiveness
125
+ useEffect(() => {
126
+ const el = ref.current;
127
+ if (!el) return;
128
+
129
+ const debouncedRecalc = (): void => {
130
+ cancelAnimationFrame(rafRef.current);
131
+ rafRef.current = requestAnimationFrame(recalculate);
132
+ };
133
+
134
+ const ro =
135
+ typeof ResizeObserver !== 'undefined'
136
+ ? new ResizeObserver(debouncedRecalc)
137
+ : null;
138
+ ro?.observe(el);
139
+ window.addEventListener('resize', debouncedRecalc);
140
+
141
+ const onFontsLoaded = (): void => {
142
+ debouncedRecalc();
143
+ };
144
+ const fontSet = 'fonts' in document ? document.fonts : null;
145
+ fontSet?.addEventListener?.('loadingdone', onFontsLoaded);
146
+
147
+ return () => {
148
+ ro?.disconnect();
149
+ window.removeEventListener('resize', debouncedRecalc);
150
+ cancelAnimationFrame(rafRef.current);
151
+ fontSet?.removeEventListener?.('loadingdone', onFontsLoaded);
152
+ };
153
+ }, [recalculate]);
154
+
155
+ return {
156
+ ref,
157
+ measureRef,
158
+ displayText: state.displayText,
159
+ isTruncated: state.isTruncated,
160
+ prefixGraphemeCount: state.prefixGraphemeCount,
161
+ prefixText: state.prefixText,
162
+ suffixGraphemeCount: state.suffixGraphemeCount,
163
+ suffixText: state.suffixText,
164
+ };
165
+ }
166
+
167
+ export { useMiddleTruncate };
@@ -11,8 +11,7 @@ import {
11
11
  } from 'react';
12
12
  import { cn } from '../../../lib/utils';
13
13
 
14
- /** Wide enough for comfortable dragging; visual line stays 1px centered. */
15
- const GUTTER_PX = 9;
14
+ const GUTTER_PX = 1;
16
15
  const MIN_PX = 50;
17
16
  const DEFAULT_START_PX = 340;
18
17
 
@@ -155,7 +154,7 @@ export function SplitPane({
155
154
  <div
156
155
  ref={containerRef}
157
156
  className={cn(
158
- 'grid flex-1 min-h-0 content-start overflow-x-hidden overflow-y-auto',
157
+ 'grid flex-1 min-h-0 overflow-x-hidden overflow-y-auto',
159
158
  isDragging && 'select-none'
160
159
  )}
161
160
  style={{ gridTemplateColumns: colTemplate }}