@workflow/web-shared 5.0.0-beta.26 → 5.0.0-beta.28

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 (94) hide show
  1. package/dist/components/new-trace-viewer/components/detail-panel-width.d.ts +34 -0
  2. package/dist/components/new-trace-viewer/components/detail-panel-width.d.ts.map +1 -0
  3. package/dist/components/new-trace-viewer/components/detail-panel-width.js +67 -0
  4. package/dist/components/new-trace-viewer/components/detail-panel.d.ts +14 -0
  5. package/dist/components/new-trace-viewer/components/detail-panel.d.ts.map +1 -0
  6. package/dist/components/new-trace-viewer/components/detail-panel.js +122 -0
  7. package/dist/components/new-trace-viewer/components/draggable-border.d.ts +31 -0
  8. package/dist/components/new-trace-viewer/components/draggable-border.d.ts.map +1 -0
  9. package/dist/components/new-trace-viewer/components/draggable-border.js +140 -0
  10. package/dist/components/new-trace-viewer/components/event-list.d.ts +1 -1
  11. package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -1
  12. package/dist/components/new-trace-viewer/components/event-list.js +3 -5
  13. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts +1 -1
  14. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts.map +1 -1
  15. package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js +2 -2
  16. package/dist/components/new-trace-viewer/components/minimap.d.ts +14 -0
  17. package/dist/components/new-trace-viewer/components/minimap.d.ts.map +1 -0
  18. package/dist/components/new-trace-viewer/components/minimap.js +337 -0
  19. package/dist/components/new-trace-viewer/components/pane-constants.d.ts +14 -0
  20. package/dist/components/new-trace-viewer/components/pane-constants.d.ts.map +1 -0
  21. package/dist/components/new-trace-viewer/components/pane-constants.js +16 -0
  22. package/dist/components/new-trace-viewer/components/split-pane.d.ts +1 -4
  23. package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -1
  24. package/dist/components/new-trace-viewer/components/split-pane.js +18 -16
  25. package/dist/components/new-trace-viewer/components/timeline.d.ts +2 -2
  26. package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
  27. package/dist/components/new-trace-viewer/components/timeline.js +5 -7
  28. package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.d.ts.map +1 -1
  29. package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.js +10 -3
  30. package/dist/components/new-trace-viewer/components/use-element-width.d.ts +8 -0
  31. package/dist/components/new-trace-viewer/components/use-element-width.d.ts.map +1 -0
  32. package/dist/components/new-trace-viewer/components/use-element-width.js +25 -0
  33. package/dist/components/new-trace-viewer/context.d.ts +2 -0
  34. package/dist/components/new-trace-viewer/context.d.ts.map +1 -1
  35. package/dist/components/new-trace-viewer/context.js +3 -2
  36. package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
  37. package/dist/components/new-trace-viewer/trace-viewer.js +53 -181
  38. package/dist/components/new-trace-viewer/utils.d.ts +21 -0
  39. package/dist/components/new-trace-viewer/utils.d.ts.map +1 -1
  40. package/dist/components/new-trace-viewer/utils.js +44 -3
  41. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  42. package/dist/components/sidebar/attribute-panel.js +38 -35
  43. package/dist/components/sidebar/attributes-block.d.ts +18 -7
  44. package/dist/components/sidebar/attributes-block.d.ts.map +1 -1
  45. package/dist/components/sidebar/attributes-block.js +76 -18
  46. package/dist/components/sidebar/events-list.d.ts +1 -1
  47. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  48. package/dist/components/sidebar/events-list.js +9 -10
  49. package/dist/components/trace-viewer/components/map.d.ts +2 -2
  50. package/dist/components/trace-viewer/components/map.d.ts.map +1 -1
  51. package/dist/components/trace-viewer/components/node.d.ts +7 -7
  52. package/dist/components/trace-viewer/components/node.d.ts.map +1 -1
  53. package/dist/components/ui/collapsible.d.ts +46 -0
  54. package/dist/components/ui/collapsible.d.ts.map +1 -0
  55. package/dist/components/ui/collapsible.js +66 -0
  56. package/dist/components/ui/data-inspector.js +2 -7
  57. package/dist/index.d.ts +2 -2
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +3 -3
  60. package/dist/lib/hydration.d.ts +12 -2
  61. package/dist/lib/hydration.d.ts.map +1 -1
  62. package/dist/lib/hydration.js +41 -18
  63. package/dist/lib/utils.d.ts +8 -5
  64. package/dist/lib/utils.d.ts.map +1 -1
  65. package/dist/lib/utils.js +13 -16
  66. package/package.json +6 -6
  67. package/src/components/new-trace-viewer/components/detail-panel-width.test.ts +123 -0
  68. package/src/components/new-trace-viewer/components/detail-panel-width.ts +76 -0
  69. package/src/components/new-trace-viewer/components/detail-panel.tsx +272 -0
  70. package/src/components/new-trace-viewer/components/draggable-border.tsx +216 -0
  71. package/src/components/new-trace-viewer/components/event-list.tsx +3 -5
  72. package/src/components/new-trace-viewer/components/middle-truncate/truncate.ts +1 -1
  73. package/src/components/new-trace-viewer/components/minimap.tsx +559 -0
  74. package/src/components/new-trace-viewer/components/pane-constants.ts +19 -0
  75. package/src/components/new-trace-viewer/components/split-pane.tsx +44 -55
  76. package/src/components/new-trace-viewer/components/timeline.tsx +3 -5
  77. package/src/components/new-trace-viewer/components/trace-viewer-skeleton.tsx +21 -1
  78. package/src/components/new-trace-viewer/components/use-element-width.ts +29 -0
  79. package/src/components/new-trace-viewer/context.tsx +4 -1
  80. package/src/components/new-trace-viewer/trace-viewer.tsx +93 -312
  81. package/src/components/new-trace-viewer/utils.test.ts +65 -0
  82. package/src/components/new-trace-viewer/utils.ts +76 -2
  83. package/src/components/sidebar/attribute-panel.tsx +106 -105
  84. package/src/components/sidebar/attributes-block.tsx +170 -50
  85. package/src/components/sidebar/events-list.tsx +107 -92
  86. package/src/components/ui/collapsible.tsx +219 -0
  87. package/src/index.ts +8 -7
  88. package/src/lib/hydration.ts +48 -25
  89. package/src/lib/utils.test.ts +33 -0
  90. package/src/lib/utils.ts +12 -16
  91. package/dist/components/sidebar/detail-card.d.ts +0 -13
  92. package/dist/components/sidebar/detail-card.d.ts.map +0 -1
  93. package/dist/components/sidebar/detail-card.js +0 -36
  94. package/src/components/sidebar/detail-card.tsx +0 -143
@@ -1,4 +1,8 @@
1
- import { formatDuration, getHighResInMs } from '../trace-viewer/util/timing';
1
+ import {
2
+ formatDuration,
3
+ formatDurationPrecise,
4
+ getHighResInMs,
5
+ } from '../trace-viewer/util/timing';
2
6
  import type { Span, SpanEvent } from './types';
3
7
 
4
8
  // ---------------------------------------------------------------------------
@@ -37,6 +41,64 @@ export function getSpanDurationMs(span: Span): number {
37
41
  );
38
42
  }
39
43
 
44
+ export function isSpanErrored(span: Span): boolean {
45
+ const workflowStatus = (span.attributes.data as Record<string, unknown>)
46
+ ?.status as string | undefined;
47
+ return span.status.code === 2 || workflowStatus === 'failed';
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Viewport
52
+ // ---------------------------------------------------------------------------
53
+
54
+ export interface ViewportRange {
55
+ start: number;
56
+ end: number;
57
+ }
58
+
59
+ /**
60
+ * Clamp a candidate viewport to the root extent. The requested duration is
61
+ * preserved where possible (clamped to [minDurationMs, root duration]), then
62
+ * the window is shifted back inside the root bounds.
63
+ */
64
+ export function clampViewportToRoot(
65
+ next: ViewportRange,
66
+ rootStart: number,
67
+ rootEnd: number,
68
+ minDurationMs: number
69
+ ): ViewportRange {
70
+ const rootDuration = Math.max(rootEnd - rootStart, minDurationMs);
71
+ const duration = Math.min(
72
+ rootDuration,
73
+ Math.max(minDurationMs, next.end - next.start)
74
+ );
75
+ const maxStart = rootEnd - duration;
76
+ const start = Math.min(Math.max(next.start, rootStart), maxStart);
77
+ return { start, end: start + duration };
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Wheel gestures — shared between the timeline and the minimap
82
+ // ---------------------------------------------------------------------------
83
+
84
+ /** Convert a wheel delta to pixel units (line-mode deltas arrive in lines). */
85
+ export function wheelDeltaToPixels(delta: number, deltaMode: number): number {
86
+ return deltaMode === 1 ? delta * 16 : delta;
87
+ }
88
+
89
+ /**
90
+ * Exponential zoom factor for a wheel gesture. Coarse mouse-wheel steps are
91
+ * damped harder than trackpad pinches so both feel similar.
92
+ */
93
+ export function wheelZoomScaleFactor(event: {
94
+ deltaY: number;
95
+ deltaMode: number;
96
+ }): number {
97
+ const dy = wheelDeltaToPixels(event.deltaY, event.deltaMode);
98
+ const isMouseWheel = event.deltaMode === 1 || Math.abs(event.deltaY) >= 50;
99
+ return 2 ** (dy / (isMouseWheel ? 200 : 60));
100
+ }
101
+
40
102
  // ---------------------------------------------------------------------------
41
103
  // Time markers
42
104
  // ---------------------------------------------------------------------------
@@ -56,6 +118,8 @@ const NICE_INTERVALS = [
56
118
 
57
119
  const MAX_MARKERS = 8;
58
120
 
121
+ const MS_IN_SECOND = 1000;
122
+
59
123
  function pickInterval(viewDuration: number, maxTicks: number): number {
60
124
  for (const interval of NICE_INTERVALS) {
61
125
  if (viewDuration / interval <= maxTicks) return interval;
@@ -72,6 +136,13 @@ export function computeTimeMarkers(
72
136
  const maxTicks = 6;
73
137
  const interval = pickInterval(viewDuration, maxTicks);
74
138
 
139
+ // Sub-second steps need fractional labels, or ticks past 1s collide as
140
+ // duplicate whole seconds ("…1s, 2s, 2s, 3s"). Scale decimals to the step.
141
+ const fractionDigits =
142
+ interval >= MS_IN_SECOND
143
+ ? 0
144
+ : Math.ceil(-Math.log10(interval / MS_IN_SECOND));
145
+
75
146
  const firstTick = Math.ceil(offset / interval) * interval;
76
147
  const markers: TimeMarker[] = [];
77
148
 
@@ -80,7 +151,10 @@ export function computeTimeMarkers(
80
151
  if (position < -0.01 || position > 1.01) continue;
81
152
  markers.push({
82
153
  position: Math.min(Math.max(position, 0), 1),
83
- label: formatDuration(Math.abs(t), true),
154
+ label:
155
+ fractionDigits === 0
156
+ ? formatDuration(Math.abs(t), true)
157
+ : formatDurationPrecise(Math.abs(t), fractionDigits),
84
158
  value: t,
85
159
  });
86
160
  if (markers.length >= MAX_MARKERS) break;
@@ -8,8 +8,12 @@ import type { KeyboardEvent, ReactNode } from 'react';
8
8
  import { useCallback, useContext, useMemo, useState } from 'react';
9
9
  import { isEncryptedMarker, isExpiredMarker } from '../../lib/hydration';
10
10
  import { extractConversation, isDoStreamStep } from '../../lib/utils';
11
- import { CopyButton } from '../new-trace-viewer/components/copy-button';
12
- import { MiddleTruncate } from '../new-trace-viewer/components/middle-truncate/middle-truncate';
11
+ import {
12
+ Collapsible,
13
+ CollapsibleContent,
14
+ CollapsibleRoot,
15
+ CollapsibleTrigger,
16
+ } from '../ui/collapsible';
13
17
  import { ContextCardProvider } from '../ui/context-card';
14
18
  import {
15
19
  DecryptClickContext,
@@ -20,10 +24,13 @@ import { ErrorCard } from '../ui/error-card';
20
24
  import { ErrorStackBlock, isStructuredError } from '../ui/error-stack-block';
21
25
  import { Skeleton } from '../ui/skeleton';
22
26
  import { TimestampTooltip } from '../ui/timestamp-tooltip';
23
- import { RunAttributesCard } from './attributes-block';
27
+ import {
28
+ DetailMonoKeyValueRow,
29
+ RunAttributesCard,
30
+ RunMetadataCard,
31
+ } from './attributes-block';
24
32
  import { ConversationView } from './conversation-view';
25
33
  import { CopyableDataBlock, EncryptedDataBlock } from './copyable-data-block';
26
- import { DetailCard } from './detail-card';
27
34
 
28
35
  /**
29
36
  * Tab button for conversation/JSON toggle
@@ -150,7 +157,7 @@ function ConversationWithTabs({
150
157
  );
151
158
 
152
159
  return (
153
- <DetailCard summary="Input">
160
+ <Collapsible label="Input">
154
161
  <TabbedContainer
155
162
  tabs={conversationTabs}
156
163
  activeTab={activeTab}
@@ -171,7 +178,7 @@ function ConversationWithTabs({
171
178
  </div>
172
179
  )}
173
180
  </TabbedContainer>
174
- </DetailCard>
181
+ </Collapsible>
175
182
  );
176
183
  }
177
184
 
@@ -413,7 +420,7 @@ const attributeToDisplayFn: Record<
413
420
  environment: (_value: unknown) => null,
414
421
  executionContext: (_value: unknown) => null,
415
422
  // Attributes — string-string metadata attached to the run.
416
- // Rendered as key-value rows in its own collapsible DetailCard;
423
+ // Rendered as key-value rows in its own collapsible section;
417
424
  // if empty/missing, hidden by the hasDisplayContent gate.
418
425
  attributes: (value: unknown) => {
419
426
  if (!hasDisplayContent(value)) return null;
@@ -431,16 +438,28 @@ const attributeToDisplayFn: Record<
431
438
  // Resolved attributes, won't actually use this function
432
439
  metadata: (value: unknown) => {
433
440
  if (!hasDisplayContent(value)) return null;
434
- if (isEncryptedMarker(value)) return <EncryptedDataBlock />;
435
- if (isExpiredMarker(value)) return <ExpiredFieldBlock />;
436
- return JsonBlock(value);
441
+ if (isEncryptedMarker(value)) {
442
+ return (
443
+ <Collapsible label="Metadata">
444
+ <EncryptedDataBlock />
445
+ </Collapsible>
446
+ );
447
+ }
448
+ if (isExpiredMarker(value)) {
449
+ return (
450
+ <Collapsible label="Metadata">
451
+ <ExpiredFieldBlock />
452
+ </Collapsible>
453
+ );
454
+ }
455
+ return <RunMetadataCard metadata={value} />;
437
456
  },
438
457
  input: (value: unknown, context?: DisplayContext) => {
439
458
  if (isEncryptedMarker(value)) {
440
459
  return (
441
- <DetailCard summary="Input">
460
+ <Collapsible label="Input">
442
461
  <EncryptedFieldBlock />
443
- </DetailCard>
462
+ </Collapsible>
444
463
  );
445
464
  }
446
465
  if (isExpiredMarker(value)) return <ExpiredFieldBlock />;
@@ -463,14 +482,14 @@ const attributeToDisplayFn: Record<
463
482
  <>
464
483
  <ConversationWithTabs conversation={conversation} args={args} />
465
484
  {hasClosureVars && (
466
- <DetailCard summary="Closure Variables">
485
+ <Collapsible label="Closure Variables">
467
486
  {JsonBlock(closureVars)}
468
- </DetailCard>
487
+ </Collapsible>
469
488
  )}
470
489
  {hasThisVal && (
471
- <DetailCard summary="This Value">
490
+ <Collapsible label="This Value">
472
491
  {JsonBlock(thisVal)}
473
- </DetailCard>
492
+ </Collapsible>
474
493
  )}
475
494
  </>
476
495
  );
@@ -479,12 +498,12 @@ const attributeToDisplayFn: Record<
479
498
 
480
499
  // Don't render an empty "Input (0 arguments)" card when no input exists.
481
500
  if (!hasArgs && !hasClosureVars && !hasThisVal) {
482
- return <DetailCard summary="Input (no data)" disabled />;
501
+ return <Collapsible label="Input (no data)" disabled />;
483
502
  }
484
503
 
485
504
  return (
486
505
  <>
487
- <DetailCard summary="Input">
506
+ <Collapsible label="Input">
488
507
  {Array.isArray(args)
489
508
  ? args.map((v, i) => (
490
509
  <div className="mt-2 first:mt-0" key={i}>
@@ -492,14 +511,14 @@ const attributeToDisplayFn: Record<
492
511
  </div>
493
512
  ))
494
513
  : JsonBlock(args)}
495
- </DetailCard>
514
+ </Collapsible>
496
515
  {hasClosureVars && (
497
- <DetailCard summary="Closure Variables">
516
+ <Collapsible label="Closure Variables">
498
517
  {JsonBlock(closureVars)}
499
- </DetailCard>
518
+ </Collapsible>
500
519
  )}
501
520
  {hasThisVal && (
502
- <DetailCard summary="Context">{JsonBlock(thisVal)}</DetailCard>
521
+ <Collapsible label="Context">{JsonBlock(thisVal)}</Collapsible>
503
522
  )}
504
523
  </>
505
524
  );
@@ -507,10 +526,10 @@ const attributeToDisplayFn: Record<
507
526
 
508
527
  // Fallback: treat as plain array or object
509
528
  if (!hasDisplayContent(value)) {
510
- return <DetailCard summary="Input (no data)" disabled />;
529
+ return <Collapsible label="Input (no data)" disabled />;
511
530
  }
512
531
  return (
513
- <DetailCard summary="Input">
532
+ <Collapsible label="Input">
514
533
  {Array.isArray(value)
515
534
  ? value.map((v, i) => (
516
535
  <div className="mt-2 first:mt-0" key={i}>
@@ -518,27 +537,27 @@ const attributeToDisplayFn: Record<
518
537
  </div>
519
538
  ))
520
539
  : JsonBlock(value)}
521
- </DetailCard>
540
+ </Collapsible>
522
541
  );
523
542
  },
524
543
  output: (value: unknown) => {
525
544
  if (isEncryptedMarker(value)) {
526
545
  return (
527
- <DetailCard summary="Output">
546
+ <Collapsible label="Output">
528
547
  <EncryptedFieldBlock />
529
- </DetailCard>
548
+ </Collapsible>
530
549
  );
531
550
  }
532
551
  if (!hasDisplayContent(value)) return null;
533
552
  if (isExpiredMarker(value)) return <ExpiredFieldBlock />;
534
- return <DetailCard summary="Output">{JsonBlock(value)}</DetailCard>;
553
+ return <Collapsible label="Output">{JsonBlock(value)}</Collapsible>;
535
554
  },
536
555
  error: (value: unknown) => {
537
556
  if (isEncryptedMarker(value)) {
538
557
  return (
539
- <DetailCard summary="Error" defaultOpen>
558
+ <Collapsible label="Error" defaultOpen>
540
559
  <EncryptedFieldBlock />
541
- </DetailCard>
560
+ </Collapsible>
542
561
  );
543
562
  }
544
563
  if (isExpiredMarker(value)) return <ExpiredFieldBlock />;
@@ -548,32 +567,32 @@ const attributeToDisplayFn: Record<
548
567
  // `{ message, stack }`. Render both with the dedicated error block.
549
568
  if (isStructuredError(value)) {
550
569
  return (
551
- <DetailCard summary="Error" defaultOpen>
570
+ <Collapsible label="Error" defaultOpen>
552
571
  <ErrorStackBlock value={value} />
553
- </DetailCard>
572
+ </Collapsible>
554
573
  );
555
574
  }
556
575
 
557
576
  return (
558
- <DetailCard summary="Error" defaultOpen>
577
+ <Collapsible label="Error" defaultOpen>
559
578
  {JsonBlock(value)}
560
- </DetailCard>
579
+ </Collapsible>
561
580
  );
562
581
  },
563
582
  eventData: (value: unknown) => {
564
583
  if (isEncryptedMarker(value)) {
565
584
  return (
566
- <DetailCard summary="Event Data" defaultOpen>
585
+ <Collapsible label="Event Data" defaultOpen>
567
586
  <EncryptedFieldBlock />
568
- </DetailCard>
587
+ </Collapsible>
569
588
  );
570
589
  }
571
590
  if (isExpiredMarker(value)) return <ExpiredFieldBlock />;
572
591
  if (!hasDisplayContent(value)) return null;
573
592
  return (
574
- <DetailCard summary="Event Data" defaultOpen>
593
+ <Collapsible label="Event Data" defaultOpen>
575
594
  {JsonBlock(value)}
576
- </DetailCard>
595
+ </Collapsible>
577
596
  );
578
597
  },
579
598
  errorCode: (value: unknown) => {
@@ -591,12 +610,13 @@ const resolvableAttributes = [
591
610
  'eventData',
592
611
  ];
593
612
 
594
- // Attributes whose displayFn renders its own section header via DetailCard,
613
+ // Attributes whose displayFn renders its own section header via Collapsible,
595
614
  // so the outer AttributeBlock should not duplicate the label.
596
615
  const selfHeaderedAttributes = new Set([
597
616
  'input',
598
617
  'output',
599
618
  'error',
619
+ 'metadata',
600
620
  'attributes',
601
621
  'eventData',
602
622
  ]);
@@ -649,12 +669,12 @@ export const AttributeBlock = ({
649
669
  : 'Input';
650
670
  if (decryptCtx?.hasEncryptedData) {
651
671
  return (
652
- <DetailCard summary={label} defaultOpen={attribute === 'eventData'}>
672
+ <Collapsible label={label} defaultOpen={attribute === 'eventData'}>
653
673
  <EncryptedFieldBlock />
654
- </DetailCard>
674
+ </Collapsible>
655
675
  );
656
676
  }
657
- return <DetailCard summary={label} />;
677
+ return <Collapsible label={label} />;
658
678
  }
659
679
 
660
680
  const displayFn =
@@ -835,61 +855,44 @@ export const AttributePanel = ({
835
855
  <StreamClickContext.Provider value={onStreamClick}>
836
856
  <DecryptClickContext.Provider value={decryptValue}>
837
857
  {visibleBasicAttributes.length > 0 && (
838
- <div className="flex flex-col overflow-hidden divide-y divide-gray-alpha-400 mb-3">
839
- {orderedBasicAttributes.map((attribute) => {
840
- const displayValue = attributeToDisplayFn[
841
- attribute as keyof typeof attributeToDisplayFn
842
- ]?.(displayData[attribute as keyof typeof displayData]);
843
- const isCopyableBasicAttribute =
844
- copyableBasicAttributes.has(attribute as AttributeKey) &&
845
- typeof displayValue === 'string';
846
-
847
- return (
848
- <div
849
- className="flex items-center justify-between py-2"
850
- key={attribute}
851
- >
852
- <span className="text-label-14 text-gray-900">
853
- {getAttributeDisplayName(attribute)}
854
- </span>
855
- {isCopyableBasicAttribute ? (
856
- <div
857
- className="flex min-w-0 max-w-[70%] items-center justify-end gap-1 text-[13px] font-mono text-gray-1000"
858
- title={displayValue}
859
- >
860
- <MiddleTruncate
861
- value={displayValue}
862
- className="text-right"
863
- style={{ gridTemplateColumns: 'minmax(0, 1fr)' }}
864
- />
865
- <CopyButton
866
- copyText={displayValue}
867
- ariaLabel={`Copy ${getAttributeDisplayName(attribute)}`}
868
- className="shrink-0 -mr-1"
869
- />
858
+ <CollapsibleRoot defaultOpen>
859
+ <CollapsibleTrigger>Metadata</CollapsibleTrigger>
860
+ <CollapsibleContent className="mt-0 mb-2">
861
+ <div className="flex flex-col">
862
+ {orderedBasicAttributes.map((attribute) => {
863
+ const displayValue = attributeToDisplayFn[
864
+ attribute as keyof typeof attributeToDisplayFn
865
+ ]?.(displayData[attribute as keyof typeof displayData]);
866
+ const isCopyableBasicAttribute =
867
+ copyableBasicAttributes.has(
868
+ attribute as AttributeKey
869
+ ) && typeof displayValue === 'string';
870
+ const label = getAttributeDisplayName(attribute);
871
+
872
+ return (
873
+ <DetailMonoKeyValueRow
874
+ key={attribute}
875
+ label={label}
876
+ value={displayValue}
877
+ copyText={
878
+ isCopyableBasicAttribute ? displayValue : undefined
879
+ }
880
+ />
881
+ );
882
+ })}
883
+ {isLoading &&
884
+ resource === 'sleep' &&
885
+ !displayData.resumeAt && (
886
+ <div className="flex items-center justify-between gap-3 py-0.5">
887
+ <span className="text-label-13 text-gray-900">
888
+ Resume
889
+ </span>
890
+ <Skeleton className="h-4 w-[55%]" />
870
891
  </div>
871
- ) : (
872
- <span className="text-right text-label-13 font-mono">
873
- {displayValue}
874
- </span>
875
892
  )}
876
- </div>
877
- );
878
- })}
879
- {isLoading && resource === 'sleep' && !displayData.resumeAt && (
880
- <div className="py-1">
881
- <div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
882
- <span
883
- className="text-[14px] first-letter:uppercase"
884
- style={{ color: 'var(--ds-gray-700)' }}
885
- >
886
- resumeAt
887
- </span>
888
- <Skeleton className="h-4 w-[55%]" />
889
- </div>
890
893
  </div>
891
- )}
892
- </div>
894
+ </CollapsibleContent>
895
+ </CollapsibleRoot>
893
896
  )}
894
897
  {error ? (
895
898
  <ErrorCard
@@ -900,17 +903,15 @@ export const AttributePanel = ({
900
903
  ) : hasExpired ? (
901
904
  <ExpiredDataMessage />
902
905
  ) : resolvedAttributes.length > 0 ? (
903
- <>
904
- {resolvedAttributes.map((attribute) => (
905
- <AttributeBlock
906
- isLoading={isLoading}
907
- key={attribute}
908
- attribute={attribute}
909
- value={displayData[attribute as keyof typeof displayData]}
910
- context={displayContext}
911
- />
912
- ))}
913
- </>
906
+ resolvedAttributes.map((attribute) => (
907
+ <AttributeBlock
908
+ isLoading={isLoading}
909
+ key={attribute}
910
+ attribute={attribute}
911
+ value={displayData[attribute as keyof typeof displayData]}
912
+ context={displayContext}
913
+ />
914
+ ))
914
915
  ) : null}
915
916
  </DecryptClickContext.Provider>
916
917
  </StreamClickContext.Provider>