inspect-ai 0.3.98__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.
- inspect_ai/__init__.py +2 -0
- inspect_ai/_cli/log.py +1 -1
- inspect_ai/_display/core/config.py +11 -5
- inspect_ai/_display/core/panel.py +66 -2
- inspect_ai/_display/core/textual.py +5 -2
- inspect_ai/_display/plain/display.py +1 -0
- inspect_ai/_display/rich/display.py +2 -2
- inspect_ai/_display/textual/widgets/transcript.py +41 -1
- inspect_ai/_eval/run.py +12 -4
- inspect_ai/_eval/score.py +2 -4
- inspect_ai/_eval/task/log.py +1 -1
- inspect_ai/_eval/task/run.py +59 -81
- inspect_ai/_eval/task/task.py +1 -1
- inspect_ai/_util/_async.py +1 -1
- inspect_ai/_util/content.py +11 -6
- inspect_ai/_util/interrupt.py +2 -2
- inspect_ai/_util/text.py +7 -0
- inspect_ai/_util/working.py +8 -37
- inspect_ai/_view/__init__.py +0 -0
- inspect_ai/_view/schema.py +3 -1
- inspect_ai/_view/view.py +14 -0
- inspect_ai/_view/www/CLAUDE.md +15 -0
- inspect_ai/_view/www/dist/assets/index.css +273 -169
- inspect_ai/_view/www/dist/assets/index.js +20079 -17019
- inspect_ai/_view/www/log-schema.json +122 -8
- inspect_ai/_view/www/package.json +5 -1
- inspect_ai/_view/www/src/@types/log.d.ts +20 -2
- inspect_ai/_view/www/src/app/App.tsx +1 -15
- inspect_ai/_view/www/src/app/appearance/icons.ts +4 -1
- inspect_ai/_view/www/src/app/content/MetaDataGrid.tsx +24 -6
- inspect_ai/_view/www/src/app/content/MetadataGrid.module.css +0 -5
- inspect_ai/_view/www/src/app/content/RenderedContent.tsx +221 -205
- inspect_ai/_view/www/src/app/log-view/LogViewContainer.tsx +2 -1
- inspect_ai/_view/www/src/app/log-view/tabs/SamplesTab.tsx +5 -0
- inspect_ai/_view/www/src/app/routing/url.ts +84 -4
- inspect_ai/_view/www/src/app/samples/InlineSampleDisplay.module.css +0 -5
- inspect_ai/_view/www/src/app/samples/SampleDialog.module.css +1 -1
- inspect_ai/_view/www/src/app/samples/SampleDisplay.module.css +7 -0
- inspect_ai/_view/www/src/app/samples/SampleDisplay.tsx +26 -19
- inspect_ai/_view/www/src/app/samples/SampleSummaryView.module.css +1 -2
- inspect_ai/_view/www/src/app/samples/chat/ChatMessage.tsx +8 -6
- inspect_ai/_view/www/src/app/samples/chat/ChatMessageRow.tsx +0 -4
- inspect_ai/_view/www/src/app/samples/chat/ChatViewVirtualList.tsx +3 -2
- inspect_ai/_view/www/src/app/samples/chat/MessageContent.tsx +2 -0
- inspect_ai/_view/www/src/app/samples/chat/MessageContents.tsx +2 -0
- inspect_ai/_view/www/src/app/samples/chat/messages.ts +1 -0
- inspect_ai/_view/www/src/app/samples/chat/tools/ToolCallView.tsx +1 -0
- inspect_ai/_view/www/src/app/samples/list/SampleRow.tsx +1 -1
- inspect_ai/_view/www/src/app/samples/scores/SampleScoresGrid.module.css +2 -2
- inspect_ai/_view/www/src/app/samples/transcript/ErrorEventView.tsx +2 -3
- inspect_ai/_view/www/src/app/samples/transcript/InfoEventView.tsx +1 -1
- inspect_ai/_view/www/src/app/samples/transcript/InputEventView.tsx +1 -2
- inspect_ai/_view/www/src/app/samples/transcript/ModelEventView.module.css +1 -1
- inspect_ai/_view/www/src/app/samples/transcript/ModelEventView.tsx +1 -1
- inspect_ai/_view/www/src/app/samples/transcript/SampleInitEventView.tsx +1 -1
- inspect_ai/_view/www/src/app/samples/transcript/SampleLimitEventView.tsx +3 -2
- inspect_ai/_view/www/src/app/samples/transcript/SandboxEventView.tsx +4 -5
- inspect_ai/_view/www/src/app/samples/transcript/ScoreEventView.tsx +1 -1
- inspect_ai/_view/www/src/app/samples/transcript/SpanEventView.tsx +1 -2
- inspect_ai/_view/www/src/app/samples/transcript/StepEventView.tsx +1 -3
- inspect_ai/_view/www/src/app/samples/transcript/SubtaskEventView.tsx +1 -2
- inspect_ai/_view/www/src/app/samples/transcript/ToolEventView.tsx +3 -4
- inspect_ai/_view/www/src/app/samples/transcript/TranscriptPanel.module.css +42 -0
- inspect_ai/_view/www/src/app/samples/transcript/TranscriptPanel.tsx +77 -0
- inspect_ai/_view/www/src/app/samples/transcript/TranscriptVirtualList.tsx +27 -71
- inspect_ai/_view/www/src/app/samples/transcript/TranscriptVirtualListComponent.module.css +13 -3
- inspect_ai/_view/www/src/app/samples/transcript/TranscriptVirtualListComponent.tsx +27 -2
- inspect_ai/_view/www/src/app/samples/transcript/event/EventPanel.module.css +1 -0
- inspect_ai/_view/www/src/app/samples/transcript/event/EventPanel.tsx +21 -22
- inspect_ai/_view/www/src/app/samples/transcript/outline/OutlineRow.module.css +45 -0
- inspect_ai/_view/www/src/app/samples/transcript/outline/OutlineRow.tsx +223 -0
- inspect_ai/_view/www/src/app/samples/transcript/outline/TranscriptOutline.module.css +10 -0
- inspect_ai/_view/www/src/app/samples/transcript/outline/TranscriptOutline.tsx +258 -0
- inspect_ai/_view/www/src/app/samples/transcript/outline/tree-visitors.ts +187 -0
- inspect_ai/_view/www/src/app/samples/transcript/state/StateEventRenderers.tsx +8 -1
- inspect_ai/_view/www/src/app/samples/transcript/state/StateEventView.tsx +3 -4
- inspect_ai/_view/www/src/app/samples/transcript/transform/hooks.ts +78 -0
- inspect_ai/_view/www/src/app/samples/transcript/transform/treeify.ts +340 -135
- inspect_ai/_view/www/src/app/samples/transcript/transform/utils.ts +3 -0
- inspect_ai/_view/www/src/app/samples/transcript/types.ts +2 -0
- inspect_ai/_view/www/src/app/types.ts +5 -1
- inspect_ai/_view/www/src/client/api/api-browser.ts +2 -2
- inspect_ai/_view/www/src/components/LiveVirtualList.tsx +6 -1
- inspect_ai/_view/www/src/components/MarkdownDiv.tsx +1 -1
- inspect_ai/_view/www/src/components/PopOver.tsx +422 -0
- inspect_ai/_view/www/src/components/PulsingDots.module.css +9 -9
- inspect_ai/_view/www/src/components/PulsingDots.tsx +4 -1
- inspect_ai/_view/www/src/components/StickyScroll.tsx +183 -0
- inspect_ai/_view/www/src/components/TabSet.tsx +4 -0
- inspect_ai/_view/www/src/state/hooks.ts +52 -2
- inspect_ai/_view/www/src/state/logSlice.ts +4 -3
- inspect_ai/_view/www/src/state/samplePolling.ts +8 -0
- inspect_ai/_view/www/src/state/sampleSlice.ts +53 -9
- inspect_ai/_view/www/src/state/scrolling.ts +152 -0
- inspect_ai/_view/www/src/utils/attachments.ts +7 -0
- inspect_ai/_view/www/src/utils/python.ts +18 -0
- inspect_ai/_view/www/yarn.lock +269 -6
- inspect_ai/agent/_react.py +12 -7
- inspect_ai/agent/_run.py +46 -11
- inspect_ai/analysis/beta/_dataframe/samples/table.py +19 -18
- inspect_ai/log/_bundle.py +5 -3
- inspect_ai/log/_log.py +3 -3
- inspect_ai/log/_recorders/file.py +2 -9
- inspect_ai/log/_transcript.py +1 -1
- inspect_ai/model/_call_tools.py +6 -2
- inspect_ai/model/_openai.py +1 -1
- inspect_ai/model/_openai_responses.py +78 -39
- inspect_ai/model/_openai_web_search.py +31 -0
- inspect_ai/model/_providers/anthropic.py +3 -6
- inspect_ai/model/_providers/azureai.py +72 -3
- inspect_ai/model/_providers/openai.py +2 -1
- inspect_ai/model/_providers/providers.py +1 -1
- inspect_ai/scorer/_metric.py +1 -2
- inspect_ai/solver/_task_state.py +2 -2
- inspect_ai/tool/_tool.py +6 -2
- inspect_ai/tool/_tool_def.py +27 -4
- inspect_ai/tool/_tool_info.py +2 -0
- inspect_ai/tool/_tools/_web_search/_google.py +15 -4
- inspect_ai/tool/_tools/_web_search/_tavily.py +35 -12
- inspect_ai/tool/_tools/_web_search/_web_search.py +214 -45
- inspect_ai/util/__init__.py +6 -0
- inspect_ai/util/_json.py +3 -0
- inspect_ai/util/_limit.py +374 -141
- inspect_ai/util/_sandbox/docker/compose.py +20 -11
- inspect_ai/util/_span.py +1 -1
- {inspect_ai-0.3.98.dist-info → inspect_ai-0.3.100.dist-info}/METADATA +3 -3
- {inspect_ai-0.3.98.dist-info → inspect_ai-0.3.100.dist-info}/RECORD +131 -117
- {inspect_ai-0.3.98.dist-info → inspect_ai-0.3.100.dist-info}/WHEEL +1 -1
- {inspect_ai-0.3.98.dist-info → inspect_ai-0.3.100.dist-info}/entry_points.txt +0 -0
- {inspect_ai-0.3.98.dist-info → inspect_ai-0.3.100.dist-info}/licenses/LICENSE +0 -0
- {inspect_ai-0.3.98.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:
|
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-
|
37
|
+
background-color: var(--bs-secondary-bg-subtle);
|
38
38
|
}
|
39
39
|
|
40
40
|
.primary {
|
41
|
-
background-color: var(--bs-
|
41
|
+
background-color: var(--bs-secondary);
|
42
42
|
}
|
43
43
|
|
44
44
|
.small .dot {
|
45
|
-
width:
|
46
|
-
height:
|
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(
|
75
|
-
opacity: 0.
|
74
|
+
transform: scale(0.7);
|
75
|
+
opacity: 0.4;
|
76
76
|
}
|
77
77
|
50% {
|
78
|
-
transform: scale(1
|
79
|
-
opacity:
|
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.
|
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
|
+
};
|