@workflow/web-shared 5.0.0-beta.41 → 5.0.0-beta.43
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/components/event-list-view.d.ts +10 -2
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +52 -26
- package/dist/components/sidebar/attributes-block.d.ts.map +1 -1
- package/dist/components/sidebar/attributes-block.js +3 -6
- package/dist/components/sidebar/conversation-view.d.ts.map +1 -1
- package/dist/components/sidebar/conversation-view.js +19 -36
- package/dist/components/sidebar/entity-detail-panel.d.ts +5 -0
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +2 -2
- package/dist/components/sidebar/events-list.d.ts +7 -1
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +6 -5
- package/dist/components/sidebar/resolve-hook-modal.d.ts.map +1 -1
- package/dist/components/sidebar/resolve-hook-modal.js +4 -145
- package/dist/components/sidebar/sidebar-data-context.d.ts +6 -0
- package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -1
- package/dist/components/sidebar/sidebar-data-context.js +1 -1
- package/dist/components/stream-viewer-skeleton.js +6 -6
- package/dist/components/stream-viewer.js +2 -2
- package/dist/components/trace-viewer/components/detail-panel.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/detail-panel.js +2 -1
- package/dist/components/trace-viewer/components/middle-truncate/measurement-batch.d.ts +9 -0
- package/dist/components/trace-viewer/components/middle-truncate/measurement-batch.d.ts.map +1 -0
- package/dist/components/trace-viewer/components/middle-truncate/measurement-batch.js +29 -0
- package/dist/components/trace-viewer/components/middle-truncate/middle-truncate.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/middle-truncate/middle-truncate.js +16 -8
- package/dist/components/trace-viewer/components/middle-truncate/use-middle-truncate.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/middle-truncate/use-middle-truncate.js +83 -19
- package/dist/components/trace-viewer/components/timeline.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/timeline.js +13 -22
- package/dist/components/trace-viewer/components/trace-viewer-skeleton.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/trace-viewer-skeleton.js +58 -18
- package/dist/components/trace-viewer/icons.js +5 -5
- package/dist/components/trace-viewer/utils.d.ts +6 -10
- package/dist/components/trace-viewer/utils.d.ts.map +1 -1
- package/dist/components/trace-viewer/utils.js +14 -24
- package/dist/components/trace-viewer.d.ts.map +1 -1
- package/dist/components/trace-viewer.js +12 -4
- package/dist/components/ui/context-card.d.ts.map +1 -1
- package/dist/components/ui/context-card.js +9 -21
- package/dist/components/ui/data-inspector.d.ts.map +1 -1
- package/dist/components/ui/data-inspector.js +8 -35
- package/dist/components/ui/duplicate-event-tooltip.d.ts +15 -0
- package/dist/components/ui/duplicate-event-tooltip.d.ts.map +1 -0
- package/dist/components/ui/duplicate-event-tooltip.js +19 -0
- package/dist/components/ui/menu-dropdown.d.ts.map +1 -1
- package/dist/components/ui/menu-dropdown.js +3 -19
- package/dist/components/ui/skeleton.js +2 -2
- package/dist/components/ui/spinner.d.ts.map +1 -1
- package/dist/components/ui/spinner.js +5 -11
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/lib/duplicate-events.d.ts +27 -0
- package/dist/lib/duplicate-events.d.ts.map +1 -0
- package/dist/lib/duplicate-events.js +166 -0
- package/dist/lib/event-materialization.d.ts +15 -2
- package/dist/lib/event-materialization.d.ts.map +1 -1
- package/dist/lib/event-materialization.js +19 -4
- package/dist/lib/trace-builder.d.ts +16 -1
- package/dist/lib/trace-builder.d.ts.map +1 -1
- package/dist/lib/trace-builder.js +23 -4
- package/package.json +4 -4
- package/src/components/event-list-view.tsx +70 -32
- package/src/components/sidebar/attributes-block.tsx +4 -11
- package/src/components/sidebar/conversation-view.tsx +25 -79
- package/src/components/sidebar/entity-detail-panel.tsx +6 -0
- package/src/components/sidebar/events-list.tsx +21 -3
- package/src/components/sidebar/resolve-hook-modal.tsx +23 -186
- package/src/components/sidebar/sidebar-data-context.tsx +6 -0
- package/src/components/stream-viewer-skeleton.tsx +5 -5
- package/src/components/stream-viewer.tsx +1 -1
- package/src/components/trace-viewer/components/detail-panel.tsx +1 -0
- package/src/components/trace-viewer/components/middle-truncate/measurement-batch.ts +47 -0
- package/src/components/trace-viewer/components/middle-truncate/middle-truncate.tsx +15 -7
- package/src/components/trace-viewer/components/middle-truncate/use-middle-truncate.ts +132 -38
- package/src/components/trace-viewer/components/timeline.tsx +21 -39
- package/src/components/trace-viewer/components/trace-viewer-skeleton.tsx +63 -35
- package/src/components/trace-viewer/icons.tsx +4 -4
- package/src/components/trace-viewer/utils.test.ts +5 -7
- package/src/components/trace-viewer/utils.ts +17 -31
- package/src/components/trace-viewer.tsx +15 -3
- package/src/components/ui/context-card.tsx +16 -26
- package/src/components/ui/data-inspector.tsx +20 -65
- package/src/components/ui/duplicate-event-tooltip.tsx +44 -0
- package/src/components/ui/menu-dropdown.tsx +8 -22
- package/src/components/ui/skeleton.tsx +2 -2
- package/src/components/ui/spinner.tsx +4 -8
- package/src/index.ts +4 -0
- package/src/lib/duplicate-events.test.ts +339 -0
- package/src/lib/duplicate-events.ts +188 -0
- package/src/lib/event-materialization.test.ts +117 -0
- package/src/lib/event-materialization.ts +23 -3
- package/src/lib/trace-builder.test.ts +74 -0
- package/src/lib/trace-builder.ts +30 -3
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Send, X } from 'lucide-react';
|
|
4
4
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { cn } from '../../lib/cn';
|
|
5
6
|
|
|
6
7
|
interface ResolveHookModalProps {
|
|
7
8
|
/** Whether the modal is open */
|
|
@@ -110,60 +111,24 @@ export function ResolveHookModal({
|
|
|
110
111
|
|
|
111
112
|
return (
|
|
112
113
|
<div
|
|
113
|
-
|
|
114
|
-
position: 'fixed',
|
|
115
|
-
inset: 0,
|
|
116
|
-
zIndex: 50,
|
|
117
|
-
display: 'flex',
|
|
118
|
-
alignItems: 'center',
|
|
119
|
-
justifyContent: 'center',
|
|
120
|
-
}}
|
|
114
|
+
className="fixed inset-0 z-50 flex items-center justify-center"
|
|
121
115
|
role="dialog"
|
|
122
116
|
aria-modal="true"
|
|
123
117
|
aria-labelledby="resolve-hook-modal-title"
|
|
124
118
|
>
|
|
125
119
|
{/* Backdrop — matches Geist dialog ::backdrop */}
|
|
126
120
|
<div
|
|
127
|
-
|
|
128
|
-
position: 'absolute',
|
|
129
|
-
inset: 0,
|
|
130
|
-
background: 'rgba(0, 0, 0, 0.7)',
|
|
131
|
-
}}
|
|
121
|
+
className="absolute inset-0 bg-black/70"
|
|
132
122
|
onClick={isSubmitting ? undefined : onClose}
|
|
133
123
|
/>
|
|
134
124
|
|
|
135
125
|
{/* Modal card — matches Geist dialog.geist-dialog */}
|
|
136
|
-
<div
|
|
137
|
-
style={{
|
|
138
|
-
position: 'relative',
|
|
139
|
-
zIndex: 10,
|
|
140
|
-
width: 480,
|
|
141
|
-
maxWidth: 'calc(100% - 32px)',
|
|
142
|
-
borderRadius: 12,
|
|
143
|
-
border: 'none',
|
|
144
|
-
boxShadow: 'var(--ds-shadow-menu)',
|
|
145
|
-
background: 'var(--ds-background-100)',
|
|
146
|
-
color: 'var(--ds-gray-1000)',
|
|
147
|
-
overflow: 'hidden',
|
|
148
|
-
}}
|
|
149
|
-
>
|
|
126
|
+
<div className="relative z-10 w-[480px] max-w-[calc(100%_-_32px)] overflow-hidden !border-none bg-background-100 text-gray-1000 material-menu">
|
|
150
127
|
{/* Header */}
|
|
151
|
-
<div
|
|
152
|
-
style={{
|
|
153
|
-
display: 'flex',
|
|
154
|
-
alignItems: 'center',
|
|
155
|
-
justifyContent: 'space-between',
|
|
156
|
-
padding: '16px 24px',
|
|
157
|
-
}}
|
|
158
|
-
>
|
|
128
|
+
<div className="flex items-center justify-between px-6 py-4">
|
|
159
129
|
<h2
|
|
160
130
|
id="resolve-hook-modal-title"
|
|
161
|
-
|
|
162
|
-
margin: 0,
|
|
163
|
-
fontSize: 16,
|
|
164
|
-
fontWeight: 600,
|
|
165
|
-
color: 'var(--ds-gray-1000)',
|
|
166
|
-
}}
|
|
131
|
+
className="m-0 font-semibold text-base text-gray-1000"
|
|
167
132
|
>
|
|
168
133
|
Resolve Hook
|
|
169
134
|
</h2>
|
|
@@ -172,66 +137,24 @@ export function ResolveHookModal({
|
|
|
172
137
|
onClick={onClose}
|
|
173
138
|
disabled={isSubmitting}
|
|
174
139
|
aria-label="Close modal"
|
|
175
|
-
|
|
176
|
-
display: 'flex',
|
|
177
|
-
alignItems: 'center',
|
|
178
|
-
justifyContent: 'center',
|
|
179
|
-
padding: 4,
|
|
180
|
-
borderRadius: 6,
|
|
181
|
-
border: 'none',
|
|
182
|
-
background: 'transparent',
|
|
183
|
-
color: 'var(--ds-gray-900)',
|
|
184
|
-
cursor: isSubmitting ? 'not-allowed' : 'pointer',
|
|
185
|
-
opacity: isSubmitting ? 0.5 : 1,
|
|
186
|
-
transition: 'background 0.15s',
|
|
187
|
-
}}
|
|
188
|
-
onMouseEnter={(e) => {
|
|
189
|
-
e.currentTarget.style.background = 'var(--ds-gray-alpha-200)';
|
|
190
|
-
}}
|
|
191
|
-
onMouseLeave={(e) => {
|
|
192
|
-
e.currentTarget.style.background = 'transparent';
|
|
193
|
-
}}
|
|
140
|
+
className="flex cursor-pointer items-center justify-center rounded-md !border-none !bg-transparent p-1 text-gray-900 transition-colors hover:!bg-gray-alpha-200 disabled:cursor-not-allowed disabled:opacity-50"
|
|
194
141
|
>
|
|
195
|
-
<X
|
|
142
|
+
<X className="size-4" />
|
|
196
143
|
</button>
|
|
197
144
|
</div>
|
|
198
145
|
|
|
199
146
|
{/* Body */}
|
|
200
147
|
<form onSubmit={handleSubmit}>
|
|
201
|
-
<div
|
|
148
|
+
<div className="px-6 pb-4">
|
|
202
149
|
<label
|
|
203
150
|
htmlFor="json-payload"
|
|
204
|
-
|
|
205
|
-
display: 'block',
|
|
206
|
-
fontSize: 14,
|
|
207
|
-
fontWeight: 500,
|
|
208
|
-
marginBottom: 6,
|
|
209
|
-
color: 'var(--ds-gray-1000)',
|
|
210
|
-
}}
|
|
151
|
+
className="mb-1.5 block font-medium text-gray-1000 text-sm"
|
|
211
152
|
>
|
|
212
153
|
JSON Payload
|
|
213
154
|
</label>
|
|
214
|
-
<p
|
|
215
|
-
style={{
|
|
216
|
-
fontSize: 13,
|
|
217
|
-
marginBottom: 12,
|
|
218
|
-
marginTop: 0,
|
|
219
|
-
color: 'var(--ds-gray-900)',
|
|
220
|
-
lineHeight: 1.5,
|
|
221
|
-
}}
|
|
222
|
-
>
|
|
155
|
+
<p className="mt-0 mb-3 text-[13px] text-gray-900 leading-[1.5]">
|
|
223
156
|
Enter a JSON value to send to the hook. Leave empty to send{' '}
|
|
224
|
-
<code
|
|
225
|
-
style={{
|
|
226
|
-
padding: '2px 6px',
|
|
227
|
-
borderRadius: 4,
|
|
228
|
-
fontSize: 12,
|
|
229
|
-
fontFamily:
|
|
230
|
-
'var(--font-mono, ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace)',
|
|
231
|
-
background: 'var(--ds-gray-alpha-200)',
|
|
232
|
-
color: 'var(--ds-gray-1000)',
|
|
233
|
-
}}
|
|
234
|
-
>
|
|
157
|
+
<code className="rounded bg-gray-alpha-200 px-1.5 py-0.5 font-mono text-gray-1000 text-xs">
|
|
235
158
|
null
|
|
236
159
|
</code>
|
|
237
160
|
.
|
|
@@ -247,57 +170,17 @@ export function ResolveHookModal({
|
|
|
247
170
|
onKeyDown={handleKeyDown}
|
|
248
171
|
disabled={isSubmitting}
|
|
249
172
|
placeholder='{"key": "value"}'
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
fontFamily:
|
|
255
|
-
'var(--font-mono, ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace)',
|
|
256
|
-
fontSize: 13,
|
|
257
|
-
lineHeight: 1.5,
|
|
258
|
-
borderRadius: 8,
|
|
259
|
-
border: `1px solid ${parseError ? 'var(--ds-red-700)' : 'var(--ds-gray-alpha-400)'}`,
|
|
260
|
-
color: 'var(--ds-gray-1000)',
|
|
261
|
-
background: 'var(--ds-background-100)',
|
|
262
|
-
outline: 'none',
|
|
263
|
-
resize: 'none',
|
|
264
|
-
opacity: isSubmitting ? 0.5 : 1,
|
|
265
|
-
cursor: isSubmitting ? 'not-allowed' : 'text',
|
|
266
|
-
boxSizing: 'border-box',
|
|
267
|
-
}}
|
|
173
|
+
className={cn(
|
|
174
|
+
'box-border h-40 w-full resize-none rounded-lg border bg-background-100 px-3 py-2 font-mono text-[13px] text-gray-1000 leading-[1.5] outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
|
175
|
+
parseError ? 'border-red-700' : 'border-gray-alpha-400'
|
|
176
|
+
)}
|
|
268
177
|
/>
|
|
269
178
|
{parseError && (
|
|
270
|
-
<p
|
|
271
|
-
style={{
|
|
272
|
-
marginTop: 8,
|
|
273
|
-
fontSize: 13,
|
|
274
|
-
color: 'var(--ds-red-900)',
|
|
275
|
-
margin: '8px 0 0',
|
|
276
|
-
}}
|
|
277
|
-
>
|
|
278
|
-
{parseError}
|
|
279
|
-
</p>
|
|
179
|
+
<p className="mt-2 mb-0 text-[13px] text-red-900">{parseError}</p>
|
|
280
180
|
)}
|
|
281
|
-
<p
|
|
282
|
-
style={{
|
|
283
|
-
marginTop: 8,
|
|
284
|
-
fontSize: 12,
|
|
285
|
-
color: 'var(--ds-gray-800)',
|
|
286
|
-
margin: '8px 0 0',
|
|
287
|
-
}}
|
|
288
|
-
>
|
|
181
|
+
<p className="mt-2 mb-0 text-gray-800 text-xs">
|
|
289
182
|
Press{' '}
|
|
290
|
-
<kbd
|
|
291
|
-
style={{
|
|
292
|
-
padding: '2px 5px',
|
|
293
|
-
borderRadius: 4,
|
|
294
|
-
fontSize: 11,
|
|
295
|
-
fontFamily:
|
|
296
|
-
'var(--font-mono, ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace)',
|
|
297
|
-
background: 'var(--ds-gray-alpha-200)',
|
|
298
|
-
border: '1px solid var(--ds-gray-alpha-400)',
|
|
299
|
-
}}
|
|
300
|
-
>
|
|
183
|
+
<kbd className="rounded border border-gray-alpha-400 bg-gray-alpha-200 px-[5px] py-0.5 font-mono text-[11px]">
|
|
301
184
|
{isMacPlatform ? '⌘' : 'Ctrl'}
|
|
302
185
|
+Enter
|
|
303
186
|
</kbd>{' '}
|
|
@@ -306,38 +189,12 @@ export function ResolveHookModal({
|
|
|
306
189
|
</div>
|
|
307
190
|
|
|
308
191
|
{/* Footer */}
|
|
309
|
-
<div
|
|
310
|
-
style={{
|
|
311
|
-
display: 'flex',
|
|
312
|
-
alignItems: 'center',
|
|
313
|
-
justifyContent: 'flex-end',
|
|
314
|
-
gap: 8,
|
|
315
|
-
padding: '12px 24px',
|
|
316
|
-
borderTop: '1px solid var(--ds-gray-alpha-400)',
|
|
317
|
-
}}
|
|
318
|
-
>
|
|
192
|
+
<div className="flex items-center justify-end gap-2 border-gray-alpha-400 border-t px-6 py-3">
|
|
319
193
|
<button
|
|
320
194
|
type="button"
|
|
321
195
|
onClick={onClose}
|
|
322
196
|
disabled={isSubmitting}
|
|
323
|
-
|
|
324
|
-
padding: '8px 16px',
|
|
325
|
-
fontSize: 14,
|
|
326
|
-
fontWeight: 500,
|
|
327
|
-
borderRadius: 8,
|
|
328
|
-
border: '1px solid var(--ds-gray-alpha-400)',
|
|
329
|
-
background: 'var(--ds-background-100)',
|
|
330
|
-
color: 'var(--ds-gray-1000)',
|
|
331
|
-
cursor: isSubmitting ? 'not-allowed' : 'pointer',
|
|
332
|
-
opacity: isSubmitting ? 0.5 : 1,
|
|
333
|
-
transition: 'background 0.15s',
|
|
334
|
-
}}
|
|
335
|
-
onMouseEnter={(e) => {
|
|
336
|
-
e.currentTarget.style.background = 'var(--ds-gray-alpha-100)';
|
|
337
|
-
}}
|
|
338
|
-
onMouseLeave={(e) => {
|
|
339
|
-
e.currentTarget.style.background = 'var(--ds-background-100)';
|
|
340
|
-
}}
|
|
197
|
+
className="cursor-pointer rounded-lg border border-gray-alpha-400 bg-background-100 px-4 py-2 font-medium text-gray-1000 text-sm transition-colors hover:bg-gray-alpha-100 disabled:cursor-not-allowed disabled:opacity-50"
|
|
341
198
|
>
|
|
342
199
|
Cancel
|
|
343
200
|
</button>
|
|
@@ -345,29 +202,9 @@ export function ResolveHookModal({
|
|
|
345
202
|
type="button"
|
|
346
203
|
onClick={() => void submitPayload()}
|
|
347
204
|
disabled={isSubmitting}
|
|
348
|
-
|
|
349
|
-
display: 'flex',
|
|
350
|
-
alignItems: 'center',
|
|
351
|
-
gap: 6,
|
|
352
|
-
padding: '8px 16px',
|
|
353
|
-
fontSize: 14,
|
|
354
|
-
fontWeight: 500,
|
|
355
|
-
borderRadius: 8,
|
|
356
|
-
border: 'none',
|
|
357
|
-
background: 'var(--ds-gray-1000)',
|
|
358
|
-
color: 'var(--ds-background-100)',
|
|
359
|
-
cursor: isSubmitting ? 'not-allowed' : 'pointer',
|
|
360
|
-
opacity: isSubmitting ? 0.5 : 1,
|
|
361
|
-
transition: 'opacity 0.15s',
|
|
362
|
-
}}
|
|
363
|
-
onMouseEnter={(e) => {
|
|
364
|
-
if (!isSubmitting) e.currentTarget.style.opacity = '0.9';
|
|
365
|
-
}}
|
|
366
|
-
onMouseLeave={(e) => {
|
|
367
|
-
if (!isSubmitting) e.currentTarget.style.opacity = '1';
|
|
368
|
-
}}
|
|
205
|
+
className="flex cursor-pointer items-center gap-1.5 rounded-lg !border-none bg-gray-1000 px-4 py-2 font-medium text-background-100 text-sm transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:opacity-50"
|
|
369
206
|
>
|
|
370
|
-
<Send
|
|
207
|
+
<Send className="size-3.5" />
|
|
371
208
|
{isSubmitting ? 'Sending...' : 'Send Payload'}
|
|
372
209
|
</button>
|
|
373
210
|
</div>
|
|
@@ -7,6 +7,12 @@ import type { FetchSpanDetail } from './use-selected-span-detail';
|
|
|
7
7
|
export interface SidebarDataContextValue {
|
|
8
8
|
run: WorkflowRun;
|
|
9
9
|
events: Event[];
|
|
10
|
+
/**
|
|
11
|
+
* Events every replay reads past as repeats, computed once from the whole
|
|
12
|
+
* of `events` by whoever knows the list is complete. Absent when nobody
|
|
13
|
+
* could vouch for that, in which case nothing is marked.
|
|
14
|
+
*/
|
|
15
|
+
duplicateEventIds?: ReadonlySet<string>;
|
|
10
16
|
fetchSpanDetail: FetchSpanDetail;
|
|
11
17
|
onStreamClick?: (streamId: string) => void;
|
|
12
18
|
onRunClick?: (runId: string) => void;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Skeleton } from './ui/skeleton';
|
|
2
2
|
|
|
3
3
|
const STREAM_SKELETON_ROWS = [
|
|
4
|
-
{ id: 'stream-skeleton-row-1',
|
|
5
|
-
{ id: 'stream-skeleton-row-2',
|
|
6
|
-
{ id: 'stream-skeleton-row-3',
|
|
7
|
-
{ id: 'stream-skeleton-row-4',
|
|
4
|
+
{ id: 'stream-skeleton-row-1', widthClassName: 'w-[72%]' },
|
|
5
|
+
{ id: 'stream-skeleton-row-2', widthClassName: 'w-[58%]' },
|
|
6
|
+
{ id: 'stream-skeleton-row-3', widthClassName: 'w-4/5' },
|
|
7
|
+
{ id: 'stream-skeleton-row-4', widthClassName: 'w-[64%]' },
|
|
8
8
|
];
|
|
9
9
|
|
|
10
10
|
export function StreamViewerSkeleton() {
|
|
@@ -24,7 +24,7 @@ export function StreamViewerSkeleton() {
|
|
|
24
24
|
key={row.id}
|
|
25
25
|
className="flex h-10 items-center gap-2 border-b border-gray-alpha-400 px-3"
|
|
26
26
|
>
|
|
27
|
-
<Skeleton className=
|
|
27
|
+
<Skeleton className={`h-3 ${row.widthClassName}`} />
|
|
28
28
|
<Skeleton className="h-4 w-4 shrink-0 rounded-sm" />
|
|
29
29
|
</div>
|
|
30
30
|
))}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
interface MeasurementTask<T> {
|
|
2
|
+
measure: (measurement: T) => void;
|
|
3
|
+
read: () => T | null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
type PendingMeasurement = () => (() => void) | null;
|
|
7
|
+
|
|
8
|
+
const pendingMeasurements = new Map<object, PendingMeasurement>();
|
|
9
|
+
let measurementFrame = 0;
|
|
10
|
+
|
|
11
|
+
function flushMeasurements(): void {
|
|
12
|
+
measurementFrame = 0;
|
|
13
|
+
|
|
14
|
+
const reads = [...pendingMeasurements.values()];
|
|
15
|
+
pendingMeasurements.clear();
|
|
16
|
+
const measures = reads.map((read) => read());
|
|
17
|
+
|
|
18
|
+
for (const measure of measures) {
|
|
19
|
+
measure?.();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function scheduleMeasurement<T>(
|
|
24
|
+
key: object,
|
|
25
|
+
{ read, measure }: MeasurementTask<T>
|
|
26
|
+
): void {
|
|
27
|
+
pendingMeasurements.set(key, () => {
|
|
28
|
+
const measurement = read();
|
|
29
|
+
return measurement === null ? null : () => measure(measurement);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (measurementFrame === 0) {
|
|
33
|
+
measurementFrame = requestAnimationFrame(flushMeasurements);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function cancelMeasurement(key: object): void {
|
|
38
|
+
pendingMeasurements.delete(key);
|
|
39
|
+
|
|
40
|
+
if (pendingMeasurements.size === 0 && measurementFrame !== 0) {
|
|
41
|
+
cancelAnimationFrame(measurementFrame);
|
|
42
|
+
measurementFrame = 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { cancelMeasurement, scheduleMeasurement };
|
|
47
|
+
export type { MeasurementTask };
|
|
@@ -28,13 +28,20 @@ function getRangeOffsets(
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
let startRange: Range;
|
|
32
|
+
let endRange: Range;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
startRange = document.createRange();
|
|
36
|
+
startRange.selectNodeContents(container);
|
|
37
|
+
startRange.setEnd(range.startContainer, range.startOffset);
|
|
38
|
+
|
|
39
|
+
endRange = document.createRange();
|
|
40
|
+
endRange.selectNodeContents(container);
|
|
41
|
+
endRange.setEnd(range.endContainer, range.endOffset);
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
38
45
|
|
|
39
46
|
return {
|
|
40
47
|
end: endRange.toString().length,
|
|
@@ -80,6 +87,7 @@ function MiddleTruncate({
|
|
|
80
87
|
let copyText: string | null = null;
|
|
81
88
|
|
|
82
89
|
if (
|
|
90
|
+
selectionText.includes(ELLIPSIS) &&
|
|
83
91
|
e.currentTarget.contains(range.startContainer) &&
|
|
84
92
|
e.currentTarget.contains(range.endContainer) &&
|
|
85
93
|
visibleEl
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useRef,
|
|
9
9
|
useState,
|
|
10
10
|
} from 'react';
|
|
11
|
+
import { cancelMeasurement, scheduleMeasurement } from './measurement-batch';
|
|
11
12
|
import { middleTruncate, toGraphemes } from './truncate';
|
|
12
13
|
|
|
13
14
|
const useIsomorphicLayoutEffect =
|
|
@@ -22,6 +23,17 @@ interface MiddleTruncateState {
|
|
|
22
23
|
suffixText: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
interface MiddleTruncateMeasurement {
|
|
27
|
+
availableWidth: number;
|
|
28
|
+
typography: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface TextMeasurementCache {
|
|
32
|
+
typography: string;
|
|
33
|
+
value: string;
|
|
34
|
+
widths: Map<string, number>;
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
function createFullState(
|
|
26
38
|
value: string,
|
|
27
39
|
graphemes: string[]
|
|
@@ -62,7 +74,15 @@ function useMiddleTruncate(value: string): {
|
|
|
62
74
|
const ref = useRef<HTMLSpanElement | null>(null);
|
|
63
75
|
const measureRef = useRef<HTMLSpanElement | null>(null);
|
|
64
76
|
const [state, setState] = useState<MiddleTruncateState>(() => fullState);
|
|
65
|
-
const
|
|
77
|
+
const measurementKeyRef = useRef<object>({});
|
|
78
|
+
const textMeasurementCacheRef = useRef<TextMeasurementCache>({
|
|
79
|
+
typography: '',
|
|
80
|
+
value,
|
|
81
|
+
widths: new Map(),
|
|
82
|
+
});
|
|
83
|
+
const lastMeasurementRef = useRef<
|
|
84
|
+
(MiddleTruncateMeasurement & { value: string }) | null
|
|
85
|
+
>(null);
|
|
66
86
|
|
|
67
87
|
const updateState = useCallback((nextState: MiddleTruncateState) => {
|
|
68
88
|
setState((currentState) => {
|
|
@@ -81,76 +101,150 @@ function useMiddleTruncate(value: string): {
|
|
|
81
101
|
});
|
|
82
102
|
}, []);
|
|
83
103
|
|
|
84
|
-
const
|
|
104
|
+
const readMeasurement = useCallback((): MiddleTruncateMeasurement | null => {
|
|
85
105
|
const el = ref.current;
|
|
86
106
|
const measureEl = measureRef.current;
|
|
87
|
-
if (!el || !measureEl) return;
|
|
107
|
+
if (!el || !measureEl) return null;
|
|
88
108
|
|
|
89
|
-
const
|
|
109
|
+
const availableWidth = el.clientWidth;
|
|
110
|
+
const style = getComputedStyle(measureEl);
|
|
90
111
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
return {
|
|
113
|
+
availableWidth,
|
|
114
|
+
typography: [
|
|
115
|
+
style.fontFamily,
|
|
116
|
+
style.fontFeatureSettings,
|
|
117
|
+
style.fontKerning,
|
|
118
|
+
style.fontSize,
|
|
119
|
+
style.fontStretch,
|
|
120
|
+
style.fontStyle,
|
|
121
|
+
style.fontVariationSettings,
|
|
122
|
+
style.fontWeight,
|
|
123
|
+
style.letterSpacing,
|
|
124
|
+
style.textTransform,
|
|
125
|
+
].join('\0'),
|
|
99
126
|
};
|
|
127
|
+
}, []);
|
|
100
128
|
|
|
101
|
-
|
|
129
|
+
const recalculate = useCallback(
|
|
130
|
+
({ availableWidth, typography }: MiddleTruncateMeasurement) => {
|
|
131
|
+
const measureEl = measureRef.current;
|
|
132
|
+
if (!measureEl) return;
|
|
102
133
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
134
|
+
const lastMeasurement = lastMeasurementRef.current;
|
|
135
|
+
if (
|
|
136
|
+
lastMeasurement?.availableWidth === availableWidth &&
|
|
137
|
+
lastMeasurement.typography === typography &&
|
|
138
|
+
lastMeasurement.value === value
|
|
139
|
+
) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
107
142
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
143
|
+
lastMeasurementRef.current = {
|
|
144
|
+
availableWidth,
|
|
145
|
+
typography,
|
|
146
|
+
value,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (availableWidth <= 0) {
|
|
150
|
+
updateState(fullState);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const textMeasurementCache = textMeasurementCacheRef.current;
|
|
155
|
+
if (
|
|
156
|
+
textMeasurementCache.typography !== typography ||
|
|
157
|
+
textMeasurementCache.value !== value
|
|
158
|
+
) {
|
|
159
|
+
textMeasurementCache.typography = typography;
|
|
160
|
+
textMeasurementCache.value = value;
|
|
161
|
+
textMeasurementCache.widths.clear();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const measure = (text: string): number => {
|
|
165
|
+
const cachedWidth = textMeasurementCache.widths.get(text);
|
|
166
|
+
if (cachedWidth !== undefined) {
|
|
167
|
+
return cachedWidth;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
measureEl.textContent = text;
|
|
171
|
+
const width = measureEl.scrollWidth;
|
|
172
|
+
textMeasurementCache.widths.set(text, width);
|
|
173
|
+
return width;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const fullWidth = measure(value);
|
|
177
|
+
|
|
178
|
+
if (fullWidth <= availableWidth) {
|
|
179
|
+
updateState(fullState);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const result = middleTruncate(
|
|
184
|
+
graphemes,
|
|
185
|
+
availableWidth,
|
|
186
|
+
measure,
|
|
187
|
+
fullWidth
|
|
188
|
+
);
|
|
189
|
+
updateState({
|
|
190
|
+
displayText: result.text,
|
|
191
|
+
isTruncated: result.truncated,
|
|
192
|
+
prefixGraphemeCount: result.prefixGraphemeCount,
|
|
193
|
+
prefixText: result.prefixText,
|
|
194
|
+
suffixGraphemeCount: result.suffixGraphemeCount,
|
|
195
|
+
suffixText: result.suffixText,
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
[fullState, graphemes, updateState, value]
|
|
199
|
+
);
|
|
118
200
|
|
|
119
201
|
// Measure on mount and when value changes - before paint
|
|
120
202
|
useIsomorphicLayoutEffect(() => {
|
|
121
|
-
|
|
122
|
-
|
|
203
|
+
const measurement = readMeasurement();
|
|
204
|
+
if (measurement) {
|
|
205
|
+
recalculate(measurement);
|
|
206
|
+
}
|
|
207
|
+
}, [readMeasurement, recalculate]);
|
|
123
208
|
|
|
124
209
|
// ResizeObserver + font loading for ongoing responsiveness
|
|
125
210
|
useEffect(() => {
|
|
126
211
|
const el = ref.current;
|
|
127
212
|
if (!el) return;
|
|
128
213
|
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
214
|
+
const measurementKey = measurementKeyRef.current;
|
|
215
|
+
const scheduleRecalculation = (): void => {
|
|
216
|
+
scheduleMeasurement(measurementKey, {
|
|
217
|
+
read: readMeasurement,
|
|
218
|
+
measure: recalculate,
|
|
219
|
+
});
|
|
132
220
|
};
|
|
133
221
|
|
|
134
222
|
const ro =
|
|
135
223
|
typeof ResizeObserver !== 'undefined'
|
|
136
|
-
? new ResizeObserver(
|
|
224
|
+
? new ResizeObserver(scheduleRecalculation)
|
|
137
225
|
: null;
|
|
138
226
|
ro?.observe(el);
|
|
139
|
-
|
|
227
|
+
if (!ro) {
|
|
228
|
+
window.addEventListener('resize', scheduleRecalculation);
|
|
229
|
+
}
|
|
140
230
|
|
|
141
231
|
const onFontsLoaded = (): void => {
|
|
142
|
-
|
|
232
|
+
textMeasurementCacheRef.current.widths.clear();
|
|
233
|
+
lastMeasurementRef.current = null;
|
|
234
|
+
scheduleRecalculation();
|
|
143
235
|
};
|
|
144
236
|
const fontSet = 'fonts' in document ? document.fonts : null;
|
|
145
237
|
fontSet?.addEventListener?.('loadingdone', onFontsLoaded);
|
|
146
238
|
|
|
147
239
|
return () => {
|
|
148
240
|
ro?.disconnect();
|
|
149
|
-
|
|
150
|
-
|
|
241
|
+
if (!ro) {
|
|
242
|
+
window.removeEventListener('resize', scheduleRecalculation);
|
|
243
|
+
}
|
|
244
|
+
cancelMeasurement(measurementKey);
|
|
151
245
|
fontSet?.removeEventListener?.('loadingdone', onFontsLoaded);
|
|
152
246
|
};
|
|
153
|
-
}, [recalculate]);
|
|
247
|
+
}, [readMeasurement, recalculate]);
|
|
154
248
|
|
|
155
249
|
return {
|
|
156
250
|
ref,
|