@sampleapp.ai/sdk 1.0.34 → 1.0.35
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/sandbox/guardian/code-focus-section.js +166 -28
- package/dist/components/sandbox/guardian/right-view/simplified-editor.js +7 -7
- package/dist/components/sandbox/guardian/types/ide-types.js +0 -1
- package/dist/index.es.js +8999 -18555
- package/dist/index.standalone.umd.js +11 -43
- package/dist/index.umd.js +99 -127
- package/dist/lib/ssr-safe-decode-entity.js +16 -0
- package/dist/lib/ssr-safe-decode-named-character-reference.js +257 -0
- package/dist/sdk.css +1 -1
- package/package.json +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import React, { useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
|
3
3
|
import { useGuardianContext } from "./context/guardian-context";
|
|
4
4
|
// Helper function to find a node in the file tree by path
|
|
5
5
|
function getTargetNode(fileTree, targetPath) {
|
|
@@ -13,15 +13,159 @@ function getTargetNode(fileTree, targetPath) {
|
|
|
13
13
|
}
|
|
14
14
|
return current;
|
|
15
15
|
}
|
|
16
|
+
class CodeFocusSectionCoordinator {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.sections = new Map();
|
|
19
|
+
this.lastActiveSectionId = null;
|
|
20
|
+
this.scrollContainers = new Set();
|
|
21
|
+
this.scrollHandler = null;
|
|
22
|
+
if (typeof window !== "undefined") {
|
|
23
|
+
this.scrollHandler = this.handleScroll.bind(this);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
handleScroll() {
|
|
27
|
+
this.updateActiveSection();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Calculate how "centered" an element is relative to its scroll container.
|
|
31
|
+
* Returns a score from 0 to 1, where 1 means the element's top is at the ideal reading position.
|
|
32
|
+
*/
|
|
33
|
+
calculateScore(element) {
|
|
34
|
+
// Find the scroll container by walking up the DOM
|
|
35
|
+
const scrollContainer = this.findScrollContainer(element);
|
|
36
|
+
if (!scrollContainer)
|
|
37
|
+
return 0;
|
|
38
|
+
const containerRect = scrollContainer.getBoundingClientRect();
|
|
39
|
+
const elementRect = element.getBoundingClientRect();
|
|
40
|
+
// Calculate position relative to the scroll container
|
|
41
|
+
const relativeTop = elementRect.top - containerRect.top;
|
|
42
|
+
const relativeBottom = elementRect.bottom - containerRect.top;
|
|
43
|
+
const containerHeight = containerRect.height;
|
|
44
|
+
// Check if element is visible in the container
|
|
45
|
+
if (relativeBottom < 0 || relativeTop > containerHeight) {
|
|
46
|
+
return 0; // Not visible
|
|
47
|
+
}
|
|
48
|
+
// Target point: 20-30% from top of container for natural reading flow
|
|
49
|
+
// This is where the user's eyes naturally focus when reading
|
|
50
|
+
const targetPoint = containerHeight * 0.25;
|
|
51
|
+
// Calculate how close the element's top is to the target point
|
|
52
|
+
const distanceFromTarget = Math.abs(relativeTop - targetPoint);
|
|
53
|
+
const maxDistance = containerHeight;
|
|
54
|
+
// Base score from distance (closer to target = higher score)
|
|
55
|
+
const distanceScore = Math.max(0, 1 - distanceFromTarget / maxDistance);
|
|
56
|
+
// Bonus for elements that are in the "reading zone" (top 50% of container)
|
|
57
|
+
const inReadingZone = relativeTop >= 0 && relativeTop < containerHeight * 0.5;
|
|
58
|
+
const readingZoneBonus = inReadingZone ? 0.3 : 0;
|
|
59
|
+
// Visibility factor - how much of the element is visible
|
|
60
|
+
const visibleTop = Math.max(0, relativeTop);
|
|
61
|
+
const visibleBottom = Math.min(containerHeight, relativeBottom);
|
|
62
|
+
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
|
|
63
|
+
const elementHeight = elementRect.height;
|
|
64
|
+
const visibilityRatio = elementHeight > 0 ? visibleHeight / elementHeight : 0;
|
|
65
|
+
const visibilityFactor = visibilityRatio * 0.2;
|
|
66
|
+
return distanceScore * 0.5 + readingZoneBonus + visibilityFactor;
|
|
67
|
+
}
|
|
68
|
+
findScrollContainer(element) {
|
|
69
|
+
let current = element.parentElement;
|
|
70
|
+
while (current) {
|
|
71
|
+
const style = window.getComputedStyle(current);
|
|
72
|
+
const overflowY = style.overflowY;
|
|
73
|
+
if ((overflowY === "auto" || overflowY === "scroll") &&
|
|
74
|
+
current.scrollHeight > current.clientHeight) {
|
|
75
|
+
return current;
|
|
76
|
+
}
|
|
77
|
+
current = current.parentElement;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
updateActiveSection() {
|
|
82
|
+
if (this.sections.size === 0)
|
|
83
|
+
return;
|
|
84
|
+
// Find the section with the highest score
|
|
85
|
+
let bestSection = null;
|
|
86
|
+
let bestScore = -1;
|
|
87
|
+
let bestId = null;
|
|
88
|
+
for (const [id, entry] of this.sections) {
|
|
89
|
+
const score = this.calculateScore(entry.element);
|
|
90
|
+
if (score > bestScore) {
|
|
91
|
+
bestScore = score;
|
|
92
|
+
bestSection = entry;
|
|
93
|
+
bestId = id;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Only trigger if we have a valid section with a reasonable score
|
|
97
|
+
// and it's different from the last one
|
|
98
|
+
if (bestSection && bestId && bestScore > 0.1 && bestId !== this.lastActiveSectionId) {
|
|
99
|
+
this.lastActiveSectionId = bestId;
|
|
100
|
+
bestSection.focusCallback();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
register(id, element, focusCallback) {
|
|
104
|
+
const entry = { element, focusCallback, id };
|
|
105
|
+
this.sections.set(id, entry);
|
|
106
|
+
element.setAttribute("data-code-focus-id", id);
|
|
107
|
+
// Find and attach to the scroll container
|
|
108
|
+
const scrollContainer = this.findScrollContainer(element);
|
|
109
|
+
if (scrollContainer && this.scrollHandler) {
|
|
110
|
+
if (!this.scrollContainers.has(scrollContainer)) {
|
|
111
|
+
scrollContainer.addEventListener("scroll", this.scrollHandler, { passive: true });
|
|
112
|
+
this.scrollContainers.add(scrollContainer);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Trigger initial update
|
|
116
|
+
this.updateActiveSection();
|
|
117
|
+
// Return cleanup function
|
|
118
|
+
return () => {
|
|
119
|
+
this.sections.delete(id);
|
|
120
|
+
if (this.lastActiveSectionId === id) {
|
|
121
|
+
this.lastActiveSectionId = null;
|
|
122
|
+
}
|
|
123
|
+
// Clean up scroll listeners if no more sections
|
|
124
|
+
if (this.sections.size === 0) {
|
|
125
|
+
for (const container of this.scrollContainers) {
|
|
126
|
+
if (this.scrollHandler) {
|
|
127
|
+
container.removeEventListener("scroll", this.scrollHandler);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
this.scrollContainers.clear();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// Force focus a specific section (for click handling)
|
|
135
|
+
forceFocus(id) {
|
|
136
|
+
const section = this.sections.get(id);
|
|
137
|
+
if (section) {
|
|
138
|
+
this.lastActiveSectionId = id;
|
|
139
|
+
section.focusCallback();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Singleton instance - created lazily to avoid SSR issues
|
|
144
|
+
let coordinatorInstance = null;
|
|
145
|
+
function getCoordinator() {
|
|
146
|
+
if (!coordinatorInstance && typeof window !== "undefined") {
|
|
147
|
+
coordinatorInstance = new CodeFocusSectionCoordinator();
|
|
148
|
+
}
|
|
149
|
+
return coordinatorInstance;
|
|
150
|
+
}
|
|
151
|
+
// Counter for generating unique IDs
|
|
152
|
+
let sectionIdCounter = 0;
|
|
16
153
|
/**
|
|
17
154
|
* Component that focuses on a specific file and line range in the code editor
|
|
18
155
|
* when it comes into view while scrolling, or when the user clicks "View Code".
|
|
156
|
+
*
|
|
157
|
+
* Uses a global coordinator pattern (similar to Stripe docs) to ensure:
|
|
158
|
+
* - Only ONE section is active at a time
|
|
159
|
+
* - The section closest to the viewport center is selected
|
|
160
|
+
* - Smooth transitions without flickering during fast scrolling
|
|
161
|
+
* - Natural reading flow with slight bias towards top of viewport
|
|
19
162
|
*/
|
|
20
|
-
export default function CodeFocusSection({ filePath, lineRange, title, description, children,
|
|
163
|
+
export default function CodeFocusSection({ filePath, lineRange, title, description, children, themeColor, }) {
|
|
21
164
|
const sectionRef = useRef(null);
|
|
165
|
+
const sectionIdRef = useRef(`code-focus-${++sectionIdCounter}`);
|
|
22
166
|
const { generatedCode, fileTree, setActiveFilePath, setActiveFileName, setActiveLineNumber, setActiveLineRange, updateCode, setLanguage, filesEdited, activeFilePath, activeLineRange, } = useGuardianContext();
|
|
23
|
-
//
|
|
24
|
-
const focusCode = () => {
|
|
167
|
+
// Memoize the focus code function to prevent unnecessary recreations
|
|
168
|
+
const focusCode = useCallback(() => {
|
|
25
169
|
// Get the file code from generatedCode or fileTree
|
|
26
170
|
// Always search by file_path property first, as that's the source of truth
|
|
27
171
|
let code = "";
|
|
@@ -94,41 +238,35 @@ export default function CodeFocusSection({ filePath, lineRange, title, descripti
|
|
|
94
238
|
setActiveLineNumber(undefined);
|
|
95
239
|
}
|
|
96
240
|
}
|
|
97
|
-
};
|
|
98
|
-
useEffect(() => {
|
|
99
|
-
const element = sectionRef.current;
|
|
100
|
-
if (!element)
|
|
101
|
-
return;
|
|
102
|
-
const observer = new IntersectionObserver((entries) => {
|
|
103
|
-
entries.forEach((entry) => {
|
|
104
|
-
// Trigger when the element enters the middle of the viewport
|
|
105
|
-
if (entry.isIntersecting) {
|
|
106
|
-
focusCode();
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
}, {
|
|
110
|
-
threshold,
|
|
111
|
-
rootMargin: "-40% 0px -40% 0px", // Trigger when element enters the middle 20% of viewport
|
|
112
|
-
});
|
|
113
|
-
observer.observe(element);
|
|
114
|
-
return () => {
|
|
115
|
-
observer.disconnect();
|
|
116
|
-
};
|
|
117
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
118
241
|
}, [
|
|
119
242
|
filePath,
|
|
120
243
|
lineRange,
|
|
121
244
|
generatedCode,
|
|
122
245
|
fileTree,
|
|
246
|
+
filesEdited,
|
|
123
247
|
setActiveFilePath,
|
|
124
248
|
setActiveFileName,
|
|
125
249
|
setActiveLineNumber,
|
|
126
250
|
setActiveLineRange,
|
|
127
251
|
updateCode,
|
|
128
252
|
setLanguage,
|
|
129
|
-
filesEdited,
|
|
130
|
-
threshold,
|
|
131
253
|
]);
|
|
254
|
+
// Register with the global coordinator
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
const element = sectionRef.current;
|
|
257
|
+
if (!element || typeof window === "undefined")
|
|
258
|
+
return;
|
|
259
|
+
const coordinator = getCoordinator();
|
|
260
|
+
const cleanup = coordinator.register(sectionIdRef.current, element, focusCode);
|
|
261
|
+
return cleanup;
|
|
262
|
+
}, [focusCode]);
|
|
263
|
+
// Handle click to force focus
|
|
264
|
+
const handleClick = useCallback(() => {
|
|
265
|
+
if (typeof window !== "undefined") {
|
|
266
|
+
const coordinator = getCoordinator();
|
|
267
|
+
coordinator.forceFocus(sectionIdRef.current);
|
|
268
|
+
}
|
|
269
|
+
}, []);
|
|
132
270
|
// Get the resolved file path for comparison
|
|
133
271
|
// This ensures we compare against the actual file_path from the code structure
|
|
134
272
|
const resolvedFilePathForComparison = useMemo(() => {
|
|
@@ -154,7 +292,7 @@ export default function CodeFocusSection({ filePath, lineRange, title, descripti
|
|
|
154
292
|
}
|
|
155
293
|
return filePath;
|
|
156
294
|
}, [filePath, generatedCode, fileTree]);
|
|
157
|
-
return (React.createElement("div", { ref: sectionRef, className: `relative transition-colors py-6 cursor-pointer`, onClick:
|
|
295
|
+
return (React.createElement("div", { ref: sectionRef, className: `relative transition-colors py-6 cursor-pointer`, onClick: handleClick },
|
|
158
296
|
React.createElement("div", { className: "pointer-events-none absolute inset-x-[-2rem] top-0 border-t border-border" }),
|
|
159
297
|
React.createElement("div", { className: "pointer-events-none absolute inset-x-[-2rem] bottom-0 border-b border-border" }),
|
|
160
298
|
(activeFilePath === resolvedFilePathForComparison ||
|
|
@@ -207,27 +207,27 @@ export default function SimplifiedEditor({ themeColor }) {
|
|
|
207
207
|
colorDecorators: true,
|
|
208
208
|
})), { domReadOnly: isGenerating, readOnly: isGenerating }) })),
|
|
209
209
|
React.createElement("style", null, `
|
|
210
|
-
|
|
210
|
+
.search-highlight-line {
|
|
211
211
|
background-color: ${highlightBackground} !important;
|
|
212
212
|
}
|
|
213
|
-
|
|
213
|
+
.search-highlight-glyph {
|
|
214
214
|
background-color: ${glyphBackground};
|
|
215
215
|
}
|
|
216
|
-
|
|
216
|
+
.muted-code-text {
|
|
217
217
|
opacity: 0.5;
|
|
218
218
|
filter: grayscale(0.4);
|
|
219
219
|
}
|
|
220
220
|
/* Remove blue focus outline from Monaco Editor */
|
|
221
|
-
|
|
221
|
+
.monaco-editor .inputarea.ime-input {
|
|
222
222
|
outline: none !important;
|
|
223
223
|
}
|
|
224
|
-
|
|
224
|
+
.monaco-editor {
|
|
225
225
|
outline: none !important;
|
|
226
226
|
}
|
|
227
|
-
|
|
227
|
+
.monaco-editor .monaco-editor-background {
|
|
228
228
|
outline: none !important;
|
|
229
229
|
}
|
|
230
|
-
|
|
230
|
+
.monaco-editor .view-overlays {
|
|
231
231
|
outline: none !important;
|
|
232
232
|
}
|
|
233
233
|
`)));
|