@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.
@@ -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, threshold = 0, themeColor, }) {
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
- // Moves the editor's focus to this file/lines
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: focusCode },
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
- :global(.search-highlight-line) {
210
+ .search-highlight-line {
211
211
  background-color: ${highlightBackground} !important;
212
212
  }
213
- :global(.search-highlight-glyph) {
213
+ .search-highlight-glyph {
214
214
  background-color: ${glyphBackground};
215
215
  }
216
- :global(.muted-code-text) {
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
- :global(.monaco-editor .inputarea.ime-input) {
221
+ .monaco-editor .inputarea.ime-input {
222
222
  outline: none !important;
223
223
  }
224
- :global(.monaco-editor) {
224
+ .monaco-editor {
225
225
  outline: none !important;
226
226
  }
227
- :global(.monaco-editor .monaco-editor-background) {
227
+ .monaco-editor .monaco-editor-background {
228
228
  outline: none !important;
229
229
  }
230
- :global(.monaco-editor .view-overlays) {
230
+ .monaco-editor .view-overlays {
231
231
  outline: none !important;
232
232
  }
233
233
  `)));
@@ -144,7 +144,6 @@ export function filePathToCodeLanguage(filePath) {
144
144
  return CodeLanguage.ELIXIR;
145
145
  case "lua":
146
146
  return CodeLanguage.LUA;
147
- case "m":
148
147
  case "mm":
149
148
  return CodeLanguage.OBJECTIVEC;
150
149
  case "md":