@yoamigo.com/core 0.3.7 → 0.3.9

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/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import React$1, { ReactNode, CSSProperties } from 'react';
3
3
  export { C as ContentStoreProviderProd, g as EmbedFieldValue, h as EmbedType, d as MarkdownText, e as MarkdownTextProps, P as PageInfo, b as StaticImage, c as StaticImageProps, M as StaticText, S as StaticTextProps, Y as YaEmbed, f as YaEmbedProps, i as YaLink, j as YaLinkProps, p as parseEmbedUrl, s as serializeEmbedValue, u as useContentStoreProd } from './MarkdownText-DHJo0ofY.js';
4
- export { Link, LinkProps, NavigateFunction, Router, RouterProps, useNavigate } from './router.js';
4
+ export { Link, LinkProps, NavigateFunction, Router, RouterProps, ScrollRestoration, useNavigate } from './router.js';
5
5
  export { Route, Switch, useParams } from 'wouter';
6
6
  export { A as AssetResolverFn, C as ContentRegistry, c as contentRegistry, a as getAllContent, g as getContent, h as hasContent, r as registerContent, b as resolveAssetUrl, s as setAssetResolver } from './asset-resolver-BnIvDkVv.js';
7
7
  export { i as initBuilderSelection } from './builder-selection-CYP91nRu.js';
package/dist/index.js CHANGED
@@ -181,9 +181,13 @@ var BuilderSelectionManager = class {
181
181
  page: window.location.pathname
182
182
  });
183
183
  }
184
- sendToParent(message) {
184
+ sendToParent(message, transfer) {
185
185
  try {
186
- window.parent.postMessage(message, "*");
186
+ if (transfer && transfer.length > 0) {
187
+ window.parent.postMessage(message, "*", transfer);
188
+ } else {
189
+ window.parent.postMessage(message, "*");
190
+ }
187
191
  } catch (error) {
188
192
  console.error("[BuilderSelection] Failed to send message to parent:", error);
189
193
  }
@@ -210,6 +214,25 @@ var BuilderSelectionManager = class {
210
214
  this.captureRegionScreenshot(data.region);
211
215
  }
212
216
  break;
217
+ case "NAVIGATE":
218
+ if (data.path) {
219
+ this.handleNavigate(data.path);
220
+ }
221
+ break;
222
+ case "CAPTURE_ALL_PAGES":
223
+ if (data.checkpointId && data.viewport) {
224
+ this.captureAllPagesScreenshots(data.checkpointId, data.viewport);
225
+ }
226
+ break;
227
+ case "GET_SCROLL_POSITION":
228
+ this.sendToParent({ type: "SCROLL_POSITION", scrollY: window.scrollY });
229
+ break;
230
+ case "GET_ALL_ROUTES":
231
+ this.sendToParent({ type: "ALL_ROUTES", routes: this.getAllRoutes() });
232
+ break;
233
+ case "GET_CURRENT_PAGE":
234
+ this.sendToParent({ type: "CURRENT_PAGE", pagePath: window.location.pathname });
235
+ break;
213
236
  }
214
237
  });
215
238
  }
@@ -285,6 +308,25 @@ var BuilderSelectionManager = class {
285
308
  handleKeyDown = (e) => {
286
309
  const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
287
310
  const modifier = isMac ? e.metaKey : e.ctrlKey;
311
+ const activeEl = document.activeElement;
312
+ const isEditing = activeEl?.tagName === "INPUT" || activeEl?.tagName === "TEXTAREA" || activeEl?.getAttribute("contenteditable") === "true" || activeEl?.closest(".ya-text-editing") || activeEl?.closest(".ya-link-editing");
313
+ if (!isEditing && modifier) {
314
+ if (e.key === "z" && !e.shiftKey) {
315
+ e.preventDefault();
316
+ this.sendToParent({ type: "IFRAME_UNDO" });
317
+ return;
318
+ }
319
+ if (e.key === "z" && e.shiftKey) {
320
+ e.preventDefault();
321
+ this.sendToParent({ type: "IFRAME_REDO" });
322
+ return;
323
+ }
324
+ }
325
+ if (!isEditing && e.ctrlKey && e.key === "y") {
326
+ e.preventDefault();
327
+ this.sendToParent({ type: "IFRAME_REDO" });
328
+ return;
329
+ }
288
330
  if (modifier) {
289
331
  if (e.key === "=" || e.key === "+" || e.key === "-" || e.key === "0" || e.key === "1") {
290
332
  e.preventDefault();
@@ -293,12 +335,8 @@ var BuilderSelectionManager = class {
293
335
  return;
294
336
  }
295
337
  }
296
- if (e.key === "Shift") {
297
- const activeElement = document.activeElement;
298
- const isEditing = activeElement?.closest(".ya-text-editing") || activeElement?.closest(".ya-link-editing");
299
- if (!isEditing) {
300
- this.sendToParent({ type: "SHIFT_KEY_PRESSED" });
301
- }
338
+ if (e.key === "Shift" && !isEditing) {
339
+ this.sendToParent({ type: "SHIFT_KEY_PRESSED" });
302
340
  }
303
341
  if (e.key === "Escape") {
304
342
  this.sendToParent({ type: "ESCAPE_KEY_PRESSED" });
@@ -517,6 +555,35 @@ var BuilderSelectionManager = class {
517
555
  this.notifyPageReady();
518
556
  };
519
557
  }
558
+ /**
559
+ * Navigate to a new route with retry logic for HMR timing
560
+ * Used by AI agent to navigate after creating new pages
561
+ * Retries navigation if route doesn't exist yet (new page being created)
562
+ */
563
+ handleNavigate(path) {
564
+ console.log("[BuilderSelection] Navigate request:", path);
565
+ const MAX_RETRIES = 10;
566
+ const RETRY_DELAY_MS = 300;
567
+ let retries = 0;
568
+ const attemptNavigation = () => {
569
+ history.pushState({}, "", path);
570
+ window.dispatchEvent(new PopStateEvent("popstate"));
571
+ setTimeout(() => {
572
+ if (window.location.pathname === path) {
573
+ console.log("[BuilderSelection] Navigation successful:", path);
574
+ this.notifyPageReady();
575
+ } else if (retries < MAX_RETRIES) {
576
+ retries++;
577
+ console.log(`[BuilderSelection] Navigation retry ${retries}/${MAX_RETRIES} for path: ${path}`);
578
+ setTimeout(attemptNavigation, RETRY_DELAY_MS);
579
+ } else {
580
+ console.warn("[BuilderSelection] Navigation failed after max retries:", path);
581
+ this.notifyPageReady();
582
+ }
583
+ }, 100);
584
+ };
585
+ attemptNavigation();
586
+ }
520
587
  showHoverOverlay(element) {
521
588
  if (!this.hoverOverlay) return;
522
589
  const rect = element.getBoundingClientRect();
@@ -757,6 +824,154 @@ var BuilderSelectionManager = class {
757
824
  document.body.appendChild(container);
758
825
  this.selections.set(selectionId, { element, container, badge, border });
759
826
  }
827
+ /**
828
+ * Get all available routes from the page
829
+ * Looks for routes registered via wouter or standard link patterns
830
+ */
831
+ getAllRoutes() {
832
+ const wouterRoutes = window.__wouterRoutes;
833
+ if (wouterRoutes && Array.isArray(wouterRoutes)) {
834
+ return wouterRoutes;
835
+ }
836
+ const links = document.querySelectorAll('a[href^="/"]');
837
+ const routes = /* @__PURE__ */ new Set(["/"]);
838
+ links.forEach((link) => {
839
+ const href = link.getAttribute("href");
840
+ if (href && href.startsWith("/") && !href.includes("http")) {
841
+ const path = href.split("?")[0].replace(/\/$/, "") || "/";
842
+ routes.add(path);
843
+ }
844
+ });
845
+ const navLinks = document.querySelectorAll("nav a[href]");
846
+ navLinks.forEach((link) => {
847
+ const href = link.getAttribute("href");
848
+ if (href && href.startsWith("/")) {
849
+ const path = href.split("?")[0].replace(/\/$/, "") || "/";
850
+ routes.add(path);
851
+ }
852
+ });
853
+ return Array.from(routes).sort();
854
+ }
855
+ /**
856
+ * Capture full-page screenshots for all pages
857
+ * Non-blocking: uses requestIdleCallback between pages
858
+ * Transfers ArrayBuffer directly (zero-copy) for performance
859
+ */
860
+ async captureAllPagesScreenshots(checkpointId, viewport) {
861
+ console.log("[BuilderSelection] Starting multi-page capture for checkpoint:", checkpointId);
862
+ const routes = this.getAllRoutes();
863
+ const originalPath = window.location.pathname;
864
+ const total = routes.length;
865
+ this.sendToParent({
866
+ type: "CAPTURE_PROGRESS",
867
+ checkpointId,
868
+ completed: 0,
869
+ total
870
+ });
871
+ for (let i = 0; i < routes.length; i++) {
872
+ const pagePath = routes[i];
873
+ try {
874
+ await new Promise((resolve) => {
875
+ if ("requestIdleCallback" in window) {
876
+ requestIdleCallback(() => resolve(), { timeout: 1e3 });
877
+ } else {
878
+ setTimeout(resolve, 50);
879
+ }
880
+ });
881
+ if (window.location.pathname !== pagePath) {
882
+ await this.navigateAndWait(pagePath);
883
+ }
884
+ await new Promise((resolve) => setTimeout(resolve, 100));
885
+ const buffer = await this.capturePageAsArrayBuffer();
886
+ if (buffer) {
887
+ this.sendToParent(
888
+ {
889
+ type: "PAGE_SCREENSHOT_READY",
890
+ checkpointId,
891
+ pagePath,
892
+ buffer
893
+ },
894
+ [buffer]
895
+ // Transfer list - moves ownership, no copy
896
+ );
897
+ }
898
+ this.sendToParent({
899
+ type: "CAPTURE_PROGRESS",
900
+ checkpointId,
901
+ completed: i + 1,
902
+ total
903
+ });
904
+ } catch (error) {
905
+ console.error(`[BuilderSelection] Failed to capture page ${pagePath}:`, error);
906
+ }
907
+ }
908
+ if (window.location.pathname !== originalPath) {
909
+ await this.navigateAndWait(originalPath);
910
+ }
911
+ this.sendToParent({
912
+ type: "ALL_SCREENSHOTS_COMPLETE",
913
+ checkpointId
914
+ });
915
+ console.log("[BuilderSelection] Multi-page capture complete:", checkpointId);
916
+ }
917
+ /**
918
+ * Navigate to a path and wait for page ready
919
+ */
920
+ async navigateAndWait(path) {
921
+ return new Promise((resolve) => {
922
+ history.pushState({}, "", path);
923
+ window.dispatchEvent(new PopStateEvent("popstate"));
924
+ requestAnimationFrame(() => {
925
+ requestAnimationFrame(() => {
926
+ resolve();
927
+ });
928
+ });
929
+ });
930
+ }
931
+ /**
932
+ * Capture current page as ArrayBuffer (for zero-copy transfer)
933
+ */
934
+ async capturePageAsArrayBuffer() {
935
+ try {
936
+ const html2canvas = (await import("html2canvas-pro")).default;
937
+ const overlays = document.querySelectorAll(".builder-selection-container, #builder-hover-overlay");
938
+ overlays.forEach((el) => {
939
+ ;
940
+ el.style.display = "none";
941
+ });
942
+ const originalScrollY = window.scrollY;
943
+ window.scrollTo(0, 0);
944
+ await new Promise((resolve) => requestAnimationFrame(resolve));
945
+ const canvas = await html2canvas(document.body, {
946
+ scale: 0.5,
947
+ // Lower resolution for reasonable file size
948
+ logging: false,
949
+ useCORS: true,
950
+ allowTaint: true,
951
+ backgroundColor: "#ffffff",
952
+ windowHeight: document.body.scrollHeight,
953
+ // Full page height
954
+ height: document.body.scrollHeight
955
+ });
956
+ window.scrollTo(0, originalScrollY);
957
+ overlays.forEach((el) => {
958
+ ;
959
+ el.style.display = "";
960
+ });
961
+ const blob = await new Promise((resolve) => {
962
+ canvas.toBlob((b) => resolve(b), "image/jpeg", 0.6);
963
+ });
964
+ if (!blob) {
965
+ console.error("[BuilderSelection] Failed to create blob from canvas");
966
+ return null;
967
+ }
968
+ const buffer = await blob.arrayBuffer();
969
+ return buffer;
970
+ } catch (error) {
971
+ console.error("[BuilderSelection] Page capture failed:", error);
972
+ return null;
973
+ }
974
+ }
760
975
  };
761
976
  var instance = null;
762
977
  function initBuilderSelection() {
@@ -5909,6 +6124,102 @@ function Router({ children, base }) {
5909
6124
  return /* @__PURE__ */ jsx21(WouterRouter, { base: basename, children });
5910
6125
  }
5911
6126
 
6127
+ // src/router/ScrollRestoration.tsx
6128
+ import { useEffect as useEffect15, useRef as useRef15 } from "react";
6129
+ import { useLocation as useLocation3 } from "wouter";
6130
+ var SCROLL_POSITIONS_KEY = "yoamigo-scroll-positions";
6131
+ var HISTORY_INDEX_KEY = "yoamigo-history-index";
6132
+ function getScrollPositions() {
6133
+ if (typeof sessionStorage === "undefined") return {};
6134
+ try {
6135
+ const stored = sessionStorage.getItem(SCROLL_POSITIONS_KEY);
6136
+ return stored ? JSON.parse(stored) : {};
6137
+ } catch {
6138
+ return {};
6139
+ }
6140
+ }
6141
+ function saveScrollPositions(positions) {
6142
+ if (typeof sessionStorage === "undefined") return;
6143
+ try {
6144
+ sessionStorage.setItem(SCROLL_POSITIONS_KEY, JSON.stringify(positions));
6145
+ } catch {
6146
+ }
6147
+ }
6148
+ function getHistoryIndex() {
6149
+ if (typeof history === "undefined") return 0;
6150
+ const state = history.state;
6151
+ if (state && typeof state[HISTORY_INDEX_KEY] === "number") {
6152
+ return state[HISTORY_INDEX_KEY];
6153
+ }
6154
+ return 0;
6155
+ }
6156
+ function setHistoryIndex(index) {
6157
+ if (typeof history === "undefined") return;
6158
+ const newState = { ...history.state, [HISTORY_INDEX_KEY]: index };
6159
+ history.replaceState(newState, "");
6160
+ }
6161
+ var globalHistoryIndex = 0;
6162
+ function ScrollRestoration() {
6163
+ const [location] = useLocation3();
6164
+ const previousLocation = useRef15(location);
6165
+ const isPopState = useRef15(false);
6166
+ const scrollPositionsRef = useRef15({});
6167
+ useEffect15(() => {
6168
+ if (typeof history !== "undefined" && "scrollRestoration" in history) {
6169
+ history.scrollRestoration = "manual";
6170
+ }
6171
+ scrollPositionsRef.current = getScrollPositions();
6172
+ const existingIndex = getHistoryIndex();
6173
+ if (existingIndex === 0 && !history.state?.[HISTORY_INDEX_KEY]) {
6174
+ globalHistoryIndex = 1;
6175
+ setHistoryIndex(globalHistoryIndex);
6176
+ } else {
6177
+ globalHistoryIndex = existingIndex;
6178
+ }
6179
+ const handlePopState = () => {
6180
+ isPopState.current = true;
6181
+ };
6182
+ window.addEventListener("popstate", handlePopState);
6183
+ return () => window.removeEventListener("popstate", handlePopState);
6184
+ }, []);
6185
+ useEffect15(() => {
6186
+ if (previousLocation.current === location) return;
6187
+ const prevHistoryIndex = globalHistoryIndex;
6188
+ const currentScrollY = window.scrollY;
6189
+ if (isPopState.current) {
6190
+ isPopState.current = false;
6191
+ globalHistoryIndex = getHistoryIndex() || globalHistoryIndex;
6192
+ const savedScroll = scrollPositionsRef.current[`${globalHistoryIndex}-${location}`];
6193
+ requestAnimationFrame(() => {
6194
+ window.scrollTo({ top: savedScroll ?? 0, behavior: "instant" });
6195
+ });
6196
+ } else {
6197
+ scrollPositionsRef.current[`${prevHistoryIndex}-${previousLocation.current}`] = currentScrollY;
6198
+ saveScrollPositions(scrollPositionsRef.current);
6199
+ globalHistoryIndex = prevHistoryIndex + 1;
6200
+ setHistoryIndex(globalHistoryIndex);
6201
+ window.scrollTo({ top: 0, behavior: "instant" });
6202
+ }
6203
+ previousLocation.current = location;
6204
+ }, [location]);
6205
+ useEffect15(() => {
6206
+ let timeoutId;
6207
+ const handleScroll = () => {
6208
+ clearTimeout(timeoutId);
6209
+ timeoutId = setTimeout(() => {
6210
+ scrollPositionsRef.current[`${globalHistoryIndex}-${location}`] = window.scrollY;
6211
+ saveScrollPositions(scrollPositionsRef.current);
6212
+ }, 100);
6213
+ };
6214
+ window.addEventListener("scroll", handleScroll, { passive: true });
6215
+ return () => {
6216
+ clearTimeout(timeoutId);
6217
+ window.removeEventListener("scroll", handleScroll);
6218
+ };
6219
+ }, [location]);
6220
+ return null;
6221
+ }
6222
+
5912
6223
  // src/router/index.ts
5913
6224
  import { Route, Switch, useParams } from "wouter";
5914
6225
  export {
@@ -5921,6 +6232,7 @@ export {
5921
6232
  Router,
5922
6233
  SafeHtml,
5923
6234
  SafeTriangleBelow,
6235
+ ScrollRestoration,
5924
6236
  MpImage as StaticImage,
5925
6237
  MpText as StaticText,
5926
6238
  Switch,
package/dist/lib.js CHANGED
@@ -140,9 +140,13 @@ var BuilderSelectionManager = class {
140
140
  page: window.location.pathname
141
141
  });
142
142
  }
143
- sendToParent(message) {
143
+ sendToParent(message, transfer) {
144
144
  try {
145
- window.parent.postMessage(message, "*");
145
+ if (transfer && transfer.length > 0) {
146
+ window.parent.postMessage(message, "*", transfer);
147
+ } else {
148
+ window.parent.postMessage(message, "*");
149
+ }
146
150
  } catch (error) {
147
151
  console.error("[BuilderSelection] Failed to send message to parent:", error);
148
152
  }
@@ -169,6 +173,25 @@ var BuilderSelectionManager = class {
169
173
  this.captureRegionScreenshot(data.region);
170
174
  }
171
175
  break;
176
+ case "NAVIGATE":
177
+ if (data.path) {
178
+ this.handleNavigate(data.path);
179
+ }
180
+ break;
181
+ case "CAPTURE_ALL_PAGES":
182
+ if (data.checkpointId && data.viewport) {
183
+ this.captureAllPagesScreenshots(data.checkpointId, data.viewport);
184
+ }
185
+ break;
186
+ case "GET_SCROLL_POSITION":
187
+ this.sendToParent({ type: "SCROLL_POSITION", scrollY: window.scrollY });
188
+ break;
189
+ case "GET_ALL_ROUTES":
190
+ this.sendToParent({ type: "ALL_ROUTES", routes: this.getAllRoutes() });
191
+ break;
192
+ case "GET_CURRENT_PAGE":
193
+ this.sendToParent({ type: "CURRENT_PAGE", pagePath: window.location.pathname });
194
+ break;
172
195
  }
173
196
  });
174
197
  }
@@ -244,6 +267,25 @@ var BuilderSelectionManager = class {
244
267
  handleKeyDown = (e) => {
245
268
  const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
246
269
  const modifier = isMac ? e.metaKey : e.ctrlKey;
270
+ const activeEl = document.activeElement;
271
+ const isEditing = activeEl?.tagName === "INPUT" || activeEl?.tagName === "TEXTAREA" || activeEl?.getAttribute("contenteditable") === "true" || activeEl?.closest(".ya-text-editing") || activeEl?.closest(".ya-link-editing");
272
+ if (!isEditing && modifier) {
273
+ if (e.key === "z" && !e.shiftKey) {
274
+ e.preventDefault();
275
+ this.sendToParent({ type: "IFRAME_UNDO" });
276
+ return;
277
+ }
278
+ if (e.key === "z" && e.shiftKey) {
279
+ e.preventDefault();
280
+ this.sendToParent({ type: "IFRAME_REDO" });
281
+ return;
282
+ }
283
+ }
284
+ if (!isEditing && e.ctrlKey && e.key === "y") {
285
+ e.preventDefault();
286
+ this.sendToParent({ type: "IFRAME_REDO" });
287
+ return;
288
+ }
247
289
  if (modifier) {
248
290
  if (e.key === "=" || e.key === "+" || e.key === "-" || e.key === "0" || e.key === "1") {
249
291
  e.preventDefault();
@@ -252,12 +294,8 @@ var BuilderSelectionManager = class {
252
294
  return;
253
295
  }
254
296
  }
255
- if (e.key === "Shift") {
256
- const activeElement = document.activeElement;
257
- const isEditing = activeElement?.closest(".ya-text-editing") || activeElement?.closest(".ya-link-editing");
258
- if (!isEditing) {
259
- this.sendToParent({ type: "SHIFT_KEY_PRESSED" });
260
- }
297
+ if (e.key === "Shift" && !isEditing) {
298
+ this.sendToParent({ type: "SHIFT_KEY_PRESSED" });
261
299
  }
262
300
  if (e.key === "Escape") {
263
301
  this.sendToParent({ type: "ESCAPE_KEY_PRESSED" });
@@ -476,6 +514,35 @@ var BuilderSelectionManager = class {
476
514
  this.notifyPageReady();
477
515
  };
478
516
  }
517
+ /**
518
+ * Navigate to a new route with retry logic for HMR timing
519
+ * Used by AI agent to navigate after creating new pages
520
+ * Retries navigation if route doesn't exist yet (new page being created)
521
+ */
522
+ handleNavigate(path) {
523
+ console.log("[BuilderSelection] Navigate request:", path);
524
+ const MAX_RETRIES = 10;
525
+ const RETRY_DELAY_MS = 300;
526
+ let retries = 0;
527
+ const attemptNavigation = () => {
528
+ history.pushState({}, "", path);
529
+ window.dispatchEvent(new PopStateEvent("popstate"));
530
+ setTimeout(() => {
531
+ if (window.location.pathname === path) {
532
+ console.log("[BuilderSelection] Navigation successful:", path);
533
+ this.notifyPageReady();
534
+ } else if (retries < MAX_RETRIES) {
535
+ retries++;
536
+ console.log(`[BuilderSelection] Navigation retry ${retries}/${MAX_RETRIES} for path: ${path}`);
537
+ setTimeout(attemptNavigation, RETRY_DELAY_MS);
538
+ } else {
539
+ console.warn("[BuilderSelection] Navigation failed after max retries:", path);
540
+ this.notifyPageReady();
541
+ }
542
+ }, 100);
543
+ };
544
+ attemptNavigation();
545
+ }
479
546
  showHoverOverlay(element) {
480
547
  if (!this.hoverOverlay) return;
481
548
  const rect = element.getBoundingClientRect();
@@ -716,6 +783,154 @@ var BuilderSelectionManager = class {
716
783
  document.body.appendChild(container);
717
784
  this.selections.set(selectionId, { element, container, badge, border });
718
785
  }
786
+ /**
787
+ * Get all available routes from the page
788
+ * Looks for routes registered via wouter or standard link patterns
789
+ */
790
+ getAllRoutes() {
791
+ const wouterRoutes = window.__wouterRoutes;
792
+ if (wouterRoutes && Array.isArray(wouterRoutes)) {
793
+ return wouterRoutes;
794
+ }
795
+ const links = document.querySelectorAll('a[href^="/"]');
796
+ const routes = /* @__PURE__ */ new Set(["/"]);
797
+ links.forEach((link) => {
798
+ const href = link.getAttribute("href");
799
+ if (href && href.startsWith("/") && !href.includes("http")) {
800
+ const path = href.split("?")[0].replace(/\/$/, "") || "/";
801
+ routes.add(path);
802
+ }
803
+ });
804
+ const navLinks = document.querySelectorAll("nav a[href]");
805
+ navLinks.forEach((link) => {
806
+ const href = link.getAttribute("href");
807
+ if (href && href.startsWith("/")) {
808
+ const path = href.split("?")[0].replace(/\/$/, "") || "/";
809
+ routes.add(path);
810
+ }
811
+ });
812
+ return Array.from(routes).sort();
813
+ }
814
+ /**
815
+ * Capture full-page screenshots for all pages
816
+ * Non-blocking: uses requestIdleCallback between pages
817
+ * Transfers ArrayBuffer directly (zero-copy) for performance
818
+ */
819
+ async captureAllPagesScreenshots(checkpointId, viewport) {
820
+ console.log("[BuilderSelection] Starting multi-page capture for checkpoint:", checkpointId);
821
+ const routes = this.getAllRoutes();
822
+ const originalPath = window.location.pathname;
823
+ const total = routes.length;
824
+ this.sendToParent({
825
+ type: "CAPTURE_PROGRESS",
826
+ checkpointId,
827
+ completed: 0,
828
+ total
829
+ });
830
+ for (let i = 0; i < routes.length; i++) {
831
+ const pagePath = routes[i];
832
+ try {
833
+ await new Promise((resolve) => {
834
+ if ("requestIdleCallback" in window) {
835
+ requestIdleCallback(() => resolve(), { timeout: 1e3 });
836
+ } else {
837
+ setTimeout(resolve, 50);
838
+ }
839
+ });
840
+ if (window.location.pathname !== pagePath) {
841
+ await this.navigateAndWait(pagePath);
842
+ }
843
+ await new Promise((resolve) => setTimeout(resolve, 100));
844
+ const buffer = await this.capturePageAsArrayBuffer();
845
+ if (buffer) {
846
+ this.sendToParent(
847
+ {
848
+ type: "PAGE_SCREENSHOT_READY",
849
+ checkpointId,
850
+ pagePath,
851
+ buffer
852
+ },
853
+ [buffer]
854
+ // Transfer list - moves ownership, no copy
855
+ );
856
+ }
857
+ this.sendToParent({
858
+ type: "CAPTURE_PROGRESS",
859
+ checkpointId,
860
+ completed: i + 1,
861
+ total
862
+ });
863
+ } catch (error) {
864
+ console.error(`[BuilderSelection] Failed to capture page ${pagePath}:`, error);
865
+ }
866
+ }
867
+ if (window.location.pathname !== originalPath) {
868
+ await this.navigateAndWait(originalPath);
869
+ }
870
+ this.sendToParent({
871
+ type: "ALL_SCREENSHOTS_COMPLETE",
872
+ checkpointId
873
+ });
874
+ console.log("[BuilderSelection] Multi-page capture complete:", checkpointId);
875
+ }
876
+ /**
877
+ * Navigate to a path and wait for page ready
878
+ */
879
+ async navigateAndWait(path) {
880
+ return new Promise((resolve) => {
881
+ history.pushState({}, "", path);
882
+ window.dispatchEvent(new PopStateEvent("popstate"));
883
+ requestAnimationFrame(() => {
884
+ requestAnimationFrame(() => {
885
+ resolve();
886
+ });
887
+ });
888
+ });
889
+ }
890
+ /**
891
+ * Capture current page as ArrayBuffer (for zero-copy transfer)
892
+ */
893
+ async capturePageAsArrayBuffer() {
894
+ try {
895
+ const html2canvas = (await import("html2canvas-pro")).default;
896
+ const overlays = document.querySelectorAll(".builder-selection-container, #builder-hover-overlay");
897
+ overlays.forEach((el) => {
898
+ ;
899
+ el.style.display = "none";
900
+ });
901
+ const originalScrollY = window.scrollY;
902
+ window.scrollTo(0, 0);
903
+ await new Promise((resolve) => requestAnimationFrame(resolve));
904
+ const canvas = await html2canvas(document.body, {
905
+ scale: 0.5,
906
+ // Lower resolution for reasonable file size
907
+ logging: false,
908
+ useCORS: true,
909
+ allowTaint: true,
910
+ backgroundColor: "#ffffff",
911
+ windowHeight: document.body.scrollHeight,
912
+ // Full page height
913
+ height: document.body.scrollHeight
914
+ });
915
+ window.scrollTo(0, originalScrollY);
916
+ overlays.forEach((el) => {
917
+ ;
918
+ el.style.display = "";
919
+ });
920
+ const blob = await new Promise((resolve) => {
921
+ canvas.toBlob((b) => resolve(b), "image/jpeg", 0.6);
922
+ });
923
+ if (!blob) {
924
+ console.error("[BuilderSelection] Failed to create blob from canvas");
925
+ return null;
926
+ }
927
+ const buffer = await blob.arrayBuffer();
928
+ return buffer;
929
+ } catch (error) {
930
+ console.error("[BuilderSelection] Page capture failed:", error);
931
+ return null;
932
+ }
933
+ }
719
934
  };
720
935
  var instance = null;
721
936
  function initBuilderSelection() {
package/dist/plugin.js CHANGED
@@ -10,6 +10,7 @@ function yoamigoPlugin(options = {}) {
10
10
  name: "yoamigo:content-hmr",
11
11
  handleHotUpdate({ file, server }) {
12
12
  if (!file.endsWith("/content.ts") && !file.endsWith("\\content.ts")) return;
13
+ console.log("[yoamigo:content-hmr] content.ts changed, sending HMR event to clients");
13
14
  server.ws.send({
14
15
  type: "custom",
15
16
  event: "yoamigo:content-update"
@@ -21,12 +22,15 @@ function yoamigoPlugin(options = {}) {
21
22
  const hmrCode = `
22
23
  // YoAmigo Content HMR - injected by yoamigo:content-hmr plugin
23
24
  if (import.meta.hot) {
25
+ console.log('[yoamigo:content-hmr] HMR listener registered')
24
26
  import.meta.hot.on('yoamigo:content-update', async () => {
27
+ console.log('[yoamigo:content-hmr] Received content update event')
25
28
  try {
26
29
  const { default: newContent } = await import(/* @vite-ignore */ './content?t=' + Date.now())
27
30
  const { registerContent } = await import('@yoamigo.com/core')
28
31
  registerContent(newContent)
29
32
  window.dispatchEvent(new CustomEvent('content-updated'))
33
+ console.log('[yoamigo:content-hmr] Content updated successfully')
30
34
  } catch (err) {
31
35
  console.error('[yoamigo:content-hmr] Failed to hot reload content:', err)
32
36
  }
package/dist/prod.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { a as ContentStore, E as ContentStoreMode, C as ContentStoreProvider, d as MarkdownText, e as MarkdownTextProps, P as PageInfo, b as StaticImage, c as StaticImageProps, M as StaticText, S as StaticTextProps, b as YaImage, c as YaImageProps, M as YaText, S as YaTextProps, p as parseEmbedUrl, u as useContentStore } from './MarkdownText-DHJo0ofY.js';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import React, { CSSProperties, ReactNode } from 'react';
4
- export { Link, LinkProps, NavigateFunction, Router, RouterProps, useNavigate } from './router.js';
4
+ export { Link, LinkProps, NavigateFunction, Router, RouterProps, ScrollRestoration, useNavigate } from './router.js';
5
5
  export { Route, Switch, useParams } from 'wouter';
6
6
  export { A as AssetResolverFn, C as ContentRegistry, c as contentRegistry, a as getAllContent, g as getContent, h as hasContent, r as registerContent, b as resolveAssetUrl, s as setAssetResolver } from './asset-resolver-BnIvDkVv.js';
7
7
 
package/dist/prod.js CHANGED
@@ -953,6 +953,102 @@ function Router({ children, base }) {
953
953
  return /* @__PURE__ */ jsx12(WouterRouter, { base: basename, children });
954
954
  }
955
955
 
956
+ // src/router/ScrollRestoration.tsx
957
+ import { useEffect as useEffect4, useRef as useRef4 } from "react";
958
+ import { useLocation as useLocation3 } from "wouter";
959
+ var SCROLL_POSITIONS_KEY = "yoamigo-scroll-positions";
960
+ var HISTORY_INDEX_KEY = "yoamigo-history-index";
961
+ function getScrollPositions() {
962
+ if (typeof sessionStorage === "undefined") return {};
963
+ try {
964
+ const stored = sessionStorage.getItem(SCROLL_POSITIONS_KEY);
965
+ return stored ? JSON.parse(stored) : {};
966
+ } catch {
967
+ return {};
968
+ }
969
+ }
970
+ function saveScrollPositions(positions) {
971
+ if (typeof sessionStorage === "undefined") return;
972
+ try {
973
+ sessionStorage.setItem(SCROLL_POSITIONS_KEY, JSON.stringify(positions));
974
+ } catch {
975
+ }
976
+ }
977
+ function getHistoryIndex() {
978
+ if (typeof history === "undefined") return 0;
979
+ const state = history.state;
980
+ if (state && typeof state[HISTORY_INDEX_KEY] === "number") {
981
+ return state[HISTORY_INDEX_KEY];
982
+ }
983
+ return 0;
984
+ }
985
+ function setHistoryIndex(index) {
986
+ if (typeof history === "undefined") return;
987
+ const newState = { ...history.state, [HISTORY_INDEX_KEY]: index };
988
+ history.replaceState(newState, "");
989
+ }
990
+ var globalHistoryIndex = 0;
991
+ function ScrollRestoration() {
992
+ const [location] = useLocation3();
993
+ const previousLocation = useRef4(location);
994
+ const isPopState = useRef4(false);
995
+ const scrollPositionsRef = useRef4({});
996
+ useEffect4(() => {
997
+ if (typeof history !== "undefined" && "scrollRestoration" in history) {
998
+ history.scrollRestoration = "manual";
999
+ }
1000
+ scrollPositionsRef.current = getScrollPositions();
1001
+ const existingIndex = getHistoryIndex();
1002
+ if (existingIndex === 0 && !history.state?.[HISTORY_INDEX_KEY]) {
1003
+ globalHistoryIndex = 1;
1004
+ setHistoryIndex(globalHistoryIndex);
1005
+ } else {
1006
+ globalHistoryIndex = existingIndex;
1007
+ }
1008
+ const handlePopState = () => {
1009
+ isPopState.current = true;
1010
+ };
1011
+ window.addEventListener("popstate", handlePopState);
1012
+ return () => window.removeEventListener("popstate", handlePopState);
1013
+ }, []);
1014
+ useEffect4(() => {
1015
+ if (previousLocation.current === location) return;
1016
+ const prevHistoryIndex = globalHistoryIndex;
1017
+ const currentScrollY = window.scrollY;
1018
+ if (isPopState.current) {
1019
+ isPopState.current = false;
1020
+ globalHistoryIndex = getHistoryIndex() || globalHistoryIndex;
1021
+ const savedScroll = scrollPositionsRef.current[`${globalHistoryIndex}-${location}`];
1022
+ requestAnimationFrame(() => {
1023
+ window.scrollTo({ top: savedScroll ?? 0, behavior: "instant" });
1024
+ });
1025
+ } else {
1026
+ scrollPositionsRef.current[`${prevHistoryIndex}-${previousLocation.current}`] = currentScrollY;
1027
+ saveScrollPositions(scrollPositionsRef.current);
1028
+ globalHistoryIndex = prevHistoryIndex + 1;
1029
+ setHistoryIndex(globalHistoryIndex);
1030
+ window.scrollTo({ top: 0, behavior: "instant" });
1031
+ }
1032
+ previousLocation.current = location;
1033
+ }, [location]);
1034
+ useEffect4(() => {
1035
+ let timeoutId;
1036
+ const handleScroll = () => {
1037
+ clearTimeout(timeoutId);
1038
+ timeoutId = setTimeout(() => {
1039
+ scrollPositionsRef.current[`${globalHistoryIndex}-${location}`] = window.scrollY;
1040
+ saveScrollPositions(scrollPositionsRef.current);
1041
+ }, 100);
1042
+ };
1043
+ window.addEventListener("scroll", handleScroll, { passive: true });
1044
+ return () => {
1045
+ clearTimeout(timeoutId);
1046
+ window.removeEventListener("scroll", handleScroll);
1047
+ };
1048
+ }, [location]);
1049
+ return null;
1050
+ }
1051
+
956
1052
  // src/router/index.ts
957
1053
  import { Route, Switch, useParams } from "wouter";
958
1054
  export {
@@ -962,6 +1058,7 @@ export {
962
1058
  Route,
963
1059
  Router,
964
1060
  SafeHtml,
1061
+ ScrollRestoration,
965
1062
  StaticContainer,
966
1063
  StaticEmbed,
967
1064
  MpImage as StaticImage,
package/dist/router.d.ts CHANGED
@@ -44,4 +44,15 @@ interface RouterProps {
44
44
  }
45
45
  declare function Router({ children, base }: RouterProps): react_jsx_runtime.JSX.Element;
46
46
 
47
- export { Link, type LinkProps, type NavigateFunction, Router, type RouterProps$1 as RouterProps, useNavigate };
47
+ /**
48
+ * ScrollRestoration Component
49
+ *
50
+ * Handles scroll position management for SPA navigation:
51
+ * - Forward navigation (link clicks) → instantly at top (no scroll animation)
52
+ * - Back navigation (browser back) → restore previous scroll position
53
+ *
54
+ * Uses sessionStorage to persist scroll positions across page refreshes.
55
+ */
56
+ declare function ScrollRestoration(): null;
57
+
58
+ export { Link, type LinkProps, type NavigateFunction, Router, type RouterProps$1 as RouterProps, ScrollRestoration, useNavigate };
package/dist/router.js CHANGED
@@ -40,12 +40,109 @@ function Router({ children, base }) {
40
40
  return /* @__PURE__ */ jsx2(WouterRouter, { base: basename, children });
41
41
  }
42
42
 
43
+ // src/router/ScrollRestoration.tsx
44
+ import { useEffect, useRef } from "react";
45
+ import { useLocation as useLocation2 } from "wouter";
46
+ var SCROLL_POSITIONS_KEY = "yoamigo-scroll-positions";
47
+ var HISTORY_INDEX_KEY = "yoamigo-history-index";
48
+ function getScrollPositions() {
49
+ if (typeof sessionStorage === "undefined") return {};
50
+ try {
51
+ const stored = sessionStorage.getItem(SCROLL_POSITIONS_KEY);
52
+ return stored ? JSON.parse(stored) : {};
53
+ } catch {
54
+ return {};
55
+ }
56
+ }
57
+ function saveScrollPositions(positions) {
58
+ if (typeof sessionStorage === "undefined") return;
59
+ try {
60
+ sessionStorage.setItem(SCROLL_POSITIONS_KEY, JSON.stringify(positions));
61
+ } catch {
62
+ }
63
+ }
64
+ function getHistoryIndex() {
65
+ if (typeof history === "undefined") return 0;
66
+ const state = history.state;
67
+ if (state && typeof state[HISTORY_INDEX_KEY] === "number") {
68
+ return state[HISTORY_INDEX_KEY];
69
+ }
70
+ return 0;
71
+ }
72
+ function setHistoryIndex(index) {
73
+ if (typeof history === "undefined") return;
74
+ const newState = { ...history.state, [HISTORY_INDEX_KEY]: index };
75
+ history.replaceState(newState, "");
76
+ }
77
+ var globalHistoryIndex = 0;
78
+ function ScrollRestoration() {
79
+ const [location] = useLocation2();
80
+ const previousLocation = useRef(location);
81
+ const isPopState = useRef(false);
82
+ const scrollPositionsRef = useRef({});
83
+ useEffect(() => {
84
+ if (typeof history !== "undefined" && "scrollRestoration" in history) {
85
+ history.scrollRestoration = "manual";
86
+ }
87
+ scrollPositionsRef.current = getScrollPositions();
88
+ const existingIndex = getHistoryIndex();
89
+ if (existingIndex === 0 && !history.state?.[HISTORY_INDEX_KEY]) {
90
+ globalHistoryIndex = 1;
91
+ setHistoryIndex(globalHistoryIndex);
92
+ } else {
93
+ globalHistoryIndex = existingIndex;
94
+ }
95
+ const handlePopState = () => {
96
+ isPopState.current = true;
97
+ };
98
+ window.addEventListener("popstate", handlePopState);
99
+ return () => window.removeEventListener("popstate", handlePopState);
100
+ }, []);
101
+ useEffect(() => {
102
+ if (previousLocation.current === location) return;
103
+ const prevHistoryIndex = globalHistoryIndex;
104
+ const currentScrollY = window.scrollY;
105
+ if (isPopState.current) {
106
+ isPopState.current = false;
107
+ globalHistoryIndex = getHistoryIndex() || globalHistoryIndex;
108
+ const savedScroll = scrollPositionsRef.current[`${globalHistoryIndex}-${location}`];
109
+ requestAnimationFrame(() => {
110
+ window.scrollTo({ top: savedScroll ?? 0, behavior: "instant" });
111
+ });
112
+ } else {
113
+ scrollPositionsRef.current[`${prevHistoryIndex}-${previousLocation.current}`] = currentScrollY;
114
+ saveScrollPositions(scrollPositionsRef.current);
115
+ globalHistoryIndex = prevHistoryIndex + 1;
116
+ setHistoryIndex(globalHistoryIndex);
117
+ window.scrollTo({ top: 0, behavior: "instant" });
118
+ }
119
+ previousLocation.current = location;
120
+ }, [location]);
121
+ useEffect(() => {
122
+ let timeoutId;
123
+ const handleScroll = () => {
124
+ clearTimeout(timeoutId);
125
+ timeoutId = setTimeout(() => {
126
+ scrollPositionsRef.current[`${globalHistoryIndex}-${location}`] = window.scrollY;
127
+ saveScrollPositions(scrollPositionsRef.current);
128
+ }, 100);
129
+ };
130
+ window.addEventListener("scroll", handleScroll, { passive: true });
131
+ return () => {
132
+ clearTimeout(timeoutId);
133
+ window.removeEventListener("scroll", handleScroll);
134
+ };
135
+ }, [location]);
136
+ return null;
137
+ }
138
+
43
139
  // src/router/index.ts
44
140
  import { Route, Switch, useParams } from "wouter";
45
141
  export {
46
142
  Link,
47
143
  Route,
48
144
  Router,
145
+ ScrollRestoration,
49
146
  Switch,
50
147
  useNavigate,
51
148
  useParams
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoamigo.com/core",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Core components, router, and utilities for YoAmigo templates",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",