inspect-ai 0.3.99__py3-none-any.whl → 0.3.100__py3-none-any.whl

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 (120) hide show
  1. inspect_ai/_display/core/config.py +11 -5
  2. inspect_ai/_display/core/panel.py +66 -2
  3. inspect_ai/_display/core/textual.py +5 -2
  4. inspect_ai/_display/plain/display.py +1 -0
  5. inspect_ai/_display/rich/display.py +2 -2
  6. inspect_ai/_display/textual/widgets/transcript.py +37 -9
  7. inspect_ai/_eval/score.py +2 -4
  8. inspect_ai/_eval/task/run.py +59 -81
  9. inspect_ai/_util/content.py +11 -6
  10. inspect_ai/_util/interrupt.py +2 -2
  11. inspect_ai/_util/text.py +7 -0
  12. inspect_ai/_util/working.py +8 -37
  13. inspect_ai/_view/__init__.py +0 -0
  14. inspect_ai/_view/schema.py +2 -1
  15. inspect_ai/_view/www/CLAUDE.md +15 -0
  16. inspect_ai/_view/www/dist/assets/index.css +263 -159
  17. inspect_ai/_view/www/dist/assets/index.js +22153 -19093
  18. inspect_ai/_view/www/log-schema.json +77 -3
  19. inspect_ai/_view/www/package.json +5 -1
  20. inspect_ai/_view/www/src/@types/log.d.ts +9 -0
  21. inspect_ai/_view/www/src/app/App.tsx +1 -15
  22. inspect_ai/_view/www/src/app/appearance/icons.ts +4 -1
  23. inspect_ai/_view/www/src/app/content/MetaDataGrid.tsx +24 -6
  24. inspect_ai/_view/www/src/app/content/MetadataGrid.module.css +0 -5
  25. inspect_ai/_view/www/src/app/content/RenderedContent.tsx +220 -205
  26. inspect_ai/_view/www/src/app/log-view/LogViewContainer.tsx +2 -1
  27. inspect_ai/_view/www/src/app/log-view/tabs/SamplesTab.tsx +5 -0
  28. inspect_ai/_view/www/src/app/routing/url.ts +84 -4
  29. inspect_ai/_view/www/src/app/samples/InlineSampleDisplay.module.css +0 -5
  30. inspect_ai/_view/www/src/app/samples/SampleDialog.module.css +1 -1
  31. inspect_ai/_view/www/src/app/samples/SampleDisplay.module.css +7 -0
  32. inspect_ai/_view/www/src/app/samples/SampleDisplay.tsx +24 -17
  33. inspect_ai/_view/www/src/app/samples/SampleSummaryView.module.css +1 -2
  34. inspect_ai/_view/www/src/app/samples/chat/ChatMessage.tsx +8 -6
  35. inspect_ai/_view/www/src/app/samples/chat/ChatMessageRow.tsx +0 -4
  36. inspect_ai/_view/www/src/app/samples/chat/ChatViewVirtualList.tsx +3 -2
  37. inspect_ai/_view/www/src/app/samples/chat/MessageContent.tsx +2 -0
  38. inspect_ai/_view/www/src/app/samples/chat/MessageContents.tsx +2 -0
  39. inspect_ai/_view/www/src/app/samples/chat/messages.ts +1 -0
  40. inspect_ai/_view/www/src/app/samples/chat/tools/ToolCallView.tsx +1 -0
  41. inspect_ai/_view/www/src/app/samples/list/SampleRow.tsx +1 -1
  42. inspect_ai/_view/www/src/app/samples/transcript/ErrorEventView.tsx +1 -2
  43. inspect_ai/_view/www/src/app/samples/transcript/InfoEventView.tsx +1 -1
  44. inspect_ai/_view/www/src/app/samples/transcript/InputEventView.tsx +1 -2
  45. inspect_ai/_view/www/src/app/samples/transcript/ModelEventView.module.css +1 -1
  46. inspect_ai/_view/www/src/app/samples/transcript/ModelEventView.tsx +1 -1
  47. inspect_ai/_view/www/src/app/samples/transcript/SampleInitEventView.tsx +1 -1
  48. inspect_ai/_view/www/src/app/samples/transcript/SampleLimitEventView.tsx +3 -2
  49. inspect_ai/_view/www/src/app/samples/transcript/SandboxEventView.tsx +4 -5
  50. inspect_ai/_view/www/src/app/samples/transcript/ScoreEventView.tsx +1 -1
  51. inspect_ai/_view/www/src/app/samples/transcript/SpanEventView.tsx +1 -2
  52. inspect_ai/_view/www/src/app/samples/transcript/StepEventView.tsx +1 -3
  53. inspect_ai/_view/www/src/app/samples/transcript/SubtaskEventView.tsx +1 -2
  54. inspect_ai/_view/www/src/app/samples/transcript/ToolEventView.tsx +3 -4
  55. inspect_ai/_view/www/src/app/samples/transcript/TranscriptPanel.module.css +42 -0
  56. inspect_ai/_view/www/src/app/samples/transcript/TranscriptPanel.tsx +77 -0
  57. inspect_ai/_view/www/src/app/samples/transcript/TranscriptVirtualList.tsx +27 -71
  58. inspect_ai/_view/www/src/app/samples/transcript/TranscriptVirtualListComponent.module.css +13 -3
  59. inspect_ai/_view/www/src/app/samples/transcript/TranscriptVirtualListComponent.tsx +27 -2
  60. inspect_ai/_view/www/src/app/samples/transcript/event/EventPanel.module.css +1 -0
  61. inspect_ai/_view/www/src/app/samples/transcript/event/EventPanel.tsx +21 -22
  62. inspect_ai/_view/www/src/app/samples/transcript/outline/OutlineRow.module.css +45 -0
  63. inspect_ai/_view/www/src/app/samples/transcript/outline/OutlineRow.tsx +223 -0
  64. inspect_ai/_view/www/src/app/samples/transcript/outline/TranscriptOutline.module.css +10 -0
  65. inspect_ai/_view/www/src/app/samples/transcript/outline/TranscriptOutline.tsx +258 -0
  66. inspect_ai/_view/www/src/app/samples/transcript/outline/tree-visitors.ts +187 -0
  67. inspect_ai/_view/www/src/app/samples/transcript/state/StateEventRenderers.tsx +8 -1
  68. inspect_ai/_view/www/src/app/samples/transcript/state/StateEventView.tsx +3 -4
  69. inspect_ai/_view/www/src/app/samples/transcript/transform/hooks.ts +78 -0
  70. inspect_ai/_view/www/src/app/samples/transcript/transform/treeify.ts +340 -135
  71. inspect_ai/_view/www/src/app/samples/transcript/transform/utils.ts +3 -0
  72. inspect_ai/_view/www/src/app/samples/transcript/types.ts +2 -0
  73. inspect_ai/_view/www/src/app/types.ts +5 -1
  74. inspect_ai/_view/www/src/client/api/api-browser.ts +2 -2
  75. inspect_ai/_view/www/src/components/LiveVirtualList.tsx +6 -1
  76. inspect_ai/_view/www/src/components/MarkdownDiv.tsx +1 -1
  77. inspect_ai/_view/www/src/components/PopOver.tsx +422 -0
  78. inspect_ai/_view/www/src/components/PulsingDots.module.css +9 -9
  79. inspect_ai/_view/www/src/components/PulsingDots.tsx +4 -1
  80. inspect_ai/_view/www/src/components/StickyScroll.tsx +183 -0
  81. inspect_ai/_view/www/src/components/TabSet.tsx +4 -0
  82. inspect_ai/_view/www/src/state/hooks.ts +52 -2
  83. inspect_ai/_view/www/src/state/logSlice.ts +4 -3
  84. inspect_ai/_view/www/src/state/samplePolling.ts +8 -0
  85. inspect_ai/_view/www/src/state/sampleSlice.ts +53 -9
  86. inspect_ai/_view/www/src/state/scrolling.ts +152 -0
  87. inspect_ai/_view/www/src/utils/attachments.ts +7 -0
  88. inspect_ai/_view/www/src/utils/python.ts +18 -0
  89. inspect_ai/_view/www/yarn.lock +269 -6
  90. inspect_ai/agent/_react.py +12 -7
  91. inspect_ai/agent/_run.py +2 -3
  92. inspect_ai/analysis/beta/_dataframe/samples/table.py +19 -18
  93. inspect_ai/log/_log.py +1 -1
  94. inspect_ai/log/_recorders/file.py +2 -9
  95. inspect_ai/log/_transcript.py +1 -1
  96. inspect_ai/model/_call_tools.py +6 -2
  97. inspect_ai/model/_openai.py +1 -1
  98. inspect_ai/model/_openai_responses.py +78 -39
  99. inspect_ai/model/_openai_web_search.py +31 -0
  100. inspect_ai/model/_providers/azureai.py +72 -3
  101. inspect_ai/model/_providers/openai.py +2 -1
  102. inspect_ai/scorer/_metric.py +1 -2
  103. inspect_ai/solver/_task_state.py +2 -2
  104. inspect_ai/tool/_tool.py +6 -2
  105. inspect_ai/tool/_tool_def.py +27 -4
  106. inspect_ai/tool/_tool_info.py +2 -0
  107. inspect_ai/tool/_tools/_web_search/_google.py +15 -4
  108. inspect_ai/tool/_tools/_web_search/_tavily.py +35 -12
  109. inspect_ai/tool/_tools/_web_search/_web_search.py +214 -45
  110. inspect_ai/util/__init__.py +4 -0
  111. inspect_ai/util/_json.py +3 -0
  112. inspect_ai/util/_limit.py +230 -20
  113. inspect_ai/util/_sandbox/docker/compose.py +20 -11
  114. inspect_ai/util/_span.py +1 -1
  115. {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.100.dist-info}/METADATA +3 -3
  116. {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.100.dist-info}/RECORD +120 -106
  117. {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.100.dist-info}/WHEEL +1 -1
  118. {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.100.dist-info}/entry_points.txt +0 -0
  119. {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.100.dist-info}/licenses/LICENSE +0 -0
  120. {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.100.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,422 @@
1
+ import { Placement } from "@popperjs/core";
2
+ import clsx from "clsx";
3
+ import React, {
4
+ CSSProperties,
5
+ ReactNode,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ } from "react";
10
+ import { createPortal } from "react-dom";
11
+ import { usePopper } from "react-popper";
12
+
13
+ interface PopOverProps {
14
+ id: string;
15
+ isOpen: boolean;
16
+ positionEl: HTMLElement | null;
17
+ placement?: Placement;
18
+ showArrow?: boolean;
19
+ offset?: [number, number];
20
+ usePortal?: boolean;
21
+ hoverDelay?: number;
22
+
23
+ className?: string | string[];
24
+ arrowClassName?: string | string[];
25
+
26
+ children: ReactNode;
27
+ }
28
+
29
+ /**
30
+ * A controlled Popper component for displaying content relative to a reference element
31
+ */
32
+ export const PopOver: React.FC<PopOverProps> = ({
33
+ id,
34
+ isOpen,
35
+ positionEl,
36
+ children,
37
+ placement = "bottom",
38
+ showArrow = true,
39
+ offset = [0, 8],
40
+ className = "",
41
+ arrowClassName = "",
42
+ usePortal = true,
43
+ hoverDelay = 250,
44
+ }) => {
45
+ const popperRef = useRef<HTMLDivElement | null>(null);
46
+ const arrowRef = useRef<HTMLDivElement | null>(null);
47
+ const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(
48
+ null,
49
+ );
50
+
51
+ // For delayed hover functionality
52
+ const [shouldShowPopover, setShouldShowPopover] = useState(false);
53
+ const hoverTimerRef = useRef<number | null>(null);
54
+ const isMouseMovingRef = useRef(false);
55
+
56
+ // Setup hover timer and mouse movement detection
57
+ useEffect(() => {
58
+ if (!isOpen || hoverDelay <= 0) {
59
+ setShouldShowPopover(isOpen);
60
+ return;
61
+ }
62
+
63
+ const handleMouseMove = () => {
64
+ isMouseMovingRef.current = true;
65
+
66
+ // Clear any existing timer when mouse moves
67
+ if (hoverTimerRef.current !== null) {
68
+ window.clearTimeout(hoverTimerRef.current);
69
+ }
70
+
71
+ // Start a new timer to check if mouse has stopped moving
72
+ hoverTimerRef.current = window.setTimeout(() => {
73
+ if (isOpen) {
74
+ isMouseMovingRef.current = false;
75
+ setShouldShowPopover(true);
76
+ }
77
+ }, hoverDelay);
78
+ };
79
+
80
+ const handleMouseLeave = () => {
81
+ if (hoverTimerRef.current !== null) {
82
+ window.clearTimeout(hoverTimerRef.current);
83
+ }
84
+ isMouseMovingRef.current = false;
85
+ setShouldShowPopover(false);
86
+ };
87
+
88
+ const handleMouseDown = () => {
89
+ // Cancel popover on any mouse down
90
+ if (hoverTimerRef.current !== null) {
91
+ window.clearTimeout(hoverTimerRef.current);
92
+ }
93
+ setShouldShowPopover(false);
94
+ };
95
+
96
+ // Add event listeners to the positionEl (the trigger element)
97
+ if (positionEl && isOpen) {
98
+ positionEl.addEventListener("mousemove", handleMouseMove);
99
+ positionEl.addEventListener("mouseleave", handleMouseLeave);
100
+
101
+ // Add document-wide mousedown listener to dismiss on interaction
102
+ document.addEventListener("mousedown", handleMouseDown);
103
+ document.addEventListener("click", handleMouseDown);
104
+
105
+ // Initial mouse move to start the timer
106
+ handleMouseMove();
107
+ } else {
108
+ setShouldShowPopover(false);
109
+ }
110
+
111
+ return () => {
112
+ if (positionEl) {
113
+ positionEl.removeEventListener("mousemove", handleMouseMove);
114
+ positionEl.removeEventListener("mouseleave", handleMouseLeave);
115
+ }
116
+
117
+ // Clean up the document mousedown listener
118
+ document.removeEventListener("mousedown", handleMouseDown);
119
+
120
+ document.removeEventListener("click", handleMouseDown);
121
+
122
+ if (hoverTimerRef.current !== null) {
123
+ window.clearTimeout(hoverTimerRef.current);
124
+ }
125
+ };
126
+ }, [isOpen, positionEl, hoverDelay]);
127
+
128
+ // Effect to create portal container when needed
129
+ useEffect(() => {
130
+ // Only create portal when the popover is open
131
+ if (usePortal && isOpen && shouldShowPopover) {
132
+ let container = document.getElementById(id);
133
+
134
+ if (!container) {
135
+ container = document.createElement("div");
136
+ container.id = id;
137
+ container.style.position = "absolute";
138
+ container.style.top = "0";
139
+ container.style.left = "0";
140
+ container.style.zIndex = "9999";
141
+ container.style.width = "0";
142
+ container.style.height = "0";
143
+ container.style.overflow = "visible";
144
+
145
+ document.body.appendChild(container);
146
+ }
147
+
148
+ setPortalContainer(container);
149
+
150
+ return () => {
151
+ // Clean up only when unmounting or when the popover closes
152
+ if (document.body.contains(container)) {
153
+ document.body.removeChild(container);
154
+ setPortalContainer(null);
155
+ }
156
+ };
157
+ }
158
+
159
+ return undefined;
160
+ }, [usePortal, isOpen, shouldShowPopover, id]);
161
+
162
+ // Configure modifiers for popper
163
+ const modifiers = [
164
+ { name: "offset", options: { offset } },
165
+ { name: "preventOverflow", options: { padding: 8 } },
166
+ {
167
+ name: "arrow",
168
+ enabled: showArrow,
169
+ options: {
170
+ element: arrowRef.current,
171
+ padding: 5, // This keeps the arrow from getting too close to the corner
172
+ },
173
+ },
174
+ {
175
+ name: "computeStyles",
176
+ options: {
177
+ gpuAcceleration: false,
178
+ adaptive: true,
179
+ },
180
+ },
181
+ // Ensure popper is positioned correctly with respect to its reference element
182
+ {
183
+ name: "flip",
184
+ options: {
185
+ fallbackPlacements: ["top", "right", "bottom", "left"],
186
+ },
187
+ },
188
+ ];
189
+
190
+ // Use popper hook with modifiers
191
+ const { styles, attributes, state, update } = usePopper(
192
+ positionEl,
193
+ popperRef.current,
194
+ {
195
+ placement,
196
+ strategy: "fixed",
197
+ modifiers,
198
+ },
199
+ );
200
+
201
+ // Force update when needed refs change
202
+ useEffect(() => {
203
+ if (update && isOpen && shouldShowPopover) {
204
+ // Need to delay the update slightly to ensure refs are properly set
205
+ const timer = setTimeout(() => {
206
+ update();
207
+ }, 10);
208
+ return () => clearTimeout(timer);
209
+ }
210
+ }, [update, isOpen, shouldShowPopover, showArrow, arrowRef.current]);
211
+
212
+ // Define arrow data-* attribute based on placement
213
+ const getArrowDataPlacement = () => {
214
+ if (!state || !state.placement) return placement;
215
+ return state.placement;
216
+ };
217
+
218
+ // Get the actual placement from Popper state
219
+ const actualPlacement = state?.placement || placement;
220
+
221
+ // For a CSS triangle, we use the border trick
222
+ // A CSS triangle doesn't need separate border styling like a rotated square would
223
+
224
+ // Popper container styles
225
+ const defaultPopperStyles: CSSProperties = {
226
+ backgroundColor: "var(--bs-body-bg)",
227
+ padding: "12px",
228
+ borderRadius: "4px",
229
+ boxShadow: "0 2px 10px rgba(0,0,0,0.1)",
230
+ border: "1px solid #eee",
231
+ zIndex: 1200,
232
+ position: "relative",
233
+ // Apply opacity transition to smooth the appearance
234
+ opacity: state?.placement ? 1 : 0,
235
+ transition: "opacity 0.1s",
236
+ };
237
+
238
+ // Early return if not open or should not show due to hover delay
239
+ if (!isOpen || (hoverDelay > 0 && !shouldShowPopover)) {
240
+ return null;
241
+ }
242
+
243
+ // For position-aware rendering
244
+ const positionedStyle =
245
+ state && state.styles && state.styles.popper
246
+ ? {
247
+ ...styles.popper,
248
+ opacity: 1,
249
+ }
250
+ : {
251
+ ...styles.popper,
252
+ opacity: 0,
253
+ // Position offscreen initially to prevent flicker
254
+ position: "fixed" as const,
255
+ top: "-9999px",
256
+ left: "-9999px",
257
+ };
258
+
259
+ // Create the popper content with position-aware styles
260
+ const popperContent = (
261
+ <div
262
+ ref={popperRef}
263
+ style={{ ...defaultPopperStyles, ...positionedStyle }}
264
+ className={clsx(className)}
265
+ {...attributes.popper}
266
+ >
267
+ {children}
268
+
269
+ {showArrow && (
270
+ <>
271
+ {/* Invisible div for Popper.js to use as reference */}
272
+ <div
273
+ ref={arrowRef}
274
+ style={{ position: "absolute", visibility: "hidden" }}
275
+ data-placement={getArrowDataPlacement()}
276
+ />
277
+
278
+ {/* Arrow container - positioned by Popper */}
279
+ <div
280
+ className={clsx("popper-arrow-container", arrowClassName)}
281
+ style={{
282
+ ...styles.arrow,
283
+ position: "absolute",
284
+ zIndex: 1,
285
+ // Size and positioning based on placement - smaller arrow
286
+ ...(actualPlacement.startsWith("top") && {
287
+ bottom: "-8px",
288
+ width: "16px",
289
+ height: "8px",
290
+ }),
291
+ ...(actualPlacement.startsWith("bottom") && {
292
+ top: "-8px",
293
+ width: "16px",
294
+ height: "8px",
295
+ }),
296
+ ...(actualPlacement.startsWith("left") && {
297
+ right: "-8px",
298
+ width: "8px",
299
+ height: "16px",
300
+ }),
301
+ ...(actualPlacement.startsWith("right") && {
302
+ left: "-8px",
303
+ width: "8px",
304
+ height: "16px",
305
+ }),
306
+ // Content positioning
307
+ overflow: "hidden",
308
+ }}
309
+ >
310
+ {/* Border element (rendered behind) */}
311
+ {actualPlacement.startsWith("top") && (
312
+ <div
313
+ style={{
314
+ position: "absolute",
315
+ width: 0,
316
+ height: 0,
317
+ borderStyle: "solid",
318
+ borderWidth: "8px 8px 0 8px",
319
+ borderColor: "#eee transparent transparent transparent",
320
+ top: "0px",
321
+ left: "0px",
322
+ }}
323
+ />
324
+ )}
325
+ {actualPlacement.startsWith("bottom") && (
326
+ <div
327
+ style={{
328
+ position: "absolute",
329
+ width: 0,
330
+ height: 0,
331
+ borderStyle: "solid",
332
+ borderWidth: "0 8px 8px 8px",
333
+ borderColor: "transparent transparent #eee transparent",
334
+ top: "0px",
335
+ left: "0px",
336
+ }}
337
+ />
338
+ )}
339
+ {actualPlacement.startsWith("left") && (
340
+ <div
341
+ style={{
342
+ position: "absolute",
343
+ width: 0,
344
+ height: 0,
345
+ borderStyle: "solid",
346
+ borderWidth: "8px 0 8px 8px",
347
+ borderColor: "transparent transparent transparent #eee",
348
+ top: "0px",
349
+ left: "0px",
350
+ }}
351
+ />
352
+ )}
353
+ {actualPlacement.startsWith("right") && (
354
+ <div
355
+ style={{
356
+ position: "absolute",
357
+ width: 0,
358
+ height: 0,
359
+ borderStyle: "solid",
360
+ borderWidth: "8px 8px 8px 0",
361
+ borderColor: "transparent #eee transparent transparent",
362
+ top: "0px",
363
+ left: "0px",
364
+ }}
365
+ />
366
+ )}
367
+
368
+ {/* Actual triangle created with CSS borders, slightly smaller and offset to create border effect */}
369
+ <div
370
+ style={{
371
+ position: "absolute",
372
+ width: 0,
373
+ height: 0,
374
+ borderStyle: "solid",
375
+ backgroundColor: "transparent",
376
+ // Position relative to border triangle
377
+ left: "0px",
378
+ zIndex: 1,
379
+
380
+ // Top placement - pointing down
381
+ ...(actualPlacement.startsWith("top") && {
382
+ borderWidth: "7px 7px 0 7px",
383
+ borderColor: "white transparent transparent transparent",
384
+ top: "0px",
385
+ }),
386
+
387
+ // Bottom placement - pointing up
388
+ ...(actualPlacement.startsWith("bottom") && {
389
+ borderWidth: "0 7px 7px 7px",
390
+ borderColor: "transparent transparent white transparent",
391
+ top: "1px",
392
+ }),
393
+
394
+ // Left placement - pointing right
395
+ ...(actualPlacement.startsWith("left") && {
396
+ borderWidth: "7px 0 7px 7px",
397
+ borderColor: "transparent transparent transparent white",
398
+ left: "0px",
399
+ }),
400
+
401
+ // Right placement - pointing left
402
+ ...(actualPlacement.startsWith("right") && {
403
+ borderWidth: "7px 7px 7px 0",
404
+ borderColor: "transparent white transparent transparent",
405
+ left: "1px",
406
+ }),
407
+ }}
408
+ />
409
+ </div>
410
+ </>
411
+ )}
412
+ </div>
413
+ );
414
+
415
+ // If using portal and the container exists, render through the portal
416
+ if (usePortal && portalContainer) {
417
+ return createPortal(popperContent, portalContainer);
418
+ }
419
+
420
+ // Otherwise render normally
421
+ return popperContent;
422
+ };
@@ -13,7 +13,7 @@
13
13
  }
14
14
 
15
15
  .small .dotsContainer {
16
- column-gap: 3px;
16
+ column-gap: 2px;
17
17
  }
18
18
 
19
19
  .medium .dotsContainer {
@@ -34,16 +34,16 @@
34
34
  }
35
35
 
36
36
  .subtle {
37
- background-color: var(--bs-primary-bg-subtle);
37
+ background-color: var(--bs-secondary-bg-subtle);
38
38
  }
39
39
 
40
40
  .primary {
41
- background-color: var(--bs-primary);
41
+ background-color: var(--bs-secondary);
42
42
  }
43
43
 
44
44
  .small .dot {
45
- width: 4px;
46
- height: 4px;
45
+ width: 3px;
46
+ height: 3px;
47
47
  }
48
48
 
49
49
  .medium .dot {
@@ -71,11 +71,11 @@
71
71
  @keyframes pulse {
72
72
  0%,
73
73
  100% {
74
- transform: scale(1);
75
- opacity: 0.3;
74
+ transform: scale(0.7);
75
+ opacity: 0.4;
76
76
  }
77
77
  50% {
78
- transform: scale(1.2);
79
- opacity: 1;
78
+ transform: scale(1);
79
+ opacity: 0.8;
80
80
  }
81
81
  }
@@ -7,6 +7,7 @@ interface PulsingDotsProps {
7
7
  dotsCount?: number;
8
8
  subtle?: boolean;
9
9
  size?: "small" | "medium" | "large";
10
+ className?: string | string[];
10
11
  }
11
12
 
12
13
  export const PulsingDots: FC<PulsingDotsProps> = ({
@@ -14,6 +15,7 @@ export const PulsingDots: FC<PulsingDotsProps> = ({
14
15
  dotsCount = 3,
15
16
  subtle = true,
16
17
  size = "small",
18
+ className,
17
19
  }) => {
18
20
  return (
19
21
  <div
@@ -24,6 +26,7 @@ export const PulsingDots: FC<PulsingDotsProps> = ({
24
26
  : size === "medium"
25
27
  ? styles.medium
26
28
  : styles.large,
29
+ className,
27
30
  )}
28
31
  role="status"
29
32
  >
@@ -35,7 +38,7 @@ export const PulsingDots: FC<PulsingDotsProps> = ({
35
38
  styles.dot,
36
39
  subtle ? styles.subtle : styles.primary,
37
40
  )}
38
- style={{ animationDelay: `${index * 0.15}s` }}
41
+ style={{ animationDelay: `${index * 0.2}s` }}
39
42
  />
40
43
  ))}
41
44
  </div>
@@ -0,0 +1,183 @@
1
+ import {
2
+ CSSProperties,
3
+ FC,
4
+ ReactNode,
5
+ RefObject,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ } from "react";
10
+
11
+ interface StickyScrollProps {
12
+ children: ReactNode;
13
+ scrollRef: RefObject<HTMLElement | null>;
14
+ offsetTop?: number;
15
+ zIndex?: number;
16
+ className?: string;
17
+ stickyClassName?: string;
18
+ onStickyChange?: (isSticky: boolean) => void;
19
+ }
20
+
21
+ export const StickyScroll: FC<StickyScrollProps> = ({
22
+ children,
23
+ scrollRef,
24
+ offsetTop = 0,
25
+ zIndex = 100,
26
+ className = "",
27
+ stickyClassName = "is-sticky",
28
+ onStickyChange,
29
+ }) => {
30
+ const wrapperRef = useRef<HTMLDivElement>(null);
31
+ const contentRef = useRef<HTMLDivElement>(null);
32
+ const [isSticky, setIsSticky] = useState(false);
33
+ const [dimensions, setDimensions] = useState({
34
+ width: 0,
35
+ height: 0,
36
+ left: 0,
37
+ stickyTop: 0, // Store the position where the element should stick
38
+ });
39
+
40
+ useEffect(() => {
41
+ const wrapper = wrapperRef.current;
42
+ const content = contentRef.current;
43
+ const scrollContainer = scrollRef.current;
44
+
45
+ if (!wrapper || !content || !scrollContainer) {
46
+ return;
47
+ }
48
+
49
+ // Create a sentinel element that will be positioned at the desired sticky point
50
+ const sentinel = document.createElement("div");
51
+ sentinel.style.position = "absolute";
52
+ sentinel.style.top = "0px"; // Position at the top of the wrapper
53
+ sentinel.style.left = "0";
54
+ sentinel.style.width = "1px";
55
+ sentinel.style.height = "1px";
56
+ sentinel.style.pointerEvents = "none";
57
+ wrapper.prepend(sentinel);
58
+
59
+ // Create a width tracker element that always has the same width as the wrapper
60
+ // This helps us know what width to apply to the fixed element
61
+ const widthTracker = document.createElement("div");
62
+ widthTracker.style.position = "absolute";
63
+ widthTracker.style.top = "0";
64
+ widthTracker.style.left = "0";
65
+ widthTracker.style.width = "100%";
66
+ widthTracker.style.height = "0";
67
+ widthTracker.style.pointerEvents = "none";
68
+ widthTracker.style.visibility = "hidden";
69
+ wrapper.prepend(widthTracker);
70
+
71
+ // Measure element dimensions and calculate sticky position
72
+ const updateDimensions = () => {
73
+ if (wrapper && scrollContainer) {
74
+ const contentRect = content.getBoundingClientRect();
75
+ const containerRect = scrollContainer.getBoundingClientRect();
76
+ const trackerRect = widthTracker.getBoundingClientRect();
77
+
78
+ // Calculate where the top of the content should be when sticky
79
+ // This is the distance from the top of the scroll container
80
+ // plus any additional offsetTop
81
+ const stickyTop = containerRect.top + offsetTop;
82
+
83
+ setDimensions({
84
+ // Use the width tracker to get the right width that respects
85
+ // the parent container's current width, rather than the content's width
86
+ width: trackerRect.width,
87
+ height: contentRect.height,
88
+ left: trackerRect.left,
89
+ stickyTop,
90
+ });
91
+ }
92
+ };
93
+
94
+ // Initial measurement
95
+ updateDimensions();
96
+
97
+ // Monitor size changes
98
+ const resizeObserver = new ResizeObserver(() => {
99
+ // Use animationFrame to ensure dimensions are updated after DOM has settled
100
+ requestAnimationFrame(() => {
101
+ updateDimensions();
102
+ // If sticky, force a re-measurement of position to update layout
103
+ if (isSticky) {
104
+ handleScroll();
105
+ }
106
+ });
107
+ });
108
+
109
+ resizeObserver.observe(wrapper);
110
+ resizeObserver.observe(scrollContainer);
111
+ resizeObserver.observe(content);
112
+
113
+ // Add scroll event listener for more precise control
114
+ const handleScroll = () => {
115
+ const sentinelRect = sentinel.getBoundingClientRect();
116
+ const containerRect = scrollContainer.getBoundingClientRect();
117
+
118
+ // Check if sentinel is above the top of the viewport + offset
119
+ const shouldBeSticky = sentinelRect.top < containerRect.top + offsetTop;
120
+
121
+ if (shouldBeSticky !== isSticky) {
122
+ updateDimensions();
123
+ setIsSticky(shouldBeSticky);
124
+
125
+ if (onStickyChange) {
126
+ onStickyChange(shouldBeSticky);
127
+ }
128
+ }
129
+ };
130
+
131
+ scrollContainer.addEventListener("scroll", handleScroll);
132
+
133
+ // Trigger initial check
134
+ handleScroll();
135
+
136
+ // Clean up
137
+ return () => {
138
+ resizeObserver.disconnect();
139
+ scrollContainer.removeEventListener("scroll", handleScroll);
140
+ if (sentinel.parentNode) {
141
+ sentinel.parentNode.removeChild(sentinel);
142
+ }
143
+ if (widthTracker.parentNode) {
144
+ widthTracker.parentNode.removeChild(widthTracker);
145
+ }
146
+ };
147
+ }, [scrollRef, offsetTop, onStickyChange, isSticky]);
148
+
149
+ // Wrapper styles - this div serves as the placeholder
150
+ // When sticky, we need to ensure the wrapper has the right dimensions
151
+ // to prevent content jumping when the element is detached from normal flow
152
+ const wrapperStyle: CSSProperties = {
153
+ position: "relative",
154
+ height: isSticky ? `${dimensions.height}px` : "auto",
155
+ // Don't constrain width - let it flow naturally with the content
156
+ };
157
+
158
+ // Content styles - position at the calculated stickyTop when sticky
159
+ // For sticky mode, use fixed positioning but maintain the original width
160
+ const contentStyle: CSSProperties = isSticky
161
+ ? {
162
+ position: "fixed",
163
+ top: `${dimensions.stickyTop}px`,
164
+ left: `${dimensions.left}px`,
165
+ width: `${dimensions.width}px`, // Keep explicit width to prevent expanding to 100%
166
+ maxHeight: `calc(100vh - ${dimensions.stickyTop}px)`,
167
+ zIndex,
168
+ }
169
+ : {};
170
+
171
+ const contentClassName =
172
+ isSticky && stickyClassName
173
+ ? `${className} ${stickyClassName}`.trim()
174
+ : className;
175
+
176
+ return (
177
+ <div ref={wrapperRef} style={wrapperStyle}>
178
+ <div ref={contentRef} className={contentClassName} style={contentStyle}>
179
+ {children}
180
+ </div>
181
+ </div>
182
+ );
183
+ };