@yoamigo.com/core 0.1.0

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.js ADDED
@@ -0,0 +1,2579 @@
1
+ // src/lib/content-registry.ts
2
+ var contentMap = /* @__PURE__ */ new Map();
3
+ function registerContent(content) {
4
+ contentMap = new Map(Object.entries(content));
5
+ }
6
+ function getContent(fieldId) {
7
+ return contentMap.get(fieldId) ?? "";
8
+ }
9
+ function getAllContent() {
10
+ return Object.fromEntries(contentMap);
11
+ }
12
+ function hasContent(fieldId) {
13
+ return contentMap.has(fieldId);
14
+ }
15
+ var contentRegistry = {
16
+ registerContent,
17
+ getContent,
18
+ getAllContent,
19
+ hasContent
20
+ };
21
+
22
+ // src/lib/utils/content-loader.ts
23
+ function getAllContent2() {
24
+ return getAllContent();
25
+ }
26
+
27
+ // src/components/ContentStoreProvider.tsx
28
+ import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
29
+
30
+ // src/lib/image-cache.ts
31
+ var cache = /* @__PURE__ */ new Map();
32
+ function setCachedImages(images) {
33
+ cache.clear();
34
+ const timestamp = Date.now();
35
+ for (const img of images) {
36
+ cache.set(img.key, {
37
+ blobUrl: img.blobUrl,
38
+ originalUrl: img.originalUrl,
39
+ timestamp
40
+ });
41
+ }
42
+ }
43
+ function clearImageCache() {
44
+ cache.clear();
45
+ }
46
+
47
+ // src/lib/image-cache-listener.ts
48
+ var isListening = false;
49
+ function handleMessage(event) {
50
+ const data = event.data;
51
+ if (data?.type === "YA_IMAGE_CACHE_UPDATE") {
52
+ console.log("[ImageCache] Received cache update:", data.images.length, "images");
53
+ setCachedImages(data.images);
54
+ }
55
+ if (data?.type === "YA_IMAGE_CACHE_CLEAR") {
56
+ console.log("[ImageCache] Cache cleared by parent");
57
+ clearImageCache();
58
+ }
59
+ }
60
+ function initImageCacheListener() {
61
+ if (isListening) {
62
+ window.parent.postMessage({ type: "YA_IMAGE_CACHE_REQUEST" }, "*");
63
+ return () => {
64
+ };
65
+ }
66
+ window.addEventListener("message", handleMessage);
67
+ isListening = true;
68
+ window.parent.postMessage({ type: "YA_IMAGE_CACHE_REQUEST" }, "*");
69
+ return () => {
70
+ window.removeEventListener("message", handleMessage);
71
+ isListening = false;
72
+ clearImageCache();
73
+ };
74
+ }
75
+
76
+ // src/components/ContentStoreProvider.tsx
77
+ import { jsx } from "react/jsx-runtime";
78
+ var ContentStoreContext = createContext(null);
79
+ function useContentStore() {
80
+ const context = useContext(ContentStoreContext);
81
+ if (!context) {
82
+ throw new Error("useContentStore must be used within a ContentStoreProvider");
83
+ }
84
+ return context;
85
+ }
86
+ function extractSessionId() {
87
+ const match = window.location.pathname.match(/\/session\/([^/]+)/);
88
+ return match ? match[1] : null;
89
+ }
90
+ function isBuilderPreview() {
91
+ return typeof window !== "undefined" && (window.__YOAMIGO_EDIT_MODE__ === true || import.meta.env?.YA_EDIT_MODE === "true");
92
+ }
93
+ function ContentStoreProvider({
94
+ children,
95
+ initialContent,
96
+ initialMode,
97
+ pages = []
98
+ }) {
99
+ const defaultContent = initialContent ?? getAllContent2();
100
+ const defaultMode = initialMode ?? (isBuilderPreview() ? "inline-edit" : "read-only");
101
+ const [content, setContent] = useState(
102
+ new Map(Object.entries(defaultContent))
103
+ );
104
+ const [mode, setModeState] = useState(defaultMode);
105
+ const [listeners, setListeners] = useState(/* @__PURE__ */ new Set());
106
+ const [activeFieldId, setActiveFieldId] = useState(null);
107
+ useEffect(() => {
108
+ const handleContentUpdate = () => {
109
+ const newContent = getAllContent2();
110
+ setContent(new Map(Object.entries(newContent)));
111
+ };
112
+ window.addEventListener("content-updated", handleContentUpdate);
113
+ return () => window.removeEventListener("content-updated", handleContentUpdate);
114
+ }, []);
115
+ useEffect(() => {
116
+ if (mode !== "inline-edit") return;
117
+ return initImageCacheListener();
118
+ }, [mode]);
119
+ const activeCallbacksRef = useRef(null);
120
+ const notifyListeners = useCallback(() => {
121
+ listeners.forEach((listener) => listener());
122
+ }, [listeners]);
123
+ const getValue = useCallback(
124
+ (fieldId) => {
125
+ return content.get(fieldId) ?? "";
126
+ },
127
+ [content]
128
+ );
129
+ const setValue = useCallback(
130
+ (fieldId, value2) => {
131
+ setContent((prev) => {
132
+ const next = new Map(prev);
133
+ next.set(fieldId, value2);
134
+ return next;
135
+ });
136
+ notifyListeners();
137
+ },
138
+ [notifyListeners]
139
+ );
140
+ const setMode = useCallback(
141
+ (newMode) => {
142
+ setModeState(newMode);
143
+ notifyListeners();
144
+ },
145
+ [notifyListeners]
146
+ );
147
+ const subscribe = useCallback((listener) => {
148
+ setListeners((prev) => {
149
+ const next = new Set(prev);
150
+ next.add(listener);
151
+ return next;
152
+ });
153
+ return () => {
154
+ setListeners((prev) => {
155
+ const next = new Set(prev);
156
+ next.delete(listener);
157
+ return next;
158
+ });
159
+ };
160
+ }, []);
161
+ const clearActiveField = useCallback(() => {
162
+ if (activeCallbacksRef.current) {
163
+ activeCallbacksRef.current.close();
164
+ }
165
+ activeCallbacksRef.current = null;
166
+ setActiveFieldId(null);
167
+ }, []);
168
+ const setActiveField = useCallback((fieldId, callbacks) => {
169
+ if (activeFieldId && activeFieldId !== fieldId && activeCallbacksRef.current) {
170
+ activeCallbacksRef.current.close();
171
+ }
172
+ activeCallbacksRef.current = callbacks;
173
+ setActiveFieldId(fieldId);
174
+ }, [activeFieldId]);
175
+ const saveToWorker = useCallback(
176
+ async (fieldId, value2) => {
177
+ const sessionId = extractSessionId();
178
+ if (!sessionId) {
179
+ console.warn("saveToWorker: Not in builder preview mode, skipping");
180
+ return;
181
+ }
182
+ try {
183
+ const response = await fetch(`/api/session/${sessionId}/inline-edit`, {
184
+ method: "POST",
185
+ headers: { "Content-Type": "application/json" },
186
+ credentials: "include",
187
+ body: JSON.stringify({ fieldId, value: value2 })
188
+ });
189
+ if (!response.ok) {
190
+ const error = await response.json();
191
+ console.error("saveToWorker failed:", error);
192
+ throw new Error(error.error || "Failed to save");
193
+ }
194
+ console.log(`saveToWorker: Saved ${fieldId}`);
195
+ } catch (error) {
196
+ console.error("saveToWorker error:", error);
197
+ throw error;
198
+ }
199
+ },
200
+ []
201
+ );
202
+ useEffect(() => {
203
+ if (!activeFieldId) return;
204
+ const handleClickOutside = (event) => {
205
+ const target = event.target;
206
+ const isInsideYaText = target.closest("[data-field-id]");
207
+ const isInsideBubbleMenu = target.closest(".ya-bubble-menu");
208
+ const isInsideActions = target.closest(".ya-text-actions");
209
+ if (!isInsideYaText && !isInsideBubbleMenu && !isInsideActions) {
210
+ if (activeCallbacksRef.current) {
211
+ activeCallbacksRef.current.close();
212
+ }
213
+ activeCallbacksRef.current = null;
214
+ setActiveFieldId(null);
215
+ }
216
+ };
217
+ document.addEventListener("mousedown", handleClickOutside);
218
+ return () => {
219
+ document.removeEventListener("mousedown", handleClickOutside);
220
+ };
221
+ }, [activeFieldId]);
222
+ useEffect(() => {
223
+ ;
224
+ window.mpContentStore = {
225
+ getValue,
226
+ setValue,
227
+ getMode: () => mode,
228
+ setMode,
229
+ subscribe,
230
+ saveToWorker
231
+ };
232
+ return () => {
233
+ delete window.mpContentStore;
234
+ };
235
+ }, [getValue, setValue, mode, setMode, subscribe, saveToWorker]);
236
+ const getPages = useCallback(() => pages, [pages]);
237
+ const value = {
238
+ getValue,
239
+ setValue,
240
+ mode,
241
+ setMode,
242
+ subscribe,
243
+ saveToWorker,
244
+ activeFieldId,
245
+ setActiveField,
246
+ clearActiveField,
247
+ getPages
248
+ };
249
+ return /* @__PURE__ */ jsx(ContentStoreContext.Provider, { value, children });
250
+ }
251
+
252
+ // src/components/ContentStoreProvider.prod.tsx
253
+ import { createContext as createContext2, useContext as useContext2 } from "react";
254
+ import { jsx as jsx2 } from "react/jsx-runtime";
255
+ var contentMap2 = new Map(Object.entries(getAllContent()));
256
+ var staticStore = {
257
+ getValue: (fieldId) => contentMap2.get(fieldId) ?? "",
258
+ setValue: () => {
259
+ },
260
+ // No-op in production
261
+ mode: "read-only",
262
+ setMode: () => {
263
+ },
264
+ // No-op in production
265
+ subscribe: () => () => {
266
+ },
267
+ // No-op, return empty unsubscribe
268
+ saveToWorker: void 0
269
+ };
270
+ var ContentStoreContext2 = createContext2(staticStore);
271
+ function useContentStore2() {
272
+ return useContext2(ContentStoreContext2);
273
+ }
274
+ function ContentStoreProvider2({ children }) {
275
+ return /* @__PURE__ */ jsx2(ContentStoreContext2.Provider, { value: staticStore, children });
276
+ }
277
+
278
+ // src/components/YaText.tsx
279
+ import { useEffect as useEffect3, useRef as useRef3, useState as useState3, useCallback as useCallback3 } from "react";
280
+ import { createPortal as createPortal2 } from "react-dom";
281
+ import { useEditor, EditorContent } from "@tiptap/react";
282
+ import { BubbleMenu } from "@tiptap/react/menus";
283
+ import StarterKit from "@tiptap/starter-kit";
284
+ import Link from "@tiptap/extension-link";
285
+ import { TextStyle } from "@tiptap/extension-text-style";
286
+ import { Extension } from "@tiptap/core";
287
+
288
+ // src/components/SafeHtml.tsx
289
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2, useCallback as useCallback2 } from "react";
290
+ import { createPortal } from "react-dom";
291
+ import DOMPurify from "dompurify";
292
+ import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
293
+ var ALLOWED_TAGS = ["strong", "em", "a", "span", "br", "b", "i", "u"];
294
+ var ALLOWED_ATTR = ["href", "style", "class", "target", "rel"];
295
+ DOMPurify.addHook("uponSanitizeAttribute", (_node, data) => {
296
+ if (data.attrName === "style") {
297
+ const allowedStyles = ["font-size", "font-weight"];
298
+ const styleValue = data.attrValue;
299
+ const filteredStyles = styleValue.split(";").map((s) => s.trim()).filter((s) => {
300
+ const [prop] = s.split(":").map((p) => p.trim());
301
+ return allowedStyles.includes(prop);
302
+ }).join("; ");
303
+ data.attrValue = filteredStyles;
304
+ }
305
+ });
306
+ function extractLinkDisplayText(href) {
307
+ if (!href) return { text: "link", isExternal: false };
308
+ if (href.startsWith("#")) {
309
+ const section = href.slice(1).replace(/-/g, " ");
310
+ return { text: section || "section", isExternal: false };
311
+ }
312
+ if (href.startsWith("/")) {
313
+ const segments = href.split("/").filter(Boolean);
314
+ const lastSegment = segments[segments.length - 1] || "home";
315
+ const text = lastSegment.replace(/-/g, " ").replace(/^\w/, (c) => c.toUpperCase());
316
+ return { text, isExternal: false };
317
+ }
318
+ if (href.startsWith("mailto:")) {
319
+ return { text: "email", isExternal: true };
320
+ }
321
+ if (href.startsWith("tel:")) {
322
+ return { text: "phone", isExternal: true };
323
+ }
324
+ try {
325
+ const url = new URL(href);
326
+ return {
327
+ text: url.hostname.replace(/^www\./, ""),
328
+ isExternal: true
329
+ };
330
+ } catch {
331
+ return { text: href.slice(0, 20), isExternal: true };
332
+ }
333
+ }
334
+ function LinkIcon() {
335
+ return /* @__PURE__ */ jsxs(
336
+ "svg",
337
+ {
338
+ className: "mp-link-popover-icon",
339
+ viewBox: "0 0 24 24",
340
+ fill: "none",
341
+ stroke: "currentColor",
342
+ strokeWidth: "2",
343
+ strokeLinecap: "round",
344
+ strokeLinejoin: "round",
345
+ children: [
346
+ /* @__PURE__ */ jsx3("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }),
347
+ /* @__PURE__ */ jsx3("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })
348
+ ]
349
+ }
350
+ );
351
+ }
352
+ function LinkPopover({
353
+ displayText,
354
+ position,
355
+ onMouseEnter,
356
+ onMouseLeave,
357
+ onClick
358
+ }) {
359
+ return /* @__PURE__ */ jsxs(
360
+ "div",
361
+ {
362
+ className: "mp-link-popover",
363
+ style: {
364
+ top: position.top,
365
+ left: position.left
366
+ },
367
+ onMouseEnter,
368
+ onMouseLeave,
369
+ onClick,
370
+ role: "button",
371
+ tabIndex: 0,
372
+ onKeyDown: (e) => {
373
+ if (e.key === "Enter" || e.key === " ") {
374
+ e.preventDefault();
375
+ onClick();
376
+ }
377
+ },
378
+ children: [
379
+ /* @__PURE__ */ jsx3(LinkIcon, {}),
380
+ /* @__PURE__ */ jsxs("span", { className: "mp-link-popover-text", children: [
381
+ /* @__PURE__ */ jsx3("span", { className: "mp-link-popover-prefix", children: "Go to " }),
382
+ /* @__PURE__ */ jsx3("span", { className: "mp-link-popover-name", children: displayText })
383
+ ] })
384
+ ]
385
+ }
386
+ );
387
+ }
388
+ function SafeHtml({ content, className, mode = "read-only" }) {
389
+ const containerRef = useRef2(null);
390
+ const showTimerRef = useRef2(void 0);
391
+ const hideTimerRef = useRef2(void 0);
392
+ const [popoverState, setPopoverState] = useState2({
393
+ isVisible: false,
394
+ href: "",
395
+ displayText: "",
396
+ isExternal: false,
397
+ position: { top: 0, left: 0 }
398
+ });
399
+ const sanitized = DOMPurify.sanitize(content, {
400
+ ALLOWED_TAGS,
401
+ ALLOWED_ATTR
402
+ });
403
+ const hidePopover = useCallback2(() => {
404
+ clearTimeout(showTimerRef.current);
405
+ setPopoverState((prev) => ({ ...prev, isVisible: false }));
406
+ }, []);
407
+ const scheduleHide = useCallback2(() => {
408
+ clearTimeout(showTimerRef.current);
409
+ hideTimerRef.current = window.setTimeout(() => {
410
+ hidePopover();
411
+ }, 100);
412
+ }, [hidePopover]);
413
+ const cancelHide = useCallback2(() => {
414
+ clearTimeout(hideTimerRef.current);
415
+ }, []);
416
+ const handlePopoverClick = useCallback2(() => {
417
+ if (popoverState.isExternal) {
418
+ window.open(popoverState.href, "_blank", "noopener,noreferrer");
419
+ } else {
420
+ window.location.href = popoverState.href;
421
+ }
422
+ hidePopover();
423
+ }, [popoverState.href, popoverState.isExternal, hidePopover]);
424
+ useEffect2(() => {
425
+ if (mode !== "inline-edit" || !containerRef.current) return;
426
+ const container = containerRef.current;
427
+ const handleMouseOver = (e) => {
428
+ const link = e.target.closest("a");
429
+ if (!link) return;
430
+ if (e.target.closest(".mp-text-editing, .mp-text-editable")) return;
431
+ const selectModeEnabled = window.__builderSelectModeEnabled;
432
+ if (selectModeEnabled) return;
433
+ clearTimeout(hideTimerRef.current);
434
+ showTimerRef.current = window.setTimeout(() => {
435
+ const href = link.getAttribute("href") || "";
436
+ const { text, isExternal } = extractLinkDisplayText(href);
437
+ const rect = link.getBoundingClientRect();
438
+ const top = rect.bottom + window.scrollY + 8;
439
+ const left = rect.left + rect.width / 2 + window.scrollX;
440
+ setPopoverState({
441
+ isVisible: true,
442
+ href,
443
+ displayText: text,
444
+ isExternal,
445
+ position: { top, left }
446
+ });
447
+ }, 150);
448
+ };
449
+ const handleMouseOut = (e) => {
450
+ const link = e.target.closest("a");
451
+ if (!link) return;
452
+ scheduleHide();
453
+ };
454
+ const handleClick = (e) => {
455
+ if (e.target.closest(".mp-text-editing, .mp-text-editable")) return;
456
+ const yaLink = e.target.closest(".ya-link-editable, .ya-link-editing");
457
+ if (yaLink) {
458
+ if (yaLink.classList.contains("ya-link-editing")) {
459
+ e.preventDefault();
460
+ e.stopPropagation();
461
+ }
462
+ return;
463
+ }
464
+ const link = e.target.closest("a");
465
+ if (link) {
466
+ e.preventDefault();
467
+ e.stopPropagation();
468
+ }
469
+ };
470
+ container.addEventListener("mouseover", handleMouseOver);
471
+ container.addEventListener("mouseout", handleMouseOut);
472
+ container.addEventListener("click", handleClick, true);
473
+ return () => {
474
+ container.removeEventListener("mouseover", handleMouseOver);
475
+ container.removeEventListener("mouseout", handleMouseOut);
476
+ container.removeEventListener("click", handleClick, true);
477
+ clearTimeout(showTimerRef.current);
478
+ clearTimeout(hideTimerRef.current);
479
+ };
480
+ }, [mode, scheduleHide]);
481
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
482
+ /* @__PURE__ */ jsx3(
483
+ "span",
484
+ {
485
+ ref: containerRef,
486
+ className,
487
+ dangerouslySetInnerHTML: { __html: sanitized }
488
+ }
489
+ ),
490
+ mode === "inline-edit" && popoverState.isVisible && createPortal(
491
+ /* @__PURE__ */ jsx3(
492
+ LinkPopover,
493
+ {
494
+ displayText: popoverState.displayText,
495
+ position: popoverState.position,
496
+ onMouseEnter: cancelHide,
497
+ onMouseLeave: hidePopover,
498
+ onClick: handlePopoverClick
499
+ }
500
+ ),
501
+ document.body
502
+ )
503
+ ] });
504
+ }
505
+
506
+ // src/components/YaText.tsx
507
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
508
+ var FontSize = Extension.create({
509
+ name: "fontSize",
510
+ addOptions() {
511
+ return {
512
+ types: ["textStyle"]
513
+ };
514
+ },
515
+ addGlobalAttributes() {
516
+ return [
517
+ {
518
+ types: this.options.types,
519
+ attributes: {
520
+ fontSize: {
521
+ default: null,
522
+ parseHTML: (element) => element.style.fontSize || null,
523
+ renderHTML: (attributes) => {
524
+ if (!attributes.fontSize) {
525
+ return {};
526
+ }
527
+ return {
528
+ style: `font-size: ${attributes.fontSize}`
529
+ };
530
+ }
531
+ }
532
+ }
533
+ }
534
+ ];
535
+ },
536
+ addCommands() {
537
+ return {
538
+ setFontSize: (fontSize) => ({ chain }) => {
539
+ return chain().setMark("textStyle", { fontSize }).run();
540
+ },
541
+ unsetFontSize: () => ({ chain }) => {
542
+ return chain().setMark("textStyle", { fontSize: null }).removeEmptyTextStyle().run();
543
+ }
544
+ };
545
+ }
546
+ });
547
+ var FontWeight = Extension.create({
548
+ name: "fontWeight",
549
+ addOptions() {
550
+ return {
551
+ types: ["textStyle"]
552
+ };
553
+ },
554
+ addGlobalAttributes() {
555
+ return [
556
+ {
557
+ types: this.options.types,
558
+ attributes: {
559
+ fontWeight: {
560
+ default: null,
561
+ parseHTML: (element) => element.style.fontWeight || null,
562
+ renderHTML: (attributes) => {
563
+ if (!attributes.fontWeight) {
564
+ return {};
565
+ }
566
+ return {
567
+ style: `font-weight: ${attributes.fontWeight}`
568
+ };
569
+ }
570
+ }
571
+ }
572
+ }
573
+ ];
574
+ },
575
+ addCommands() {
576
+ return {
577
+ setFontWeight: (fontWeight) => ({ chain }) => {
578
+ return chain().setMark("textStyle", { fontWeight }).run();
579
+ },
580
+ unsetFontWeight: () => ({ chain }) => {
581
+ return chain().setMark("textStyle", { fontWeight: null }).removeEmptyTextStyle().run();
582
+ }
583
+ };
584
+ }
585
+ });
586
+ var SIZE_PRESETS = {
587
+ S: "14px",
588
+ M: "16px",
589
+ L: "20px",
590
+ XL: "24px"
591
+ };
592
+ var WEIGHT_PRESETS = {
593
+ Regular: "400",
594
+ "Semi-bold": "600",
595
+ Bold: "700",
596
+ "Extra-bold": "800"
597
+ };
598
+ function YaText({ fieldId, className, as: Component = "span", children }) {
599
+ const { getValue, setValue, mode, saveToWorker, activeFieldId, setActiveField } = useContentStore();
600
+ const storeContent = getValue(fieldId);
601
+ const content = storeContent || (typeof children === "string" ? children : "");
602
+ const [isEditing, setIsEditing] = useState3(false);
603
+ const [originalContent, setOriginalContent] = useState3(content);
604
+ const containerRef = useRef3(null);
605
+ const originalContentRef = useRef3(content);
606
+ const editor = useEditor({
607
+ extensions: [
608
+ StarterKit.configure({
609
+ // Disable block-level features for inline editing
610
+ heading: false,
611
+ bulletList: false,
612
+ orderedList: false,
613
+ blockquote: false,
614
+ codeBlock: false,
615
+ horizontalRule: false,
616
+ // Disable Link since we configure it separately below
617
+ link: false
618
+ }),
619
+ Link.configure({
620
+ openOnClick: false,
621
+ HTMLAttributes: {
622
+ class: "text-[var(--color-primary)] underline"
623
+ }
624
+ }),
625
+ TextStyle,
626
+ FontSize,
627
+ FontWeight
628
+ ],
629
+ content,
630
+ // Content is now HTML, no conversion needed
631
+ editable: true,
632
+ editorProps: {
633
+ attributes: {
634
+ class: "outline-none"
635
+ }
636
+ },
637
+ parseOptions: { preserveWhitespace: "full" }
638
+ });
639
+ useEffect3(() => {
640
+ if (editor && !isEditing) {
641
+ if (editor.getHTML() !== content) {
642
+ editor.commands.setContent(content, { parseOptions: { preserveWhitespace: "full" } });
643
+ }
644
+ }
645
+ }, [content, editor, isEditing]);
646
+ useEffect3(() => {
647
+ if (isEditing && activeFieldId !== null && activeFieldId !== fieldId) {
648
+ setIsEditing(false);
649
+ }
650
+ }, [activeFieldId, fieldId, isEditing]);
651
+ const handleSave = useCallback3(() => {
652
+ if (!editor) return;
653
+ let html = editor.getHTML();
654
+ html = html.replace(/<\/p><p>/g, "<br><br>").replace(/^<p>/, "").replace(/<\/p>$/, "");
655
+ setValue(fieldId, html);
656
+ saveToWorker?.(fieldId, html);
657
+ setIsEditing(false);
658
+ }, [editor, fieldId, setValue, saveToWorker]);
659
+ const handleCancel = useCallback3(() => {
660
+ if (editor) {
661
+ editor.commands.setContent(originalContent, { parseOptions: { preserveWhitespace: "full" } });
662
+ }
663
+ setIsEditing(false);
664
+ }, [editor, originalContent]);
665
+ const handleClose = useCallback3(() => {
666
+ if (!editor) {
667
+ setIsEditing(false);
668
+ return;
669
+ }
670
+ let currentHtml = editor.getHTML();
671
+ currentHtml = currentHtml.replace(/<\/p><p>/g, "<br><br>").replace(/^<p>/, "").replace(/<\/p>$/, "");
672
+ if (currentHtml !== originalContentRef.current) {
673
+ setValue(fieldId, currentHtml);
674
+ saveToWorker?.(fieldId, currentHtml);
675
+ }
676
+ setIsEditing(false);
677
+ }, [editor, fieldId, setValue, saveToWorker]);
678
+ const handleClick = useCallback3((e) => {
679
+ if (isEditing) {
680
+ e.preventDefault();
681
+ e.stopPropagation();
682
+ return;
683
+ }
684
+ const selectModeEnabled = window.__builderSelectModeEnabled;
685
+ if (selectModeEnabled) {
686
+ return;
687
+ }
688
+ const isInsideLink = e.currentTarget.closest("a") !== null;
689
+ if (isInsideLink) {
690
+ return;
691
+ }
692
+ if (mode === "inline-edit" && !isEditing) {
693
+ e.preventDefault();
694
+ e.stopPropagation();
695
+ setOriginalContent(content);
696
+ originalContentRef.current = content;
697
+ setActiveField(fieldId, { close: handleClose });
698
+ setIsEditing(true);
699
+ setTimeout(() => {
700
+ editor?.chain().focus().selectAll().run();
701
+ }, 20);
702
+ }
703
+ }, [mode, isEditing, content, editor, fieldId, setActiveField, handleClose]);
704
+ const handleKeyDown = useCallback3(
705
+ (event) => {
706
+ if (!isEditing) return;
707
+ if (event.key === "Enter" && !event.shiftKey) {
708
+ event.preventDefault();
709
+ handleSave();
710
+ return;
711
+ }
712
+ if (event.key === "Escape") {
713
+ event.preventDefault();
714
+ handleCancel();
715
+ return;
716
+ }
717
+ if ((event.metaKey || event.ctrlKey) && event.key === "s") {
718
+ event.preventDefault();
719
+ handleSave();
720
+ return;
721
+ }
722
+ },
723
+ [isEditing, handleSave, handleCancel]
724
+ );
725
+ const handleLink = useCallback3(() => {
726
+ if (!editor) return;
727
+ const previousUrl = editor.getAttributes("link").href;
728
+ const url = window.prompt("Enter URL:", previousUrl || "https://");
729
+ if (url === null) return;
730
+ if (url === "") {
731
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
732
+ } else {
733
+ editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
734
+ }
735
+ }, [editor]);
736
+ const handleFontSizeChange = useCallback3(
737
+ (e) => {
738
+ if (!editor) return;
739
+ const size = e.target.value;
740
+ if (size === "") {
741
+ editor.chain().focus().unsetFontSize().run();
742
+ } else {
743
+ editor.chain().focus().setFontSize(size).run();
744
+ }
745
+ },
746
+ [editor]
747
+ );
748
+ const handleFontWeightChange = useCallback3(
749
+ (e) => {
750
+ if (!editor) return;
751
+ const weight = e.target.value;
752
+ if (weight === "") {
753
+ editor.chain().focus().unsetFontWeight().run();
754
+ } else {
755
+ editor.chain().focus().setFontWeight(weight).run();
756
+ }
757
+ },
758
+ [editor]
759
+ );
760
+ const getCurrentFontSize = () => {
761
+ if (!editor) return "";
762
+ const attrs = editor.getAttributes("textStyle");
763
+ return attrs.fontSize || "";
764
+ };
765
+ const getCurrentFontWeight = () => {
766
+ if (!editor) return "";
767
+ const attrs = editor.getAttributes("textStyle");
768
+ return attrs.fontWeight || "";
769
+ };
770
+ if (mode === "read-only") {
771
+ return /* @__PURE__ */ jsx4(
772
+ Component,
773
+ {
774
+ ref: containerRef,
775
+ className,
776
+ "data-ya-restricted": "true",
777
+ "data-field-id": fieldId,
778
+ children: /* @__PURE__ */ jsx4(SafeHtml, { content, mode })
779
+ }
780
+ );
781
+ }
782
+ return /* @__PURE__ */ jsx4(
783
+ Component,
784
+ {
785
+ ref: containerRef,
786
+ className: `${className || ""} ${isEditing ? "ya-text-editing" : "ya-text-editable"}`,
787
+ "data-ya-restricted": "true",
788
+ "data-field-id": fieldId,
789
+ onClick: handleClick,
790
+ onKeyDown: handleKeyDown,
791
+ children: editor ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
792
+ createPortal2(
793
+ /* @__PURE__ */ jsxs2(
794
+ BubbleMenu,
795
+ {
796
+ editor,
797
+ shouldShow: () => isEditing,
798
+ options: { offset: 6, placement: "top" },
799
+ className: "ya-bubble-menu",
800
+ children: [
801
+ /* @__PURE__ */ jsx4(
802
+ "button",
803
+ {
804
+ type: "button",
805
+ onClick: () => editor.chain().focus().toggleBold().run(),
806
+ className: `ya-bubble-btn ${editor.isActive("bold") ? "is-active" : ""}`,
807
+ title: "Bold",
808
+ children: /* @__PURE__ */ jsx4("strong", { children: "B" })
809
+ }
810
+ ),
811
+ /* @__PURE__ */ jsx4(
812
+ "button",
813
+ {
814
+ type: "button",
815
+ onClick: () => editor.chain().focus().toggleItalic().run(),
816
+ className: `ya-bubble-btn ${editor.isActive("italic") ? "is-active" : ""}`,
817
+ title: "Italic",
818
+ children: /* @__PURE__ */ jsx4("em", { children: "I" })
819
+ }
820
+ ),
821
+ /* @__PURE__ */ jsx4(
822
+ "button",
823
+ {
824
+ type: "button",
825
+ onClick: handleLink,
826
+ className: `ya-bubble-btn ${editor.isActive("link") ? "is-active" : ""}`,
827
+ title: "Link",
828
+ children: /* @__PURE__ */ jsx4("span", { children: "\u{1F517}" })
829
+ }
830
+ ),
831
+ /* @__PURE__ */ jsx4("span", { className: "ya-bubble-divider" }),
832
+ /* @__PURE__ */ jsxs2(
833
+ "select",
834
+ {
835
+ value: getCurrentFontSize(),
836
+ onChange: handleFontSizeChange,
837
+ className: "ya-bubble-select",
838
+ title: "Font Size",
839
+ children: [
840
+ /* @__PURE__ */ jsx4("option", { value: "", children: "Size" }),
841
+ Object.entries(SIZE_PRESETS).map(([name, size]) => /* @__PURE__ */ jsx4("option", { value: size, children: name }, name))
842
+ ]
843
+ }
844
+ ),
845
+ /* @__PURE__ */ jsxs2(
846
+ "select",
847
+ {
848
+ value: getCurrentFontWeight(),
849
+ onChange: handleFontWeightChange,
850
+ className: "ya-bubble-select",
851
+ title: "Font Weight",
852
+ children: [
853
+ /* @__PURE__ */ jsx4("option", { value: "", children: "Weight" }),
854
+ Object.entries(WEIGHT_PRESETS).map(([name, weight]) => /* @__PURE__ */ jsx4("option", { value: weight, children: name }, name))
855
+ ]
856
+ }
857
+ )
858
+ ]
859
+ }
860
+ ),
861
+ document.body
862
+ ),
863
+ isEditing ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
864
+ /* @__PURE__ */ jsx4(EditorContent, { editor }),
865
+ /* @__PURE__ */ jsxs2("div", { className: "ya-text-actions", children: [
866
+ /* @__PURE__ */ jsx4(
867
+ "button",
868
+ {
869
+ type: "button",
870
+ onClick: handleCancel,
871
+ className: "ya-text-btn ya-text-btn-cancel",
872
+ children: "Cancel"
873
+ }
874
+ ),
875
+ /* @__PURE__ */ jsx4(
876
+ "button",
877
+ {
878
+ type: "button",
879
+ onClick: handleSave,
880
+ className: "ya-text-btn ya-text-btn-save",
881
+ children: "Save"
882
+ }
883
+ )
884
+ ] })
885
+ ] }) : /* @__PURE__ */ jsx4(SafeHtml, { content, mode })
886
+ ] }) : /* @__PURE__ */ jsx4(SafeHtml, { content, mode })
887
+ }
888
+ );
889
+ }
890
+
891
+ // src/components/YaImage.tsx
892
+ import { useCallback as useCallback5, useEffect as useEffect5, useRef as useRef5, useState as useState5 } from "react";
893
+
894
+ // src/lib/asset-resolver.ts
895
+ var assetResolver = (path) => path;
896
+ function setAssetResolver(resolver) {
897
+ assetResolver = resolver;
898
+ }
899
+ function resolveAssetUrl(path) {
900
+ if (!path) return "";
901
+ if (path.startsWith("http://") || path.startsWith("https://")) {
902
+ return path;
903
+ }
904
+ return assetResolver(path);
905
+ }
906
+
907
+ // src/components/YaTooltip.tsx
908
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState4, useCallback as useCallback4 } from "react";
909
+ import { createPortal as createPortal3 } from "react-dom";
910
+ import { jsx as jsx5 } from "react/jsx-runtime";
911
+ function YaTooltip({
912
+ anchorRef,
913
+ children,
914
+ show,
915
+ preferredPosition = "bottom"
916
+ }) {
917
+ const [position, setPosition] = useState4(preferredPosition);
918
+ const [coords, setCoords] = useState4({ top: 0, left: 0 });
919
+ const [isPositioned, setIsPositioned] = useState4(false);
920
+ const tooltipRef = useRef4(null);
921
+ const calculatePosition = useCallback4(() => {
922
+ if (!anchorRef.current) return;
923
+ const anchor = anchorRef.current.getBoundingClientRect();
924
+ const tooltip = tooltipRef.current?.getBoundingClientRect();
925
+ const tooltipWidth = tooltip?.width || 120;
926
+ const tooltipHeight = tooltip?.height || 40;
927
+ const padding = 8;
928
+ const edgeMargin = 10;
929
+ const spaceBelow = window.innerHeight - anchor.bottom - edgeMargin;
930
+ const spaceAbove = anchor.top - edgeMargin;
931
+ const spaceRight = window.innerWidth - anchor.right - edgeMargin;
932
+ const spaceLeft = anchor.left - edgeMargin;
933
+ let pos = preferredPosition;
934
+ let top = 0;
935
+ let left = 0;
936
+ const positions = [preferredPosition, "bottom", "top", "right", "left"];
937
+ const uniquePositions = [...new Set(positions)];
938
+ for (const tryPos of uniquePositions) {
939
+ let fits = false;
940
+ switch (tryPos) {
941
+ case "bottom":
942
+ if (spaceBelow >= tooltipHeight + padding) {
943
+ pos = "bottom";
944
+ top = anchor.bottom + padding;
945
+ left = anchor.left + anchor.width / 2;
946
+ fits = true;
947
+ }
948
+ break;
949
+ case "top":
950
+ if (spaceAbove >= tooltipHeight + padding) {
951
+ pos = "top";
952
+ top = anchor.top - tooltipHeight - padding;
953
+ left = anchor.left + anchor.width / 2;
954
+ fits = true;
955
+ }
956
+ break;
957
+ case "right":
958
+ if (spaceRight >= tooltipWidth + padding) {
959
+ pos = "right";
960
+ top = anchor.top + anchor.height / 2;
961
+ left = anchor.right + padding;
962
+ fits = true;
963
+ }
964
+ break;
965
+ case "left":
966
+ if (spaceLeft >= tooltipWidth + padding) {
967
+ pos = "left";
968
+ top = anchor.top + anchor.height / 2;
969
+ left = anchor.left - tooltipWidth - padding;
970
+ fits = true;
971
+ }
972
+ break;
973
+ }
974
+ if (fits) break;
975
+ }
976
+ if (pos === "top" || pos === "bottom") {
977
+ const halfWidth = tooltipWidth / 2;
978
+ if (left - halfWidth < edgeMargin) {
979
+ left = edgeMargin + halfWidth;
980
+ } else if (left + halfWidth > window.innerWidth - edgeMargin) {
981
+ left = window.innerWidth - edgeMargin - halfWidth;
982
+ }
983
+ }
984
+ if (pos === "left" || pos === "right") {
985
+ const halfHeight = tooltipHeight / 2;
986
+ if (top - halfHeight < edgeMargin) {
987
+ top = edgeMargin + halfHeight;
988
+ } else if (top + halfHeight > window.innerHeight - edgeMargin) {
989
+ top = window.innerHeight - edgeMargin - halfHeight;
990
+ }
991
+ }
992
+ setPosition(pos);
993
+ setCoords({ top, left });
994
+ setIsPositioned(true);
995
+ }, [anchorRef, preferredPosition]);
996
+ useEffect4(() => {
997
+ if (!show) {
998
+ setIsPositioned(false);
999
+ return;
1000
+ }
1001
+ calculatePosition();
1002
+ window.addEventListener("scroll", calculatePosition, true);
1003
+ window.addEventListener("resize", calculatePosition);
1004
+ return () => {
1005
+ window.removeEventListener("scroll", calculatePosition, true);
1006
+ window.removeEventListener("resize", calculatePosition);
1007
+ };
1008
+ }, [show, calculatePosition]);
1009
+ useEffect4(() => {
1010
+ if (show && tooltipRef.current) {
1011
+ calculatePosition();
1012
+ }
1013
+ }, [show, children, calculatePosition]);
1014
+ if (!show) return null;
1015
+ return createPortal3(
1016
+ /* @__PURE__ */ jsx5(
1017
+ "div",
1018
+ {
1019
+ ref: tooltipRef,
1020
+ className: `ya-tooltip ya-tooltip-${position}`,
1021
+ style: {
1022
+ top: coords.top,
1023
+ left: coords.left,
1024
+ opacity: isPositioned ? 1 : 0,
1025
+ pointerEvents: isPositioned ? "auto" : "none"
1026
+ },
1027
+ role: "tooltip",
1028
+ children
1029
+ }
1030
+ ),
1031
+ document.body
1032
+ );
1033
+ }
1034
+
1035
+ // src/components/YaImage.tsx
1036
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1037
+ function parseImageValue(value) {
1038
+ if (!value) {
1039
+ return { src: "" };
1040
+ }
1041
+ try {
1042
+ const parsed = JSON.parse(value);
1043
+ if (typeof parsed === "object" && parsed.src) {
1044
+ return parsed;
1045
+ }
1046
+ } catch {
1047
+ }
1048
+ return { src: value };
1049
+ }
1050
+ function serializeImageValue(value) {
1051
+ return JSON.stringify(value);
1052
+ }
1053
+ function getObjectPosition(imageData) {
1054
+ if (imageData.focalPoint) {
1055
+ return `${imageData.focalPoint.x}% ${imageData.focalPoint.y}%`;
1056
+ }
1057
+ return imageData.objectPosition || "50% 50%";
1058
+ }
1059
+ var SMALL_IMAGE_THRESHOLD = 100;
1060
+ function YaImage({
1061
+ fieldId,
1062
+ className,
1063
+ alt,
1064
+ objectFit: propObjectFit,
1065
+ objectPosition: propObjectPosition,
1066
+ loading = "lazy",
1067
+ fallbackSrc,
1068
+ fallbackAlt
1069
+ }) {
1070
+ const { getValue, mode } = useContentStore();
1071
+ const containerRef = useRef5(null);
1072
+ const imgRef = useRef5(null);
1073
+ const [isSelected, setIsSelected] = useState5(false);
1074
+ const [isHovered, setIsHovered] = useState5(false);
1075
+ const [isSmallImage, setIsSmallImage] = useState5(false);
1076
+ const rawValue = getValue(fieldId);
1077
+ const imageData = parseImageValue(rawValue);
1078
+ const src = imageData.src || fallbackSrc || "";
1079
+ const altText = imageData.alt || alt || fallbackAlt || "";
1080
+ const objectFit = imageData.objectFit || propObjectFit || "cover";
1081
+ const objectPosition = getObjectPosition(imageData) || propObjectPosition || "50% 50%";
1082
+ const handleClick = useCallback5(() => {
1083
+ if (mode !== "inline-edit") return;
1084
+ if (document.body.classList.contains("builder-selector-active")) return;
1085
+ setIsSelected(true);
1086
+ const rect = containerRef.current?.getBoundingClientRect();
1087
+ window.parent.postMessage(
1088
+ {
1089
+ type: "YA_IMAGE_EDIT_REQUEST",
1090
+ fieldId,
1091
+ currentValue: {
1092
+ ...imageData,
1093
+ src,
1094
+ // Raw path, parent resolves display URL via asset cache
1095
+ alt: altText,
1096
+ objectFit,
1097
+ objectPosition
1098
+ },
1099
+ // Include element rect for floating popover positioning
1100
+ elementRect: rect ? {
1101
+ top: rect.top,
1102
+ left: rect.left,
1103
+ width: rect.width,
1104
+ height: rect.height
1105
+ } : void 0
1106
+ },
1107
+ "*"
1108
+ );
1109
+ }, [mode, fieldId, imageData, src, altText, objectFit, objectPosition]);
1110
+ useEffect5(() => {
1111
+ if (mode !== "inline-edit") return;
1112
+ const handleMessage2 = (event) => {
1113
+ if (event.data?.type === "YA_IMAGE_EDIT_COMPLETE" && event.data.fieldId === fieldId) {
1114
+ setIsSelected(false);
1115
+ }
1116
+ if (event.data?.type === "YA_IMAGE_EDIT_CANCEL" && event.data.fieldId === fieldId) {
1117
+ setIsSelected(false);
1118
+ }
1119
+ if (event.data?.type === "YA_IMAGE_UPDATE_PREVIEW" && event.data.fieldId === fieldId) {
1120
+ }
1121
+ };
1122
+ window.addEventListener("message", handleMessage2);
1123
+ return () => window.removeEventListener("message", handleMessage2);
1124
+ }, [mode, fieldId]);
1125
+ useEffect5(() => {
1126
+ if (mode !== "inline-edit") return;
1127
+ const checkSize = () => {
1128
+ if (imgRef.current) {
1129
+ const { width, height } = imgRef.current.getBoundingClientRect();
1130
+ setIsSmallImage(width < SMALL_IMAGE_THRESHOLD || height < SMALL_IMAGE_THRESHOLD);
1131
+ }
1132
+ };
1133
+ const img = imgRef.current;
1134
+ if (img) {
1135
+ if (img.complete) {
1136
+ checkSize();
1137
+ } else {
1138
+ img.addEventListener("load", checkSize);
1139
+ }
1140
+ }
1141
+ window.addEventListener("resize", checkSize);
1142
+ return () => {
1143
+ img?.removeEventListener("load", checkSize);
1144
+ window.removeEventListener("resize", checkSize);
1145
+ };
1146
+ }, [mode]);
1147
+ useEffect5(() => {
1148
+ if (!isSelected || mode !== "inline-edit") return;
1149
+ let lastRectKey = "";
1150
+ let lastTime = 0;
1151
+ let rafId;
1152
+ const loop = (time) => {
1153
+ if (time - lastTime >= 50) {
1154
+ const rect = containerRef.current?.getBoundingClientRect();
1155
+ if (rect) {
1156
+ const rectKey = `${Math.round(rect.top)},${Math.round(rect.left)},${Math.round(rect.width)},${Math.round(rect.height)}`;
1157
+ if (rectKey !== lastRectKey) {
1158
+ lastRectKey = rectKey;
1159
+ window.parent.postMessage(
1160
+ {
1161
+ type: "YA_IMAGE_POSITION_UPDATE",
1162
+ fieldId,
1163
+ elementRect: {
1164
+ top: rect.top,
1165
+ left: rect.left,
1166
+ width: rect.width,
1167
+ height: rect.height
1168
+ }
1169
+ },
1170
+ "*"
1171
+ );
1172
+ }
1173
+ }
1174
+ lastTime = time;
1175
+ }
1176
+ rafId = requestAnimationFrame(loop);
1177
+ };
1178
+ rafId = requestAnimationFrame(loop);
1179
+ return () => cancelAnimationFrame(rafId);
1180
+ }, [isSelected, fieldId, mode]);
1181
+ if (mode === "read-only") {
1182
+ return /* @__PURE__ */ jsx6(
1183
+ "img",
1184
+ {
1185
+ src: resolveAssetUrl(src),
1186
+ alt: altText,
1187
+ className,
1188
+ style: {
1189
+ objectFit,
1190
+ objectPosition
1191
+ },
1192
+ loading,
1193
+ "data-ya-restricted": "true",
1194
+ "data-field-id": fieldId
1195
+ }
1196
+ );
1197
+ }
1198
+ const editIcon = /* @__PURE__ */ jsxs3(
1199
+ "svg",
1200
+ {
1201
+ width: "24",
1202
+ height: "24",
1203
+ viewBox: "0 0 24 24",
1204
+ fill: "none",
1205
+ stroke: "currentColor",
1206
+ strokeWidth: "2",
1207
+ strokeLinecap: "round",
1208
+ strokeLinejoin: "round",
1209
+ children: [
1210
+ /* @__PURE__ */ jsx6("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
1211
+ /* @__PURE__ */ jsx6("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
1212
+ /* @__PURE__ */ jsx6("polyline", { points: "21 15 16 10 5 21" })
1213
+ ]
1214
+ }
1215
+ );
1216
+ return /* @__PURE__ */ jsxs3(
1217
+ "div",
1218
+ {
1219
+ ref: containerRef,
1220
+ className: `ya-image-container ${isSelected ? "ya-image-selected" : "ya-image-editable"} ${isSmallImage ? "ya-image-small" : ""}`,
1221
+ onClick: handleClick,
1222
+ onMouseEnter: () => setIsHovered(true),
1223
+ onMouseLeave: () => setIsHovered(false),
1224
+ "data-ya-restricted": "true",
1225
+ "data-field-id": fieldId,
1226
+ "data-ya-image": "true",
1227
+ role: "button",
1228
+ tabIndex: 0,
1229
+ "aria-label": `Edit image: ${altText || fieldId}`,
1230
+ onKeyDown: (e) => {
1231
+ if (e.key === "Enter" || e.key === " ") {
1232
+ e.preventDefault();
1233
+ handleClick();
1234
+ }
1235
+ },
1236
+ children: [
1237
+ /* @__PURE__ */ jsx6(
1238
+ "img",
1239
+ {
1240
+ ref: imgRef,
1241
+ src: resolveAssetUrl(src),
1242
+ alt: altText,
1243
+ className,
1244
+ style: {
1245
+ objectFit,
1246
+ objectPosition
1247
+ },
1248
+ loading
1249
+ }
1250
+ ),
1251
+ isSmallImage ? /* @__PURE__ */ jsxs3(YaTooltip, { anchorRef: containerRef, show: isHovered && !isSelected, children: [
1252
+ editIcon,
1253
+ /* @__PURE__ */ jsx6("span", { children: "Click to edit" })
1254
+ ] }) : (
1255
+ /* For large images: show overlay inside the image */
1256
+ /* @__PURE__ */ jsxs3("div", { className: "ya-image-overlay", children: [
1257
+ /* @__PURE__ */ jsx6("div", { className: "ya-image-edit-icon", children: editIcon }),
1258
+ /* @__PURE__ */ jsx6("span", { className: "ya-image-edit-label", children: "Click to edit" })
1259
+ ] })
1260
+ )
1261
+ ]
1262
+ }
1263
+ );
1264
+ }
1265
+
1266
+ // src/components/YaLink.tsx
1267
+ import { useEffect as useEffect6, useRef as useRef6, useState as useState6, useCallback as useCallback6 } from "react";
1268
+ import { createPortal as createPortal4 } from "react-dom";
1269
+ import { useEditor as useEditor2, EditorContent as EditorContent2 } from "@tiptap/react";
1270
+ import { BubbleMenu as BubbleMenu2 } from "@tiptap/react/menus";
1271
+ import StarterKit2 from "@tiptap/starter-kit";
1272
+ import { TextStyle as TextStyle2 } from "@tiptap/extension-text-style";
1273
+ import { Extension as Extension2 } from "@tiptap/core";
1274
+ import { Link as WouterLink, useLocation } from "wouter";
1275
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
1276
+ function isInternalPath(path) {
1277
+ if (!path) return false;
1278
+ if (path.startsWith("#")) return false;
1279
+ if (path.startsWith("//")) return false;
1280
+ if (path.includes("://")) return false;
1281
+ if (path.startsWith("mailto:") || path.startsWith("tel:")) return false;
1282
+ return path.startsWith("/");
1283
+ }
1284
+ var FontSize2 = Extension2.create({
1285
+ name: "fontSize",
1286
+ addOptions() {
1287
+ return { types: ["textStyle"] };
1288
+ },
1289
+ addGlobalAttributes() {
1290
+ return [
1291
+ {
1292
+ types: this.options.types,
1293
+ attributes: {
1294
+ fontSize: {
1295
+ default: null,
1296
+ parseHTML: (element) => element.style.fontSize || null,
1297
+ renderHTML: (attributes) => {
1298
+ if (!attributes.fontSize) return {};
1299
+ return { style: `font-size: ${attributes.fontSize}` };
1300
+ }
1301
+ }
1302
+ }
1303
+ }
1304
+ ];
1305
+ },
1306
+ addCommands() {
1307
+ return {
1308
+ setFontSize: (fontSize) => ({ chain }) => chain().setMark("textStyle", { fontSize }).run(),
1309
+ unsetFontSize: () => ({ chain }) => chain().setMark("textStyle", { fontSize: null }).removeEmptyTextStyle().run()
1310
+ };
1311
+ }
1312
+ });
1313
+ var FontWeight2 = Extension2.create({
1314
+ name: "fontWeight",
1315
+ addOptions() {
1316
+ return { types: ["textStyle"] };
1317
+ },
1318
+ addGlobalAttributes() {
1319
+ return [
1320
+ {
1321
+ types: this.options.types,
1322
+ attributes: {
1323
+ fontWeight: {
1324
+ default: null,
1325
+ parseHTML: (element) => element.style.fontWeight || null,
1326
+ renderHTML: (attributes) => {
1327
+ if (!attributes.fontWeight) return {};
1328
+ return { style: `font-weight: ${attributes.fontWeight}` };
1329
+ }
1330
+ }
1331
+ }
1332
+ }
1333
+ ];
1334
+ },
1335
+ addCommands() {
1336
+ return {
1337
+ setFontWeight: (fontWeight) => ({ chain }) => chain().setMark("textStyle", { fontWeight }).run(),
1338
+ unsetFontWeight: () => ({ chain }) => chain().setMark("textStyle", { fontWeight: null }).removeEmptyTextStyle().run()
1339
+ };
1340
+ }
1341
+ });
1342
+ var SIZE_PRESETS2 = {
1343
+ S: "14px",
1344
+ M: "16px",
1345
+ L: "20px",
1346
+ XL: "24px"
1347
+ };
1348
+ var WEIGHT_PRESETS2 = {
1349
+ Regular: "400",
1350
+ "Semi-bold": "600",
1351
+ Bold: "700",
1352
+ "Extra-bold": "800"
1353
+ };
1354
+ function discoverSectionsFromDOM() {
1355
+ const sections = [];
1356
+ const selectableElements = document.querySelectorAll("[data-mp-selectable]");
1357
+ selectableElements.forEach((el) => {
1358
+ const id = el.getAttribute("data-mp-selectable") || el.id;
1359
+ if (id) {
1360
+ const label = id.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1361
+ sections.push({ path: `#${id}`, label });
1362
+ }
1363
+ });
1364
+ return sections;
1365
+ }
1366
+ function YaLink({ fieldId, href: defaultHref = "#", className, as: Component = "a", children, availablePages, onClick }) {
1367
+ const { getValue, setValue, mode, saveToWorker, getPages } = useContentStore();
1368
+ const [, navigate] = useLocation();
1369
+ const pages = availablePages ?? getPages();
1370
+ const [sections, setSections] = useState6([]);
1371
+ const [sectionsExpanded, setSectionsExpanded] = useState6(false);
1372
+ const textFieldId = `${fieldId}.text`;
1373
+ const hrefFieldId = `${fieldId}.href`;
1374
+ const storeText = getValue(textFieldId);
1375
+ const storeHref = getValue(hrefFieldId);
1376
+ const isIconMode = children != null && typeof children !== "string";
1377
+ const text = storeText || (typeof children === "string" ? children : "");
1378
+ const href = storeHref || defaultHref;
1379
+ const [editingMode, setEditingMode] = useState6(null);
1380
+ const [showEditPopover, setShowEditPopover] = useState6(false);
1381
+ const [originalText, setOriginalText] = useState6(text);
1382
+ const [originalHref, setOriginalHref] = useState6(href);
1383
+ const [currentHref, setCurrentHref] = useState6(href);
1384
+ const [isExternalUrl, setIsExternalUrl] = useState6(false);
1385
+ const [externalUrl, setExternalUrl] = useState6("");
1386
+ const containerRef = useRef6(null);
1387
+ const hrefPopoverRef = useRef6(null);
1388
+ const hidePopoverTimeoutRef = useRef6(null);
1389
+ const editor = useEditor2({
1390
+ extensions: [
1391
+ StarterKit2.configure({
1392
+ heading: false,
1393
+ bulletList: false,
1394
+ orderedList: false,
1395
+ blockquote: false,
1396
+ codeBlock: false,
1397
+ horizontalRule: false
1398
+ }),
1399
+ TextStyle2,
1400
+ FontSize2,
1401
+ FontWeight2
1402
+ ],
1403
+ content: text,
1404
+ editable: true,
1405
+ editorProps: {
1406
+ attributes: {
1407
+ class: "outline-none"
1408
+ }
1409
+ }
1410
+ });
1411
+ useEffect6(() => {
1412
+ if (editor && editingMode !== "text") {
1413
+ if (editor.getHTML() !== text) {
1414
+ editor.commands.setContent(text);
1415
+ }
1416
+ }
1417
+ }, [text, editor, editingMode]);
1418
+ useEffect6(() => {
1419
+ if (editingMode !== "link") {
1420
+ setCurrentHref(href);
1421
+ }
1422
+ }, [href, editingMode]);
1423
+ useEffect6(() => {
1424
+ return () => {
1425
+ if (hidePopoverTimeoutRef.current) {
1426
+ clearTimeout(hidePopoverTimeoutRef.current);
1427
+ }
1428
+ };
1429
+ }, []);
1430
+ useEffect6(() => {
1431
+ if (editingMode !== "link") return;
1432
+ const handleClickOutside = (event) => {
1433
+ const target = event.target;
1434
+ if (hrefPopoverRef.current?.contains(target)) return;
1435
+ if (containerRef.current?.contains(target)) return;
1436
+ setCurrentHref(originalHref);
1437
+ setEditingMode(null);
1438
+ setIsExternalUrl(false);
1439
+ setExternalUrl("");
1440
+ };
1441
+ document.addEventListener("mousedown", handleClickOutside);
1442
+ return () => document.removeEventListener("mousedown", handleClickOutside);
1443
+ }, [editingMode, originalHref]);
1444
+ const handleSaveText = useCallback6(() => {
1445
+ if (!editor) return;
1446
+ let html = editor.getHTML();
1447
+ html = html.replace(/<\/p><p>/g, "<br><br>").replace(/^<p>/, "").replace(/<\/p>$/, "");
1448
+ setValue(textFieldId, html);
1449
+ saveToWorker?.(textFieldId, html);
1450
+ setEditingMode(null);
1451
+ }, [editor, textFieldId, setValue, saveToWorker]);
1452
+ const handleSaveLink = useCallback6(() => {
1453
+ setValue(hrefFieldId, currentHref);
1454
+ saveToWorker?.(hrefFieldId, currentHref);
1455
+ setEditingMode(null);
1456
+ setIsExternalUrl(false);
1457
+ setExternalUrl("");
1458
+ }, [hrefFieldId, currentHref, setValue, saveToWorker]);
1459
+ const handleCancelText = useCallback6(() => {
1460
+ if (editor) {
1461
+ editor.commands.setContent(originalText);
1462
+ }
1463
+ setEditingMode(null);
1464
+ }, [editor, originalText]);
1465
+ const handleCancelLink = useCallback6(() => {
1466
+ setCurrentHref(originalHref);
1467
+ setEditingMode(null);
1468
+ setIsExternalUrl(false);
1469
+ setExternalUrl("");
1470
+ }, [originalHref]);
1471
+ const handleClick = useCallback6(
1472
+ (e) => {
1473
+ const selectModeEnabled = window.__builderSelectModeEnabled;
1474
+ if (selectModeEnabled) {
1475
+ e.preventDefault();
1476
+ return;
1477
+ }
1478
+ if (href.startsWith("#")) {
1479
+ e.preventDefault();
1480
+ const targetId = href.substring(1);
1481
+ const targetElement = document.getElementById(targetId);
1482
+ if (targetElement) {
1483
+ const navHeight = 118;
1484
+ const targetPosition = targetElement.offsetTop - navHeight;
1485
+ window.scrollTo({
1486
+ top: targetPosition,
1487
+ behavior: "smooth"
1488
+ });
1489
+ }
1490
+ onClick?.();
1491
+ return;
1492
+ }
1493
+ if (isInternalPath(href)) {
1494
+ e.preventDefault();
1495
+ navigate(href);
1496
+ onClick?.();
1497
+ return;
1498
+ }
1499
+ onClick?.();
1500
+ },
1501
+ [href, navigate, onClick]
1502
+ );
1503
+ const handleMouseEnter = useCallback6(() => {
1504
+ if (hidePopoverTimeoutRef.current) {
1505
+ clearTimeout(hidePopoverTimeoutRef.current);
1506
+ hidePopoverTimeoutRef.current = null;
1507
+ }
1508
+ if (mode === "inline-edit" && !editingMode) {
1509
+ setShowEditPopover(true);
1510
+ }
1511
+ }, [mode, editingMode]);
1512
+ const handleMouseLeave = useCallback6(() => {
1513
+ hidePopoverTimeoutRef.current = window.setTimeout(() => {
1514
+ if (!editingMode) {
1515
+ setShowEditPopover(false);
1516
+ }
1517
+ }, 150);
1518
+ }, [editingMode]);
1519
+ const handleFocus = useCallback6(() => {
1520
+ if (mode === "inline-edit" && !editingMode) {
1521
+ setShowEditPopover(true);
1522
+ }
1523
+ }, [mode, editingMode]);
1524
+ const startEditText = useCallback6(() => {
1525
+ setShowEditPopover(false);
1526
+ setEditingMode("text");
1527
+ setOriginalText(text);
1528
+ setTimeout(() => {
1529
+ editor?.chain().focus().selectAll().run();
1530
+ }, 20);
1531
+ }, [text, editor]);
1532
+ const startEditLink = useCallback6(() => {
1533
+ setShowEditPopover(false);
1534
+ setEditingMode("link");
1535
+ setOriginalHref(href);
1536
+ setCurrentHref(href);
1537
+ setSections(discoverSectionsFromDOM());
1538
+ }, [href]);
1539
+ const handleKeyDown = useCallback6(
1540
+ (event) => {
1541
+ if (editingMode !== "text") return;
1542
+ if (event.key === "Enter" && !event.shiftKey) {
1543
+ event.preventDefault();
1544
+ handleSaveText();
1545
+ return;
1546
+ }
1547
+ if (event.key === "Escape") {
1548
+ event.preventDefault();
1549
+ handleCancelText();
1550
+ return;
1551
+ }
1552
+ if ((event.metaKey || event.ctrlKey) && event.key === "s") {
1553
+ event.preventDefault();
1554
+ handleSaveText();
1555
+ return;
1556
+ }
1557
+ },
1558
+ [editingMode, handleSaveText, handleCancelText]
1559
+ );
1560
+ const handleFontSizeChange = useCallback6(
1561
+ (e) => {
1562
+ if (!editor) return;
1563
+ const size = e.target.value;
1564
+ if (size === "") {
1565
+ editor.chain().focus().unsetFontSize().run();
1566
+ } else {
1567
+ editor.chain().focus().setFontSize(size).run();
1568
+ }
1569
+ },
1570
+ [editor]
1571
+ );
1572
+ const handleFontWeightChange = useCallback6(
1573
+ (e) => {
1574
+ if (!editor) return;
1575
+ const weight = e.target.value;
1576
+ if (weight === "") {
1577
+ editor.chain().focus().unsetFontWeight().run();
1578
+ } else {
1579
+ editor.chain().focus().setFontWeight(weight).run();
1580
+ }
1581
+ },
1582
+ [editor]
1583
+ );
1584
+ const handlePageSelect = useCallback6((path) => {
1585
+ setCurrentHref(path);
1586
+ setIsExternalUrl(false);
1587
+ }, []);
1588
+ const handleExternalUrlApply = useCallback6(() => {
1589
+ if (externalUrl) {
1590
+ setCurrentHref(externalUrl);
1591
+ }
1592
+ }, [externalUrl]);
1593
+ const getCurrentFontSize = () => {
1594
+ if (!editor) return "";
1595
+ const attrs = editor.getAttributes("textStyle");
1596
+ return attrs.fontSize || "";
1597
+ };
1598
+ const getCurrentFontWeight = () => {
1599
+ if (!editor) return "";
1600
+ const attrs = editor.getAttributes("textStyle");
1601
+ return attrs.fontWeight || "";
1602
+ };
1603
+ if (mode === "read-only") {
1604
+ const content = isIconMode ? children : /* @__PURE__ */ jsx7(SafeHtml, { content: text, mode });
1605
+ if (isInternalPath(href)) {
1606
+ return /* @__PURE__ */ jsx7(
1607
+ WouterLink,
1608
+ {
1609
+ href,
1610
+ className,
1611
+ "data-ya-restricted": "true",
1612
+ "data-field-id": fieldId,
1613
+ children: content
1614
+ }
1615
+ );
1616
+ }
1617
+ return /* @__PURE__ */ jsx7(
1618
+ Component,
1619
+ {
1620
+ ref: containerRef,
1621
+ href: Component === "a" ? href : void 0,
1622
+ className,
1623
+ "data-ya-restricted": "true",
1624
+ "data-field-id": fieldId,
1625
+ children: content
1626
+ }
1627
+ );
1628
+ }
1629
+ return /* @__PURE__ */ jsxs4("span", { className: "ya-link-wrapper", children: [
1630
+ /* @__PURE__ */ jsx7(
1631
+ Component,
1632
+ {
1633
+ ref: containerRef,
1634
+ href: Component === "a" ? href : void 0,
1635
+ className: `${className || ""} ${editingMode ? "ya-link-editing" : "ya-link-editable"}`,
1636
+ "data-ya-restricted": "true",
1637
+ "data-field-id": fieldId,
1638
+ onClick: handleClick,
1639
+ onMouseEnter: handleMouseEnter,
1640
+ onMouseLeave: handleMouseLeave,
1641
+ onFocus: handleFocus,
1642
+ onKeyDown: handleKeyDown,
1643
+ children: isIconMode ? (
1644
+ // Icon mode: render children directly, no text editing
1645
+ children
1646
+ ) : editor ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
1647
+ createPortal4(
1648
+ /* @__PURE__ */ jsxs4(
1649
+ BubbleMenu2,
1650
+ {
1651
+ editor,
1652
+ shouldShow: () => editingMode === "text",
1653
+ options: { offset: 6, placement: "top" },
1654
+ className: "ya-bubble-menu",
1655
+ children: [
1656
+ /* @__PURE__ */ jsx7(
1657
+ "button",
1658
+ {
1659
+ type: "button",
1660
+ onClick: () => editor.chain().focus().toggleBold().run(),
1661
+ className: `ya-bubble-btn ${editor.isActive("bold") ? "is-active" : ""}`,
1662
+ title: "Bold",
1663
+ children: /* @__PURE__ */ jsx7("strong", { children: "B" })
1664
+ }
1665
+ ),
1666
+ /* @__PURE__ */ jsx7(
1667
+ "button",
1668
+ {
1669
+ type: "button",
1670
+ onClick: () => editor.chain().focus().toggleItalic().run(),
1671
+ className: `ya-bubble-btn ${editor.isActive("italic") ? "is-active" : ""}`,
1672
+ title: "Italic",
1673
+ children: /* @__PURE__ */ jsx7("em", { children: "I" })
1674
+ }
1675
+ ),
1676
+ /* @__PURE__ */ jsx7("span", { className: "ya-bubble-divider" }),
1677
+ /* @__PURE__ */ jsxs4(
1678
+ "select",
1679
+ {
1680
+ value: getCurrentFontSize(),
1681
+ onChange: handleFontSizeChange,
1682
+ className: "ya-bubble-select",
1683
+ title: "Font Size",
1684
+ children: [
1685
+ /* @__PURE__ */ jsx7("option", { value: "", children: "Size" }),
1686
+ Object.entries(SIZE_PRESETS2).map(([name, size]) => /* @__PURE__ */ jsx7("option", { value: size, children: name }, name))
1687
+ ]
1688
+ }
1689
+ ),
1690
+ /* @__PURE__ */ jsxs4(
1691
+ "select",
1692
+ {
1693
+ value: getCurrentFontWeight(),
1694
+ onChange: handleFontWeightChange,
1695
+ className: "ya-bubble-select",
1696
+ title: "Font Weight",
1697
+ children: [
1698
+ /* @__PURE__ */ jsx7("option", { value: "", children: "Weight" }),
1699
+ Object.entries(WEIGHT_PRESETS2).map(([name, weight]) => /* @__PURE__ */ jsx7("option", { value: weight, children: name }, name))
1700
+ ]
1701
+ }
1702
+ )
1703
+ ]
1704
+ }
1705
+ ),
1706
+ document.body
1707
+ ),
1708
+ editingMode === "text" ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
1709
+ /* @__PURE__ */ jsx7(EditorContent2, { editor }),
1710
+ /* @__PURE__ */ jsxs4("div", { className: "ya-link-actions", children: [
1711
+ /* @__PURE__ */ jsx7("button", { type: "button", onClick: handleCancelText, className: "ya-link-btn ya-link-btn-cancel", children: "Cancel" }),
1712
+ /* @__PURE__ */ jsx7("button", { type: "button", onClick: handleSaveText, className: "ya-link-btn ya-link-btn-save", children: "Save" })
1713
+ ] })
1714
+ ] }) : /* @__PURE__ */ jsx7(SafeHtml, { content: text, mode })
1715
+ ] }) : /* @__PURE__ */ jsx7(SafeHtml, { content: text, mode })
1716
+ }
1717
+ ),
1718
+ showEditPopover && !editingMode && mode === "inline-edit" && /* @__PURE__ */ jsxs4("div", { className: "ya-link-edit-popover", onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [
1719
+ !isIconMode && /* @__PURE__ */ jsx7("button", { type: "button", onClick: startEditText, children: "Edit text" }),
1720
+ /* @__PURE__ */ jsx7("button", { type: "button", onClick: startEditLink, children: "Edit link" })
1721
+ ] }),
1722
+ editingMode === "link" && /* @__PURE__ */ jsxs4("div", { ref: hrefPopoverRef, className: "ya-href-popover", children: [
1723
+ /* @__PURE__ */ jsx7("div", { className: "ya-href-popover-header", children: "Link destination" }),
1724
+ !isExternalUrl ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
1725
+ sections.length > 0 && /* @__PURE__ */ jsxs4("div", { className: "ya-href-popover-section", children: [
1726
+ /* @__PURE__ */ jsxs4(
1727
+ "button",
1728
+ {
1729
+ type: "button",
1730
+ className: "ya-href-popover-label ya-href-collapsible-header",
1731
+ onClick: () => setSectionsExpanded(!sectionsExpanded),
1732
+ children: [
1733
+ /* @__PURE__ */ jsx7("span", { className: "ya-href-chevron", children: sectionsExpanded ? "\u25BC" : "\u25B6" }),
1734
+ "Scroll to section (",
1735
+ sections.length,
1736
+ ")"
1737
+ ]
1738
+ }
1739
+ ),
1740
+ sectionsExpanded && /* @__PURE__ */ jsx7("div", { className: "ya-href-popover-pages", children: sections.map((section) => /* @__PURE__ */ jsxs4(
1741
+ "button",
1742
+ {
1743
+ type: "button",
1744
+ className: `ya-href-page-btn ${currentHref === section.path ? "is-selected" : ""}`,
1745
+ onClick: () => handlePageSelect(section.path),
1746
+ children: [
1747
+ section.label,
1748
+ /* @__PURE__ */ jsx7("span", { className: "ya-href-page-path", children: section.path })
1749
+ ]
1750
+ },
1751
+ section.path
1752
+ )) })
1753
+ ] }),
1754
+ pages.length > 0 && /* @__PURE__ */ jsxs4("div", { className: "ya-href-popover-section", children: [
1755
+ /* @__PURE__ */ jsx7("label", { className: "ya-href-popover-label", children: "Navigate to page" }),
1756
+ /* @__PURE__ */ jsx7("div", { className: "ya-href-popover-pages", children: pages.map((page) => /* @__PURE__ */ jsxs4(
1757
+ "button",
1758
+ {
1759
+ type: "button",
1760
+ className: `ya-href-page-btn ${currentHref === page.path ? "is-selected" : ""}`,
1761
+ onClick: () => handlePageSelect(page.path),
1762
+ children: [
1763
+ page.label,
1764
+ /* @__PURE__ */ jsx7("span", { className: "ya-href-page-path", children: page.path })
1765
+ ]
1766
+ },
1767
+ page.path
1768
+ )) })
1769
+ ] }),
1770
+ /* @__PURE__ */ jsx7(
1771
+ "button",
1772
+ {
1773
+ type: "button",
1774
+ className: "ya-href-external-toggle",
1775
+ onClick: () => {
1776
+ setIsExternalUrl(true);
1777
+ setExternalUrl(currentHref.startsWith("http") ? currentHref : "");
1778
+ },
1779
+ children: "Use external URL instead"
1780
+ }
1781
+ )
1782
+ ] }) : /* @__PURE__ */ jsxs4(Fragment3, { children: [
1783
+ /* @__PURE__ */ jsxs4("div", { className: "ya-href-popover-section", children: [
1784
+ /* @__PURE__ */ jsx7("label", { className: "ya-href-popover-label", children: "External URL" }),
1785
+ /* @__PURE__ */ jsx7(
1786
+ "input",
1787
+ {
1788
+ type: "url",
1789
+ className: "ya-href-url-input",
1790
+ placeholder: "https://example.com",
1791
+ value: externalUrl,
1792
+ onChange: (e) => setExternalUrl(e.target.value),
1793
+ autoFocus: true
1794
+ }
1795
+ )
1796
+ ] }),
1797
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "ya-href-external-toggle", onClick: () => setIsExternalUrl(false), children: "\u2190 Back to pages" })
1798
+ ] }),
1799
+ /* @__PURE__ */ jsxs4("div", { className: "ya-href-popover-actions", children: [
1800
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "ya-link-btn ya-link-btn-cancel", onClick: handleCancelLink, children: "Cancel" }),
1801
+ isExternalUrl ? /* @__PURE__ */ jsx7("button", { type: "button", className: "ya-link-btn ya-link-btn-save", onClick: handleExternalUrlApply, children: "Apply" }) : /* @__PURE__ */ jsx7("button", { type: "button", className: "ya-link-btn ya-link-btn-save", onClick: handleSaveLink, children: "Save" })
1802
+ ] })
1803
+ ] })
1804
+ ] });
1805
+ }
1806
+
1807
+ // src/components/StaticText.tsx
1808
+ import { jsx as jsx8 } from "react/jsx-runtime";
1809
+ function MpText({ fieldId, className, as: Component = "span", children }) {
1810
+ const content = getContent(fieldId) || (typeof children === "string" ? children : "");
1811
+ return /* @__PURE__ */ jsx8(
1812
+ Component,
1813
+ {
1814
+ className,
1815
+ "data-field-id": fieldId,
1816
+ dangerouslySetInnerHTML: { __html: content }
1817
+ }
1818
+ );
1819
+ }
1820
+
1821
+ // src/components/StaticImage.tsx
1822
+ import { jsx as jsx9 } from "react/jsx-runtime";
1823
+ function parseImageValue2(value) {
1824
+ if (!value) {
1825
+ return { src: "" };
1826
+ }
1827
+ try {
1828
+ const parsed = JSON.parse(value);
1829
+ if (typeof parsed === "object" && parsed.src) {
1830
+ return parsed;
1831
+ }
1832
+ } catch {
1833
+ }
1834
+ return { src: value };
1835
+ }
1836
+ function getObjectPosition2(imageData) {
1837
+ if (imageData.focalPoint) {
1838
+ return `${imageData.focalPoint.x}% ${imageData.focalPoint.y}%`;
1839
+ }
1840
+ return imageData.objectPosition || "50% 50%";
1841
+ }
1842
+ function MpImage({
1843
+ fieldId,
1844
+ className,
1845
+ alt,
1846
+ objectFit: propObjectFit,
1847
+ objectPosition: propObjectPosition,
1848
+ loading = "lazy",
1849
+ fallbackSrc,
1850
+ fallbackAlt
1851
+ }) {
1852
+ const rawValue = getContent(fieldId);
1853
+ const imageData = parseImageValue2(rawValue);
1854
+ const src = imageData.src || fallbackSrc || "";
1855
+ const altText = imageData.alt || alt || fallbackAlt || "";
1856
+ const objectFit = imageData.objectFit || propObjectFit || "cover";
1857
+ const objectPosition = getObjectPosition2(imageData) || propObjectPosition || "50% 50%";
1858
+ return /* @__PURE__ */ jsx9(
1859
+ "img",
1860
+ {
1861
+ src: resolveAssetUrl(src),
1862
+ alt: altText,
1863
+ className,
1864
+ style: {
1865
+ objectFit,
1866
+ objectPosition
1867
+ },
1868
+ loading,
1869
+ "data-field-id": fieldId
1870
+ }
1871
+ );
1872
+ }
1873
+
1874
+ // src/components/MarkdownText.tsx
1875
+ import { Fragment as Fragment4 } from "react";
1876
+ import { jsx as jsx10 } from "react/jsx-runtime";
1877
+ function tokenize(text) {
1878
+ const tokens = [];
1879
+ let remaining = text;
1880
+ const patterns = [
1881
+ // Bold: **text**
1882
+ { regex: /^\*\*(.+?)\*\*/, type: "bold" },
1883
+ // Italic: *text* (but not **)
1884
+ { regex: /^\*([^*]+?)\*/, type: "italic" },
1885
+ // Link: [text](url)
1886
+ { regex: /^\[([^\]]+)\]\(([^)]+)\)/, type: "link" },
1887
+ // Newline
1888
+ { regex: /^\n/, type: "newline" }
1889
+ ];
1890
+ while (remaining.length > 0) {
1891
+ let matched = false;
1892
+ for (const pattern of patterns) {
1893
+ const match = remaining.match(pattern.regex);
1894
+ if (match) {
1895
+ if (pattern.type === "bold") {
1896
+ tokens.push({ type: "bold", content: match[1] });
1897
+ } else if (pattern.type === "italic") {
1898
+ tokens.push({ type: "italic", content: match[1] });
1899
+ } else if (pattern.type === "link") {
1900
+ tokens.push({ type: "link", text: match[1], url: match[2] });
1901
+ } else if (pattern.type === "newline") {
1902
+ tokens.push({ type: "newline" });
1903
+ }
1904
+ remaining = remaining.slice(match[0].length);
1905
+ matched = true;
1906
+ break;
1907
+ }
1908
+ }
1909
+ if (!matched) {
1910
+ const nextSpecial = remaining.search(/[*[\n]/);
1911
+ if (nextSpecial === -1) {
1912
+ tokens.push({ type: "text", content: remaining });
1913
+ remaining = "";
1914
+ } else if (nextSpecial === 0) {
1915
+ tokens.push({ type: "text", content: remaining[0] });
1916
+ remaining = remaining.slice(1);
1917
+ } else {
1918
+ tokens.push({ type: "text", content: remaining.slice(0, nextSpecial) });
1919
+ remaining = remaining.slice(nextSpecial);
1920
+ }
1921
+ }
1922
+ }
1923
+ const merged = [];
1924
+ for (const token of tokens) {
1925
+ const last = merged[merged.length - 1];
1926
+ if (token.type === "text" && last?.type === "text") {
1927
+ last.content += token.content;
1928
+ } else {
1929
+ merged.push(token);
1930
+ }
1931
+ }
1932
+ return merged;
1933
+ }
1934
+ function tokensToElements(tokens) {
1935
+ return tokens.map((token, index) => {
1936
+ switch (token.type) {
1937
+ case "text":
1938
+ return /* @__PURE__ */ jsx10(Fragment4, { children: token.content }, index);
1939
+ case "bold":
1940
+ return /* @__PURE__ */ jsx10("strong", { children: token.content }, index);
1941
+ case "italic":
1942
+ return /* @__PURE__ */ jsx10("em", { children: token.content }, index);
1943
+ case "link":
1944
+ return /* @__PURE__ */ jsx10(
1945
+ "a",
1946
+ {
1947
+ href: token.url,
1948
+ target: "_blank",
1949
+ rel: "noopener noreferrer",
1950
+ className: "text-[var(--color-primary)] underline hover:text-[var(--color-secondary)]",
1951
+ children: token.text
1952
+ },
1953
+ index
1954
+ );
1955
+ case "newline":
1956
+ return /* @__PURE__ */ jsx10("br", {}, index);
1957
+ default:
1958
+ return null;
1959
+ }
1960
+ });
1961
+ }
1962
+ function parseMarkdownToElements(content) {
1963
+ const tokens = tokenize(content);
1964
+ return tokensToElements(tokens);
1965
+ }
1966
+ function MarkdownText({ content, className }) {
1967
+ const elements = parseMarkdownToElements(content);
1968
+ return /* @__PURE__ */ jsx10("span", { className, children: elements });
1969
+ }
1970
+
1971
+ // src/router/Link.tsx
1972
+ import { Link as WouterLink2 } from "wouter";
1973
+ import { jsx as jsx11 } from "react/jsx-runtime";
1974
+ function Link2({ to, href, children, className, onClick, replace, ...props }) {
1975
+ const target = href ?? to ?? "/";
1976
+ return /* @__PURE__ */ jsx11(WouterLink2, { href: target, className, onClick, replace, ...props, children });
1977
+ }
1978
+
1979
+ // src/router/useNavigate.ts
1980
+ import { useLocation as useLocation2 } from "wouter";
1981
+ function useNavigate() {
1982
+ const [, setLocation] = useLocation2();
1983
+ return (to, options) => {
1984
+ if (options?.replace) {
1985
+ window.history.replaceState(null, "", to);
1986
+ setLocation(to);
1987
+ } else {
1988
+ setLocation(to);
1989
+ }
1990
+ };
1991
+ }
1992
+
1993
+ // src/router/Router.tsx
1994
+ import { Router as WouterRouter } from "wouter";
1995
+ import { jsx as jsx12 } from "react/jsx-runtime";
1996
+ function detectBasename() {
1997
+ if (typeof window === "undefined") return "";
1998
+ const sessionMatch = window.location.pathname.match(/^\/session\/[^/]+/);
1999
+ if (sessionMatch) return sessionMatch[0];
2000
+ const previewMatch = window.location.pathname.match(/^\/deploy-preview\/[^/]+/);
2001
+ if (previewMatch) return previewMatch[0];
2002
+ return "";
2003
+ }
2004
+ function Router({ children, base }) {
2005
+ const basename = base ?? detectBasename();
2006
+ return /* @__PURE__ */ jsx12(WouterRouter, { base: basename, children });
2007
+ }
2008
+
2009
+ // src/lib/builder-selection.ts
2010
+ var SELECTABLE_SELECTORS = [
2011
+ // Interactive elements
2012
+ "button",
2013
+ // Buttons
2014
+ "a[href]",
2015
+ // Links with href
2016
+ '[role="button"]',
2017
+ // ARIA buttons
2018
+ // Form elements
2019
+ "input",
2020
+ // Input fields (text, checkbox, etc.)
2021
+ "textarea",
2022
+ // Text areas
2023
+ "select",
2024
+ // Dropdown selects
2025
+ "label",
2026
+ // Form labels
2027
+ "form",
2028
+ // Forms as containers
2029
+ // Media elements
2030
+ "img",
2031
+ // Images
2032
+ "video",
2033
+ // Videos
2034
+ "audio",
2035
+ // Audio players
2036
+ "iframe",
2037
+ // Embedded content
2038
+ "svg",
2039
+ // SVG graphics
2040
+ "figure",
2041
+ // Figures with captions
2042
+ "figcaption",
2043
+ // Figure captions
2044
+ // Text/content elements
2045
+ "h1",
2046
+ "h2",
2047
+ "h3",
2048
+ "h4",
2049
+ "h5",
2050
+ "h6",
2051
+ // Headings
2052
+ "p",
2053
+ // Paragraphs
2054
+ "blockquote",
2055
+ // Block quotes
2056
+ "table",
2057
+ // Tables
2058
+ "tr",
2059
+ // Table rows
2060
+ "td",
2061
+ // Table cells
2062
+ "th",
2063
+ // Table headers
2064
+ // List elements
2065
+ "li",
2066
+ // List items (event cards, etc.)
2067
+ "ol",
2068
+ // Ordered lists
2069
+ "ul",
2070
+ // Unordered lists
2071
+ // Other
2072
+ "article",
2073
+ // Article cards
2074
+ "[data-mp-selectable]"
2075
+ // Explicit selectables (backward compatible)
2076
+ ];
2077
+ var CONTAINER_SELECTORS = [
2078
+ "section",
2079
+ "[data-mp-editable]",
2080
+ "nav",
2081
+ "header",
2082
+ "footer"
2083
+ ];
2084
+ var BuilderSelectionManager = class {
2085
+ enabled = false;
2086
+ selections = /* @__PURE__ */ new Map();
2087
+ hoverOverlay = null;
2088
+ currentHoveredElement = null;
2089
+ // Cache element references for lookup when syncing selections
2090
+ elementMap = /* @__PURE__ */ new Map();
2091
+ // Current selections from parent (for re-rendering on mode change)
2092
+ currentSelections = [];
2093
+ constructor() {
2094
+ if (window.parent === window) {
2095
+ console.log("[BuilderSelection] Not in iframe, skipping initialization");
2096
+ return;
2097
+ }
2098
+ console.log("[BuilderSelection] Initializing in iframe context");
2099
+ this.setupMessageListener();
2100
+ this.createHoverOverlay();
2101
+ this.setupScrollResizeListeners();
2102
+ this.setupKeyboardListener();
2103
+ this.setupWheelForwarding();
2104
+ this.notifyPageReady();
2105
+ window.addEventListener("popstate", () => this.notifyPageReady());
2106
+ }
2107
+ notifyPageReady() {
2108
+ this.sendToParent({
2109
+ type: "PAGE_READY",
2110
+ page: window.location.pathname
2111
+ });
2112
+ }
2113
+ sendToParent(message) {
2114
+ try {
2115
+ window.parent.postMessage(message, "*");
2116
+ } catch (error) {
2117
+ console.error("[BuilderSelection] Failed to send message to parent:", error);
2118
+ }
2119
+ }
2120
+ setupMessageListener() {
2121
+ window.addEventListener("message", (event) => {
2122
+ const data = event.data;
2123
+ if (!data?.type) return;
2124
+ switch (data.type) {
2125
+ case "SELECTOR_MODE":
2126
+ this.setEnabled(data.enabled ?? false);
2127
+ break;
2128
+ case "SELECTION_SYNC":
2129
+ this.syncSelections(data.selections ?? [], data.currentPage ?? "");
2130
+ break;
2131
+ case "CLEAR_SELECTIONS":
2132
+ this.clearAllSelections();
2133
+ break;
2134
+ }
2135
+ });
2136
+ }
2137
+ setEnabled(enabled) {
2138
+ console.log("[BuilderSelection] Selector mode:", enabled);
2139
+ this.enabled = enabled;
2140
+ window.__builderSelectModeEnabled = enabled;
2141
+ if (enabled) {
2142
+ document.body.style.cursor = "crosshair";
2143
+ document.body.classList.add("builder-selector-active");
2144
+ this.attachEventListeners();
2145
+ this.renderSelectionMasks();
2146
+ } else {
2147
+ document.body.style.cursor = "";
2148
+ document.body.classList.remove("builder-selector-active");
2149
+ this.detachEventListeners();
2150
+ this.hideHoverOverlay();
2151
+ this.clearAllSelections();
2152
+ }
2153
+ }
2154
+ attachEventListeners() {
2155
+ document.addEventListener("mouseover", this.handleMouseOver);
2156
+ document.addEventListener("mouseout", this.handleMouseOut);
2157
+ document.addEventListener("click", this.handleClick, true);
2158
+ }
2159
+ detachEventListeners() {
2160
+ document.removeEventListener("mouseover", this.handleMouseOver);
2161
+ document.removeEventListener("mouseout", this.handleMouseOut);
2162
+ document.removeEventListener("click", this.handleClick, true);
2163
+ }
2164
+ handleMouseOver = (e) => {
2165
+ if (!this.enabled) return;
2166
+ const target = this.findDeepestSelectableAt(e.target);
2167
+ if (!target || target === this.currentHoveredElement) return;
2168
+ this.currentHoveredElement = target;
2169
+ this.showHoverOverlay(target);
2170
+ const selectorId = this.generateSelectorId(target);
2171
+ this.sendToParent({
2172
+ type: "ELEMENT_HOVER",
2173
+ selectorId
2174
+ });
2175
+ };
2176
+ handleMouseOut = (e) => {
2177
+ if (!this.enabled) return;
2178
+ const relatedTarget = e.relatedTarget;
2179
+ const stillInSelectable = relatedTarget ? this.findDeepestSelectableAt(relatedTarget) : null;
2180
+ if (!stillInSelectable || stillInSelectable !== this.currentHoveredElement) {
2181
+ this.currentHoveredElement = null;
2182
+ this.hideHoverOverlay();
2183
+ this.sendToParent({
2184
+ type: "ELEMENT_HOVER",
2185
+ selectorId: null
2186
+ });
2187
+ }
2188
+ };
2189
+ handleClick = (e) => {
2190
+ if (!this.enabled) return;
2191
+ const target = this.findDeepestSelectableAt(e.target);
2192
+ if (!target) return;
2193
+ e.preventDefault();
2194
+ e.stopPropagation();
2195
+ const selectorId = this.generateSelectorId(target);
2196
+ const label = this.deriveLabel(target);
2197
+ this.elementMap.set(selectorId, target);
2198
+ this.sendToParent({
2199
+ type: "ELEMENT_CLICKED",
2200
+ selectorId,
2201
+ label,
2202
+ page: window.location.pathname,
2203
+ modifier: e.shiftKey ? "shift" : e.metaKey || e.ctrlKey ? "cmd" : null
2204
+ });
2205
+ };
2206
+ handleKeyDown = (e) => {
2207
+ if (e.key === "Shift") {
2208
+ const activeElement = document.activeElement;
2209
+ const isEditing = activeElement?.closest(".ya-text-editing") || activeElement?.closest(".ya-link-editing");
2210
+ if (!isEditing) {
2211
+ this.sendToParent({ type: "SHIFT_KEY_PRESSED" });
2212
+ }
2213
+ }
2214
+ };
2215
+ /**
2216
+ * Check if element matches any selectable selector
2217
+ */
2218
+ isSelectableElement(el) {
2219
+ for (const selector of SELECTABLE_SELECTORS) {
2220
+ if (el.matches(selector)) return true;
2221
+ }
2222
+ for (const selector of CONTAINER_SELECTORS) {
2223
+ if (el.matches(selector)) return true;
2224
+ }
2225
+ return false;
2226
+ }
2227
+ /**
2228
+ * Find the deepest selectable element from the click/hover target
2229
+ * Walks up the DOM tree and returns the first (deepest) selectable found
2230
+ */
2231
+ findDeepestSelectableAt(target) {
2232
+ if (target.closest(".mp-text-editing")) {
2233
+ return null;
2234
+ }
2235
+ let current = target;
2236
+ const selectables = [];
2237
+ while (current && current !== document.body) {
2238
+ if (this.isSelectableElement(current)) {
2239
+ selectables.push(current);
2240
+ }
2241
+ current = current.parentElement;
2242
+ }
2243
+ return selectables[0] || null;
2244
+ }
2245
+ /**
2246
+ * Generate a unique selector ID for an element
2247
+ * Uses data-mp-selectable if available, otherwise generates from context
2248
+ */
2249
+ generateSelectorId(el) {
2250
+ if (el.dataset.mpSelectable) return el.dataset.mpSelectable;
2251
+ if (el.id) return el.id;
2252
+ const tag = el.tagName.toLowerCase();
2253
+ const parent = el.closest("section, [data-mp-selectable], [data-mp-editable]");
2254
+ const parentId = parent?.dataset?.mpSelectable || parent?.dataset?.mpEditable || parent?.id || "page";
2255
+ const siblings = parent ? Array.from(parent.querySelectorAll(tag)) : [];
2256
+ const index = siblings.indexOf(el);
2257
+ return `${parentId}.${tag}${index > 0 ? `.${index}` : ""}`;
2258
+ }
2259
+ /**
2260
+ * Derive a human-readable label from the element
2261
+ */
2262
+ deriveLabel(el) {
2263
+ if (el.dataset.mpSelectable) {
2264
+ return this.toTitleCase(el.dataset.mpSelectable);
2265
+ }
2266
+ const tag = el.tagName.toLowerCase();
2267
+ const text = el.textContent?.trim().slice(0, 30) || "";
2268
+ if (tag === "button" || el.getAttribute("role") === "button") {
2269
+ return text ? `"${text}" Button` : "Button";
2270
+ }
2271
+ if (tag === "a") {
2272
+ return text ? `"${text}" Link` : "Link";
2273
+ }
2274
+ if (tag === "img") {
2275
+ return el.alt || "Image";
2276
+ }
2277
+ if (tag === "video") {
2278
+ return "Video";
2279
+ }
2280
+ if (tag === "audio") {
2281
+ return "Audio Player";
2282
+ }
2283
+ if (tag === "iframe") {
2284
+ return "Embedded Content";
2285
+ }
2286
+ if (tag === "svg") {
2287
+ return "SVG Graphic";
2288
+ }
2289
+ if (tag === "figure") {
2290
+ const caption = el.querySelector("figcaption")?.textContent?.trim();
2291
+ return caption ? `Figure: ${caption.slice(0, 20)}` : "Figure";
2292
+ }
2293
+ if (tag === "figcaption") {
2294
+ return text ? `Caption: ${text.slice(0, 20)}` : "Caption";
2295
+ }
2296
+ if (tag === "input") {
2297
+ const type = el.type || "text";
2298
+ const placeholder = el.placeholder;
2299
+ if (type === "submit") return "Submit Button";
2300
+ if (type === "checkbox") return "Checkbox";
2301
+ if (type === "radio") return "Radio Button";
2302
+ return placeholder ? `"${placeholder}" Input` : `${type.charAt(0).toUpperCase() + type.slice(1)} Input`;
2303
+ }
2304
+ if (tag === "textarea") {
2305
+ const placeholder = el.placeholder;
2306
+ return placeholder ? `"${placeholder}" Text Area` : "Text Area";
2307
+ }
2308
+ if (tag === "select") {
2309
+ return "Dropdown";
2310
+ }
2311
+ if (tag === "label") {
2312
+ return text ? `Label: ${text.slice(0, 20)}` : "Label";
2313
+ }
2314
+ if (tag === "form") {
2315
+ return "Form";
2316
+ }
2317
+ if (["h1", "h2", "h3", "h4", "h5", "h6"].includes(tag)) {
2318
+ return text ? `Heading: ${text.slice(0, 25)}` : `${tag.toUpperCase()} Heading`;
2319
+ }
2320
+ if (tag === "p") {
2321
+ return text ? `Paragraph: ${text.slice(0, 20)}...` : "Paragraph";
2322
+ }
2323
+ if (tag === "blockquote") {
2324
+ return text ? `Quote: ${text.slice(0, 20)}...` : "Block Quote";
2325
+ }
2326
+ if (tag === "table") {
2327
+ return "Table";
2328
+ }
2329
+ if (tag === "tr") {
2330
+ return "Table Row";
2331
+ }
2332
+ if (tag === "td" || tag === "th") {
2333
+ return text ? `Cell: ${text.slice(0, 15)}` : "Table Cell";
2334
+ }
2335
+ if (tag === "ol") {
2336
+ return "Ordered List";
2337
+ }
2338
+ if (tag === "ul") {
2339
+ return "Unordered List";
2340
+ }
2341
+ if (tag === "li") {
2342
+ const preview = text.length > 20 ? text.slice(0, 20) + "..." : text;
2343
+ return preview ? `List Item: ${preview}` : "List Item";
2344
+ }
2345
+ if (tag === "section") {
2346
+ const heading = el.querySelector("h1, h2, h3")?.textContent?.trim();
2347
+ return heading ? `${heading} Section` : "Section";
2348
+ }
2349
+ if (tag === "article") {
2350
+ const heading = el.querySelector("h1, h2, h3, h4")?.textContent?.trim();
2351
+ return heading ? heading : "Article";
2352
+ }
2353
+ if (tag === "nav") {
2354
+ return "Navigation";
2355
+ }
2356
+ if (tag === "header") {
2357
+ return "Header";
2358
+ }
2359
+ if (tag === "footer") {
2360
+ return "Footer";
2361
+ }
2362
+ return text || this.toTitleCase(tag);
2363
+ }
2364
+ /**
2365
+ * Convert kebab-case or tag names to Title Case
2366
+ */
2367
+ toTitleCase(str) {
2368
+ return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
2369
+ }
2370
+ createHoverOverlay() {
2371
+ this.hoverOverlay = document.createElement("div");
2372
+ this.hoverOverlay.id = "builder-hover-overlay";
2373
+ this.hoverOverlay.style.cssText = `
2374
+ position: fixed;
2375
+ pointer-events: none;
2376
+ border: 2px dashed #3B82F6;
2377
+ background: rgba(59, 130, 246, 0.05);
2378
+ z-index: 9999;
2379
+ display: none;
2380
+ transition: all 0.15s ease-out;
2381
+ border-radius: 4px;
2382
+ `;
2383
+ document.body.appendChild(this.hoverOverlay);
2384
+ }
2385
+ setupScrollResizeListeners() {
2386
+ window.addEventListener("scroll", this.updateSelectionPositions, { passive: true });
2387
+ window.addEventListener("resize", this.updateSelectionPositions, { passive: true });
2388
+ }
2389
+ setupKeyboardListener() {
2390
+ document.addEventListener("keydown", this.handleKeyDown);
2391
+ }
2392
+ /**
2393
+ * Forward wheel events with ctrlKey (pinch zoom) to parent
2394
+ * This allows pinch-to-zoom gestures inside the iframe to control
2395
+ * the parent's zoom state instead of zooming the iframe content
2396
+ */
2397
+ setupWheelForwarding() {
2398
+ document.addEventListener("wheel", (e) => {
2399
+ if (e.ctrlKey) {
2400
+ e.preventDefault();
2401
+ this.sendToParent({
2402
+ type: "IFRAME_WHEEL",
2403
+ deltaY: e.deltaY,
2404
+ clientX: e.clientX,
2405
+ clientY: e.clientY,
2406
+ ctrlKey: true
2407
+ });
2408
+ }
2409
+ }, { passive: false });
2410
+ }
2411
+ showHoverOverlay(element) {
2412
+ if (!this.hoverOverlay) return;
2413
+ const rect = element.getBoundingClientRect();
2414
+ this.hoverOverlay.style.top = `${rect.top}px`;
2415
+ this.hoverOverlay.style.left = `${rect.left}px`;
2416
+ this.hoverOverlay.style.width = `${rect.width}px`;
2417
+ this.hoverOverlay.style.height = `${rect.height}px`;
2418
+ this.hoverOverlay.style.display = "block";
2419
+ }
2420
+ hideHoverOverlay() {
2421
+ if (this.hoverOverlay) {
2422
+ this.hoverOverlay.style.display = "none";
2423
+ }
2424
+ }
2425
+ syncSelections(selections, _currentPage) {
2426
+ console.log("[BuilderSelection] Syncing selections:", selections.length, "current page:", _currentPage);
2427
+ this.currentSelections = selections;
2428
+ if (!this.enabled) {
2429
+ this.clearAllSelections();
2430
+ return;
2431
+ }
2432
+ this.renderSelectionMasks();
2433
+ }
2434
+ /**
2435
+ * Render selection masks for current selections (only when enabled)
2436
+ */
2437
+ renderSelectionMasks() {
2438
+ this.clearAllSelections();
2439
+ for (const sel of this.currentSelections) {
2440
+ if (sel.page !== window.location.pathname) continue;
2441
+ const element = this.findElementBySelectorId(sel.selectorId);
2442
+ if (!element) {
2443
+ console.warn(`[BuilderSelection] Element not found for selector: ${sel.selectorId}`);
2444
+ continue;
2445
+ }
2446
+ this.renderSelectionIndicator(element, sel.id, sel.color);
2447
+ }
2448
+ }
2449
+ /**
2450
+ * Find element by selector ID (supports both explicit and auto-generated IDs)
2451
+ */
2452
+ findElementBySelectorId(selectorId) {
2453
+ const explicit = document.querySelector(`[data-mp-selectable="${selectorId}"]`);
2454
+ if (explicit) return explicit;
2455
+ const byId = document.getElementById(selectorId);
2456
+ if (byId) return byId;
2457
+ const cached = this.elementMap.get(selectorId);
2458
+ if (cached && document.body.contains(cached)) return cached;
2459
+ const parts = selectorId.split(".");
2460
+ if (parts.length >= 2) {
2461
+ const parentId = parts[0];
2462
+ const tag = parts[1];
2463
+ const index = parts.length > 2 ? parseInt(parts[2], 10) : 0;
2464
+ let parent = null;
2465
+ if (parentId === "page") {
2466
+ parent = document.body;
2467
+ } else {
2468
+ parent = document.querySelector(`[data-mp-selectable="${parentId}"]`) || document.querySelector(`[data-mp-editable="${parentId}"]`) || document.getElementById(parentId) || document.querySelector(`section#${parentId}`);
2469
+ }
2470
+ if (parent) {
2471
+ const candidates = parent.querySelectorAll(tag);
2472
+ if (candidates[index]) {
2473
+ return candidates[index];
2474
+ }
2475
+ }
2476
+ }
2477
+ return null;
2478
+ }
2479
+ clearAllSelections() {
2480
+ this.selections.forEach(({ container }) => {
2481
+ container.remove();
2482
+ });
2483
+ this.selections.clear();
2484
+ }
2485
+ /**
2486
+ * Update positions of selection overlays when viewport changes (scroll/resize)
2487
+ */
2488
+ updateSelectionPositions = () => {
2489
+ this.selections.forEach(({ element, container }) => {
2490
+ if (!document.body.contains(element)) {
2491
+ return;
2492
+ }
2493
+ const rect = element.getBoundingClientRect();
2494
+ container.style.top = `${rect.top}px`;
2495
+ container.style.left = `${rect.left}px`;
2496
+ container.style.width = `${rect.width}px`;
2497
+ container.style.height = `${rect.height}px`;
2498
+ });
2499
+ };
2500
+ renderSelectionIndicator(element, selectionId, color) {
2501
+ const rect = element.getBoundingClientRect();
2502
+ const container = document.createElement("div");
2503
+ container.className = "builder-selection-container";
2504
+ container.dataset.selectionId = selectionId;
2505
+ container.style.cssText = `
2506
+ position: fixed;
2507
+ top: ${rect.top}px;
2508
+ left: ${rect.left}px;
2509
+ width: ${rect.width}px;
2510
+ height: ${rect.height}px;
2511
+ pointer-events: none;
2512
+ z-index: 9999;
2513
+ `;
2514
+ const border = document.createElement("div");
2515
+ border.className = "builder-selection-border";
2516
+ border.style.cssText = `
2517
+ position: absolute;
2518
+ inset: 0;
2519
+ border: 2px solid ${color};
2520
+ border-radius: 4px;
2521
+ pointer-events: none;
2522
+ `;
2523
+ container.appendChild(border);
2524
+ const badge = document.createElement("div");
2525
+ badge.textContent = selectionId;
2526
+ badge.className = "builder-selection-badge";
2527
+ badge.style.cssText = `
2528
+ position: absolute;
2529
+ top: 8px;
2530
+ left: 8px;
2531
+ background: ${color};
2532
+ color: white;
2533
+ font-size: 11px;
2534
+ font-weight: 600;
2535
+ padding: 4px 10px;
2536
+ border-radius: 9999px;
2537
+ pointer-events: none;
2538
+ font-family: system-ui, -apple-system, sans-serif;
2539
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
2540
+ `;
2541
+ container.appendChild(badge);
2542
+ document.body.appendChild(container);
2543
+ this.selections.set(selectionId, { element, container, badge, border });
2544
+ }
2545
+ };
2546
+ function initBuilderSelection() {
2547
+ if (typeof window !== "undefined" && window.parent !== window) {
2548
+ if (document.readyState === "loading") {
2549
+ document.addEventListener("DOMContentLoaded", () => new BuilderSelectionManager());
2550
+ } else {
2551
+ new BuilderSelectionManager();
2552
+ }
2553
+ }
2554
+ }
2555
+ export {
2556
+ ContentStoreProvider,
2557
+ ContentStoreProvider2 as ContentStoreProviderProd,
2558
+ Link2 as Link,
2559
+ MarkdownText,
2560
+ Router,
2561
+ SafeHtml,
2562
+ MpImage as StaticImage,
2563
+ MpText as StaticText,
2564
+ YaImage,
2565
+ YaLink,
2566
+ YaText,
2567
+ contentRegistry,
2568
+ getAllContent,
2569
+ getContent,
2570
+ hasContent,
2571
+ initBuilderSelection,
2572
+ registerContent,
2573
+ resolveAssetUrl,
2574
+ serializeImageValue,
2575
+ setAssetResolver,
2576
+ useContentStore,
2577
+ useContentStore2 as useContentStoreProd,
2578
+ useNavigate
2579
+ };