@ohhwells/bridge 0.1.16 → 0.1.17-next.15

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 CHANGED
@@ -1,10 +1,752 @@
1
1
  "use client";
2
2
 
3
3
  // src/OhhwellsBridge.tsx
4
- import React2, { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
4
+ import React5, { useCallback as useCallback2, useEffect as useEffect4, useLayoutEffect, useRef as useRef3, useState as useState5 } from "react";
5
+ import { createRoot } from "react-dom/client";
6
+ import { flushSync } from "react-dom";
7
+
8
+ // src/ui/SchedulingWidget.tsx
9
+ import { useEffect, useMemo, useRef, useState as useState2 } from "react";
10
+
11
+ // src/ui/EmailCaptureModal.tsx
12
+ import { useState } from "react";
13
+ import { Dialog } from "radix-ui";
14
+ import { jsx, jsxs } from "react/jsx-runtime";
15
+ function Spinner() {
16
+ return /* @__PURE__ */ jsxs(
17
+ "svg",
18
+ {
19
+ width: "32",
20
+ height: "32",
21
+ viewBox: "0 0 32 32",
22
+ fill: "none",
23
+ style: { animation: "ohw-spin 0.75s linear infinite" },
24
+ children: [
25
+ /* @__PURE__ */ jsx("style", { children: `@keyframes ohw-spin { to { transform: rotate(360deg); } }` }),
26
+ /* @__PURE__ */ jsx("circle", { cx: "16", cy: "16", r: "12", stroke: "#e5e7eb", strokeWidth: "3" }),
27
+ /* @__PURE__ */ jsx(
28
+ "path",
29
+ {
30
+ d: "M16 4a12 12 0 0 1 12 12",
31
+ stroke: "#3b82f6",
32
+ strokeWidth: "3",
33
+ strokeLinecap: "round"
34
+ }
35
+ )
36
+ ]
37
+ }
38
+ );
39
+ }
40
+ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
41
+ const [email, setEmail] = useState("");
42
+ const [submittedEmail, setSubmittedEmail] = useState("");
43
+ const [loading, setLoading] = useState(false);
44
+ const [success, setSuccess] = useState(false);
45
+ const [error, setError] = useState(null);
46
+ const isBook = title === "Confirm your spot";
47
+ const handleSubmit = async () => {
48
+ if (!email.trim()) return;
49
+ setLoading(true);
50
+ setError(null);
51
+ try {
52
+ await onSubmit(email.trim());
53
+ setSubmittedEmail(email.trim());
54
+ setSuccess(true);
55
+ } catch (e) {
56
+ setError(e instanceof Error ? e.message : "Something went wrong. Please try again.");
57
+ } finally {
58
+ setLoading(false);
59
+ }
60
+ };
61
+ const handleKeyDown = (e) => {
62
+ if (e.key === "Enter" && !loading) handleSubmit();
63
+ };
64
+ return /* @__PURE__ */ jsx(Dialog.Root, { open: true, onOpenChange: (open) => {
65
+ if (!open) onClose();
66
+ }, children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [
67
+ /* @__PURE__ */ jsx(
68
+ Dialog.Overlay,
69
+ {
70
+ className: "fixed inset-0 z-50",
71
+ style: { background: "rgba(0,0,0,0.45)" }
72
+ }
73
+ ),
74
+ /* @__PURE__ */ jsxs(
75
+ Dialog.Content,
76
+ {
77
+ className: "fixed left-1/2 top-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 bg-white rounded-xl shadow-xl outline-none font-body box-border overflow-hidden",
78
+ style: { maxWidth: 400 },
79
+ children: [
80
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3 p-6", children: [
81
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
82
+ /* @__PURE__ */ jsx(
83
+ Dialog.Title,
84
+ {
85
+ className: "font-body text-xl font-bold m-0 leading-tight",
86
+ style: { color: "var(--color-dark,#200C02)" },
87
+ children: success ? isBook ? "Reservation confirmed!" : "You're on the waitlist!" : title
88
+ }
89
+ ),
90
+ /* @__PURE__ */ jsx(
91
+ Dialog.Description,
92
+ {
93
+ className: "font-body text-sm m-0",
94
+ style: { color: "#6B7280" },
95
+ children: success ? isBook ? `A confirmation email has been sent to ${submittedEmail}.` : "We'll let you know via email if someone cancels." : subtitle
96
+ }
97
+ )
98
+ ] }),
99
+ /* @__PURE__ */ jsx(
100
+ Dialog.Close,
101
+ {
102
+ className: "shrink-0 w-7 h-7 flex items-center justify-center rounded-full bg-transparent border-none cursor-pointer mt-0.5 absolute top-2.5 right-1.5",
103
+ style: { color: "#6B7280" },
104
+ "aria-label": "Close",
105
+ children: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
106
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
107
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
108
+ ] })
109
+ }
110
+ )
111
+ ] }),
112
+ !success && (loading ? (
113
+ /* Loading state — full body replaced */
114
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center gap-3 px-7 py-12", children: [
115
+ /* @__PURE__ */ jsx(Spinner, {}),
116
+ /* @__PURE__ */ jsx("p", { className: "font-body text-sm m-0", style: { color: "#6B7280" }, children: "Please wait" })
117
+ ] })
118
+ ) : (
119
+ /* Email form */
120
+ /* @__PURE__ */ jsxs("div", { className: "px-7 pt-2 pb-6", children: [
121
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5 mb-8", children: [
122
+ /* @__PURE__ */ jsx("label", { className: "font-body text-sm font-medium", style: { color: "var(--color-dark,#200C02)" }, children: "Email" }),
123
+ /* @__PURE__ */ jsx(
124
+ "input",
125
+ {
126
+ type: "email",
127
+ value: email,
128
+ onChange: (e) => setEmail(e.target.value),
129
+ onKeyDown: handleKeyDown,
130
+ placeholder: "hello@gmail.com",
131
+ autoFocus: true,
132
+ className: "w-full h-10 px-3 rounded-lg border font-body text-sm outline-none box-border",
133
+ style: {
134
+ borderColor: "#E7E5E4",
135
+ background: "#fff",
136
+ color: "var(--color-dark,#200C02)"
137
+ }
138
+ }
139
+ ),
140
+ error && /* @__PURE__ */ jsx("p", { className: "font-body text-xs text-red-500 m-0", children: error })
141
+ ] }),
142
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
143
+ "button",
144
+ {
145
+ onClick: handleSubmit,
146
+ disabled: !email.trim(),
147
+ className: "h-9 px-5 rounded-lg font-body text-sm font-semibold cursor-pointer border-none disabled:opacity-50 disabled:cursor-not-allowed",
148
+ style: {
149
+ background: "var(--color-primary,#3D312B)",
150
+ color: "var(--color-light,#FEFFF0)"
151
+ },
152
+ children: "Send"
153
+ }
154
+ ) })
155
+ ] })
156
+ ))
157
+ ]
158
+ }
159
+ )
160
+ ] }) });
161
+ }
162
+
163
+ // src/ui/SchedulingWidget.tsx
164
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
165
+ var API_URL = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
166
+ var DAY_ABBR = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
167
+ function getApiDomain() {
168
+ const h = window.location.hostname;
169
+ for (const base of ["ohhwells.site", "theflowops-staging.com", "theflowops.com"]) {
170
+ if (h.endsWith(`.${base}`)) return h.slice(0, -(base.length + 1));
171
+ }
172
+ return h;
173
+ }
174
+ function classRunsOnDate(cls, date, type) {
175
+ if (type === "FIXED") {
176
+ const d = String(date.getDate()).padStart(2, "0");
177
+ const m = String(date.getMonth() + 1).padStart(2, "0");
178
+ return cls.fixedDates?.some((fd) => fd.date === `${d}-${m}-${date.getFullYear()}`) ?? false;
179
+ }
180
+ return cls.weekdays?.some((w) => w.weekday === date.getDay()) ?? false;
181
+ }
182
+ function formatClassTime(cls) {
183
+ let h = parseInt(cls.startTime.hour, 10);
184
+ const m = parseInt(cls.startTime.minutes, 10);
185
+ if (cls.startTime.time === "PM" && h !== 12) h += 12;
186
+ if (cls.startTime.time === "AM" && h === 12) h = 0;
187
+ const endTotal = h * 60 + m + cls.duration;
188
+ const eh = Math.floor(endTotal / 60) % 24;
189
+ const em = endTotal % 60;
190
+ return `${h}:${cls.startTime.minutes}-${eh}:${String(em).padStart(2, "0")}`;
191
+ }
192
+ function getBookingsOnDate(cls, date) {
193
+ if (!cls.bookings?.length) return 0;
194
+ const ds = date.toISOString().split("T")[0];
195
+ return cls.bookings.filter((b) => {
196
+ try {
197
+ return new Date(b.classDate).toISOString().split("T")[0] === ds;
198
+ } catch {
199
+ return false;
200
+ }
201
+ }).length;
202
+ }
203
+ function buildTimezoneLabel(tz) {
204
+ try {
205
+ const now = /* @__PURE__ */ new Date();
206
+ const timeStr = new Intl.DateTimeFormat("en-US", {
207
+ hour: "2-digit",
208
+ minute: "2-digit",
209
+ hour12: false,
210
+ timeZone: tz
211
+ }).format(now);
212
+ const offsetPart = new Intl.DateTimeFormat("en-US", { timeZoneName: "short", timeZone: tz }).formatToParts(now).find((p) => p.type === "timeZoneName")?.value ?? "";
213
+ const city = tz.split("/").pop()?.replace(/_/g, " ") ?? tz;
214
+ return `${timeStr} (${offsetPart}) ${city} time`;
215
+ } catch {
216
+ return tz;
217
+ }
218
+ }
219
+ function EmptyState({ inEditor }) {
220
+ const handleAddSchedule = () => {
221
+ if (inEditor) {
222
+ window.parent.postMessage({ type: "ow:scheduling-not-connected" }, "*");
223
+ }
224
+ };
225
+ return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center gap-6 p-12 bg-[#FAFAF9] border-2 border-dashed border-[#E7E5E4] rounded-[10px] h-[264px] w-full box-border", children: [
226
+ /* @__PURE__ */ jsx2("div", { className: "w-10 h-10 bg-[#F5F5F4] rounded-[10px] flex items-center justify-center shrink-0", children: /* @__PURE__ */ jsxs2(
227
+ "svg",
228
+ {
229
+ width: "20",
230
+ height: "20",
231
+ viewBox: "0 0 24 24",
232
+ fill: "none",
233
+ stroke: "var(--color-accent, #A89B83)",
234
+ strokeWidth: "1.5",
235
+ strokeLinecap: "round",
236
+ strokeLinejoin: "round",
237
+ children: [
238
+ /* @__PURE__ */ jsx2("rect", { x: "3", y: "4", width: "18", height: "18", rx: "2" }),
239
+ /* @__PURE__ */ jsx2("line", { x1: "16", y1: "2", x2: "16", y2: "6" }),
240
+ /* @__PURE__ */ jsx2("line", { x1: "8", y1: "2", x2: "8", y2: "6" }),
241
+ /* @__PURE__ */ jsx2("line", { x1: "3", y1: "10", x2: "21", y2: "10" }),
242
+ /* @__PURE__ */ jsx2("line", { x1: "12", y1: "14", x2: "12", y2: "18" }),
243
+ /* @__PURE__ */ jsx2("line", { x1: "10", y1: "16", x2: "14", y2: "16" })
244
+ ]
245
+ }
246
+ ) }),
247
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center gap-2", children: [
248
+ /* @__PURE__ */ jsx2("p", { className: "font-body text-lg font-medium text-center text-[#0A0A0A] m-0", children: "No schedule yet" }),
249
+ /* @__PURE__ */ jsx2("p", { className: "font-body text-base font-normal text-center text-(--color-accent,#A89B83) m-0", children: "Add a scheduling widget to get instant bookings." })
250
+ ] }),
251
+ /* @__PURE__ */ jsx2(
252
+ "button",
253
+ {
254
+ onClick: handleAddSchedule,
255
+ className: "h-9 px-2 py-1.5 flex items-center justify-center bg-white border border-[#E7E5E4] rounded-md font-body text-sm font-medium text-(--color-dark,#200C02) cursor-pointer",
256
+ children: "Add Schedule"
257
+ }
258
+ )
259
+ ] });
260
+ }
261
+ function LoadingSkeleton() {
262
+ const shimmer = {
263
+ background: "linear-gradient(90deg,#f0f0f0 25%,#e8e8e8 50%,#f0f0f0 75%)",
264
+ backgroundSize: "200% 100%",
265
+ animation: "ohw-shimmer 1.5s infinite"
266
+ };
267
+ return /* @__PURE__ */ jsxs2("div", { className: "w-full flex flex-col gap-10", children: [
268
+ /* @__PURE__ */ jsx2("style", { children: `@keyframes ohw-shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}` }),
269
+ /* @__PURE__ */ jsx2("div", { className: "flex h-[70px] items-stretch", children: Array.from({ length: 7 }).map((_, i) => /* @__PURE__ */ jsxs2("div", { className: "flex-1 flex flex-col justify-between items-center px-1", children: [
270
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "32px", height: "14px", borderRadius: "4px" } }),
271
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "40px", height: "40px", borderRadius: "50%" } })
272
+ ] }, i)) }),
273
+ [0, 1, 2].map((i) => /* @__PURE__ */ jsxs2("div", { children: [
274
+ /* @__PURE__ */ jsxs2("div", { className: "flex gap-4 py-4 sm:hidden", children: [
275
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2 shrink-0", children: [
276
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "100px", height: "16px", borderRadius: "4px" } }),
277
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "52px", height: "20px", borderRadius: "999px" } }),
278
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "60px", height: "14px", borderRadius: "4px" } })
279
+ ] }),
280
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-1 flex-col gap-2", children: [
281
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "75%", height: "16px", borderRadius: "4px" } }),
282
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "50%", height: "14px", borderRadius: "4px" } }),
283
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "100%", height: "44px", borderRadius: "8px" } })
284
+ ] })
285
+ ] }),
286
+ /* @__PURE__ */ jsxs2("div", { className: "hidden sm:flex items-center gap-[60px] py-4", children: [
287
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "120px", height: "16px", borderRadius: "4px", flexShrink: 0 } }),
288
+ /* @__PURE__ */ jsxs2("div", { className: "flex-1 flex flex-col gap-2", children: [
289
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "55%", height: "16px", borderRadius: "4px" } }),
290
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "38%", height: "14px", borderRadius: "4px" } })
291
+ ] }),
292
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "56px", height: "32px", borderRadius: "4px", flexShrink: 0 } }),
293
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "200px", height: "44px", borderRadius: "8px", flexShrink: 0 } })
294
+ ] }),
295
+ i < 2 && /* @__PURE__ */ jsx2("div", { className: "h-px bg-black/20" })
296
+ ] }, i))
297
+ ] });
298
+ }
299
+ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal }) {
300
+ const todayMs = useMemo(() => {
301
+ const d = /* @__PURE__ */ new Date();
302
+ d.setHours(0, 0, 0, 0);
303
+ return d.getTime();
304
+ }, []);
305
+ const scrollRef = useRef(null);
306
+ const [canScrollLeft, setCanScrollLeft] = useState2(false);
307
+ const [canScrollRight, setCanScrollRight] = useState2(false);
308
+ const updateScrollState = () => {
309
+ const el = scrollRef.current;
310
+ if (!el) return;
311
+ setCanScrollLeft(el.scrollLeft > 0);
312
+ setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
313
+ };
314
+ useEffect(() => {
315
+ const el = scrollRef.current;
316
+ if (!el) return;
317
+ updateScrollState();
318
+ el.addEventListener("scroll", updateScrollState);
319
+ const ro = new ResizeObserver(updateScrollState);
320
+ ro.observe(el);
321
+ return () => {
322
+ el.removeEventListener("scroll", updateScrollState);
323
+ ro.disconnect();
324
+ };
325
+ }, [dates]);
326
+ const scroll = (dir) => {
327
+ scrollRef.current?.scrollBy({ left: dir === "left" ? -216 : 216, behavior: "smooth" });
328
+ };
329
+ const isDragging = useRef(false);
330
+ const dragStartX = useRef(0);
331
+ const dragStartScrollLeft = useRef(0);
332
+ const onDragStart = (e) => {
333
+ if (!scrollRef.current) return;
334
+ isDragging.current = true;
335
+ dragStartX.current = e.clientX;
336
+ dragStartScrollLeft.current = scrollRef.current.scrollLeft;
337
+ scrollRef.current.style.cursor = "grabbing";
338
+ scrollRef.current.style.userSelect = "none";
339
+ };
340
+ const onDragMove = (e) => {
341
+ if (!isDragging.current || !scrollRef.current) return;
342
+ const delta = e.clientX - dragStartX.current;
343
+ scrollRef.current.scrollLeft = dragStartScrollLeft.current - delta;
344
+ };
345
+ const onDragEnd = () => {
346
+ if (!scrollRef.current) return;
347
+ isDragging.current = false;
348
+ scrollRef.current.style.cursor = "grab";
349
+ scrollRef.current.style.userSelect = "";
350
+ };
351
+ const datesWithClasses = useMemo(
352
+ () => new Set(dates.map(
353
+ (d, i) => schedule.classes?.some((c) => classRunsOnDate(c, d, schedule.type)) ? i : -1
354
+ ).filter((i) => i >= 0)),
355
+ [dates, schedule]
356
+ );
357
+ const selectedDate = dates[selectedIdx];
358
+ const selectedClasses = useMemo(
359
+ () => (schedule.classes ?? []).filter((c) => selectedDate && classRunsOnDate(c, selectedDate, schedule.type)),
360
+ [schedule, selectedDate]
361
+ );
362
+ return /* @__PURE__ */ jsxs2("div", { className: "w-full flex flex-col gap-10", children: [
363
+ /* @__PURE__ */ jsxs2("div", { className: "relative w-full", children: [
364
+ canScrollLeft && /* @__PURE__ */ jsx2(
365
+ "button",
366
+ {
367
+ onClick: () => scroll("left"),
368
+ className: "hidden sm:flex absolute left-0 top-1/2 -translate-y-1/2 z-10 w-8 h-8 items-center justify-center rounded-full border-none cursor-pointer",
369
+ style: { background: "var(--color-light,#FEFFF0)", boxShadow: "0 1px 4px rgba(0,0,0,0.15)" },
370
+ "aria-label": "Scroll left",
371
+ children: /* @__PURE__ */ jsx2("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "var(--color-dark,#200C02)", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx2("polyline", { points: "15 18 9 12 15 6" }) })
372
+ }
373
+ ),
374
+ canScrollRight && /* @__PURE__ */ jsx2(
375
+ "button",
376
+ {
377
+ onClick: () => scroll("right"),
378
+ className: "hidden sm:flex absolute right-0 top-1/2 -translate-y-1/2 z-10 w-8 h-8 items-center justify-center rounded-full border-none cursor-pointer",
379
+ style: { background: "var(--color-light,#FEFFF0)", boxShadow: "0 1px 4px rgba(0,0,0,0.15)" },
380
+ "aria-label": "Scroll right",
381
+ children: /* @__PURE__ */ jsx2("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "var(--color-dark,#200C02)", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx2("polyline", { points: "9 18 15 12 9 6" }) })
382
+ }
383
+ ),
384
+ canScrollLeft && /* @__PURE__ */ jsx2(
385
+ "div",
386
+ {
387
+ className: "sm:hidden absolute left-0 top-0 bottom-0 w-10 z-10 pointer-events-none",
388
+ style: { background: "linear-gradient(to right, var(--color-light,#FEFFF0), transparent)" }
389
+ }
390
+ ),
391
+ canScrollRight && /* @__PURE__ */ jsx2(
392
+ "div",
393
+ {
394
+ className: "sm:hidden absolute right-0 top-0 bottom-0 w-10 z-10 pointer-events-none",
395
+ style: { background: "linear-gradient(to left, var(--color-light,#FEFFF0), transparent)" }
396
+ }
397
+ ),
398
+ /* @__PURE__ */ jsx2(
399
+ "div",
400
+ {
401
+ ref: scrollRef,
402
+ className: "overflow-x-auto w-full",
403
+ style: { scrollbarWidth: "none", msOverflowStyle: "none", touchAction: "pan-x", cursor: "grab" },
404
+ onMouseDown: onDragStart,
405
+ onMouseMove: onDragMove,
406
+ onMouseUp: onDragEnd,
407
+ onMouseLeave: onDragEnd,
408
+ children: /* @__PURE__ */ jsx2("div", { className: "flex", style: { width: `max(100%, ${dates.length * 60}px)` }, children: dates.map((date, i) => {
409
+ const isToday = date.getTime() === todayMs;
410
+ const isSelected = i === selectedIdx;
411
+ const clickable = datesWithClasses.has(i);
412
+ const label = isToday ? "TODAY" : DAY_ABBR[date.getDay()];
413
+ return /* @__PURE__ */ jsxs2(
414
+ "div",
415
+ {
416
+ onClick: () => clickable && onSelectDate(i),
417
+ className: `flex-1 h-[70px] flex flex-col justify-between items-center ${clickable ? "cursor-pointer opacity-100" : "cursor-default opacity-50"}`,
418
+ children: [
419
+ /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-normal text-center text-(--color-dark,#200C02)", children: label }),
420
+ /* @__PURE__ */ jsx2(
421
+ "div",
422
+ {
423
+ className: "w-10 h-10 rounded-full flex items-center justify-center",
424
+ style: { background: isSelected ? "var(--color-primary, #3D312B)" : "transparent" },
425
+ children: /* @__PURE__ */ jsx2(
426
+ "span",
427
+ {
428
+ className: "font-body text-xl font-bold text-center tracking-display-tight",
429
+ style: { color: isSelected ? "var(--color-light, #FEFFF0)" : "var(--color-dark, #200C02)" },
430
+ children: date.getDate()
431
+ }
432
+ )
433
+ }
434
+ )
435
+ ]
436
+ },
437
+ i
438
+ );
439
+ }) })
440
+ }
441
+ )
442
+ ] }),
443
+ selectedClasses.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "font-body text-base text-(--color-accent,#A89B83) text-center py-8 m-0", children: "No classes scheduled for this day." }) : /* @__PURE__ */ jsx2("div", { className: "flex flex-col", children: selectedClasses.map((cls, i) => {
444
+ const booked = getBookingsOnDate(cls, selectedDate);
445
+ const available = cls.maxParticipants - booked;
446
+ const isFull = available <= 0;
447
+ return /* @__PURE__ */ jsxs2("div", { children: [
448
+ /* @__PURE__ */ jsxs2("div", { className: "flex gap-4 py-4 sm:items-center sm:gap-[60px] box-border", children: [
449
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2 shrink-0 sm:contents", children: [
450
+ /* @__PURE__ */ jsx2("div", { className: "sm:w-[120px] sm:shrink-0", children: /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block", children: formatClassTime(cls) }) }),
451
+ /* @__PURE__ */ jsx2(
452
+ "span",
453
+ {
454
+ className: "inline-flex items-center px-2 py-0.5 rounded-full border text-xs font-medium sm:hidden",
455
+ style: { borderColor: "#0885FE", color: "#0885FE" },
456
+ children: "GROUP"
457
+ }
458
+ ),
459
+ /* @__PURE__ */ jsx2("div", { className: "sm:hidden flex flex-col gap-px", children: isFull ? /* @__PURE__ */ jsx2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
460
+ /* @__PURE__ */ jsxs2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
461
+ available,
462
+ "/",
463
+ cls.maxParticipants
464
+ ] }),
465
+ /* @__PURE__ */ jsx2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "available" })
466
+ ] }) })
467
+ ] }),
468
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-1 flex-col gap-2 min-w-0 sm:contents", children: [
469
+ /* @__PURE__ */ jsxs2("div", { className: "flex-1 flex flex-col gap-2 min-w-0", children: [
470
+ /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block truncate", children: cls.name }),
471
+ cls.hostName && /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-normal text-(--color-dark,#200C02) opacity-80 block truncate", children: cls.hostName })
472
+ ] }),
473
+ /* @__PURE__ */ jsx2("div", { className: "hidden sm:flex w-14 shrink-0 flex-col gap-px", children: isFull ? /* @__PURE__ */ jsx2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
474
+ /* @__PURE__ */ jsxs2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
475
+ available,
476
+ "/",
477
+ cls.maxParticipants
478
+ ] }),
479
+ /* @__PURE__ */ jsx2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "available" })
480
+ ] }) }),
481
+ /* @__PURE__ */ jsx2(
482
+ "button",
483
+ {
484
+ className: "w-full sm:w-[200px] sm:shrink-0 px-5 py-[10px] flex items-center justify-center rounded-lg font-body text-base font-bold cursor-pointer box-border",
485
+ style: isFull ? {
486
+ background: "transparent",
487
+ border: "1px solid var(--color-dark, #200C02)",
488
+ color: "var(--color-dark, #200C02)"
489
+ } : {
490
+ background: "var(--color-primary, #3D312B)",
491
+ border: "none",
492
+ color: "var(--color-light, #FEFFF0)"
493
+ },
494
+ onClick: () => {
495
+ if (!cls.id) return;
496
+ onOpenModal?.(isFull ? "waitlist" : "book", cls.id, selectedDate);
497
+ },
498
+ children: isFull ? "Join Waitlist" : "Book Now"
499
+ }
500
+ )
501
+ ] })
502
+ ] }),
503
+ i < selectedClasses.length - 1 && /* @__PURE__ */ jsx2("div", { className: "h-px bg-black/20" })
504
+ ] }, cls.id ?? i);
505
+ }) })
506
+ ] });
507
+ }
508
+ function CalendarFoldIcon() {
509
+ return /* @__PURE__ */ jsxs2(
510
+ "svg",
511
+ {
512
+ width: "16",
513
+ height: "16",
514
+ viewBox: "0 0 24 24",
515
+ fill: "none",
516
+ stroke: "currentColor",
517
+ strokeWidth: "2",
518
+ strokeLinecap: "round",
519
+ strokeLinejoin: "round",
520
+ style: { flexShrink: 0 },
521
+ children: [
522
+ /* @__PURE__ */ jsx2("path", { d: "M8 2v4" }),
523
+ /* @__PURE__ */ jsx2("path", { d: "M16 2v4" }),
524
+ /* @__PURE__ */ jsx2("path", { d: "M21 17V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11Z" }),
525
+ /* @__PURE__ */ jsx2("path", { d: "M3 10h18" }),
526
+ /* @__PURE__ */ jsx2("path", { d: "M15 22v-4a2 2 0 0 1 2-2h4" })
527
+ ]
528
+ }
529
+ );
530
+ }
531
+ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAfter }) {
532
+ const [schedule, setSchedule] = useState2(null);
533
+ const [loading, setLoading] = useState2(true);
534
+ const [inEditor, setInEditor] = useState2(false);
535
+ const [isHovered, setIsHovered] = useState2(false);
536
+ const [modalState, setModalState] = useState2(null);
537
+ const switchScheduleIdRef = useRef(null);
538
+ const dates = useMemo(() => {
539
+ const today = /* @__PURE__ */ new Date();
540
+ today.setHours(0, 0, 0, 0);
541
+ return Array.from({ length: 14 }, (_, i) => {
542
+ const d = new Date(today);
543
+ d.setDate(today.getDate() + i);
544
+ return d;
545
+ });
546
+ }, []);
547
+ const [selectedIdx, setSelectedIdx] = useState2(0);
548
+ useEffect(() => {
549
+ if (!schedule?.classes) return;
550
+ for (let i = 0; i < dates.length; i++) {
551
+ if (schedule.classes.some((c) => classRunsOnDate(c, dates[i], schedule.type))) {
552
+ setSelectedIdx(i);
553
+ return;
554
+ }
555
+ }
556
+ }, [schedule, dates]);
557
+ useEffect(() => {
558
+ if (typeof window === "undefined") return;
559
+ const isInEditor = window.self !== window.top;
560
+ setInEditor(isInEditor);
561
+ if (isInEditor) {
562
+ const startEmpty = initialScheduleId === null;
563
+ if (startEmpty) setLoading(false);
564
+ let initialized = startEmpty;
565
+ const timer = !startEmpty ? setTimeout(() => {
566
+ if (!initialized) {
567
+ initialized = true;
568
+ setLoading(false);
569
+ }
570
+ }, 5e3) : null;
571
+ const handler = (e) => {
572
+ if (e.data?.type === "ow:clear-scheduling-widget") {
573
+ if (!e.data.insertAfter || e.data.insertAfter !== insertAfter) return;
574
+ setSchedule(null);
575
+ setLoading(false);
576
+ return;
577
+ }
578
+ if (e.data?.type === "ow:switch-schedule") {
579
+ if (!e.data.insertAfter || e.data.insertAfter !== insertAfter) return;
580
+ switchScheduleIdRef.current = e.data.scheduleId ?? null;
581
+ initialized = false;
582
+ setLoading(true);
583
+ window.parent.postMessage({ type: "ow:request-user-schedules", insertAfter }, "*");
584
+ return;
585
+ }
586
+ if (e.data?.type !== "ow:user-schedules-response") return;
587
+ if (e.data.insertAfter && e.data.insertAfter !== insertAfter) return;
588
+ if (initialized && switchScheduleIdRef.current === null) return;
589
+ initialized = true;
590
+ if (timer) clearTimeout(timer);
591
+ const schedules = e.data.schedules ?? [];
592
+ const isSwitching = switchScheduleIdRef.current !== null;
593
+ const target = isSwitching ? schedules.find((s) => s.id === switchScheduleIdRef.current) ?? schedules[0] ?? null : initialScheduleId ? schedules.find((s) => s.id === initialScheduleId) ?? schedules[0] ?? null : schedules[0] ?? null;
594
+ switchScheduleIdRef.current = null;
595
+ setSchedule(target);
596
+ setLoading(false);
597
+ if (notifyOnConnect && target && !isSwitching) {
598
+ window.parent.postMessage({
599
+ type: "ow:schedule-connected",
600
+ schedule: { id: target.id, name: target.name },
601
+ insertAfter
602
+ }, "*");
603
+ window.postMessage({ type: "ow:schedule-linked", scheduleId: target.id, insertAfter }, "*");
604
+ }
605
+ };
606
+ window.addEventListener("message", handler);
607
+ if (!startEmpty) window.parent.postMessage({ type: "ow:request-user-schedules", insertAfter }, "*");
608
+ return () => {
609
+ window.removeEventListener("message", handler);
610
+ if (timer) clearTimeout(timer);
611
+ };
612
+ }
613
+ if (initialScheduleId === null) {
614
+ setLoading(false);
615
+ return;
616
+ }
617
+ ;
618
+ (async () => {
619
+ try {
620
+ if (initialScheduleId) {
621
+ const res = await fetch(`${API_URL}/api/schedule/id/${initialScheduleId}`);
622
+ const data = await res.json();
623
+ setSchedule(data?.id ? data : null);
624
+ } else {
625
+ const domain = getApiDomain();
626
+ const sitemapRes = await fetch(`${API_URL}/api/schedule/sitemap/${domain}`);
627
+ const slugs = await sitemapRes.json();
628
+ if (!Array.isArray(slugs) || !slugs.length) {
629
+ setLoading(false);
630
+ return;
631
+ }
632
+ const { slug } = [...slugs].sort(
633
+ (a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
634
+ )[0];
635
+ const res = await fetch(`${API_URL}/api/schedule/${domain}/${slug}`);
636
+ const data = await res.json();
637
+ setSchedule(data?.id ? data : null);
638
+ }
639
+ } catch {
640
+ }
641
+ setLoading(false);
642
+ })();
643
+ }, []);
644
+ const timezoneLabel = schedule?.timezone ? (() => {
645
+ try {
646
+ return buildTimezoneLabel(schedule.timezone);
647
+ } catch {
648
+ return void 0;
649
+ }
650
+ })() : void 0;
651
+ const handleModalSubmit = async (email) => {
652
+ if (!modalState) return;
653
+ const { mode, classId, classDate } = modalState;
654
+ const endpoint = mode === "book" ? `${API_URL}/api/schedule/class/${classId}/booking` : `${API_URL}/api/schedule/class/${classId}/waitlist`;
655
+ const res = await fetch(endpoint, {
656
+ method: "POST",
657
+ headers: { "Content-Type": "application/json" },
658
+ body: JSON.stringify({ classDate: classDate.toISOString(), email })
659
+ });
660
+ if (!res.ok) {
661
+ const data = await res.json().catch(() => ({}));
662
+ throw new Error(data?.message ?? "Something went wrong. Please try again.");
663
+ }
664
+ };
665
+ const handleReplaceSchedule = () => {
666
+ setIsHovered(false);
667
+ if (schedule) {
668
+ window.parent.postMessage({
669
+ type: "ow:schedule-connected",
670
+ schedule: { id: schedule.id, name: schedule.name },
671
+ insertAfter
672
+ }, "*");
673
+ } else {
674
+ window.parent.postMessage({ type: "ow:scheduling-not-connected", insertAfter }, "*");
675
+ }
676
+ };
677
+ const sectionId = `scheduling-${insertAfter}`;
678
+ return /* @__PURE__ */ jsxs2(
679
+ "section",
680
+ {
681
+ "data-ohw-section": sectionId,
682
+ "data-ohw-scheduling-anchor": insertAfter,
683
+ className: "w-full box-border py-10 px-5 sm:py-20 sm:px-16 font-body bg-(--color-light,#FEFFF0) relative",
684
+ onMouseEnter: () => inEditor && setIsHovered(true),
685
+ onMouseLeave: () => setIsHovered(false),
686
+ children: [
687
+ inEditor && isHovered && !!schedule && /* @__PURE__ */ jsxs2(
688
+ "div",
689
+ {
690
+ className: "absolute inset-0 z-10 pointer-events-auto cursor-pointer",
691
+ onClick: handleReplaceSchedule,
692
+ children: [
693
+ /* @__PURE__ */ jsx2("div", { className: "absolute inset-0", style: { background: "#0885FE", opacity: 0.18 } }),
694
+ /* @__PURE__ */ jsx2("div", { className: "absolute inset-0", style: { boxShadow: "inset 0 0 0 1.5px #0885FE" } }),
695
+ /* @__PURE__ */ jsx2("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxs2(
696
+ "div",
697
+ {
698
+ className: "bg-white border border-[#E7E5E4] rounded-md px-3 py-1.5 flex items-center gap-1.5",
699
+ style: {
700
+ fontSize: 14,
701
+ lineHeight: "24px",
702
+ fontFamily: "'Figtree', system-ui, sans-serif",
703
+ fontWeight: 500,
704
+ color: "#0C0A09"
705
+ },
706
+ children: [
707
+ /* @__PURE__ */ jsx2(CalendarFoldIcon, {}),
708
+ "Replace schedule"
709
+ ]
710
+ }
711
+ ) })
712
+ ]
713
+ }
714
+ ),
715
+ /* @__PURE__ */ jsxs2("div", { className: "max-w-[1280px] mx-auto flex flex-col items-center gap-16", children: [
716
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center gap-4", children: [
717
+ /* @__PURE__ */ jsx2("h2", { className: "font-display text-4xl sm:text-5xl font-bold text-center m-0 text-(--color-dark,#200C02)", children: schedule?.name ?? "Book an appointment" }),
718
+ schedule?.description && /* @__PURE__ */ jsx2("p", { className: "font-body text-body text-center m-0 text-(--color-accent,#A89B83)", children: schedule.description })
719
+ ] }),
720
+ /* @__PURE__ */ jsxs2("div", { className: "w-full flex flex-col items-end gap-3", children: [
721
+ timezoneLabel && /* @__PURE__ */ jsx2("span", { className: "font-body text-sm font-normal text-(--color-accent,#A89B83)", children: timezoneLabel }),
722
+ /* @__PURE__ */ jsx2("div", { className: "w-full p-0 sm:p-10 box-border", children: loading ? /* @__PURE__ */ jsx2(LoadingSkeleton, {}) : !schedule ? /* @__PURE__ */ jsx2(EmptyState, { inEditor }) : /* @__PURE__ */ jsx2(
723
+ ScheduleView,
724
+ {
725
+ schedule,
726
+ dates,
727
+ selectedIdx,
728
+ onSelectDate: setSelectedIdx,
729
+ onOpenModal: inEditor ? void 0 : (mode, classId, classDate) => setModalState({ mode, classId, classDate })
730
+ }
731
+ ) })
732
+ ] })
733
+ ] }),
734
+ modalState && /* @__PURE__ */ jsx2(
735
+ EmailCaptureModal,
736
+ {
737
+ title: modalState.mode === "book" ? "Confirm your spot" : "Leave your email",
738
+ subtitle: modalState.mode === "book" ? "We'll send your booking confirmation here." : "We'll let you know when spots open up!",
739
+ onSubmit: handleModalSubmit,
740
+ onClose: () => setModalState(null)
741
+ }
742
+ )
743
+ ]
744
+ }
745
+ );
746
+ }
5
747
 
6
748
  // src/ui/toggle-group.tsx
7
- import * as React from "react";
749
+ import * as React2 from "react";
8
750
 
9
751
  // node_modules/clsx/dist/clsx.mjs
10
752
  function r(e) {
@@ -3324,7 +4066,7 @@ var cva = (base, config) => (props) => {
3324
4066
 
3325
4067
  // src/ui/toggle.tsx
3326
4068
  import { Toggle as TogglePrimitive } from "radix-ui";
3327
- import { jsx } from "react/jsx-runtime";
4069
+ import { jsx as jsx3 } from "react/jsx-runtime";
3328
4070
  var toggleVariants = cva(
3329
4071
  "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
3330
4072
  {
@@ -3351,7 +4093,7 @@ function Toggle({
3351
4093
  size,
3352
4094
  ...props
3353
4095
  }) {
3354
- return /* @__PURE__ */ jsx(
4096
+ return /* @__PURE__ */ jsx3(
3355
4097
  TogglePrimitive.Root,
3356
4098
  {
3357
4099
  "data-slot": "toggle",
@@ -3362,8 +4104,8 @@ function Toggle({
3362
4104
  }
3363
4105
 
3364
4106
  // src/ui/toggle-group.tsx
3365
- import { jsx as jsx2 } from "react/jsx-runtime";
3366
- var ToggleGroupContext = React.createContext({
4107
+ import { jsx as jsx4 } from "react/jsx-runtime";
4108
+ var ToggleGroupContext = React2.createContext({
3367
4109
  value: "",
3368
4110
  onValueChange: () => {
3369
4111
  }
@@ -3375,13 +4117,13 @@ function ToggleGroup({
3375
4117
  children,
3376
4118
  ...props
3377
4119
  }) {
3378
- return /* @__PURE__ */ jsx2(
4120
+ return /* @__PURE__ */ jsx4(
3379
4121
  "div",
3380
4122
  {
3381
4123
  role: "group",
3382
4124
  className: cn("flex items-center gap-1 p-1 bg-muted rounded-md", className),
3383
4125
  ...props,
3384
- children: /* @__PURE__ */ jsx2(ToggleGroupContext.Provider, { value: { value, onValueChange }, children })
4126
+ children: /* @__PURE__ */ jsx4(ToggleGroupContext.Provider, { value: { value, onValueChange }, children })
3385
4127
  }
3386
4128
  );
3387
4129
  }
@@ -3391,8 +4133,8 @@ function ToggleGroupItem({
3391
4133
  children,
3392
4134
  ...props
3393
4135
  }) {
3394
- const { value: groupValue, onValueChange } = React.useContext(ToggleGroupContext);
3395
- return /* @__PURE__ */ jsx2(
4136
+ const { value: groupValue, onValueChange } = React2.useContext(ToggleGroupContext);
4137
+ return /* @__PURE__ */ jsx4(
3396
4138
  Toggle,
3397
4139
  {
3398
4140
  pressed: groupValue === value,
@@ -3433,8 +4175,866 @@ function isEditSessionActive() {
3433
4175
  return new URLSearchParams(q).get("mode") === "edit";
3434
4176
  }
3435
4177
 
4178
+ // src/ui/link-modal/calcPopoverPos.ts
4179
+ function calcPopoverPos(rect, parentScroll, approxW = 483, approxH = 320) {
4180
+ const GAP = 8;
4181
+ const canvasTopInIframe = parentScroll != null ? parentScroll.headerH - parentScroll.iframeOffsetTop : 0;
4182
+ const visibleTop = rect.top - canvasTopInIframe;
4183
+ let top = rect.top - GAP;
4184
+ let transform = "translateX(-50%) translateY(-100%)";
4185
+ if (visibleTop < approxH + GAP) {
4186
+ top = rect.bottom + GAP;
4187
+ transform = "translateX(-50%)";
4188
+ }
4189
+ const rawLeft = rect.left + rect.width / 2;
4190
+ const left = Math.max(GAP + approxW / 2, Math.min(rawLeft, window.innerWidth - GAP - approxW / 2));
4191
+ return { top, left, transform };
4192
+ }
4193
+
4194
+ // src/ui/button.tsx
4195
+ import * as React3 from "react";
4196
+ import { Slot } from "radix-ui";
4197
+ import { jsx as jsx5 } from "react/jsx-runtime";
4198
+ var buttonVariants = cva(
4199
+ "inline-flex items-center justify-center gap-1 whitespace-nowrap rounded-md text-sm font-medium transition-colors outline-none disabled:pointer-events-none disabled:opacity-50 min-w-[80px] px-3 py-2",
4200
+ {
4201
+ variants: {
4202
+ variant: {
4203
+ default: "bg-primary text-primary-foreground hover:opacity-90",
4204
+ outline: "border border-border bg-background text-foreground shadow-sm hover:bg-muted/80",
4205
+ ghost: "min-w-0 px-3 py-2 text-foreground hover:bg-muted/50"
4206
+ },
4207
+ size: {
4208
+ default: "h-9",
4209
+ sm: "h-8 min-w-[64px] px-2 py-1.5 text-sm"
4210
+ }
4211
+ },
4212
+ defaultVariants: {
4213
+ variant: "default",
4214
+ size: "default"
4215
+ }
4216
+ }
4217
+ );
4218
+ var Button = React3.forwardRef(
4219
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
4220
+ const Comp = asChild ? Slot.Root : "button";
4221
+ return /* @__PURE__ */ jsx5(
4222
+ Comp,
4223
+ {
4224
+ ref,
4225
+ "data-slot": "button",
4226
+ className: cn(buttonVariants({ variant, size, className })),
4227
+ ...props
4228
+ }
4229
+ );
4230
+ }
4231
+ );
4232
+ Button.displayName = "Button";
4233
+
4234
+ // src/ui/icons.tsx
4235
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
4236
+ function IconX({ className, ...props }) {
4237
+ return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, ...props, children: [
4238
+ /* @__PURE__ */ jsx6("path", { d: "M18 6 6 18" }),
4239
+ /* @__PURE__ */ jsx6("path", { d: "m6 6 12 12" })
4240
+ ] });
4241
+ }
4242
+ function IconChevronDown({ className, ...props }) {
4243
+ return /* @__PURE__ */ jsx6("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, ...props, children: /* @__PURE__ */ jsx6("path", { d: "m6 9 6 6 6-6" }) });
4244
+ }
4245
+ function IconFile({ className, ...props }) {
4246
+ return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, ...props, children: [
4247
+ /* @__PURE__ */ jsx6("path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" }),
4248
+ /* @__PURE__ */ jsx6("path", { d: "M14 2v4a2 2 0 0 0 2 2h4" })
4249
+ ] });
4250
+ }
4251
+ function IconInfo({ className, ...props }) {
4252
+ return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, ...props, children: [
4253
+ /* @__PURE__ */ jsx6("circle", { cx: "12", cy: "12", r: "10" }),
4254
+ /* @__PURE__ */ jsx6("path", { d: "M12 16v-4" }),
4255
+ /* @__PURE__ */ jsx6("path", { d: "M12 8h.01" })
4256
+ ] });
4257
+ }
4258
+ function IconArrowRight({ className, ...props }) {
4259
+ return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, ...props, children: [
4260
+ /* @__PURE__ */ jsx6("path", { d: "M5 12h14" }),
4261
+ /* @__PURE__ */ jsx6("path", { d: "m12 5 7 7-7 7" })
4262
+ ] });
4263
+ }
4264
+ function IconSection({ className, ...props }) {
4265
+ return /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, ...props, children: [
4266
+ /* @__PURE__ */ jsx6("rect", { width: "18", height: "7", x: "3", y: "3", rx: "1" }),
4267
+ /* @__PURE__ */ jsx6("rect", { width: "9", height: "7", x: "3", y: "14", rx: "1" }),
4268
+ /* @__PURE__ */ jsx6("rect", { width: "5", height: "7", x: "16", y: "14", rx: "1" })
4269
+ ] });
4270
+ }
4271
+
4272
+ // src/ui/link-modal/DestinationBreadcrumb.tsx
4273
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
4274
+ function DestinationBreadcrumb({ pageTitle, sectionLabel }) {
4275
+ return /* @__PURE__ */ jsxs4("div", { className: "flex w-full flex-col gap-2", children: [
4276
+ /* @__PURE__ */ jsx7("p", { className: "text-sm font-medium text-foreground", children: "Destination" }),
4277
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-3", children: [
4278
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [
4279
+ /* @__PURE__ */ jsx7(IconFile, { className: "shrink-0 text-foreground", "aria-hidden": true }),
4280
+ /* @__PURE__ */ jsx7("span", { className: "text-sm font-medium leading-none text-foreground", children: pageTitle })
4281
+ ] }),
4282
+ /* @__PURE__ */ jsx7(IconArrowRight, { className: "shrink-0 text-muted-foreground", "aria-hidden": true }),
4283
+ /* @__PURE__ */ jsxs4("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [
4284
+ /* @__PURE__ */ jsx7(IconSection, { className: "shrink-0 text-foreground", "aria-hidden": true }),
4285
+ /* @__PURE__ */ jsx7("span", { className: "truncate text-sm font-medium leading-none text-foreground", children: sectionLabel })
4286
+ ] })
4287
+ ] })
4288
+ ] });
4289
+ }
4290
+
4291
+ // src/ui/link-modal/SectionTreeItem.tsx
4292
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
4293
+ function SectionTreeItem({ section, onSelect, selected }) {
4294
+ const interactive = Boolean(onSelect);
4295
+ return /* @__PURE__ */ jsxs5("div", { className: "flex h-9 w-full items-end pl-3", children: [
4296
+ /* @__PURE__ */ jsx8(
4297
+ "div",
4298
+ {
4299
+ className: "mr-[-1px] h-9 w-2 shrink-0 rounded-bl-sm border-b border-l border-border mb-4",
4300
+ "aria-hidden": true
4301
+ }
4302
+ ),
4303
+ /* @__PURE__ */ jsxs5(
4304
+ "div",
4305
+ {
4306
+ role: interactive ? "button" : void 0,
4307
+ tabIndex: interactive ? 0 : void 0,
4308
+ onClick: interactive ? () => onSelect?.(section) : void 0,
4309
+ onKeyDown: interactive ? (e) => {
4310
+ if (e.key === "Enter" || e.key === " ") {
4311
+ e.preventDefault();
4312
+ onSelect?.(section);
4313
+ }
4314
+ } : void 0,
4315
+ className: cn(
4316
+ "flex h-9 min-w-0 flex-1 items-center gap-2 rounded-md border border-border bg-background p-3",
4317
+ interactive && "cursor-pointer hover:bg-muted/30",
4318
+ interactive && selected && "border-primary"
4319
+ ),
4320
+ children: [
4321
+ /* @__PURE__ */ jsx8(IconSection, { className: "shrink-0 text-foreground", "aria-hidden": true }),
4322
+ /* @__PURE__ */ jsx8("span", { className: "truncate text-sm font-normal leading-5 text-foreground", children: section.label })
4323
+ ]
4324
+ }
4325
+ )
4326
+ ] });
4327
+ }
4328
+ function SectionPickerList({ sections, onSelect }) {
4329
+ if (sections.length === 0) {
4330
+ return /* @__PURE__ */ jsx8("p", { className: "text-sm text-muted-foreground", children: "No sections found on this page." });
4331
+ }
4332
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-1", children: [
4333
+ /* @__PURE__ */ jsx8("p", { className: "text-sm text-muted-foreground", children: "Pick a section this link should scroll to." }),
4334
+ sections.map((section) => /* @__PURE__ */ jsx8(SectionTreeItem, { section, onSelect }, section.id))
4335
+ ] });
4336
+ }
4337
+
4338
+ // src/ui/link-modal/UrlOrPageInput.tsx
4339
+ import { useEffect as useEffect2, useId, useRef as useRef2, useState as useState3 } from "react";
4340
+
4341
+ // src/ui/input.tsx
4342
+ import * as React4 from "react";
4343
+ import { jsx as jsx9 } from "react/jsx-runtime";
4344
+ var Input = React4.forwardRef(
4345
+ ({ className, type, ...props }, ref) => {
4346
+ return /* @__PURE__ */ jsx9(
4347
+ "input",
4348
+ {
4349
+ type,
4350
+ ref,
4351
+ "data-slot": "input",
4352
+ className: cn(
4353
+ "flex h-full w-full min-w-0 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground",
4354
+ className
4355
+ ),
4356
+ ...props
4357
+ }
4358
+ );
4359
+ }
4360
+ );
4361
+ Input.displayName = "Input";
4362
+
4363
+ // src/ui/label.tsx
4364
+ import { Label as LabelPrimitive } from "radix-ui";
4365
+ import { jsx as jsx10 } from "react/jsx-runtime";
4366
+ function Label({ className, ...props }) {
4367
+ return /* @__PURE__ */ jsx10(
4368
+ LabelPrimitive.Root,
4369
+ {
4370
+ "data-slot": "label",
4371
+ className: cn("text-sm font-medium leading-5 text-foreground", className),
4372
+ ...props
4373
+ }
4374
+ );
4375
+ }
4376
+
4377
+ // src/ui/popover.tsx
4378
+ import { Popover as PopoverPrimitive } from "radix-ui";
4379
+ import { jsx as jsx11 } from "react/jsx-runtime";
4380
+ function Popover({ ...props }) {
4381
+ return /* @__PURE__ */ jsx11(PopoverPrimitive.Root, { "data-slot": "popover", ...props });
4382
+ }
4383
+ function PopoverTrigger({ ...props }) {
4384
+ return /* @__PURE__ */ jsx11(PopoverPrimitive.Trigger, { "data-slot": "popover-trigger", ...props });
4385
+ }
4386
+ function PopoverContent({
4387
+ className,
4388
+ align = "start",
4389
+ sideOffset = 6,
4390
+ ...props
4391
+ }) {
4392
+ return /* @__PURE__ */ jsx11(PopoverPrimitive.Portal, { children: /* @__PURE__ */ jsx11(
4393
+ PopoverPrimitive.Content,
4394
+ {
4395
+ "data-slot": "popover-content",
4396
+ align,
4397
+ sideOffset,
4398
+ className: cn(
4399
+ "z-[2147483647] w-[var(--radix-popover-trigger-width)] overflow-auto rounded-lg border border-border bg-popover py-1 shadow-lg outline-none",
4400
+ className
4401
+ ),
4402
+ ...props
4403
+ }
4404
+ ) });
4405
+ }
4406
+
4407
+ // src/ui/link-modal/UrlOrPageInput.tsx
4408
+ import { jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
4409
+ function FieldChevron({ onClick }) {
4410
+ return /* @__PURE__ */ jsx12(
4411
+ "button",
4412
+ {
4413
+ type: "button",
4414
+ className: "flex shrink-0 items-center justify-end border-0 bg-transparent p-0 pl-4 text-muted-foreground outline-none",
4415
+ onClick,
4416
+ "aria-label": "Open page list",
4417
+ tabIndex: -1,
4418
+ children: /* @__PURE__ */ jsx12(IconChevronDown, {})
4419
+ }
4420
+ );
4421
+ }
4422
+ function UrlOrPageInput({
4423
+ value,
4424
+ selectedPage,
4425
+ placeholder = "Paste a URL or choose a page",
4426
+ dropdownOpen,
4427
+ onDropdownOpenChange,
4428
+ filteredPages,
4429
+ onInputChange,
4430
+ onPageSelect,
4431
+ readOnly = false,
4432
+ urlError
4433
+ }) {
4434
+ const inputId = useId();
4435
+ const inputRef = useRef2(null);
4436
+ const [isEditing, setIsEditing] = useState3(false);
4437
+ const [isFocused, setIsFocused] = useState3(false);
4438
+ const showFilledDisplay = Boolean(selectedPage) && !isEditing && !readOnly;
4439
+ useEffect2(() => {
4440
+ if (selectedPage) setIsEditing(false);
4441
+ }, [selectedPage]);
4442
+ const openDropdown = () => {
4443
+ if (readOnly || filteredPages.length === 0) return;
4444
+ onDropdownOpenChange(true);
4445
+ };
4446
+ const toggleDropdown = (e) => {
4447
+ e.stopPropagation();
4448
+ e.preventDefault();
4449
+ if (readOnly || filteredPages.length === 0) return;
4450
+ onDropdownOpenChange(!dropdownOpen);
4451
+ };
4452
+ const enterEditMode = () => {
4453
+ if (readOnly) return;
4454
+ setIsEditing(true);
4455
+ requestAnimationFrame(() => inputRef.current?.focus());
4456
+ };
4457
+ const fieldClassName = cn(
4458
+ "data-ohw-link-field flex h-10 w-full items-center overflow-hidden rounded-md border bg-background pl-3 pr-3 py-2 outline-none transition-[border-color,box-shadow]",
4459
+ urlError ? "border-destructive shadow-[0_0_0_1px_var(--ohw-destructive)]" : isFocused ? "border-primary shadow-[0_0_0_1px_var(--ohw-primary)]" : "border-input"
4460
+ );
4461
+ return /* @__PURE__ */ jsxs6("div", { className: "flex w-full flex-col gap-2 p-0", children: [
4462
+ /* @__PURE__ */ jsx12(Label, { htmlFor: inputId, className: cn(urlError && "text-destructive"), children: "Destination" }),
4463
+ /* @__PURE__ */ jsxs6(Popover, { open: dropdownOpen && !readOnly, onOpenChange: onDropdownOpenChange, children: [
4464
+ /* @__PURE__ */ jsx12(PopoverTrigger, { asChild: true, children: showFilledDisplay ? /* @__PURE__ */ jsxs6(
4465
+ "button",
4466
+ {
4467
+ type: "button",
4468
+ "data-ohw-link-field": true,
4469
+ className: cn(fieldClassName, "m-0 cursor-pointer border-solid text-left"),
4470
+ onClick: openDropdown,
4471
+ onFocus: () => setIsFocused(true),
4472
+ onBlur: () => setIsFocused(false),
4473
+ onKeyDown: (e) => {
4474
+ if (e.key === "Backspace" || e.key === "Delete" || e.key.length === 1) {
4475
+ enterEditMode();
4476
+ }
4477
+ },
4478
+ children: [
4479
+ /* @__PURE__ */ jsx12("div", { className: "flex shrink-0 items-center pr-2", children: /* @__PURE__ */ jsx12(IconFile, { className: "text-foreground", "aria-hidden": true }) }),
4480
+ /* @__PURE__ */ jsx12("span", { className: "min-w-0 flex-1 truncate text-sm font-normal text-foreground", children: selectedPage.title }),
4481
+ /* @__PURE__ */ jsx12(FieldChevron, { onClick: toggleDropdown })
4482
+ ]
4483
+ }
4484
+ ) : /* @__PURE__ */ jsxs6(
4485
+ "div",
4486
+ {
4487
+ "data-ohw-link-field": true,
4488
+ className: fieldClassName,
4489
+ onMouseDown: (e) => {
4490
+ if (readOnly) return;
4491
+ if (e.target === e.currentTarget) enterEditMode();
4492
+ },
4493
+ children: [
4494
+ selectedPage ? /* @__PURE__ */ jsx12("div", { className: "flex shrink-0 items-center pr-2", children: /* @__PURE__ */ jsx12(IconFile, { className: "text-foreground", "aria-hidden": true }) }) : null,
4495
+ readOnly ? /* @__PURE__ */ jsx12("span", { className: "min-w-0 flex-1 truncate text-sm leading-5 text-foreground", children: selectedPage?.title ?? value }) : /* @__PURE__ */ jsx12(
4496
+ Input,
4497
+ {
4498
+ ref: inputRef,
4499
+ id: inputId,
4500
+ value,
4501
+ onChange: (e) => onInputChange(e.target.value),
4502
+ onFocus: () => {
4503
+ setIsFocused(true);
4504
+ setIsEditing(true);
4505
+ openDropdown();
4506
+ },
4507
+ onBlur: () => setIsFocused(false),
4508
+ placeholder,
4509
+ className: cn(
4510
+ "min-w-0 flex-1 truncate border-0 bg-transparent p-0 text-sm font-normal leading-5 shadow-none outline-none ring-0",
4511
+ value ? "text-foreground" : "text-muted-foreground placeholder:text-muted-foreground"
4512
+ )
4513
+ }
4514
+ ),
4515
+ !readOnly ? /* @__PURE__ */ jsx12(FieldChevron, { onClick: toggleDropdown }) : null
4516
+ ]
4517
+ }
4518
+ ) }),
4519
+ /* @__PURE__ */ jsx12(PopoverContent, { "data-ohw-link-popover-root": "", onOpenAutoFocus: (e) => e.preventDefault(), children: filteredPages.map((page) => /* @__PURE__ */ jsxs6(
4520
+ "button",
4521
+ {
4522
+ type: "button",
4523
+ className: "flex h-9 w-full items-center gap-2 border-0 bg-transparent px-3 text-left text-sm leading-5 text-foreground outline-none hover:bg-muted",
4524
+ onClick: () => onPageSelect(page),
4525
+ children: [
4526
+ /* @__PURE__ */ jsx12(IconFile, { className: "shrink-0", "aria-hidden": true }),
4527
+ /* @__PURE__ */ jsx12("span", { className: "truncate", children: page.title })
4528
+ ]
4529
+ },
4530
+ page.path
4531
+ )) })
4532
+ ] }),
4533
+ urlError ? /* @__PURE__ */ jsx12("p", { className: "text-sm font-medium text-destructive", children: urlError }) : null
4534
+ ] });
4535
+ }
4536
+
4537
+ // src/ui/link-modal/useLinkModalState.ts
4538
+ import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useState as useState4 } from "react";
4539
+
4540
+ // src/ui/link-modal/linkModal.utils.ts
4541
+ function parseTarget(target) {
4542
+ if (!target) return { pageRoute: "", sectionId: null };
4543
+ const hashIndex = target.indexOf("#");
4544
+ if (hashIndex === -1) return { pageRoute: target, sectionId: null };
4545
+ return {
4546
+ pageRoute: target.slice(0, hashIndex),
4547
+ sectionId: target.slice(hashIndex + 1) || null
4548
+ };
4549
+ }
4550
+ function normalizePath(path) {
4551
+ const trimmed = path.trim();
4552
+ if (!trimmed) return "/";
4553
+ try {
4554
+ if (/^https?:\/\//i.test(trimmed)) {
4555
+ return new URL(trimmed).pathname || "/";
4556
+ }
4557
+ } catch {
4558
+ }
4559
+ const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
4560
+ if (withSlash.length > 1 && withSlash.endsWith("/")) return withSlash.slice(0, -1);
4561
+ return withSlash;
4562
+ }
4563
+ function findPageByPath(pages, path) {
4564
+ const normalized = normalizePath(path);
4565
+ return pages.find((p) => normalizePath(p.path) === normalized);
4566
+ }
4567
+ function inferPageTitleFromPath(path) {
4568
+ const normalized = normalizePath(path);
4569
+ if (normalized === "/") return "Home";
4570
+ const segment = normalized.split("/").filter(Boolean).pop() ?? "";
4571
+ if (!segment) return "Home";
4572
+ return segment.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
4573
+ }
4574
+ function isInternalPath(route) {
4575
+ const trimmed = route.trim();
4576
+ if (!trimmed) return false;
4577
+ if (/^https?:\/\//i.test(trimmed)) return false;
4578
+ if (trimmed.startsWith("/")) return true;
4579
+ if (trimmed.includes(" ") || trimmed.includes(".")) return false;
4580
+ return true;
4581
+ }
4582
+ function resolvePage(route, pages) {
4583
+ const found = findPageByPath(pages, route);
4584
+ if (found) return { path: found.path, title: found.title };
4585
+ return { path: normalizePath(route), title: inferPageTitleFromPath(route) };
4586
+ }
4587
+ function getSectionsForPath(sectionsByPath, pageRoute, fallback) {
4588
+ if (!sectionsByPath) return fallback;
4589
+ const normalized = normalizePath(pageRoute);
4590
+ return sectionsByPath[normalized] ?? sectionsByPath[pageRoute] ?? fallback;
4591
+ }
4592
+ function buildTarget(page, section) {
4593
+ if (section) return `${page.path}#${section.id}`;
4594
+ return page.path;
4595
+ }
4596
+ function isValidUrl(urlString) {
4597
+ try {
4598
+ const url = new URL(urlString);
4599
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
4600
+ const hostname = url.hostname;
4601
+ if (!hostname || hostname.length === 0) return false;
4602
+ if (hostname.includes(" ")) return false;
4603
+ return hostname.includes(".") || hostname === "localhost";
4604
+ } catch {
4605
+ return false;
4606
+ }
4607
+ }
4608
+ function normalizeUrl(value) {
4609
+ const trimmed = value.trim();
4610
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
4611
+ return `https://${trimmed}`;
4612
+ }
4613
+ function validateUrlInput(value, pages, allPages) {
4614
+ if (!value.trim()) return "";
4615
+ const filtered = pages.filter((p) => p.title.toLowerCase().startsWith(value.toLowerCase()));
4616
+ if (filtered.length > 0) return "";
4617
+ const matchingPage = allPages.find((p) => p.title.toLowerCase() === value.toLowerCase());
4618
+ if (matchingPage) return "";
4619
+ if (!isValidUrl(normalizeUrl(value))) {
4620
+ return "Please enter a valid URL (e.g., https://example.com)";
4621
+ }
4622
+ return "";
4623
+ }
4624
+ function getEditModeInitialState(target, pages, sections) {
4625
+ const empty = {
4626
+ searchValue: "",
4627
+ selectedPage: null,
4628
+ selectedSection: null,
4629
+ step: "input"
4630
+ };
4631
+ if (!target) return empty;
4632
+ if (isValidUrl(normalizeUrl(target))) {
4633
+ return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
4634
+ }
4635
+ const { pageRoute, sectionId } = parseTarget(target);
4636
+ if (sectionId) {
4637
+ if (isInternalPath(pageRoute)) {
4638
+ const page = resolvePage(pageRoute, pages);
4639
+ const sec = sections.find((s) => s.id === sectionId);
4640
+ return {
4641
+ searchValue: page.title,
4642
+ selectedPage: page,
4643
+ selectedSection: sec ? { id: sec.id, label: sec.label } : null,
4644
+ step: "input"
4645
+ };
4646
+ }
4647
+ return { searchValue: pageRoute || target, selectedPage: null, selectedSection: null, step: "input" };
4648
+ }
4649
+ if (isInternalPath(pageRoute)) {
4650
+ const page = resolvePage(pageRoute, pages);
4651
+ return {
4652
+ searchValue: page.title,
4653
+ selectedPage: page,
4654
+ selectedSection: null,
4655
+ step: "input"
4656
+ };
4657
+ }
4658
+ return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
4659
+ }
4660
+ function filterAvailablePages(pages, existingTargets) {
4661
+ const taken = new Set(existingTargets);
4662
+ return pages.filter((p) => !taken.has(p.path));
4663
+ }
4664
+
4665
+ // src/ui/link-modal/useLinkModalState.ts
4666
+ function useLinkModalState({
4667
+ open,
4668
+ mode,
4669
+ pages,
4670
+ sections,
4671
+ sectionsByPath,
4672
+ initialTarget,
4673
+ existingTargets,
4674
+ onClose,
4675
+ onSubmit
4676
+ }) {
4677
+ const availablePages = useMemo2(
4678
+ () => mode === "create" ? filterAvailablePages(pages, existingTargets) : pages,
4679
+ [mode, pages, existingTargets]
4680
+ );
4681
+ const [searchValue, setSearchValue] = useState4("");
4682
+ const [selectedPage, setSelectedPage] = useState4(null);
4683
+ const [selectedSection, setSelectedSection] = useState4(null);
4684
+ const [step, setStep] = useState4("input");
4685
+ const [dropdownOpen, setDropdownOpen] = useState4(false);
4686
+ const [urlError, setUrlError] = useState4("");
4687
+ const reset = useCallback(() => {
4688
+ setSearchValue("");
4689
+ setSelectedPage(null);
4690
+ setSelectedSection(null);
4691
+ setStep("input");
4692
+ setDropdownOpen(false);
4693
+ setUrlError("");
4694
+ }, []);
4695
+ useEffect3(() => {
4696
+ if (!open) return;
4697
+ if (mode === "edit" && initialTarget) {
4698
+ const { pageRoute } = parseTarget(initialTarget);
4699
+ const targetSections = getSectionsForPath(sectionsByPath, pageRoute, sections);
4700
+ const init = getEditModeInitialState(initialTarget, pages, targetSections);
4701
+ setSearchValue(init.searchValue);
4702
+ setSelectedPage(init.selectedPage);
4703
+ setSelectedSection(init.selectedSection);
4704
+ setStep(init.step);
4705
+ } else {
4706
+ reset();
4707
+ }
4708
+ setDropdownOpen(false);
4709
+ setUrlError("");
4710
+ }, [open, mode, initialTarget, pages, sections, sectionsByPath, reset]);
4711
+ const filteredPages = useMemo2(() => {
4712
+ if (!searchValue.trim()) return availablePages;
4713
+ return availablePages.filter((p) => p.title.toLowerCase().startsWith(searchValue.toLowerCase()));
4714
+ }, [availablePages, searchValue]);
4715
+ const activeSections = useMemo2(() => {
4716
+ if (selectedPage && sectionsByPath) {
4717
+ return getSectionsForPath(sectionsByPath, selectedPage.path, sections);
4718
+ }
4719
+ return sections;
4720
+ }, [selectedPage, sectionsByPath, sections]);
4721
+ const showBreadcrumb = step === "confirmed" && selectedPage && selectedSection;
4722
+ const showSectionPicker = step === "sectionPicker" && selectedPage;
4723
+ const showSectionRow = mode === "edit" && selectedPage && selectedSection && step === "input";
4724
+ const showChooseSection = step === "input" && selectedPage && !selectedSection && !showSectionRow;
4725
+ const handleInputChange = (value) => {
4726
+ setSearchValue(value);
4727
+ setSelectedPage(null);
4728
+ setSelectedSection(null);
4729
+ setStep("input");
4730
+ setUrlError("");
4731
+ const filtered = value.trim() ? availablePages.filter((p) => p.title.toLowerCase().startsWith(value.toLowerCase())) : availablePages;
4732
+ setDropdownOpen(filtered.length > 0);
4733
+ if (value.trim() && filtered.length === 0) {
4734
+ const error = validateUrlInput(value, availablePages, pages);
4735
+ if (error) setUrlError(error);
4736
+ }
4737
+ };
4738
+ const handlePageSelect = (page) => {
4739
+ setSelectedPage({ path: page.path, title: page.title });
4740
+ setSelectedSection(null);
4741
+ setSearchValue(page.title);
4742
+ setStep("input");
4743
+ setDropdownOpen(false);
4744
+ setUrlError("");
4745
+ };
4746
+ const handleChooseSection = () => {
4747
+ setStep("sectionPicker");
4748
+ };
4749
+ const handleSectionSelect = (section) => {
4750
+ setSelectedSection({ id: section.id, label: section.label });
4751
+ if (mode === "create") {
4752
+ setStep("confirmed");
4753
+ } else {
4754
+ setStep("input");
4755
+ }
4756
+ };
4757
+ const handleBackToSections = () => {
4758
+ setStep("sectionPicker");
4759
+ };
4760
+ const handleClose = () => {
4761
+ reset();
4762
+ onClose();
4763
+ };
4764
+ const isValid = useMemo2(() => {
4765
+ if (urlError) return false;
4766
+ if (showBreadcrumb) return true;
4767
+ if (selectedPage) return true;
4768
+ if (!searchValue.trim()) return false;
4769
+ return validateUrlInput(searchValue, availablePages, pages) === "";
4770
+ }, [urlError, showBreadcrumb, selectedPage, searchValue, availablePages, pages]);
4771
+ const handleSubmit = () => {
4772
+ if (selectedPage && selectedSection) {
4773
+ onSubmit(buildTarget(selectedPage, selectedSection));
4774
+ handleClose();
4775
+ return;
4776
+ }
4777
+ if (selectedPage) {
4778
+ onSubmit(buildTarget(selectedPage, null));
4779
+ handleClose();
4780
+ return;
4781
+ }
4782
+ if (!searchValue.trim()) return;
4783
+ const error = validateUrlInput(searchValue, availablePages, pages);
4784
+ if (error) {
4785
+ setUrlError(error);
4786
+ return;
4787
+ }
4788
+ const matchingPage = pages.find((p) => p.title.toLowerCase() === searchValue.toLowerCase());
4789
+ if (matchingPage) {
4790
+ onSubmit(matchingPage.path);
4791
+ handleClose();
4792
+ return;
4793
+ }
4794
+ onSubmit(normalizeUrl(searchValue));
4795
+ handleClose();
4796
+ };
4797
+ const submitLabel = mode === "edit" ? "Save" : "Create";
4798
+ const title = mode === "edit" ? "Edit navigation item" : "Create navigation item";
4799
+ const secondaryLabel = showBreadcrumb ? "Back to sections" : "Cancel";
4800
+ const handleSecondary = showBreadcrumb ? handleBackToSections : handleClose;
4801
+ return {
4802
+ title,
4803
+ submitLabel,
4804
+ secondaryLabel,
4805
+ searchValue,
4806
+ selectedPage,
4807
+ selectedSection,
4808
+ step,
4809
+ dropdownOpen,
4810
+ setDropdownOpen,
4811
+ urlError,
4812
+ filteredPages,
4813
+ showBreadcrumb,
4814
+ showSectionPicker,
4815
+ showChooseSection,
4816
+ showSectionRow,
4817
+ isValid,
4818
+ activeSections,
4819
+ handleInputChange,
4820
+ handlePageSelect,
4821
+ handleChooseSection,
4822
+ handleSectionSelect,
4823
+ handleBackToSections,
4824
+ handleClose,
4825
+ handleSecondary,
4826
+ handleSubmit
4827
+ };
4828
+ }
4829
+
4830
+ // src/ui/link-modal/LinkEditorPanel.tsx
4831
+ import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
4832
+ function LinkEditorPanel({
4833
+ open = true,
4834
+ mode = "create",
4835
+ pages,
4836
+ sections = [],
4837
+ sectionsByPath,
4838
+ initialTarget,
4839
+ existingTargets = [],
4840
+ onClose,
4841
+ onSubmit
4842
+ }) {
4843
+ const state = useLinkModalState({
4844
+ open,
4845
+ mode,
4846
+ pages,
4847
+ sections,
4848
+ sectionsByPath,
4849
+ initialTarget,
4850
+ existingTargets,
4851
+ onClose,
4852
+ onSubmit
4853
+ });
4854
+ const isCancel = state.secondaryLabel === "Cancel";
4855
+ return /* @__PURE__ */ jsxs7(Fragment2, { children: [
4856
+ /* @__PURE__ */ jsx13(
4857
+ "button",
4858
+ {
4859
+ type: "button",
4860
+ className: "absolute right-[9px] top-[9px] rounded-sm p-1.5 text-foreground hover:bg-muted/50",
4861
+ onClick: state.handleClose,
4862
+ "aria-label": "Close",
4863
+ children: /* @__PURE__ */ jsx13(IconX, { "aria-hidden": true })
4864
+ }
4865
+ ),
4866
+ /* @__PURE__ */ jsx13("header", { className: "flex w-full flex-col gap-1.5 p-6 pr-12", children: /* @__PURE__ */ jsx13("h2", { className: "m-0 w-full break-words text-lg font-semibold leading-none tracking-tight text-card-foreground", children: state.title }) }),
4867
+ /* @__PURE__ */ jsxs7("div", { className: "flex w-full flex-col gap-3 px-6 pb-8 pt-1", children: [
4868
+ state.showBreadcrumb && state.selectedPage && state.selectedSection ? /* @__PURE__ */ jsx13(
4869
+ DestinationBreadcrumb,
4870
+ {
4871
+ pageTitle: state.selectedPage.title,
4872
+ sectionLabel: state.selectedSection.label
4873
+ }
4874
+ ) : /* @__PURE__ */ jsx13(
4875
+ UrlOrPageInput,
4876
+ {
4877
+ value: state.searchValue,
4878
+ selectedPage: state.selectedPage,
4879
+ dropdownOpen: state.dropdownOpen,
4880
+ onDropdownOpenChange: state.setDropdownOpen,
4881
+ filteredPages: state.filteredPages,
4882
+ onInputChange: state.handleInputChange,
4883
+ onPageSelect: state.handlePageSelect,
4884
+ urlError: state.urlError
4885
+ }
4886
+ ),
4887
+ state.showChooseSection ? /* @__PURE__ */ jsxs7("div", { className: "flex flex-col justify-center gap-2", children: [
4888
+ /* @__PURE__ */ jsx13(Button, { type: "button", variant: "outline", className: "w-fit cursor-pointer", size: "sm", onClick: state.handleChooseSection, children: "Choose a section" }),
4889
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-1 text-sm text-muted-foreground", children: [
4890
+ /* @__PURE__ */ jsx13(IconInfo, { className: "shrink-0", "aria-hidden": true }),
4891
+ /* @__PURE__ */ jsx13("span", { children: "Pick a section this link should scroll to." })
4892
+ ] })
4893
+ ] }) : null,
4894
+ state.showSectionPicker ? /* @__PURE__ */ jsx13(SectionPickerList, { sections: state.activeSections, onSelect: state.handleSectionSelect }) : null,
4895
+ state.showSectionRow && state.selectedSection ? /* @__PURE__ */ jsx13(SectionTreeItem, { section: state.selectedSection, selected: true }) : null
4896
+ ] }),
4897
+ /* @__PURE__ */ jsxs7("footer", { className: "flex w-full items-center justify-end gap-2 px-6 pb-6", children: [
4898
+ /* @__PURE__ */ jsx13(
4899
+ Button,
4900
+ {
4901
+ type: "button",
4902
+ variant: isCancel ? "outline" : "ghost",
4903
+ className: "leading-6 shadow-none cursor-pointer",
4904
+ style: isCancel ? {
4905
+ backgroundColor: "var(--ohw-background)",
4906
+ borderColor: "var(--ohw-border)",
4907
+ color: "var(--ohw-foreground)"
4908
+ } : void 0,
4909
+ onClick: state.handleSecondary,
4910
+ children: state.secondaryLabel
4911
+ }
4912
+ ),
4913
+ /* @__PURE__ */ jsx13(
4914
+ Button,
4915
+ {
4916
+ type: "button",
4917
+ className: "border-0 leading-6 shadow-none hover:opacity-90 disabled:opacity-50 cursor-pointer",
4918
+ style: {
4919
+ backgroundColor: "var(--ohw-primary)",
4920
+ color: "var(--ohw-primary-foreground)"
4921
+ },
4922
+ disabled: !state.isValid,
4923
+ onClick: state.handleSubmit,
4924
+ children: state.submitLabel
4925
+ }
4926
+ )
4927
+ ] })
4928
+ ] });
4929
+ }
4930
+
4931
+ // src/ui/link-modal/LinkPopover.tsx
4932
+ import { jsx as jsx14 } from "react/jsx-runtime";
4933
+ function LinkPopover({
4934
+ rect,
4935
+ parentScroll,
4936
+ panelRef,
4937
+ ...editorProps
4938
+ }) {
4939
+ const { top, left, transform } = calcPopoverPos(rect, parentScroll);
4940
+ return /* @__PURE__ */ jsx14(
4941
+ "div",
4942
+ {
4943
+ ref: panelRef,
4944
+ "data-ohw-link-popover-root": "",
4945
+ "data-ohw-link-modal-root": "",
4946
+ "data-ohw-bridge": "",
4947
+ className: cn(
4948
+ "pointer-events-auto fixed z-[2147483647] flex w-[483px] max-w-[calc(100vw-16px)] flex-col overflow-hidden",
4949
+ "rounded-xl border border-border bg-background font-sans shadow-lg outline-none"
4950
+ ),
4951
+ style: { top, left, transform },
4952
+ onMouseDown: (e) => e.stopPropagation(),
4953
+ children: /* @__PURE__ */ jsx14(LinkEditorPanel, { ...editorProps, open: true })
4954
+ }
4955
+ );
4956
+ }
4957
+
4958
+ // src/ui/link-modal/devFixtures.ts
4959
+ var DEV_SITE_PAGES = [
4960
+ { path: "/", title: "Home" },
4961
+ { path: "/about", title: "About" },
4962
+ { path: "/classes", title: "Classes" },
4963
+ { path: "/pricing", title: "Pricing" },
4964
+ { path: "/studio", title: "Studio" },
4965
+ { path: "/instructors", title: "Instructors" },
4966
+ { path: "/contact", title: "Contact" }
4967
+ ];
4968
+ var DEV_SECTIONS_BY_PATH = {
4969
+ "/": [
4970
+ { id: "hero", label: "Hero" },
4971
+ { id: "lagree-intro", label: "Lagree Intro" },
4972
+ { id: "classes-strip", label: "Classes" },
4973
+ { id: "feel-split", label: "Feel Split" },
4974
+ { id: "founder-teaser", label: "Founder" },
4975
+ { id: "plan-form", label: "Plan Form" },
4976
+ { id: "testimonials", label: "Testimonials" },
4977
+ { id: "wordmark", label: "Wordmark" }
4978
+ ],
4979
+ "/about": [
4980
+ { id: "manifesto", label: "Manifesto" },
4981
+ { id: "story-letter", label: "Our Story" },
4982
+ { id: "lagree-explainer", label: "Lagree Explainer" },
4983
+ { id: "waitlist-cta", label: "Waitlist" },
4984
+ { id: "personal-training", label: "Personal training" }
4985
+ ],
4986
+ "/classes": [{ id: "class-library", label: "Class Library" }],
4987
+ "/pricing": [
4988
+ { id: "benefits-marquee", label: "Benefits" },
4989
+ { id: "monthly-memberships", label: "Memberships" },
4990
+ { id: "package-matcher", label: "Packages" },
4991
+ { id: "specialty-programs", label: "Specialty Programs" },
4992
+ { id: "free-class-cta", label: "Free Class" }
4993
+ ],
4994
+ "/studio": [
4995
+ { id: "studio-intro", label: "Studio Intro" },
4996
+ { id: "studio-features", label: "Studio Features" },
4997
+ { id: "studio-gallery", label: "Studio Gallery" },
4998
+ { id: "studio-visit", label: "Visit Studio" }
4999
+ ],
5000
+ "/instructors": [{ id: "instructor-grid", label: "Instructors" }],
5001
+ "/contact": [
5002
+ { id: "hello-hero", label: "Hello" },
5003
+ { id: "contact-form", label: "Contact Form" },
5004
+ { id: "faq", label: "FAQ" }
5005
+ ]
5006
+ };
5007
+ function shouldUseDevFixtures() {
5008
+ if (typeof window === "undefined") return false;
5009
+ const raw = readPreservedSearch();
5010
+ const q = raw.startsWith("?") ? raw.slice(1) : raw;
5011
+ return new URLSearchParams(q).get("ohw-fixtures") === "1";
5012
+ }
5013
+
5014
+ // src/ui/badge.tsx
5015
+ import { jsx as jsx15 } from "react/jsx-runtime";
5016
+ var badgeVariants = cva(
5017
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
5018
+ {
5019
+ variants: {
5020
+ variant: {
5021
+ default: "border-transparent bg-primary text-primary-foreground",
5022
+ secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
5023
+ destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
5024
+ outline: "text-foreground"
5025
+ }
5026
+ },
5027
+ defaultVariants: {
5028
+ variant: "default"
5029
+ }
5030
+ }
5031
+ );
5032
+ function Badge({ className, variant, ...props }) {
5033
+ return /* @__PURE__ */ jsx15("div", { className: cn(badgeVariants({ variant }), className), ...props });
5034
+ }
5035
+
3436
5036
  // src/OhhwellsBridge.tsx
3437
- import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
5037
+ import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs8 } from "react/jsx-runtime";
3438
5038
  var PRIMARY = "#0885FE";
3439
5039
  var IMAGE_FADE_MS = 300;
3440
5040
  function runOpacityFade(el, onDone) {
@@ -3485,8 +5085,173 @@ function fadeInBgImage(el, url, onReady) {
3485
5085
  if (!prevPos || prevPos === "static") el.style.position = prevPos;
3486
5086
  });
3487
5087
  }
5088
+ function getSectionsTracker() {
5089
+ let el = document.querySelector("[data-ohw-sections-tracker]");
5090
+ if (!el) {
5091
+ el = document.createElement("div");
5092
+ el.setAttribute("data-ohw-sections-tracker", "");
5093
+ el.style.display = "none";
5094
+ document.body.appendChild(el);
5095
+ }
5096
+ return el;
5097
+ }
5098
+ function updateSectionScheduleId(insertAfter, scheduleId) {
5099
+ const tracker = getSectionsTracker();
5100
+ let sections = [];
5101
+ try {
5102
+ sections = JSON.parse(tracker.textContent || "[]");
5103
+ } catch {
5104
+ }
5105
+ const currentPath = window.location.pathname;
5106
+ const updated = sections.map((s) => {
5107
+ if (s.type !== "scheduling" || s.insertAfter !== insertAfter) return s;
5108
+ if (s.pagePath && s.pagePath !== currentPath) return s;
5109
+ return { ...s, scheduleId };
5110
+ });
5111
+ tracker.textContent = JSON.stringify(updated);
5112
+ return tracker.textContent ?? "[]";
5113
+ }
5114
+ function schedulingSectionId(insertAfter) {
5115
+ return `scheduling-${insertAfter}`;
5116
+ }
5117
+ var INSERT_BEFORE_MARKER = ":before:";
5118
+ function parseSchedulingInsertAfter(insertAfter) {
5119
+ const idx = insertAfter.indexOf(INSERT_BEFORE_MARKER);
5120
+ if (idx < 0) return { anchor: insertAfter, insertBefore: null };
5121
+ return {
5122
+ anchor: insertAfter.slice(0, idx),
5123
+ insertBefore: insertAfter.slice(idx + INSERT_BEFORE_MARKER.length)
5124
+ };
5125
+ }
5126
+ function resolveSchedulingInsert(insertAfter, explicitInsertBefore) {
5127
+ const parsed = parseSchedulingInsertAfter(insertAfter);
5128
+ const insertBefore = explicitInsertBefore ?? parsed.insertBefore;
5129
+ const effectiveInsertAfter = insertBefore && parsed.insertBefore === null ? `${insertAfter}${INSERT_BEFORE_MARKER}${insertBefore}` : insertAfter;
5130
+ return { effectiveInsertAfter, insertBefore };
5131
+ }
5132
+ function getSchedulingMountPoint(insertAfter) {
5133
+ const { anchor } = parseSchedulingInsertAfter(insertAfter);
5134
+ let anchorEl = document.querySelector(`[data-ohw-section="${anchor}"]`);
5135
+ if (!anchorEl && anchor === "scheduling") {
5136
+ const widgets = Array.from(document.querySelectorAll('[data-ohw-section^="scheduling-"]'));
5137
+ anchorEl = widgets.at(-1) ?? null;
5138
+ }
5139
+ if (!anchorEl) return null;
5140
+ return anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
5141
+ }
5142
+ function schedulingMountDepth(insertAfter) {
5143
+ if (!insertAfter.includes(INSERT_BEFORE_MARKER)) return 0;
5144
+ return insertAfter.split(INSERT_BEFORE_MARKER).length - 1;
5145
+ }
5146
+ function getPageSchedulingEntries(raw) {
5147
+ if (!raw) return [];
5148
+ try {
5149
+ const entries = JSON.parse(raw);
5150
+ const currentPath = window.location.pathname;
5151
+ return entries.filter((e) => e.type === "scheduling" && (!e.pagePath || e.pagePath === currentPath));
5152
+ } catch {
5153
+ return [];
5154
+ }
5155
+ }
5156
+ function isSchedulingWidgetMissing(entry) {
5157
+ const { effectiveInsertAfter } = resolveSchedulingInsert(entry.insertAfter);
5158
+ return !document.querySelector(`[data-ohw-section="${schedulingSectionId(effectiveInsertAfter)}"]`);
5159
+ }
5160
+ function hasMissingSchedulingWidgets(entries) {
5161
+ return entries.some(isSchedulingWidgetMissing);
5162
+ }
5163
+ function retryMissingSchedulingMounts(entries, notifyOnConnect = false) {
5164
+ if (!hasMissingSchedulingWidgets(entries)) return;
5165
+ mountSchedulingEntries(entries, notifyOnConnect);
5166
+ }
5167
+ function initSectionsFromContent(content, removeExisting = false) {
5168
+ const raw = content["__ohw_sections"];
5169
+ if (!raw) return;
5170
+ try {
5171
+ if (removeExisting) {
5172
+ document.querySelectorAll('[data-ohw-section-container="scheduling"]').forEach((el) => el.remove());
5173
+ }
5174
+ const inEditor = typeof window !== "undefined" && window.self !== window.top;
5175
+ if (inEditor) getSectionsTracker().textContent = raw;
5176
+ const pageEntries = getPageSchedulingEntries(raw);
5177
+ mountSchedulingEntries(pageEntries, false);
5178
+ requestAnimationFrame(() => retryMissingSchedulingMounts(pageEntries, false));
5179
+ setTimeout(() => retryMissingSchedulingMounts(pageEntries, false), 250);
5180
+ } catch {
5181
+ }
5182
+ }
5183
+ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId, explicitInsertBefore) {
5184
+ const { effectiveInsertAfter, insertBefore } = resolveSchedulingInsert(insertAfter, explicitInsertBefore);
5185
+ const sectionId = schedulingSectionId(effectiveInsertAfter);
5186
+ if (document.querySelector(`[data-ohw-section="${sectionId}"]`)) return false;
5187
+ const mountPoint = getSchedulingMountPoint(effectiveInsertAfter);
5188
+ if (!mountPoint) return false;
5189
+ const container = document.createElement("div");
5190
+ container.dataset.ohwSectionContainer = "scheduling";
5191
+ if (insertBefore) {
5192
+ const beforeAnchor = document.querySelector(`[data-ohw-section="${insertBefore}"]`);
5193
+ const beforePoint = beforeAnchor?.closest('[data-ohw-section-container="scheduling"]') ?? beforeAnchor;
5194
+ if (!beforePoint) return false;
5195
+ beforePoint.insertAdjacentElement("beforebegin", container);
5196
+ } else {
5197
+ let tail = mountPoint;
5198
+ while (tail.nextElementSibling?.dataset.ohwSectionContainer === "scheduling") {
5199
+ tail = tail.nextElementSibling;
5200
+ }
5201
+ tail.insertAdjacentElement("afterend", container);
5202
+ }
5203
+ const root = createRoot(container);
5204
+ flushSync(() => {
5205
+ root.render(
5206
+ /* @__PURE__ */ jsx16(
5207
+ SchedulingWidget,
5208
+ {
5209
+ notifyOnConnect,
5210
+ initialScheduleId: scheduleId,
5211
+ insertAfter: effectiveInsertAfter
5212
+ }
5213
+ )
5214
+ );
5215
+ });
5216
+ const tracker = getSectionsTracker();
5217
+ let sections = [];
5218
+ try {
5219
+ sections = JSON.parse(tracker.textContent || "[]");
5220
+ } catch {
5221
+ }
5222
+ const inEditor = typeof window !== "undefined" && window.self !== window.top;
5223
+ if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === effectiveInsertAfter)) {
5224
+ sections.push({
5225
+ type: "scheduling",
5226
+ insertAfter: effectiveInsertAfter,
5227
+ pagePath: window.location.pathname,
5228
+ ...scheduleId ? { scheduleId } : {}
5229
+ });
5230
+ tracker.textContent = JSON.stringify(sections);
5231
+ }
5232
+ return true;
5233
+ }
5234
+ function mountSchedulingEntries(entries, notifyOnConnect = false) {
5235
+ const pending = entries.filter((e) => e.type === "scheduling").sort((a, b) => schedulingMountDepth(a.insertAfter) - schedulingMountDepth(b.insertAfter));
5236
+ for (let attempt = 0; attempt < pending.length + 1 && pending.length > 0; attempt++) {
5237
+ for (let i = pending.length - 1; i >= 0; i--) {
5238
+ const { insertAfter, scheduleId } = pending[i];
5239
+ if (mountSchedulingWidget(insertAfter, notifyOnConnect, scheduleId)) {
5240
+ pending.splice(i, 1);
5241
+ }
5242
+ }
5243
+ }
5244
+ }
5245
+ function getLinkHref(el) {
5246
+ const anchor = el instanceof HTMLAnchorElement ? el : el.querySelector("a");
5247
+ return anchor?.getAttribute("href") ?? "";
5248
+ }
5249
+ function applyLinkHref(el, val) {
5250
+ const anchor = el instanceof HTMLAnchorElement ? el : el.querySelector("a");
5251
+ if (anchor) anchor.setAttribute("href", val);
5252
+ }
3488
5253
  function collectEditableNodes() {
3489
- return Array.from(document.querySelectorAll("[data-ohw-editable]")).map((el) => {
5254
+ const nodes = Array.from(document.querySelectorAll("[data-ohw-editable]")).map((el) => {
3490
5255
  if (el.dataset.ohwEditable === "image") {
3491
5256
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
3492
5257
  return { key: el.dataset.ohwKey ?? "", type: "image", text: img?.src ?? "" };
@@ -3496,12 +5261,52 @@ function collectEditableNodes() {
3496
5261
  const url = raw.replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "");
3497
5262
  return { key: el.dataset.ohwKey ?? "", type: "bg-image", text: url };
3498
5263
  }
5264
+ if (el.dataset.ohwEditable === "link") {
5265
+ return { key: el.dataset.ohwKey ?? "", type: "link", text: getLinkHref(el) };
5266
+ }
3499
5267
  return {
3500
5268
  key: el.dataset.ohwKey ?? "",
3501
5269
  type: el.dataset.ohwEditable ?? "text",
3502
5270
  text: el.dataset.ohwEditable === "plain" ? el.innerText ?? "" : el.innerHTML
3503
5271
  };
3504
5272
  });
5273
+ document.querySelectorAll("[data-ohw-href-key]").forEach((el) => {
5274
+ const key = el.getAttribute("data-ohw-href-key") ?? "";
5275
+ if (!key) return;
5276
+ nodes.push({ key, type: "link", text: getLinkHref(el) });
5277
+ });
5278
+ return nodes;
5279
+ }
5280
+ function applyLinkByKey(key, val) {
5281
+ document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
5282
+ if (el.dataset.ohwEditable === "link") applyLinkHref(el, val);
5283
+ });
5284
+ document.querySelectorAll(`[data-ohw-href-key="${key}"]`).forEach((el) => {
5285
+ applyLinkHref(el, val);
5286
+ });
5287
+ }
5288
+ function isInsideLinkEditor(target) {
5289
+ return Boolean(
5290
+ target.closest("[data-ohw-link-popover-root]") || target.closest("[data-ohw-link-modal-root]") || target.closest('[data-slot="popover-content"]')
5291
+ );
5292
+ }
5293
+ function getHrefKeyFromElement(el) {
5294
+ if (!el) return null;
5295
+ const anchor = el.closest("[data-ohw-href-key]");
5296
+ if (!anchor) return null;
5297
+ const key = anchor.getAttribute("data-ohw-href-key");
5298
+ if (!key) return null;
5299
+ return { anchor, key };
5300
+ }
5301
+ function titleCaseSectionId(id) {
5302
+ return id.split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
5303
+ }
5304
+ function collectSections() {
5305
+ return Array.from(document.querySelectorAll("[data-ohw-section]")).map((el) => {
5306
+ const id = el.getAttribute("data-ohw-section") ?? "";
5307
+ const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
5308
+ return { id, label };
5309
+ });
3505
5310
  }
3506
5311
  var FORCE_PSEUDO_STATES = [
3507
5312
  { pseudo: ":hover", state: "hover" },
@@ -3642,7 +5447,7 @@ var TOOLBAR_GROUPS = [
3642
5447
  ];
3643
5448
  function GlowFrame({ rect, elRef }) {
3644
5449
  const GAP = 6;
3645
- return /* @__PURE__ */ jsx3(
5450
+ return /* @__PURE__ */ jsx16(
3646
5451
  "div",
3647
5452
  {
3648
5453
  ref: elRef,
@@ -3690,10 +5495,12 @@ function FloatingToolbar({
3690
5495
  parentScroll,
3691
5496
  elRef,
3692
5497
  onCommand,
3693
- activeCommands
5498
+ activeCommands,
5499
+ showEditLink,
5500
+ onEditLink
3694
5501
  }) {
3695
5502
  const { top, left, transform } = calcToolbarPos(rect, parentScroll);
3696
- return /* @__PURE__ */ jsx3(
5503
+ return /* @__PURE__ */ jsxs8(
3697
5504
  "div",
3698
5505
  {
3699
5506
  ref: elRef,
@@ -3716,56 +5523,97 @@ function FloatingToolbar({
3716
5523
  pointerEvents: "auto",
3717
5524
  whiteSpace: "nowrap"
3718
5525
  },
3719
- children: TOOLBAR_GROUPS.map((btns, gi) => /* @__PURE__ */ jsxs(React2.Fragment, { children: [
3720
- gi > 0 && /* @__PURE__ */ jsx3("span", { style: { display: "block", width: 1, height: 24, background: "#E7E5E4", flexShrink: 0 } }),
3721
- btns.map((btn) => {
3722
- const isActive = activeCommands.has(btn.cmd);
3723
- return /* @__PURE__ */ jsx3(
3724
- "button",
3725
- {
3726
- title: btn.title,
3727
- onMouseDown: (e) => {
3728
- e.preventDefault();
3729
- onCommand(btn.cmd);
5526
+ onMouseDown: (e) => e.stopPropagation(),
5527
+ children: [
5528
+ TOOLBAR_GROUPS.map((btns, gi) => /* @__PURE__ */ jsxs8(React5.Fragment, { children: [
5529
+ gi > 0 && /* @__PURE__ */ jsx16("span", { style: { display: "block", width: 1, height: 24, background: "#E7E5E4", flexShrink: 0 } }),
5530
+ btns.map((btn) => {
5531
+ const isActive = activeCommands.has(btn.cmd);
5532
+ return /* @__PURE__ */ jsx16(
5533
+ "button",
5534
+ {
5535
+ title: btn.title,
5536
+ onMouseDown: (e) => {
5537
+ e.preventDefault();
5538
+ onCommand(btn.cmd);
5539
+ },
5540
+ onMouseEnter: (e) => {
5541
+ if (!isActive) e.currentTarget.style.background = "#F5F5F4";
5542
+ },
5543
+ onMouseLeave: (e) => {
5544
+ if (!isActive) e.currentTarget.style.background = "transparent";
5545
+ },
5546
+ style: {
5547
+ display: "flex",
5548
+ alignItems: "center",
5549
+ justifyContent: "center",
5550
+ border: "none",
5551
+ background: isActive ? PRIMARY : "transparent",
5552
+ borderRadius: 4,
5553
+ cursor: "pointer",
5554
+ color: isActive ? "#FFFFFF" : "#1C1917",
5555
+ flexShrink: 0,
5556
+ padding: 6
5557
+ },
5558
+ children: /* @__PURE__ */ jsx16(
5559
+ "svg",
5560
+ {
5561
+ width: "16",
5562
+ height: "16",
5563
+ viewBox: "0 0 24 24",
5564
+ fill: "none",
5565
+ stroke: isActive ? "#FFFFFF" : "#1C1917",
5566
+ strokeWidth: "2.5",
5567
+ strokeLinecap: "round",
5568
+ strokeLinejoin: "round",
5569
+ style: isActive && gi > 0 ? { filter: "drop-shadow(0 0 0.5px #fff)" } : void 0,
5570
+ dangerouslySetInnerHTML: { __html: ICONS[btn.cmd] }
5571
+ }
5572
+ )
3730
5573
  },
3731
- onMouseEnter: (e) => {
3732
- if (!isActive) e.currentTarget.style.background = "#F5F5F4";
3733
- },
3734
- onMouseLeave: (e) => {
3735
- if (!isActive) e.currentTarget.style.background = "transparent";
3736
- },
3737
- style: {
3738
- display: "flex",
3739
- alignItems: "center",
3740
- justifyContent: "center",
3741
- border: "none",
3742
- background: isActive ? PRIMARY : "transparent",
3743
- borderRadius: 4,
3744
- cursor: "pointer",
3745
- color: isActive ? "#FFFFFF" : "#1C1917",
3746
- flexShrink: 0,
3747
- padding: 6
3748
- },
3749
- children: /* @__PURE__ */ jsx3(
3750
- "svg",
3751
- {
3752
- width: "16",
3753
- height: "16",
3754
- viewBox: "0 0 24 24",
3755
- fill: "none",
3756
- stroke: isActive ? "#FFFFFF" : "#1C1917",
3757
- strokeWidth: "2.5",
3758
- strokeLinecap: "round",
3759
- strokeLinejoin: "round",
3760
- style: isActive && gi > 0 ? { filter: "drop-shadow(0 0 0.5px #fff)" } : void 0,
3761
- dangerouslySetInnerHTML: { __html: ICONS[btn.cmd] }
3762
- }
3763
- )
5574
+ btn.cmd
5575
+ );
5576
+ })
5577
+ ] }, gi)),
5578
+ showEditLink ? /* @__PURE__ */ jsx16(
5579
+ "button",
5580
+ {
5581
+ type: "button",
5582
+ title: "Edit link",
5583
+ onMouseDown: (e) => {
5584
+ e.preventDefault();
5585
+ e.stopPropagation();
5586
+ onEditLink?.();
5587
+ },
5588
+ onClick: (e) => {
5589
+ e.preventDefault();
5590
+ e.stopPropagation();
3764
5591
  },
3765
- btn.cmd
3766
- );
3767
- })
3768
- ] }, gi))
5592
+ onMouseEnter: (e) => {
5593
+ e.currentTarget.style.background = "#F5F5F4";
5594
+ },
5595
+ onMouseLeave: (e) => {
5596
+ e.currentTarget.style.background = "transparent";
5597
+ },
5598
+ style: {
5599
+ display: "flex",
5600
+ alignItems: "center",
5601
+ justifyContent: "center",
5602
+ border: "none",
5603
+ background: "transparent",
5604
+ borderRadius: 4,
5605
+ cursor: "pointer",
5606
+ color: "#1C1917",
5607
+ flexShrink: 0,
5608
+ padding: 6
5609
+ },
5610
+ children: /* @__PURE__ */ jsxs8("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
5611
+ /* @__PURE__ */ jsx16("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }),
5612
+ /* @__PURE__ */ jsx16("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })
5613
+ ] })
5614
+ }
5615
+ ) : null
5616
+ ]
3769
5617
  }
3770
5618
  );
3771
5619
  }
@@ -3779,7 +5627,7 @@ function StateToggle({
3779
5627
  states,
3780
5628
  onStateChange
3781
5629
  }) {
3782
- return /* @__PURE__ */ jsx3(
5630
+ return /* @__PURE__ */ jsx16(
3783
5631
  ToggleGroup,
3784
5632
  {
3785
5633
  "data-ohw-state-toggle": "",
@@ -3793,18 +5641,35 @@ function StateToggle({
3793
5641
  left: rect.right - 8,
3794
5642
  transform: "translateX(-100%)"
3795
5643
  },
3796
- children: states.map((state) => /* @__PURE__ */ jsx3(ToggleGroupItem, { value: state, size: "sm", children: state }, state))
5644
+ children: states.map((state) => /* @__PURE__ */ jsx16(ToggleGroupItem, { value: state, size: "sm", children: state }, state))
3797
5645
  }
3798
5646
  );
3799
5647
  }
3800
5648
  var contentCache = /* @__PURE__ */ new Map();
5649
+ function resolveSubdomain(subdomainFromQuery) {
5650
+ if (subdomainFromQuery) return subdomainFromQuery;
5651
+ if (typeof window !== "undefined") {
5652
+ const parts = window.location.hostname.split(".");
5653
+ if (parts.length >= 3 && parts[0] !== "www") return parts[0];
5654
+ }
5655
+ const siteUrl = process.env.NEXT_PUBLIC_SITE_URL;
5656
+ if (siteUrl) {
5657
+ try {
5658
+ const host = new URL(siteUrl).hostname;
5659
+ const siteParts = host.split(".");
5660
+ if (siteParts.length >= 3 && siteParts[0] !== "www") return siteParts[0];
5661
+ } catch {
5662
+ }
5663
+ }
5664
+ return "";
5665
+ }
3801
5666
  function OhhwellsBridge() {
3802
5667
  const pathname = usePathname();
3803
5668
  const router = useRouter();
3804
5669
  const searchParams = useSearchParams();
3805
5670
  const isEditMode = isEditSessionActive();
3806
- const [bridgeRoot, setBridgeRoot] = useState(null);
3807
- useEffect(() => {
5671
+ const [bridgeRoot, setBridgeRoot] = useState5(null);
5672
+ useEffect4(() => {
3808
5673
  const figtreeFontId = "ohw-figtree-font";
3809
5674
  if (!document.getElementById(figtreeFontId)) {
3810
5675
  const preconnect1 = Object.assign(document.createElement("link"), { rel: "preconnect", href: "https://fonts.googleapis.com" });
@@ -3827,42 +5692,76 @@ function OhhwellsBridge() {
3827
5692
  };
3828
5693
  }, []);
3829
5694
  const subdomainFromQuery = searchParams.get("subdomain");
3830
- const subdomain = subdomainFromQuery ?? (() => {
3831
- if (typeof window === "undefined") return "";
3832
- const parts = window.location.hostname.split(".");
3833
- return parts.length >= 3 && parts[0] !== "www" ? parts[0] : "";
3834
- })();
3835
- const postToParent = useCallback((data) => {
5695
+ const subdomain = resolveSubdomain(subdomainFromQuery);
5696
+ const postToParent = useCallback2((data) => {
3836
5697
  if (typeof window !== "undefined" && window.parent !== window) {
3837
5698
  window.parent.postMessage(data, "*");
3838
5699
  }
3839
5700
  }, []);
3840
- const [fetchState, setFetchState] = useState("idle");
3841
- const autoSaveTimers = useRef(/* @__PURE__ */ new Map());
3842
- const activeElRef = useRef(null);
3843
- const originalContentRef = useRef(null);
3844
- const activeStateElRef = useRef(null);
3845
- const parentScrollRef = useRef(null);
3846
- const toolbarElRef = useRef(null);
3847
- const glowElRef = useRef(null);
3848
- const hoveredImageRef = useRef(null);
3849
- const hoveredImageHasTextOverlapRef = useRef(false);
3850
- const imageUnhoverTimerRef = useRef(null);
3851
- const imageShowTimerRef = useRef(null);
3852
- const editStylesRef = useRef(null);
3853
- const activateRef = useRef(() => {
5701
+ const [fetchState, setFetchState] = useState5("idle");
5702
+ const autoSaveTimers = useRef3(/* @__PURE__ */ new Map());
5703
+ const activeElRef = useRef3(null);
5704
+ const originalContentRef = useRef3(null);
5705
+ const activeStateElRef = useRef3(null);
5706
+ const parentScrollRef = useRef3(null);
5707
+ const toolbarElRef = useRef3(null);
5708
+ const glowElRef = useRef3(null);
5709
+ const hoveredImageRef = useRef3(null);
5710
+ const hoveredImageHasTextOverlapRef = useRef3(false);
5711
+ const hoveredGapRef = useRef3(null);
5712
+ const imageUnhoverTimerRef = useRef3(null);
5713
+ const imageShowTimerRef = useRef3(null);
5714
+ const editStylesRef = useRef3(null);
5715
+ const activateRef = useRef3(() => {
3854
5716
  });
3855
- const deactivateRef = useRef(() => {
5717
+ const deactivateRef = useRef3(() => {
3856
5718
  });
3857
- const refreshActiveCommandsRef = useRef(() => {
5719
+ const refreshActiveCommandsRef = useRef3(() => {
3858
5720
  });
3859
- const postToParentRef = useRef(postToParent);
5721
+ const postToParentRef = useRef3(postToParent);
3860
5722
  postToParentRef.current = postToParent;
3861
- const [toolbarRect, setToolbarRect] = useState(null);
3862
- const [toggleState, setToggleState] = useState(null);
3863
- const [maxBadge, setMaxBadge] = useState(null);
3864
- const [activeCommands, setActiveCommands] = useState(/* @__PURE__ */ new Set());
3865
- useEffect(() => {
5723
+ const [toolbarRect, setToolbarRect] = useState5(null);
5724
+ const [toggleState, setToggleState] = useState5(null);
5725
+ const [maxBadge, setMaxBadge] = useState5(null);
5726
+ const [activeCommands, setActiveCommands] = useState5(/* @__PURE__ */ new Set());
5727
+ const [toolbarShowEditLink, setToolbarShowEditLink] = useState5(false);
5728
+ const [linkPopover, setLinkPopover] = useState5(null);
5729
+ const [sitePages, setSitePages] = useState5([]);
5730
+ const [sectionsByPath, setSectionsByPath] = useState5({});
5731
+ const setLinkPopoverRef = useRef3(setLinkPopover);
5732
+ const linkPopoverPanelRef = useRef3(null);
5733
+ const linkPopoverOpenRef = useRef3(false);
5734
+ const linkPopoverGraceUntilRef = useRef3(0);
5735
+ setLinkPopoverRef.current = setLinkPopover;
5736
+ const bumpLinkPopoverGrace = () => {
5737
+ linkPopoverGraceUntilRef.current = Date.now() + 350;
5738
+ };
5739
+ useEffect4(() => {
5740
+ if (!isEditMode) return;
5741
+ const useFixtures = shouldUseDevFixtures();
5742
+ if (useFixtures) {
5743
+ setSitePages(DEV_SITE_PAGES);
5744
+ setSectionsByPath(DEV_SECTIONS_BY_PATH);
5745
+ if (process.env.NODE_ENV === "development") {
5746
+ console.info("[ohhwells-bridge] Dev fixtures loaded (ohw-fixtures=1)");
5747
+ }
5748
+ }
5749
+ const onSitePages = (e) => {
5750
+ if (e.data?.type !== "ow:site-pages" || !Array.isArray(e.data.pages)) return;
5751
+ if (useFixtures) return;
5752
+ setSitePages(
5753
+ e.data.pages.map((p) => ({
5754
+ path: p.path,
5755
+ title: p.title
5756
+ }))
5757
+ );
5758
+ };
5759
+ window.addEventListener("message", onSitePages);
5760
+ if (!useFixtures) postToParent({ type: "ow:request-site-pages" });
5761
+ return () => window.removeEventListener("message", onSitePages);
5762
+ }, [isEditMode, postToParent]);
5763
+ const [sectionGap, setSectionGap] = useState5(null);
5764
+ useEffect4(() => {
3866
5765
  const update = () => {
3867
5766
  const el = activeElRef.current;
3868
5767
  if (el) setToolbarRect(el.getBoundingClientRect());
@@ -3870,6 +5769,12 @@ function OhhwellsBridge() {
3870
5769
  if (!prev || !activeStateElRef.current) return prev;
3871
5770
  return { ...prev, rect: activeStateElRef.current.getBoundingClientRect() };
3872
5771
  });
5772
+ setSectionGap((prev) => {
5773
+ if (!prev) return prev;
5774
+ const anchor = document.querySelector(`[data-ohw-section="${prev.insertAfter}"]`);
5775
+ if (!anchor) return prev;
5776
+ return { ...prev, y: anchor.getBoundingClientRect().bottom };
5777
+ });
3873
5778
  };
3874
5779
  const vvp = window.visualViewport;
3875
5780
  if (!vvp) return;
@@ -3880,10 +5785,10 @@ function OhhwellsBridge() {
3880
5785
  vvp.removeEventListener("resize", update);
3881
5786
  };
3882
5787
  }, []);
3883
- const refreshStateRules = useCallback(() => {
5788
+ const refreshStateRules = useCallback2(() => {
3884
5789
  editStylesRef.current?.forceHover && (editStylesRef.current.forceHover.textContent = collectStateRules());
3885
5790
  }, []);
3886
- const deactivate = useCallback(() => {
5791
+ const deactivate = useCallback2(() => {
3887
5792
  const el = activeElRef.current;
3888
5793
  if (!el) return;
3889
5794
  const key = el.dataset.ohwKey;
@@ -3906,9 +5811,10 @@ function OhhwellsBridge() {
3906
5811
  setToolbarRect(null);
3907
5812
  setMaxBadge(null);
3908
5813
  setActiveCommands(/* @__PURE__ */ new Set());
5814
+ setToolbarShowEditLink(false);
3909
5815
  postToParent({ type: "ow:exit-edit" });
3910
5816
  }, [postToParent]);
3911
- const activate = useCallback((el) => {
5817
+ const activate = useCallback2((el) => {
3912
5818
  if (activeElRef.current === el) return;
3913
5819
  deactivate();
3914
5820
  if (hoveredImageRef.current) {
@@ -3921,6 +5827,7 @@ function OhhwellsBridge() {
3921
5827
  originalContentRef.current = el.innerHTML;
3922
5828
  el.focus();
3923
5829
  setToolbarRect(el.getBoundingClientRect());
5830
+ setToolbarShowEditLink(Boolean(getHrefKeyFromElement(el)));
3924
5831
  postToParent({ type: "ow:enter-edit", key: el.dataset.ohwKey });
3925
5832
  requestAnimationFrame(() => refreshActiveCommandsRef.current());
3926
5833
  }, [deactivate, postToParent]);
@@ -3934,6 +5841,7 @@ function OhhwellsBridge() {
3934
5841
  const applyContent = (content) => {
3935
5842
  const imageLoads = [];
3936
5843
  for (const [key, val] of Object.entries(content)) {
5844
+ if (key === "__ohw_sections") continue;
3937
5845
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
3938
5846
  if (el.dataset.ohwEditable === "image") {
3939
5847
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -3947,11 +5855,15 @@ function OhhwellsBridge() {
3947
5855
  } else if (el.dataset.ohwEditable === "bg-image") {
3948
5856
  const next = `url('${val}')`;
3949
5857
  if (el.style.backgroundImage !== next) el.style.backgroundImage = next;
5858
+ } else if (el.dataset.ohwEditable === "link") {
5859
+ applyLinkHref(el, val);
3950
5860
  } else if (el.innerHTML !== val) {
3951
5861
  el.innerHTML = val;
3952
5862
  }
3953
5863
  });
5864
+ applyLinkByKey(key, val);
3954
5865
  }
5866
+ initSectionsFromContent(content, true);
3955
5867
  return imageLoads.length > 0 ? Promise.all(imageLoads).then(() => {
3956
5868
  }) : Promise.resolve();
3957
5869
  };
@@ -3976,12 +5888,15 @@ function OhhwellsBridge() {
3976
5888
  cancelled = true;
3977
5889
  };
3978
5890
  }, [subdomain, isEditMode, pathname]);
3979
- useEffect(() => {
5891
+ useEffect4(() => {
3980
5892
  if (!subdomain || isEditMode) return;
5893
+ let debounceTimer = null;
3981
5894
  const applyFromCache = () => {
3982
5895
  const content = contentCache.get(subdomain);
3983
5896
  if (!content) return;
5897
+ retryMissingSchedulingMounts(getPageSchedulingEntries(content["__ohw_sections"]), false);
3984
5898
  for (const [key, val] of Object.entries(content)) {
5899
+ if (key === "__ohw_sections") continue;
3985
5900
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
3986
5901
  if (el.dataset.ohwEditable === "image") {
3987
5902
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -3989,27 +5904,37 @@ function OhhwellsBridge() {
3989
5904
  } else if (el.dataset.ohwEditable === "bg-image") {
3990
5905
  const next = `url('${val}')`;
3991
5906
  if (el.style.backgroundImage !== next) el.style.backgroundImage = next;
5907
+ } else if (el.dataset.ohwEditable === "link") {
5908
+ applyLinkHref(el, val);
3992
5909
  } else if (el.innerHTML !== val) {
3993
5910
  el.innerHTML = val;
3994
5911
  }
3995
5912
  });
5913
+ applyLinkByKey(key, val);
3996
5914
  }
3997
5915
  };
3998
- applyFromCache();
3999
- const observer = new MutationObserver(applyFromCache);
5916
+ const scheduleApply = () => {
5917
+ if (debounceTimer) clearTimeout(debounceTimer);
5918
+ debounceTimer = setTimeout(applyFromCache, 150);
5919
+ };
5920
+ scheduleApply();
5921
+ const observer = new MutationObserver(scheduleApply);
4000
5922
  observer.observe(document.body, { childList: true, subtree: true });
4001
- return () => observer.disconnect();
4002
- }, [subdomain, isEditMode]);
5923
+ return () => {
5924
+ observer.disconnect();
5925
+ if (debounceTimer) clearTimeout(debounceTimer);
5926
+ };
5927
+ }, [subdomain, isEditMode, pathname]);
4003
5928
  useLayoutEffect(() => {
4004
5929
  const el = document.getElementById("ohw-loader");
4005
5930
  if (!el) return;
4006
5931
  const visible = Boolean(subdomain) && fetchState !== "done";
4007
5932
  el.style.display = visible ? "flex" : "none";
4008
5933
  }, [subdomain, fetchState]);
4009
- useEffect(() => {
5934
+ useEffect4(() => {
4010
5935
  postToParent({ type: "ow:navigation", path: pathname });
4011
5936
  }, [pathname, postToParent]);
4012
- useEffect(() => {
5937
+ useEffect4(() => {
4013
5938
  if (!isEditMode) return;
4014
5939
  const measure = () => {
4015
5940
  const h = document.body.scrollHeight;
@@ -4033,7 +5958,7 @@ function OhhwellsBridge() {
4033
5958
  window.removeEventListener("resize", handleResize);
4034
5959
  };
4035
5960
  }, [pathname, isEditMode, postToParent]);
4036
- useEffect(() => {
5961
+ useEffect4(() => {
4037
5962
  if (!subdomainFromQuery || isEditMode) return;
4038
5963
  const handleClick = (e) => {
4039
5964
  const anchor = e.target.closest("a");
@@ -4049,7 +5974,7 @@ function OhhwellsBridge() {
4049
5974
  document.addEventListener("click", handleClick, true);
4050
5975
  return () => document.removeEventListener("click", handleClick, true);
4051
5976
  }, [subdomainFromQuery, isEditMode, router]);
4052
- useEffect(() => {
5977
+ useEffect4(() => {
4053
5978
  if (!isEditMode) {
4054
5979
  editStylesRef.current?.base.remove();
4055
5980
  editStylesRef.current?.forceHover.remove();
@@ -4075,6 +6000,7 @@ function OhhwellsBridge() {
4075
6000
  [data-ohw-editable]:not([contenteditable]):not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"]) { cursor: text !important; }
4076
6001
  [data-ohw-editable="image"], [data-ohw-editable="image"] *,
4077
6002
  [data-ohw-editable="bg-image"], [data-ohw-editable="bg-image"] * { cursor: pointer !important; }
6003
+ [data-ohw-editable="link"], [data-ohw-editable="link"] * { cursor: pointer !important; }
4078
6004
  [data-ohw-hovered]:not([contenteditable]) {
4079
6005
  outline: 2px dashed ${PRIMARY} !important;
4080
6006
  outline-offset: 4px;
@@ -4112,8 +6038,21 @@ function OhhwellsBridge() {
4112
6038
  if (target.closest("[data-ohw-toolbar]")) return;
4113
6039
  if (target.closest("[data-ohw-state-toggle]")) return;
4114
6040
  if (target.closest("[data-ohw-max-badge]")) return;
6041
+ if (isInsideLinkEditor(target)) return;
4115
6042
  const editable = target.closest("[data-ohw-editable]");
4116
6043
  if (editable) {
6044
+ if (editable.dataset.ohwEditable === "link") {
6045
+ e.preventDefault();
6046
+ e.stopPropagation();
6047
+ deactivateRef.current();
6048
+ bumpLinkPopoverGrace();
6049
+ setLinkPopoverRef.current({
6050
+ key: editable.dataset.ohwKey ?? "",
6051
+ target: getLinkHref(editable),
6052
+ rect: editable.getBoundingClientRect()
6053
+ });
6054
+ return;
6055
+ }
4117
6056
  if (editable.dataset.ohwEditable === "image" || editable.dataset.ohwEditable === "bg-image") {
4118
6057
  e.preventDefault();
4119
6058
  e.stopPropagation();
@@ -4134,6 +6073,15 @@ function OhhwellsBridge() {
4134
6073
  }
4135
6074
  const anchor = target.closest("a");
4136
6075
  if (anchor) e.preventDefault();
6076
+ if (Date.now() < linkPopoverGraceUntilRef.current) return;
6077
+ if (linkPopoverOpenRef.current && activeElRef.current) {
6078
+ const hrefCtx = getHrefKeyFromElement(activeElRef.current);
6079
+ if (hrefCtx && (hrefCtx.anchor === target || hrefCtx.anchor.contains(target))) return;
6080
+ }
6081
+ if (linkPopoverOpenRef.current) {
6082
+ setLinkPopoverRef.current(null);
6083
+ return;
6084
+ }
4137
6085
  deactivateRef.current();
4138
6086
  };
4139
6087
  const handleMouseOver = (e) => {
@@ -4309,6 +6257,15 @@ function OhhwellsBridge() {
4309
6257
  textEditable.setAttribute("data-ohw-hovered", "");
4310
6258
  return;
4311
6259
  }
6260
+ if (hoveredGapRef.current) {
6261
+ if (hoveredImageRef.current) {
6262
+ hoveredImageRef.current = null;
6263
+ resumeAnimTracks();
6264
+ postToParentRef.current({ type: "ow:image-unhover" });
6265
+ }
6266
+ document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
6267
+ return;
6268
+ }
4312
6269
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
4313
6270
  if (imgEl !== hoveredImageRef.current || hoveredImageHasTextOverlapRef.current) {
4314
6271
  hoveredImageRef.current = imgEl;
@@ -4374,8 +6331,30 @@ function OhhwellsBridge() {
4374
6331
  }
4375
6332
  }
4376
6333
  };
6334
+ const probeSectionGapAt = (clientX, clientY, fromParentViewport = false) => {
6335
+ const { y } = toProbeCoords(clientX, clientY, fromParentViewport);
6336
+ const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
6337
+ const ZONE = 20;
6338
+ for (let i = 0; i < sections.length; i++) {
6339
+ const a = sections[i];
6340
+ const b = sections[i + 1] ?? null;
6341
+ const boundaryY = a.getBoundingClientRect().bottom;
6342
+ if (Math.abs(y - boundaryY) <= ZONE) {
6343
+ const insertAfter = a.dataset.ohwSection ?? "";
6344
+ const insertBefore = b?.dataset.ohwSection ?? null;
6345
+ hoveredGapRef.current = { insertAfter, insertBefore };
6346
+ setSectionGap({ insertAfter, insertBefore, y: boundaryY });
6347
+ return;
6348
+ }
6349
+ }
6350
+ if (hoveredGapRef.current) {
6351
+ hoveredGapRef.current = null;
6352
+ setSectionGap(null);
6353
+ }
6354
+ };
4377
6355
  const handleMouseMove = (e) => {
4378
6356
  const { clientX, clientY } = e;
6357
+ probeSectionGapAt(clientX, clientY);
4379
6358
  probeImageAt(clientX, clientY);
4380
6359
  probeHoverCardsAt(clientX, clientY);
4381
6360
  };
@@ -4383,6 +6362,7 @@ function OhhwellsBridge() {
4383
6362
  if (e.data?.type !== "ow:pointer-sync") return;
4384
6363
  const { clientX, clientY } = e.data;
4385
6364
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
6365
+ probeSectionGapAt(clientX, clientY);
4386
6366
  probeImageAt(clientX, clientY);
4387
6367
  probeHoverCardsAt(clientX, clientY);
4388
6368
  };
@@ -4540,27 +6520,47 @@ function OhhwellsBridge() {
4540
6520
  if (e.data?.type !== "ow:hydrate") return;
4541
6521
  const content = e.data.content;
4542
6522
  if (!content) return;
6523
+ let sectionsJson = null;
4543
6524
  for (const [key, val] of Object.entries(content)) {
6525
+ if (key === "__ohw_sections") {
6526
+ sectionsJson = val;
6527
+ continue;
6528
+ }
4544
6529
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
4545
6530
  if (el.dataset.ohwEditable === "image") {
4546
6531
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
4547
6532
  if (img) img.src = val;
4548
6533
  } else if (el.dataset.ohwEditable === "bg-image") {
4549
6534
  el.style.backgroundImage = `url('${val}')`;
6535
+ } else if (el.dataset.ohwEditable === "link") {
6536
+ applyLinkHref(el, val);
4550
6537
  } else {
4551
6538
  el.innerHTML = val;
4552
6539
  }
4553
6540
  });
6541
+ applyLinkByKey(key, val);
6542
+ }
6543
+ if (sectionsJson) {
6544
+ initSectionsFromContent({ __ohw_sections: sectionsJson }, true);
4554
6545
  }
4555
6546
  postToParentRef.current({ type: "ow:hydrate-done" });
4556
6547
  };
4557
6548
  window.addEventListener("message", handleHydrate);
4558
6549
  const handleDeactivate = (e) => {
4559
6550
  if (e.data?.type !== "ow:deactivate") return;
6551
+ if (Date.now() < linkPopoverGraceUntilRef.current) return;
6552
+ if (linkPopoverOpenRef.current) {
6553
+ setLinkPopoverRef.current(null);
6554
+ return;
6555
+ }
4560
6556
  deactivateRef.current();
4561
6557
  };
4562
6558
  window.addEventListener("message", handleDeactivate);
4563
6559
  const handleKeyDown = (e) => {
6560
+ if (e.key === "Escape" && linkPopoverOpenRef.current) {
6561
+ setLinkPopoverRef.current(null);
6562
+ return;
6563
+ }
4564
6564
  if (e.key !== "Escape") return;
4565
6565
  const el = activeElRef.current;
4566
6566
  if (el && originalContentRef.current !== null) {
@@ -4595,7 +6595,63 @@ function OhhwellsBridge() {
4595
6595
  };
4596
6596
  const handleSave = (e) => {
4597
6597
  if (e.data?.type !== "ow:save") return;
4598
- postToParentRef.current({ type: "ow:save-result", nodes: collectEditableNodes() });
6598
+ const nodes = collectEditableNodes();
6599
+ const tracker = document.querySelector("[data-ohw-sections-tracker]");
6600
+ if (tracker?.textContent) nodes.push({ key: "__ohw_sections", type: "sections", text: tracker.textContent });
6601
+ postToParentRef.current({ type: "ow:save-result", nodes });
6602
+ };
6603
+ const handleInsertSection = (e) => {
6604
+ if (e.data?.type !== "ow:insert-section") return;
6605
+ const { widgetType, insertAfter, insertBefore } = e.data;
6606
+ if (widgetType !== "scheduling") return;
6607
+ const inserted = mountSchedulingWidget(insertAfter, true, void 0, insertBefore ?? null);
6608
+ if (inserted) {
6609
+ const tracker = getSectionsTracker();
6610
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent ?? "[]" }] });
6611
+ const h = document.documentElement.scrollHeight;
6612
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
6613
+ }
6614
+ };
6615
+ const handleSwitchSchedule = (e) => {
6616
+ if (e.data?.type !== "ow:switch-schedule") return;
6617
+ const scheduleId = e.data.scheduleId;
6618
+ const insertAfter = e.data.insertAfter;
6619
+ if (!scheduleId || !insertAfter) return;
6620
+ const text = updateSectionScheduleId(insertAfter, scheduleId);
6621
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text }] });
6622
+ };
6623
+ const handleScheduleLinked = (e) => {
6624
+ if (e.data?.type !== "ow:schedule-linked") return;
6625
+ const scheduleId = e.data.scheduleId;
6626
+ const insertAfter = e.data.insertAfter;
6627
+ if (!scheduleId || !insertAfter) return;
6628
+ const text = updateSectionScheduleId(insertAfter, scheduleId);
6629
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text }] });
6630
+ };
6631
+ const handleClearSchedulingWidget = (e) => {
6632
+ if (e.data?.type !== "ow:clear-scheduling-widget") return;
6633
+ const insertAfter = e.data.insertAfter;
6634
+ if (!insertAfter) return;
6635
+ const text = updateSectionScheduleId(insertAfter, null);
6636
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text }] });
6637
+ };
6638
+ const handleRemoveSchedulingSection = (e) => {
6639
+ if (e.data?.type !== "ow:remove-scheduling-section") return;
6640
+ document.querySelectorAll('[data-ohw-section-container="scheduling"]').forEach((el) => {
6641
+ el.remove();
6642
+ });
6643
+ const tracker = getSectionsTracker();
6644
+ let sections = [];
6645
+ try {
6646
+ sections = JSON.parse(tracker.textContent || "[]");
6647
+ } catch {
6648
+ }
6649
+ const currentPath = window.location.pathname;
6650
+ const updated = sections.filter((s) => !(s.type === "scheduling" && s.pagePath === currentPath));
6651
+ tracker.textContent = JSON.stringify(updated);
6652
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent }] });
6653
+ const h = document.documentElement.scrollHeight;
6654
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
4599
6655
  };
4600
6656
  const handleSelectionChange = () => {
4601
6657
  if (!activeElRef.current) return;
@@ -4700,6 +6756,11 @@ function OhhwellsBridge() {
4700
6756
  deactivateRef.current();
4701
6757
  };
4702
6758
  window.addEventListener("message", handleSave);
6759
+ window.addEventListener("message", handleInsertSection);
6760
+ window.addEventListener("message", handleSwitchSchedule);
6761
+ window.addEventListener("message", handleScheduleLinked);
6762
+ window.addEventListener("message", handleClearSchedulingWidget);
6763
+ window.addEventListener("message", handleRemoveSchedulingSection);
4703
6764
  window.addEventListener("message", handleImageUrl);
4704
6765
  window.addEventListener("message", handleAnimLock);
4705
6766
  window.addEventListener("message", handleCanvasHeight);
@@ -4734,6 +6795,11 @@ function OhhwellsBridge() {
4734
6795
  document.removeEventListener("selectionchange", handleSelectionChange);
4735
6796
  window.removeEventListener("scroll", handleScroll, true);
4736
6797
  window.removeEventListener("message", handleSave);
6798
+ window.removeEventListener("message", handleInsertSection);
6799
+ window.removeEventListener("message", handleSwitchSchedule);
6800
+ window.removeEventListener("message", handleScheduleLinked);
6801
+ window.removeEventListener("message", handleClearSchedulingWidget);
6802
+ window.removeEventListener("message", handleRemoveSchedulingSection);
4737
6803
  window.removeEventListener("message", handleImageUrl);
4738
6804
  window.removeEventListener("message", handleAnimLock);
4739
6805
  window.removeEventListener("message", handleCanvasHeight);
@@ -4748,7 +6814,7 @@ function OhhwellsBridge() {
4748
6814
  if (imageShowTimerRef.current) clearTimeout(imageShowTimerRef.current);
4749
6815
  };
4750
6816
  }, [isEditMode, refreshStateRules]);
4751
- useEffect(() => {
6817
+ useEffect4(() => {
4752
6818
  if (!isEditMode) return;
4753
6819
  document.querySelectorAll("[data-ohw-active-state]").forEach((el) => {
4754
6820
  el.removeAttribute("data-ohw-active-state");
@@ -4763,20 +6829,26 @@ function OhhwellsBridge() {
4763
6829
  const raf = requestAnimationFrame(() => refreshStateRules());
4764
6830
  const timer = setTimeout(() => {
4765
6831
  refreshStateRules();
4766
- postToParent({ type: "ow:ready", version: "1", nodes: collectEditableNodes() });
6832
+ const sections = collectSections();
6833
+ setSectionsByPath((prev) => {
6834
+ const next = { ...prev, [pathname]: sections };
6835
+ return shouldUseDevFixtures() ? { ...DEV_SECTIONS_BY_PATH, ...next } : next;
6836
+ });
6837
+ postToParent({ type: "ow:ready", version: "1", path: pathname, nodes: collectEditableNodes(), sections });
6838
+ postToParent({ type: "ow:request-site-pages" });
4767
6839
  }, 150);
4768
6840
  return () => {
4769
6841
  cancelAnimationFrame(raf);
4770
6842
  clearTimeout(timer);
4771
6843
  };
4772
6844
  }, [pathname, isEditMode, refreshStateRules, postToParent]);
4773
- const handleCommand = useCallback((cmd) => {
6845
+ const handleCommand = useCallback2((cmd) => {
4774
6846
  document.execCommand(cmd, false);
4775
6847
  activeElRef.current?.focus();
4776
6848
  if (activeElRef.current) setToolbarRect(activeElRef.current.getBoundingClientRect());
4777
6849
  refreshActiveCommandsRef.current();
4778
6850
  }, []);
4779
- const handleStateChange = useCallback((state) => {
6851
+ const handleStateChange = useCallback2((state) => {
4780
6852
  if (!activeStateElRef.current) return;
4781
6853
  const el = activeStateElRef.current;
4782
6854
  if (state === "Default") {
@@ -4789,13 +6861,48 @@ function OhhwellsBridge() {
4789
6861
  }
4790
6862
  setToggleState((prev) => prev ? { ...prev, activeState: state } : null);
4791
6863
  }, [deactivate]);
6864
+ const closeLinkPopover = useCallback2(() => setLinkPopover(null), []);
6865
+ const openLinkPopoverForActive = useCallback2(() => {
6866
+ const hrefCtx = getHrefKeyFromElement(activeElRef.current);
6867
+ if (!hrefCtx) return;
6868
+ bumpLinkPopoverGrace();
6869
+ setLinkPopover({
6870
+ key: hrefCtx.key,
6871
+ target: getLinkHref(hrefCtx.anchor),
6872
+ rect: hrefCtx.anchor.getBoundingClientRect()
6873
+ });
6874
+ }, []);
6875
+ const handleLinkPopoverSubmit = useCallback2(
6876
+ (target) => {
6877
+ if (!linkPopover) return;
6878
+ const { key } = linkPopover;
6879
+ applyLinkByKey(key, target);
6880
+ postToParent({ type: "ow:change", nodes: [{ key, text: target }] });
6881
+ setLinkPopover(null);
6882
+ },
6883
+ [linkPopover, postToParent]
6884
+ );
6885
+ const showEditLink = toolbarShowEditLink;
6886
+ const currentSections = sectionsByPath[pathname] ?? [];
6887
+ linkPopoverOpenRef.current = linkPopover !== null;
4792
6888
  return bridgeRoot ? createPortal(
4793
- /* @__PURE__ */ jsxs(Fragment, { children: [
4794
- toolbarRect && /* @__PURE__ */ jsxs(Fragment, { children: [
4795
- /* @__PURE__ */ jsx3(GlowFrame, { rect: toolbarRect, elRef: glowElRef }),
4796
- /* @__PURE__ */ jsx3(FloatingToolbar, { rect: toolbarRect, parentScroll: parentScrollRef.current, elRef: toolbarElRef, onCommand: handleCommand, activeCommands })
6889
+ /* @__PURE__ */ jsxs8(Fragment3, { children: [
6890
+ toolbarRect && /* @__PURE__ */ jsxs8(Fragment3, { children: [
6891
+ /* @__PURE__ */ jsx16(GlowFrame, { rect: toolbarRect, elRef: glowElRef }),
6892
+ /* @__PURE__ */ jsx16(
6893
+ FloatingToolbar,
6894
+ {
6895
+ rect: toolbarRect,
6896
+ parentScroll: parentScrollRef.current,
6897
+ elRef: toolbarElRef,
6898
+ onCommand: handleCommand,
6899
+ activeCommands,
6900
+ showEditLink,
6901
+ onEditLink: openLinkPopoverForActive
6902
+ }
6903
+ )
4797
6904
  ] }),
4798
- maxBadge && /* @__PURE__ */ jsxs(
6905
+ maxBadge && /* @__PURE__ */ jsxs8(
4799
6906
  "div",
4800
6907
  {
4801
6908
  "data-ohw-max-badge": "",
@@ -4821,7 +6928,7 @@ function OhhwellsBridge() {
4821
6928
  ]
4822
6929
  }
4823
6930
  ),
4824
- toggleState && /* @__PURE__ */ jsx3(
6931
+ toggleState && /* @__PURE__ */ jsx16(
4825
6932
  StateToggle,
4826
6933
  {
4827
6934
  rect: toggleState.rect,
@@ -4829,16 +6936,66 @@ function OhhwellsBridge() {
4829
6936
  states: toggleState.states,
4830
6937
  onStateChange: handleStateChange
4831
6938
  }
6939
+ ),
6940
+ linkPopover ? /* @__PURE__ */ jsx16(
6941
+ LinkPopover,
6942
+ {
6943
+ rect: linkPopover.rect,
6944
+ parentScroll: parentScrollRef.current,
6945
+ panelRef: linkPopoverPanelRef,
6946
+ open: true,
6947
+ mode: "edit",
6948
+ pages: sitePages,
6949
+ sections: currentSections,
6950
+ sectionsByPath,
6951
+ initialTarget: linkPopover.target,
6952
+ onClose: closeLinkPopover,
6953
+ onSubmit: handleLinkPopoverSubmit
6954
+ }
6955
+ ) : null,
6956
+ sectionGap && /* @__PURE__ */ jsxs8(
6957
+ "div",
6958
+ {
6959
+ "data-ohw-section-insert-line": "",
6960
+ className: "fixed left-0 w-full z-2147483646 flex items-center pointer-events-none",
6961
+ style: { top: sectionGap.y, transform: "translateY(-50%)" },
6962
+ children: [
6963
+ /* @__PURE__ */ jsx16("div", { className: "flex-1 bg-primary", style: { height: 3 } }),
6964
+ /* @__PURE__ */ jsx16(
6965
+ Badge,
6966
+ {
6967
+ className: "px-8 py-1 bg-primary hover:bg-primary text-primary-foreground text-xs font-medium shrink-0 rounded-full cursor-pointer pointer-events-auto",
6968
+ onClick: () => {
6969
+ window.parent.postMessage(
6970
+ { type: "ow:add-section", insertAfter: sectionGap.insertAfter, insertBefore: sectionGap.insertBefore },
6971
+ "*"
6972
+ );
6973
+ },
6974
+ children: "Add Section"
6975
+ }
6976
+ ),
6977
+ /* @__PURE__ */ jsx16("div", { className: "flex-1 bg-primary", style: { height: 3 } })
6978
+ ]
6979
+ }
4832
6980
  )
4833
6981
  ] }),
4834
6982
  bridgeRoot
4835
6983
  ) : null;
4836
6984
  }
4837
6985
  export {
6986
+ LinkEditorPanel,
6987
+ LinkPopover,
4838
6988
  OhhwellsBridge,
6989
+ SchedulingWidget,
4839
6990
  Toggle,
4840
6991
  ToggleGroup,
4841
6992
  ToggleGroupItem,
4842
- toggleVariants
6993
+ buildTarget,
6994
+ filterAvailablePages,
6995
+ getEditModeInitialState,
6996
+ isValidUrl,
6997
+ parseTarget,
6998
+ toggleVariants,
6999
+ validateUrlInput
4843
7000
  };
4844
7001
  //# sourceMappingURL=index.js.map