@ohhwells/bridge 0.1.24 → 0.1.26

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.cjs CHANGED
@@ -31,17 +31,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  // src/index.ts
32
32
  var index_exports = {};
33
33
  __export(index_exports, {
34
+ LinkEditorPanel: () => LinkEditorPanel,
35
+ LinkPopover: () => LinkPopover,
34
36
  OhhwellsBridge: () => OhhwellsBridge,
35
37
  SchedulingWidget: () => SchedulingWidget,
36
38
  Toggle: () => Toggle,
37
39
  ToggleGroup: () => ToggleGroup,
38
40
  ToggleGroupItem: () => ToggleGroupItem,
39
- toggleVariants: () => toggleVariants
41
+ buildTarget: () => buildTarget,
42
+ filterAvailablePages: () => filterAvailablePages,
43
+ getEditModeInitialState: () => getEditModeInitialState,
44
+ isValidUrl: () => isValidUrl,
45
+ parseTarget: () => parseTarget,
46
+ toggleVariants: () => toggleVariants,
47
+ validateUrlInput: () => validateUrlInput
40
48
  });
41
49
  module.exports = __toCommonJS(index_exports);
42
50
 
43
51
  // src/OhhwellsBridge.tsx
44
- var import_react3 = __toESM(require("react"), 1);
52
+ var import_react5 = __toESM(require("react"), 1);
45
53
  var import_client = require("react-dom/client");
46
54
  var import_react_dom = require("react-dom");
47
55
 
@@ -204,6 +212,13 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
204
212
  var import_jsx_runtime2 = require("react/jsx-runtime");
205
213
  var API_URL = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
206
214
  var DAY_ABBR = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
215
+ function getApiDomain() {
216
+ const h = window.location.hostname;
217
+ for (const base of ["ohhwells.site", "theflowops-staging.com", "theflowops.com"]) {
218
+ if (h.endsWith(`.${base}`)) return h.slice(0, -(base.length + 1));
219
+ }
220
+ return h;
221
+ }
207
222
  function classRunsOnDate(cls, date, type) {
208
223
  if (type === "FIXED") {
209
224
  const d = String(date.getDate()).padStart(2, "0");
@@ -249,10 +264,10 @@ function buildTimezoneLabel(tz) {
249
264
  return tz;
250
265
  }
251
266
  }
252
- function EmptyState({ inEditor, insertAfter }) {
267
+ function EmptyState({ inEditor }) {
253
268
  const handleAddSchedule = () => {
254
269
  if (inEditor) {
255
- window.parent.postMessage({ type: "ow:scheduling-not-connected", insertAfter }, "*");
270
+ window.parent.postMessage({ type: "ow:scheduling-not-connected" }, "*");
256
271
  }
257
272
  };
258
273
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("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: [
@@ -477,21 +492,18 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
477
492
  const booked = getBookingsOnDate(cls, selectedDate);
478
493
  const available = cls.maxParticipants - booked;
479
494
  const isFull = available <= 0;
480
- const isPrivate = cls.maxParticipants === 1;
481
495
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
482
496
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex gap-4 py-4 sm:items-center sm:gap-[60px] box-border", children: [
483
497
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col gap-2 shrink-0 sm:contents", children: [
484
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col gap-2 sm:w-[120px] sm:shrink-0", children: [
485
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block", children: formatClassTime(cls) }),
486
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
487
- "span",
488
- {
489
- className: "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium w-fit",
490
- style: isPrivate ? { background: "#DDFBE3", color: "#199130", border: "1px solid #199130", fontWeight: 500 } : { background: "#EEEDFF", color: "#5953FF", border: "1px solid #5953FF", fontWeight: 500 },
491
- children: isPrivate ? "PRIVATE" : "GROUP"
492
- }
493
- )
494
- ] }),
498
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "sm:w-[120px] sm:shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block", children: formatClassTime(cls) }) }),
499
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
500
+ "span",
501
+ {
502
+ className: "inline-flex items-center px-2 py-0.5 rounded-full border text-xs font-medium sm:hidden",
503
+ style: { borderColor: "#0885FE", color: "#0885FE" },
504
+ children: "GROUP"
505
+ }
506
+ ),
495
507
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "sm:hidden flex flex-col gap-px", children: isFull ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
496
508
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
497
509
  available,
@@ -564,9 +576,7 @@ function CalendarFoldIcon() {
564
576
  }
565
577
  );
566
578
  }
567
- function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAfter: insertAfterProp }) {
568
- const autoId = (0, import_react2.useId)();
569
- const insertAfter = insertAfterProp ?? autoId;
579
+ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAfter }) {
570
580
  const [schedule, setSchedule] = (0, import_react2.useState)(null);
571
581
  const [loading, setLoading] = (0, import_react2.useState)(true);
572
582
  const [inEditor, setInEditor] = (0, import_react2.useState)(false);
@@ -596,99 +606,88 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
596
606
  if (typeof window === "undefined") return;
597
607
  const isInEditor = window.self !== window.top;
598
608
  setInEditor(isInEditor);
599
- function loadWithId(effectiveId) {
600
- if (isInEditor) {
601
- const startEmpty = effectiveId === null;
602
- if (startEmpty) setLoading(false);
603
- let initialized = startEmpty;
604
- const timer = !startEmpty ? setTimeout(() => {
605
- if (!initialized) {
606
- initialized = true;
607
- setLoading(false);
608
- }
609
- }, 5e3) : null;
610
- const handler = (e) => {
611
- if (e.data?.type === "ow:clear-scheduling-widget") {
612
- if (!e.data.insertAfter || e.data.insertAfter !== insertAfter) return;
613
- setSchedule(null);
614
- setLoading(false);
615
- return;
616
- }
617
- if (e.data?.type === "ow:switch-schedule") {
618
- if (!e.data.insertAfter || e.data.insertAfter !== insertAfter) return;
619
- switchScheduleIdRef.current = e.data.scheduleId ?? null;
620
- initialized = false;
621
- setLoading(true);
622
- window.parent.postMessage({ type: "ow:request-user-schedules", insertAfter }, "*");
623
- return;
624
- }
625
- if (e.data?.type !== "ow:user-schedules-response") return;
626
- if (e.data.insertAfter && e.data.insertAfter !== insertAfter) return;
627
- if (initialized && switchScheduleIdRef.current === null) return;
609
+ if (isInEditor) {
610
+ const startEmpty = initialScheduleId === null;
611
+ if (startEmpty) setLoading(false);
612
+ let initialized = startEmpty;
613
+ const timer = !startEmpty ? setTimeout(() => {
614
+ if (!initialized) {
628
615
  initialized = true;
629
- if (timer) clearTimeout(timer);
630
- const schedules = e.data.schedules ?? [];
631
- const isSwitching = switchScheduleIdRef.current !== null;
632
- const target = isSwitching ? schedules.find((s) => s.id === switchScheduleIdRef.current) ?? schedules[0] ?? null : effectiveId ? schedules.find((s) => s.id === effectiveId) ?? schedules[0] ?? null : schedules[0] ?? null;
633
- switchScheduleIdRef.current = null;
634
- setSchedule(target);
635
616
  setLoading(false);
636
- if (notifyOnConnect && target && !isSwitching) {
637
- window.parent.postMessage({
638
- type: "ow:schedule-connected",
639
- schedule: { id: target.id, name: target.name },
640
- insertAfter
641
- }, "*");
642
- window.postMessage({ type: "ow:schedule-linked", scheduleId: target.id, insertAfter }, "*");
643
- }
644
- };
645
- window.addEventListener("message", handler);
646
- if (!startEmpty) window.parent.postMessage({ type: "ow:request-user-schedules", insertAfter }, "*");
647
- return () => {
648
- window.removeEventListener("message", handler);
649
- if (timer) clearTimeout(timer);
650
- };
651
- }
652
- if (!effectiveId) {
653
- setLoading(false);
654
- return;
655
- }
656
- ;
657
- (async () => {
658
- try {
659
- const res = await fetch(`${API_URL}/api/schedule/id/${effectiveId}`);
660
- const data = await res.json();
661
- setSchedule(data?.id ? data : null);
662
- } catch {
663
617
  }
664
- setLoading(false);
665
- })();
666
- }
667
- if (initialScheduleId === void 0) {
668
- let done = false;
669
- let innerCleanup;
670
- const timer = setTimeout(() => {
671
- if (!done) {
672
- done = true;
618
+ }, 5e3) : null;
619
+ const handler = (e) => {
620
+ if (e.data?.type === "ow:clear-scheduling-widget") {
621
+ if (!e.data.insertAfter || e.data.insertAfter !== insertAfter) return;
622
+ setSchedule(null);
673
623
  setLoading(false);
624
+ return;
625
+ }
626
+ if (e.data?.type === "ow:switch-schedule") {
627
+ if (!e.data.insertAfter || e.data.insertAfter !== insertAfter) return;
628
+ switchScheduleIdRef.current = e.data.scheduleId ?? null;
629
+ initialized = false;
630
+ setLoading(true);
631
+ window.parent.postMessage({ type: "ow:request-user-schedules", insertAfter }, "*");
632
+ return;
633
+ }
634
+ if (e.data?.type !== "ow:user-schedules-response") return;
635
+ if (e.data.insertAfter && e.data.insertAfter !== insertAfter) return;
636
+ if (initialized && switchScheduleIdRef.current === null) return;
637
+ initialized = true;
638
+ if (timer) clearTimeout(timer);
639
+ const schedules = e.data.schedules ?? [];
640
+ const isSwitching = switchScheduleIdRef.current !== null;
641
+ 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;
642
+ switchScheduleIdRef.current = null;
643
+ setSchedule(target);
644
+ setLoading(false);
645
+ if (notifyOnConnect && target && !isSwitching) {
646
+ window.parent.postMessage({
647
+ type: "ow:schedule-connected",
648
+ schedule: { id: target.id, name: target.name },
649
+ insertAfter
650
+ }, "*");
651
+ window.postMessage({ type: "ow:schedule-linked", scheduleId: target.id, insertAfter }, "*");
674
652
  }
675
- }, 2e3);
676
- const configHandler = (e) => {
677
- if (e.data?.type !== "ow:schedule-config" || e.data.insertAfter !== insertAfter || done) return;
678
- done = true;
679
- clearTimeout(timer);
680
- window.removeEventListener("message", configHandler);
681
- innerCleanup = loadWithId(e.data.scheduleId ?? null);
682
653
  };
683
- window.addEventListener("message", configHandler);
684
- window.postMessage({ type: "ow:request-schedule-config", insertAfter }, "*");
654
+ window.addEventListener("message", handler);
655
+ if (!startEmpty) window.parent.postMessage({ type: "ow:request-user-schedules", insertAfter }, "*");
685
656
  return () => {
686
- window.removeEventListener("message", configHandler);
687
- clearTimeout(timer);
688
- innerCleanup?.();
657
+ window.removeEventListener("message", handler);
658
+ if (timer) clearTimeout(timer);
689
659
  };
690
660
  }
691
- return loadWithId(initialScheduleId) ?? void 0;
661
+ if (initialScheduleId === null) {
662
+ setLoading(false);
663
+ return;
664
+ }
665
+ ;
666
+ (async () => {
667
+ try {
668
+ if (initialScheduleId) {
669
+ const res = await fetch(`${API_URL}/api/schedule/id/${initialScheduleId}`);
670
+ const data = await res.json();
671
+ setSchedule(data?.id ? data : null);
672
+ } else {
673
+ const domain = getApiDomain();
674
+ const sitemapRes = await fetch(`${API_URL}/api/schedule/sitemap/${domain}`);
675
+ const slugs = await sitemapRes.json();
676
+ if (!Array.isArray(slugs) || !slugs.length) {
677
+ setLoading(false);
678
+ return;
679
+ }
680
+ const { slug } = [...slugs].sort(
681
+ (a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
682
+ )[0];
683
+ const res = await fetch(`${API_URL}/api/schedule/${domain}/${slug}`);
684
+ const data = await res.json();
685
+ setSchedule(data?.id ? data : null);
686
+ }
687
+ } catch {
688
+ }
689
+ setLoading(false);
690
+ })();
692
691
  }, []);
693
692
  const timezoneLabel = schedule?.timezone ? (() => {
694
693
  try {
@@ -723,7 +722,6 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
723
722
  window.parent.postMessage({ type: "ow:scheduling-not-connected", insertAfter }, "*");
724
723
  }
725
724
  };
726
- if (!inEditor && !loading && !schedule) return null;
727
725
  const sectionId = `scheduling-${insertAfter}`;
728
726
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
729
727
  "section",
@@ -769,7 +767,7 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
769
767
  ] }),
770
768
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "w-full flex flex-col items-end gap-3", children: [
771
769
  timezoneLabel && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-sm font-normal text-(--color-accent,#A89B83)", children: timezoneLabel }),
772
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "w-full p-0 sm:p-10 box-border", children: loading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LoadingSkeleton, {}) : !schedule ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(EmptyState, { inEditor, insertAfter }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
770
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "w-full p-0 sm:p-10 box-border", children: loading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LoadingSkeleton, {}) : !schedule ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(EmptyState, { inEditor }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
773
771
  ScheduleView,
774
772
  {
775
773
  schedule,
@@ -4225,8 +4223,844 @@ function isEditSessionActive() {
4225
4223
  return new URLSearchParams(q).get("mode") === "edit";
4226
4224
  }
4227
4225
 
4228
- // src/ui/badge.tsx
4226
+ // src/ui/link-modal/calcPopoverPos.ts
4227
+ function calcPopoverPos(rect, parentScroll, approxW = 483, approxH = 320) {
4228
+ const GAP = 8;
4229
+ const canvasTopInIframe = parentScroll != null ? parentScroll.headerH - parentScroll.iframeOffsetTop : 0;
4230
+ const visibleTop = rect.top - canvasTopInIframe;
4231
+ let top = rect.top - GAP;
4232
+ let transform = "translateX(-50%) translateY(-100%)";
4233
+ if (visibleTop < approxH + GAP) {
4234
+ top = rect.bottom + GAP;
4235
+ transform = "translateX(-50%)";
4236
+ }
4237
+ const rawLeft = rect.left + rect.width / 2;
4238
+ const left = Math.max(GAP + approxW / 2, Math.min(rawLeft, window.innerWidth - GAP - approxW / 2));
4239
+ return { top, left, transform };
4240
+ }
4241
+
4242
+ // src/ui/button.tsx
4243
+ var React3 = __toESM(require("react"), 1);
4244
+ var import_radix_ui3 = require("radix-ui");
4229
4245
  var import_jsx_runtime5 = require("react/jsx-runtime");
4246
+ var buttonVariants = cva(
4247
+ "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",
4248
+ {
4249
+ variants: {
4250
+ variant: {
4251
+ default: "bg-primary text-primary-foreground hover:opacity-90",
4252
+ outline: "border border-border bg-background text-foreground shadow-sm hover:bg-muted/80",
4253
+ ghost: "min-w-0 px-3 py-2 text-foreground hover:bg-muted/50"
4254
+ },
4255
+ size: {
4256
+ default: "h-9",
4257
+ sm: "h-8 min-w-[64px] px-2 py-1.5 text-sm"
4258
+ }
4259
+ },
4260
+ defaultVariants: {
4261
+ variant: "default",
4262
+ size: "default"
4263
+ }
4264
+ }
4265
+ );
4266
+ var Button = React3.forwardRef(
4267
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
4268
+ const Comp = asChild ? import_radix_ui3.Slot.Root : "button";
4269
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
4270
+ Comp,
4271
+ {
4272
+ ref,
4273
+ "data-slot": "button",
4274
+ className: cn(buttonVariants({ variant, size, className })),
4275
+ ...props
4276
+ }
4277
+ );
4278
+ }
4279
+ );
4280
+ Button.displayName = "Button";
4281
+
4282
+ // src/ui/icons.tsx
4283
+ var import_jsx_runtime6 = require("react/jsx-runtime");
4284
+ function IconX({ className, ...props }) {
4285
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, ...props, children: [
4286
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M18 6 6 18" }),
4287
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "m6 6 12 12" })
4288
+ ] });
4289
+ }
4290
+ function IconChevronDown({ className, ...props }) {
4291
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, ...props, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "m6 9 6 6 6-6" }) });
4292
+ }
4293
+ function IconFile({ className, ...props }) {
4294
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, ...props, children: [
4295
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" }),
4296
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M14 2v4a2 2 0 0 0 2 2h4" })
4297
+ ] });
4298
+ }
4299
+ function IconInfo({ className, ...props }) {
4300
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, ...props, children: [
4301
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
4302
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M12 16v-4" }),
4303
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M12 8h.01" })
4304
+ ] });
4305
+ }
4306
+ function IconArrowRight({ className, ...props }) {
4307
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, ...props, children: [
4308
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M5 12h14" }),
4309
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "m12 5 7 7-7 7" })
4310
+ ] });
4311
+ }
4312
+ function IconSection({ className, ...props }) {
4313
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, ...props, children: [
4314
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("rect", { width: "18", height: "7", x: "3", y: "3", rx: "1" }),
4315
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("rect", { width: "9", height: "7", x: "3", y: "14", rx: "1" }),
4316
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("rect", { width: "5", height: "7", x: "16", y: "14", rx: "1" })
4317
+ ] });
4318
+ }
4319
+
4320
+ // src/ui/link-modal/DestinationBreadcrumb.tsx
4321
+ var import_jsx_runtime7 = require("react/jsx-runtime");
4322
+ function DestinationBreadcrumb({ pageTitle, sectionLabel }) {
4323
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex w-full flex-col gap-2", children: [
4324
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-sm font-medium text-foreground", children: "Destination" }),
4325
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-3", children: [
4326
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-2", children: [
4327
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconFile, { className: "shrink-0 text-foreground", "aria-hidden": true }),
4328
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "text-sm font-medium leading-none text-foreground", children: pageTitle })
4329
+ ] }),
4330
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconArrowRight, { className: "shrink-0 text-muted-foreground", "aria-hidden": true }),
4331
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [
4332
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(IconSection, { className: "shrink-0 text-foreground", "aria-hidden": true }),
4333
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "truncate text-sm font-medium leading-none text-foreground", children: sectionLabel })
4334
+ ] })
4335
+ ] })
4336
+ ] });
4337
+ }
4338
+
4339
+ // src/ui/link-modal/SectionTreeItem.tsx
4340
+ var import_jsx_runtime8 = require("react/jsx-runtime");
4341
+ function SectionTreeItem({ section, onSelect, selected }) {
4342
+ const interactive = Boolean(onSelect);
4343
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex h-9 w-full items-end pl-3", children: [
4344
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4345
+ "div",
4346
+ {
4347
+ className: "mr-[-1px] h-9 w-2 shrink-0 rounded-bl-sm border-b border-l border-border mb-4",
4348
+ "aria-hidden": true
4349
+ }
4350
+ ),
4351
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
4352
+ "div",
4353
+ {
4354
+ role: interactive ? "button" : void 0,
4355
+ tabIndex: interactive ? 0 : void 0,
4356
+ onClick: interactive ? () => onSelect?.(section) : void 0,
4357
+ onKeyDown: interactive ? (e) => {
4358
+ if (e.key === "Enter" || e.key === " ") {
4359
+ e.preventDefault();
4360
+ onSelect?.(section);
4361
+ }
4362
+ } : void 0,
4363
+ className: cn(
4364
+ "flex h-9 min-w-0 flex-1 items-center gap-2 rounded-md border border-border bg-background p-3",
4365
+ interactive && "cursor-pointer hover:bg-muted/30",
4366
+ interactive && selected && "border-primary"
4367
+ ),
4368
+ children: [
4369
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(IconSection, { className: "shrink-0 text-foreground", "aria-hidden": true }),
4370
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "truncate text-sm font-normal leading-5 text-foreground", children: section.label })
4371
+ ]
4372
+ }
4373
+ )
4374
+ ] });
4375
+ }
4376
+ function SectionPickerList({ sections, onSelect }) {
4377
+ if (sections.length === 0) {
4378
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-sm text-muted-foreground", children: "No sections found on this page." });
4379
+ }
4380
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-col gap-1", children: [
4381
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-sm text-muted-foreground", children: "Pick a section this link should scroll to." }),
4382
+ sections.map((section) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SectionTreeItem, { section, onSelect }, section.id))
4383
+ ] });
4384
+ }
4385
+
4386
+ // src/ui/link-modal/UrlOrPageInput.tsx
4387
+ var import_react3 = require("react");
4388
+
4389
+ // src/ui/input.tsx
4390
+ var React4 = __toESM(require("react"), 1);
4391
+ var import_jsx_runtime9 = require("react/jsx-runtime");
4392
+ var Input = React4.forwardRef(
4393
+ ({ className, type, ...props }, ref) => {
4394
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4395
+ "input",
4396
+ {
4397
+ type,
4398
+ ref,
4399
+ "data-slot": "input",
4400
+ className: cn(
4401
+ "flex h-full w-full min-w-0 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground",
4402
+ className
4403
+ ),
4404
+ ...props
4405
+ }
4406
+ );
4407
+ }
4408
+ );
4409
+ Input.displayName = "Input";
4410
+
4411
+ // src/ui/label.tsx
4412
+ var import_radix_ui4 = require("radix-ui");
4413
+ var import_jsx_runtime10 = require("react/jsx-runtime");
4414
+ function Label({ className, ...props }) {
4415
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
4416
+ import_radix_ui4.Label.Root,
4417
+ {
4418
+ "data-slot": "label",
4419
+ className: cn("text-sm font-medium leading-5 text-foreground", className),
4420
+ ...props
4421
+ }
4422
+ );
4423
+ }
4424
+
4425
+ // src/ui/popover.tsx
4426
+ var import_radix_ui5 = require("radix-ui");
4427
+ var import_jsx_runtime11 = require("react/jsx-runtime");
4428
+ function Popover({ ...props }) {
4429
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_radix_ui5.Popover.Root, { "data-slot": "popover", ...props });
4430
+ }
4431
+ function PopoverTrigger({ ...props }) {
4432
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_radix_ui5.Popover.Trigger, { "data-slot": "popover-trigger", ...props });
4433
+ }
4434
+ function PopoverContent({
4435
+ className,
4436
+ align = "start",
4437
+ sideOffset = 6,
4438
+ ...props
4439
+ }) {
4440
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_radix_ui5.Popover.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
4441
+ import_radix_ui5.Popover.Content,
4442
+ {
4443
+ "data-slot": "popover-content",
4444
+ align,
4445
+ sideOffset,
4446
+ className: cn(
4447
+ "z-[2147483647] w-[var(--radix-popover-trigger-width)] overflow-auto rounded-lg border border-border bg-popover py-1 shadow-lg outline-none",
4448
+ className
4449
+ ),
4450
+ ...props
4451
+ }
4452
+ ) });
4453
+ }
4454
+
4455
+ // src/ui/link-modal/UrlOrPageInput.tsx
4456
+ var import_jsx_runtime12 = require("react/jsx-runtime");
4457
+ function FieldChevron({ onClick }) {
4458
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
4459
+ "button",
4460
+ {
4461
+ type: "button",
4462
+ className: "flex shrink-0 items-center justify-end border-0 bg-transparent p-0 pl-4 text-muted-foreground outline-none",
4463
+ onClick,
4464
+ "aria-label": "Open page list",
4465
+ tabIndex: -1,
4466
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(IconChevronDown, {})
4467
+ }
4468
+ );
4469
+ }
4470
+ function UrlOrPageInput({
4471
+ value,
4472
+ selectedPage,
4473
+ placeholder = "Paste a URL or choose a page",
4474
+ dropdownOpen,
4475
+ onDropdownOpenChange,
4476
+ filteredPages,
4477
+ onInputChange,
4478
+ onPageSelect,
4479
+ readOnly = false,
4480
+ urlError
4481
+ }) {
4482
+ const inputId = (0, import_react3.useId)();
4483
+ const inputRef = (0, import_react3.useRef)(null);
4484
+ const [isEditing, setIsEditing] = (0, import_react3.useState)(false);
4485
+ const [isFocused, setIsFocused] = (0, import_react3.useState)(false);
4486
+ const showFilledDisplay = Boolean(selectedPage) && !isEditing && !readOnly;
4487
+ (0, import_react3.useEffect)(() => {
4488
+ if (selectedPage) setIsEditing(false);
4489
+ }, [selectedPage]);
4490
+ const openDropdown = () => {
4491
+ if (readOnly || filteredPages.length === 0) return;
4492
+ onDropdownOpenChange(true);
4493
+ };
4494
+ const toggleDropdown = (e) => {
4495
+ e.stopPropagation();
4496
+ e.preventDefault();
4497
+ if (readOnly || filteredPages.length === 0) return;
4498
+ onDropdownOpenChange(!dropdownOpen);
4499
+ };
4500
+ const enterEditMode = () => {
4501
+ if (readOnly) return;
4502
+ setIsEditing(true);
4503
+ requestAnimationFrame(() => inputRef.current?.focus());
4504
+ };
4505
+ const fieldClassName = cn(
4506
+ "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]",
4507
+ urlError ? "border-destructive shadow-[0_0_0_1px_var(--ohw-destructive)]" : isFocused ? "border-primary shadow-[0_0_0_1px_var(--ohw-primary)]" : "border-input"
4508
+ );
4509
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex w-full flex-col gap-2 p-0", children: [
4510
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Label, { htmlFor: inputId, className: cn(urlError && "text-destructive"), children: "Destination" }),
4511
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(Popover, { open: dropdownOpen && !readOnly, onOpenChange: onDropdownOpenChange, children: [
4512
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PopoverTrigger, { asChild: true, children: showFilledDisplay ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
4513
+ "button",
4514
+ {
4515
+ type: "button",
4516
+ "data-ohw-link-field": true,
4517
+ className: cn(fieldClassName, "m-0 cursor-pointer border-solid text-left"),
4518
+ onClick: openDropdown,
4519
+ onFocus: () => setIsFocused(true),
4520
+ onBlur: () => setIsFocused(false),
4521
+ onKeyDown: (e) => {
4522
+ if (e.key === "Backspace" || e.key === "Delete" || e.key.length === 1) {
4523
+ enterEditMode();
4524
+ }
4525
+ },
4526
+ children: [
4527
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex shrink-0 items-center pr-2", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(IconFile, { className: "text-foreground", "aria-hidden": true }) }),
4528
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "min-w-0 flex-1 truncate text-sm font-normal text-foreground", children: selectedPage.title }),
4529
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(FieldChevron, { onClick: toggleDropdown })
4530
+ ]
4531
+ }
4532
+ ) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
4533
+ "div",
4534
+ {
4535
+ "data-ohw-link-field": true,
4536
+ className: fieldClassName,
4537
+ onMouseDown: (e) => {
4538
+ if (readOnly) return;
4539
+ if (e.target === e.currentTarget) enterEditMode();
4540
+ },
4541
+ children: [
4542
+ selectedPage ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex shrink-0 items-center pr-2", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(IconFile, { className: "text-foreground", "aria-hidden": true }) }) : null,
4543
+ readOnly ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "min-w-0 flex-1 truncate text-sm leading-5 text-foreground", children: selectedPage?.title ?? value }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
4544
+ Input,
4545
+ {
4546
+ ref: inputRef,
4547
+ id: inputId,
4548
+ value,
4549
+ onChange: (e) => onInputChange(e.target.value),
4550
+ onFocus: () => {
4551
+ setIsFocused(true);
4552
+ setIsEditing(true);
4553
+ openDropdown();
4554
+ },
4555
+ onBlur: () => setIsFocused(false),
4556
+ placeholder,
4557
+ className: cn(
4558
+ "min-w-0 flex-1 truncate border-0 bg-transparent p-0 text-sm font-normal leading-5 shadow-none outline-none ring-0",
4559
+ value ? "text-foreground" : "text-muted-foreground placeholder:text-muted-foreground"
4560
+ )
4561
+ }
4562
+ ),
4563
+ !readOnly ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(FieldChevron, { onClick: toggleDropdown }) : null
4564
+ ]
4565
+ }
4566
+ ) }),
4567
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PopoverContent, { "data-ohw-link-popover-root": "", onOpenAutoFocus: (e) => e.preventDefault(), children: filteredPages.map((page) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
4568
+ "button",
4569
+ {
4570
+ type: "button",
4571
+ 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",
4572
+ onClick: () => onPageSelect(page),
4573
+ children: [
4574
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(IconFile, { className: "shrink-0", "aria-hidden": true }),
4575
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "truncate", children: page.title })
4576
+ ]
4577
+ },
4578
+ page.path
4579
+ )) })
4580
+ ] }),
4581
+ urlError ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-sm font-medium text-destructive", children: urlError }) : null
4582
+ ] });
4583
+ }
4584
+
4585
+ // src/ui/link-modal/useLinkModalState.ts
4586
+ var import_react4 = require("react");
4587
+
4588
+ // src/ui/link-modal/linkModal.utils.ts
4589
+ function parseTarget(target) {
4590
+ if (!target) return { pageRoute: "", sectionId: null };
4591
+ const hashIndex = target.indexOf("#");
4592
+ if (hashIndex === -1) return { pageRoute: target, sectionId: null };
4593
+ return {
4594
+ pageRoute: target.slice(0, hashIndex),
4595
+ sectionId: target.slice(hashIndex + 1) || null
4596
+ };
4597
+ }
4598
+ function normalizePath(path) {
4599
+ const trimmed = path.trim();
4600
+ if (!trimmed) return "/";
4601
+ try {
4602
+ if (/^https?:\/\//i.test(trimmed)) {
4603
+ return new URL(trimmed).pathname || "/";
4604
+ }
4605
+ } catch {
4606
+ }
4607
+ const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
4608
+ if (withSlash.length > 1 && withSlash.endsWith("/")) return withSlash.slice(0, -1);
4609
+ return withSlash;
4610
+ }
4611
+ function findPageByPath(pages, path) {
4612
+ const normalized = normalizePath(path);
4613
+ return pages.find((p) => normalizePath(p.path) === normalized);
4614
+ }
4615
+ function inferPageTitleFromPath(path) {
4616
+ const normalized = normalizePath(path);
4617
+ if (normalized === "/") return "Home";
4618
+ const segment = normalized.split("/").filter(Boolean).pop() ?? "";
4619
+ if (!segment) return "Home";
4620
+ return segment.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
4621
+ }
4622
+ function isInternalPath(route) {
4623
+ const trimmed = route.trim();
4624
+ if (!trimmed) return false;
4625
+ if (/^https?:\/\//i.test(trimmed)) return false;
4626
+ if (trimmed.startsWith("/")) return true;
4627
+ if (trimmed.includes(" ") || trimmed.includes(".")) return false;
4628
+ return true;
4629
+ }
4630
+ function resolvePage(route, pages) {
4631
+ const found = findPageByPath(pages, route);
4632
+ if (found) return { path: found.path, title: found.title };
4633
+ return { path: normalizePath(route), title: inferPageTitleFromPath(route) };
4634
+ }
4635
+ function getSectionsForPath(sectionsByPath, pageRoute, fallback) {
4636
+ if (!sectionsByPath) return fallback;
4637
+ const normalized = normalizePath(pageRoute);
4638
+ return sectionsByPath[normalized] ?? sectionsByPath[pageRoute] ?? fallback;
4639
+ }
4640
+ function buildTarget(page, section) {
4641
+ if (section) return `${page.path}#${section.id}`;
4642
+ return page.path;
4643
+ }
4644
+ function isValidUrl(urlString) {
4645
+ try {
4646
+ const url = new URL(urlString);
4647
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
4648
+ const hostname = url.hostname;
4649
+ if (!hostname || hostname.length === 0) return false;
4650
+ if (hostname.includes(" ")) return false;
4651
+ return hostname.includes(".") || hostname === "localhost";
4652
+ } catch {
4653
+ return false;
4654
+ }
4655
+ }
4656
+ function normalizeUrl(value) {
4657
+ const trimmed = value.trim();
4658
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
4659
+ return `https://${trimmed}`;
4660
+ }
4661
+ function validateUrlInput(value, pages, allPages) {
4662
+ if (!value.trim()) return "";
4663
+ const filtered = pages.filter((p) => p.title.toLowerCase().startsWith(value.toLowerCase()));
4664
+ if (filtered.length > 0) return "";
4665
+ const matchingPage = allPages.find((p) => p.title.toLowerCase() === value.toLowerCase());
4666
+ if (matchingPage) return "";
4667
+ if (!isValidUrl(normalizeUrl(value))) {
4668
+ return "Please enter a valid URL (e.g., https://example.com)";
4669
+ }
4670
+ return "";
4671
+ }
4672
+ function getEditModeInitialState(target, pages, sections) {
4673
+ const empty = {
4674
+ searchValue: "",
4675
+ selectedPage: null,
4676
+ selectedSection: null,
4677
+ step: "input"
4678
+ };
4679
+ if (!target) return empty;
4680
+ if (isValidUrl(normalizeUrl(target))) {
4681
+ return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
4682
+ }
4683
+ const { pageRoute, sectionId } = parseTarget(target);
4684
+ if (sectionId) {
4685
+ if (isInternalPath(pageRoute)) {
4686
+ const page = resolvePage(pageRoute, pages);
4687
+ const sec = sections.find((s) => s.id === sectionId);
4688
+ return {
4689
+ searchValue: page.title,
4690
+ selectedPage: page,
4691
+ selectedSection: sec ? { id: sec.id, label: sec.label } : null,
4692
+ step: "input"
4693
+ };
4694
+ }
4695
+ return { searchValue: pageRoute || target, selectedPage: null, selectedSection: null, step: "input" };
4696
+ }
4697
+ if (isInternalPath(pageRoute)) {
4698
+ const page = resolvePage(pageRoute, pages);
4699
+ return {
4700
+ searchValue: page.title,
4701
+ selectedPage: page,
4702
+ selectedSection: null,
4703
+ step: "input"
4704
+ };
4705
+ }
4706
+ return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
4707
+ }
4708
+ function filterAvailablePages(pages, existingTargets) {
4709
+ const taken = new Set(existingTargets);
4710
+ return pages.filter((p) => !taken.has(p.path));
4711
+ }
4712
+
4713
+ // src/ui/link-modal/useLinkModalState.ts
4714
+ function useLinkModalState({
4715
+ open,
4716
+ mode,
4717
+ pages,
4718
+ sections,
4719
+ sectionsByPath,
4720
+ initialTarget,
4721
+ existingTargets,
4722
+ onClose,
4723
+ onSubmit
4724
+ }) {
4725
+ const availablePages = (0, import_react4.useMemo)(
4726
+ () => mode === "create" ? filterAvailablePages(pages, existingTargets) : pages,
4727
+ [mode, pages, existingTargets]
4728
+ );
4729
+ const [searchValue, setSearchValue] = (0, import_react4.useState)("");
4730
+ const [selectedPage, setSelectedPage] = (0, import_react4.useState)(null);
4731
+ const [selectedSection, setSelectedSection] = (0, import_react4.useState)(null);
4732
+ const [step, setStep] = (0, import_react4.useState)("input");
4733
+ const [dropdownOpen, setDropdownOpen] = (0, import_react4.useState)(false);
4734
+ const [urlError, setUrlError] = (0, import_react4.useState)("");
4735
+ const reset = (0, import_react4.useCallback)(() => {
4736
+ setSearchValue("");
4737
+ setSelectedPage(null);
4738
+ setSelectedSection(null);
4739
+ setStep("input");
4740
+ setDropdownOpen(false);
4741
+ setUrlError("");
4742
+ }, []);
4743
+ (0, import_react4.useEffect)(() => {
4744
+ if (!open) return;
4745
+ if (mode === "edit" && initialTarget) {
4746
+ const { pageRoute } = parseTarget(initialTarget);
4747
+ const targetSections = getSectionsForPath(sectionsByPath, pageRoute, sections);
4748
+ const init = getEditModeInitialState(initialTarget, pages, targetSections);
4749
+ setSearchValue(init.searchValue);
4750
+ setSelectedPage(init.selectedPage);
4751
+ setSelectedSection(init.selectedSection);
4752
+ setStep(init.step);
4753
+ } else {
4754
+ reset();
4755
+ }
4756
+ setDropdownOpen(false);
4757
+ setUrlError("");
4758
+ }, [open, mode, initialTarget, pages, sections, sectionsByPath, reset]);
4759
+ const filteredPages = (0, import_react4.useMemo)(() => {
4760
+ if (!searchValue.trim()) return availablePages;
4761
+ return availablePages.filter((p) => p.title.toLowerCase().startsWith(searchValue.toLowerCase()));
4762
+ }, [availablePages, searchValue]);
4763
+ const activeSections = (0, import_react4.useMemo)(() => {
4764
+ if (selectedPage && sectionsByPath) {
4765
+ return getSectionsForPath(sectionsByPath, selectedPage.path, sections);
4766
+ }
4767
+ return sections;
4768
+ }, [selectedPage, sectionsByPath, sections]);
4769
+ const showBreadcrumb = step === "confirmed" && selectedPage && selectedSection;
4770
+ const showSectionPicker = step === "sectionPicker" && selectedPage;
4771
+ const showSectionRow = mode === "edit" && selectedPage && selectedSection && step === "input";
4772
+ const showChooseSection = step === "input" && selectedPage && !selectedSection && !showSectionRow;
4773
+ const handleInputChange = (value) => {
4774
+ setSearchValue(value);
4775
+ setSelectedPage(null);
4776
+ setSelectedSection(null);
4777
+ setStep("input");
4778
+ setUrlError("");
4779
+ const filtered = value.trim() ? availablePages.filter((p) => p.title.toLowerCase().startsWith(value.toLowerCase())) : availablePages;
4780
+ setDropdownOpen(filtered.length > 0);
4781
+ if (value.trim() && filtered.length === 0) {
4782
+ const error = validateUrlInput(value, availablePages, pages);
4783
+ if (error) setUrlError(error);
4784
+ }
4785
+ };
4786
+ const handlePageSelect = (page) => {
4787
+ setSelectedPage({ path: page.path, title: page.title });
4788
+ setSelectedSection(null);
4789
+ setSearchValue(page.title);
4790
+ setStep("input");
4791
+ setDropdownOpen(false);
4792
+ setUrlError("");
4793
+ };
4794
+ const handleChooseSection = () => {
4795
+ setStep("sectionPicker");
4796
+ };
4797
+ const handleSectionSelect = (section) => {
4798
+ setSelectedSection({ id: section.id, label: section.label });
4799
+ if (mode === "create") {
4800
+ setStep("confirmed");
4801
+ } else {
4802
+ setStep("input");
4803
+ }
4804
+ };
4805
+ const handleBackToSections = () => {
4806
+ setStep("sectionPicker");
4807
+ };
4808
+ const handleClose = () => {
4809
+ reset();
4810
+ onClose();
4811
+ };
4812
+ const isValid = (0, import_react4.useMemo)(() => {
4813
+ if (urlError) return false;
4814
+ if (showBreadcrumb) return true;
4815
+ if (selectedPage) return true;
4816
+ if (!searchValue.trim()) return false;
4817
+ return validateUrlInput(searchValue, availablePages, pages) === "";
4818
+ }, [urlError, showBreadcrumb, selectedPage, searchValue, availablePages, pages]);
4819
+ const handleSubmit = () => {
4820
+ if (selectedPage && selectedSection) {
4821
+ onSubmit(buildTarget(selectedPage, selectedSection));
4822
+ handleClose();
4823
+ return;
4824
+ }
4825
+ if (selectedPage) {
4826
+ onSubmit(buildTarget(selectedPage, null));
4827
+ handleClose();
4828
+ return;
4829
+ }
4830
+ if (!searchValue.trim()) return;
4831
+ const error = validateUrlInput(searchValue, availablePages, pages);
4832
+ if (error) {
4833
+ setUrlError(error);
4834
+ return;
4835
+ }
4836
+ const matchingPage = pages.find((p) => p.title.toLowerCase() === searchValue.toLowerCase());
4837
+ if (matchingPage) {
4838
+ onSubmit(matchingPage.path);
4839
+ handleClose();
4840
+ return;
4841
+ }
4842
+ onSubmit(normalizeUrl(searchValue));
4843
+ handleClose();
4844
+ };
4845
+ const submitLabel = mode === "edit" ? "Save" : "Create";
4846
+ const title = mode === "edit" ? "Edit navigation item" : "Create navigation item";
4847
+ const secondaryLabel = showBreadcrumb ? "Back to sections" : "Cancel";
4848
+ const handleSecondary = showBreadcrumb ? handleBackToSections : handleClose;
4849
+ return {
4850
+ title,
4851
+ submitLabel,
4852
+ secondaryLabel,
4853
+ searchValue,
4854
+ selectedPage,
4855
+ selectedSection,
4856
+ step,
4857
+ dropdownOpen,
4858
+ setDropdownOpen,
4859
+ urlError,
4860
+ filteredPages,
4861
+ showBreadcrumb,
4862
+ showSectionPicker,
4863
+ showChooseSection,
4864
+ showSectionRow,
4865
+ isValid,
4866
+ activeSections,
4867
+ handleInputChange,
4868
+ handlePageSelect,
4869
+ handleChooseSection,
4870
+ handleSectionSelect,
4871
+ handleBackToSections,
4872
+ handleClose,
4873
+ handleSecondary,
4874
+ handleSubmit
4875
+ };
4876
+ }
4877
+
4878
+ // src/ui/link-modal/LinkEditorPanel.tsx
4879
+ var import_jsx_runtime13 = require("react/jsx-runtime");
4880
+ function LinkEditorPanel({
4881
+ open = true,
4882
+ mode = "create",
4883
+ pages,
4884
+ sections = [],
4885
+ sectionsByPath,
4886
+ initialTarget,
4887
+ existingTargets = [],
4888
+ onClose,
4889
+ onSubmit
4890
+ }) {
4891
+ const state = useLinkModalState({
4892
+ open,
4893
+ mode,
4894
+ pages,
4895
+ sections,
4896
+ sectionsByPath,
4897
+ initialTarget,
4898
+ existingTargets,
4899
+ onClose,
4900
+ onSubmit
4901
+ });
4902
+ const isCancel = state.secondaryLabel === "Cancel";
4903
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
4904
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4905
+ "button",
4906
+ {
4907
+ type: "button",
4908
+ className: "absolute right-[9px] top-[9px] rounded-sm p-1.5 text-foreground hover:bg-muted/50",
4909
+ onClick: state.handleClose,
4910
+ "aria-label": "Close",
4911
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(IconX, { "aria-hidden": true })
4912
+ }
4913
+ ),
4914
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("header", { className: "flex w-full flex-col gap-1.5 p-6 pr-12", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("h2", { className: "m-0 w-full break-words text-lg font-semibold leading-none tracking-tight text-card-foreground", children: state.title }) }),
4915
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex w-full flex-col gap-3 px-6 pb-8 pt-1", children: [
4916
+ state.showBreadcrumb && state.selectedPage && state.selectedSection ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4917
+ DestinationBreadcrumb,
4918
+ {
4919
+ pageTitle: state.selectedPage.title,
4920
+ sectionLabel: state.selectedSection.label
4921
+ }
4922
+ ) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4923
+ UrlOrPageInput,
4924
+ {
4925
+ value: state.searchValue,
4926
+ selectedPage: state.selectedPage,
4927
+ dropdownOpen: state.dropdownOpen,
4928
+ onDropdownOpenChange: state.setDropdownOpen,
4929
+ filteredPages: state.filteredPages,
4930
+ onInputChange: state.handleInputChange,
4931
+ onPageSelect: state.handlePageSelect,
4932
+ urlError: state.urlError
4933
+ }
4934
+ ),
4935
+ state.showChooseSection ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex flex-col justify-center gap-2", children: [
4936
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { type: "button", variant: "outline", className: "w-fit cursor-pointer", size: "sm", onClick: state.handleChooseSection, children: "Choose a section" }),
4937
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex items-center gap-1 text-sm text-muted-foreground", children: [
4938
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(IconInfo, { className: "shrink-0", "aria-hidden": true }),
4939
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { children: "Pick a section this link should scroll to." })
4940
+ ] })
4941
+ ] }) : null,
4942
+ state.showSectionPicker ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(SectionPickerList, { sections: state.activeSections, onSelect: state.handleSectionSelect }) : null,
4943
+ state.showSectionRow && state.selectedSection ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(SectionTreeItem, { section: state.selectedSection, selected: true }) : null
4944
+ ] }),
4945
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("footer", { className: "flex w-full items-center justify-end gap-2 px-6 pb-6", children: [
4946
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4947
+ Button,
4948
+ {
4949
+ type: "button",
4950
+ variant: isCancel ? "outline" : "ghost",
4951
+ className: "leading-6 shadow-none cursor-pointer",
4952
+ style: isCancel ? {
4953
+ backgroundColor: "var(--ohw-background)",
4954
+ borderColor: "var(--ohw-border)",
4955
+ color: "var(--ohw-foreground)"
4956
+ } : void 0,
4957
+ onClick: state.handleSecondary,
4958
+ children: state.secondaryLabel
4959
+ }
4960
+ ),
4961
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4962
+ Button,
4963
+ {
4964
+ type: "button",
4965
+ className: "border-0 leading-6 shadow-none hover:opacity-90 disabled:opacity-50 cursor-pointer",
4966
+ style: {
4967
+ backgroundColor: "var(--ohw-primary)",
4968
+ color: "var(--ohw-primary-foreground)"
4969
+ },
4970
+ disabled: !state.isValid,
4971
+ onClick: state.handleSubmit,
4972
+ children: state.submitLabel
4973
+ }
4974
+ )
4975
+ ] })
4976
+ ] });
4977
+ }
4978
+
4979
+ // src/ui/link-modal/LinkPopover.tsx
4980
+ var import_jsx_runtime14 = require("react/jsx-runtime");
4981
+ function LinkPopover({
4982
+ rect,
4983
+ parentScroll,
4984
+ panelRef,
4985
+ ...editorProps
4986
+ }) {
4987
+ const { top, left, transform } = calcPopoverPos(rect, parentScroll);
4988
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
4989
+ "div",
4990
+ {
4991
+ ref: panelRef,
4992
+ "data-ohw-link-popover-root": "",
4993
+ "data-ohw-link-modal-root": "",
4994
+ "data-ohw-bridge": "",
4995
+ className: cn(
4996
+ "pointer-events-auto fixed z-[2147483647] flex w-[483px] max-w-[calc(100vw-16px)] flex-col overflow-hidden",
4997
+ "rounded-xl border border-border bg-background font-sans shadow-lg outline-none"
4998
+ ),
4999
+ style: { top, left, transform },
5000
+ onMouseDown: (e) => e.stopPropagation(),
5001
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(LinkEditorPanel, { ...editorProps, open: true })
5002
+ }
5003
+ );
5004
+ }
5005
+
5006
+ // src/ui/link-modal/devFixtures.ts
5007
+ var DEV_SITE_PAGES = [
5008
+ { path: "/", title: "Home" },
5009
+ { path: "/about", title: "About" },
5010
+ { path: "/classes", title: "Classes" },
5011
+ { path: "/pricing", title: "Pricing" },
5012
+ { path: "/studio", title: "Studio" },
5013
+ { path: "/instructors", title: "Instructors" },
5014
+ { path: "/contact", title: "Contact" }
5015
+ ];
5016
+ var DEV_SECTIONS_BY_PATH = {
5017
+ "/": [
5018
+ { id: "hero", label: "Hero" },
5019
+ { id: "lagree-intro", label: "Lagree Intro" },
5020
+ { id: "classes-strip", label: "Classes" },
5021
+ { id: "feel-split", label: "Feel Split" },
5022
+ { id: "founder-teaser", label: "Founder" },
5023
+ { id: "plan-form", label: "Plan Form" },
5024
+ { id: "testimonials", label: "Testimonials" },
5025
+ { id: "wordmark", label: "Wordmark" }
5026
+ ],
5027
+ "/about": [
5028
+ { id: "manifesto", label: "Manifesto" },
5029
+ { id: "story-letter", label: "Our Story" },
5030
+ { id: "lagree-explainer", label: "Lagree Explainer" },
5031
+ { id: "waitlist-cta", label: "Waitlist" },
5032
+ { id: "personal-training", label: "Personal training" }
5033
+ ],
5034
+ "/classes": [{ id: "class-library", label: "Class Library" }],
5035
+ "/pricing": [
5036
+ { id: "benefits-marquee", label: "Benefits" },
5037
+ { id: "monthly-memberships", label: "Memberships" },
5038
+ { id: "package-matcher", label: "Packages" },
5039
+ { id: "specialty-programs", label: "Specialty Programs" },
5040
+ { id: "free-class-cta", label: "Free Class" }
5041
+ ],
5042
+ "/studio": [
5043
+ { id: "studio-intro", label: "Studio Intro" },
5044
+ { id: "studio-features", label: "Studio Features" },
5045
+ { id: "studio-gallery", label: "Studio Gallery" },
5046
+ { id: "studio-visit", label: "Visit Studio" }
5047
+ ],
5048
+ "/instructors": [{ id: "instructor-grid", label: "Instructors" }],
5049
+ "/contact": [
5050
+ { id: "hello-hero", label: "Hello" },
5051
+ { id: "contact-form", label: "Contact Form" },
5052
+ { id: "faq", label: "FAQ" }
5053
+ ]
5054
+ };
5055
+ function shouldUseDevFixtures() {
5056
+ if (typeof window === "undefined") return false;
5057
+ const raw = readPreservedSearch();
5058
+ const q = raw.startsWith("?") ? raw.slice(1) : raw;
5059
+ return new URLSearchParams(q).get("ohw-fixtures") === "1";
5060
+ }
5061
+
5062
+ // src/ui/badge.tsx
5063
+ var import_jsx_runtime15 = require("react/jsx-runtime");
4230
5064
  var badgeVariants = cva(
4231
5065
  "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",
4232
5066
  {
@@ -4244,11 +5078,11 @@ var badgeVariants = cva(
4244
5078
  }
4245
5079
  );
4246
5080
  function Badge({ className, variant, ...props }) {
4247
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: cn(badgeVariants({ variant }), className), ...props });
5081
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: cn(badgeVariants({ variant }), className), ...props });
4248
5082
  }
4249
5083
 
4250
5084
  // src/OhhwellsBridge.tsx
4251
- var import_jsx_runtime6 = require("react/jsx-runtime");
5085
+ var import_jsx_runtime16 = require("react/jsx-runtime");
4252
5086
  var PRIMARY = "#0885FE";
4253
5087
  var IMAGE_FADE_MS = 300;
4254
5088
  function runOpacityFade(el, onDone) {
@@ -4388,14 +5222,9 @@ function initSectionsFromContent(content, removeExisting = false) {
4388
5222
  const inEditor = typeof window !== "undefined" && window.self !== window.top;
4389
5223
  if (inEditor) getSectionsTracker().textContent = raw;
4390
5224
  const pageEntries = getPageSchedulingEntries(raw);
4391
- const preExisting = pageEntries.filter((e) => !isSchedulingWidgetMissing(e));
4392
- const notifyForEntry = inEditor ? (e) => !e.scheduleId : false;
4393
- mountSchedulingEntries(pageEntries, notifyForEntry);
4394
- requestAnimationFrame(() => retryMissingSchedulingMounts(pageEntries, notifyForEntry));
4395
- setTimeout(() => retryMissingSchedulingMounts(pageEntries, notifyForEntry), 250);
4396
- for (const entry of preExisting) {
4397
- window.postMessage({ type: "ow:schedule-config", insertAfter: entry.insertAfter, scheduleId: entry.scheduleId ?? null }, "*");
4398
- }
5225
+ mountSchedulingEntries(pageEntries, false);
5226
+ requestAnimationFrame(() => retryMissingSchedulingMounts(pageEntries, false));
5227
+ setTimeout(() => retryMissingSchedulingMounts(pageEntries, false), 250);
4399
5228
  } catch {
4400
5229
  }
4401
5230
  }
@@ -4422,7 +5251,7 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
4422
5251
  const root = (0, import_client.createRoot)(container);
4423
5252
  (0, import_react_dom.flushSync)(() => {
4424
5253
  root.render(
4425
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5254
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4426
5255
  SchedulingWidget,
4427
5256
  {
4428
5257
  notifyOnConnect,
@@ -4454,16 +5283,23 @@ function mountSchedulingEntries(entries, notifyOnConnect = false) {
4454
5283
  const pending = entries.filter((e) => e.type === "scheduling").sort((a, b) => schedulingMountDepth(a.insertAfter) - schedulingMountDepth(b.insertAfter));
4455
5284
  for (let attempt = 0; attempt < pending.length + 1 && pending.length > 0; attempt++) {
4456
5285
  for (let i = pending.length - 1; i >= 0; i--) {
4457
- const entry = pending[i];
4458
- const shouldNotify = typeof notifyOnConnect === "function" ? notifyOnConnect(entry) : notifyOnConnect;
4459
- if (mountSchedulingWidget(entry.insertAfter, shouldNotify, entry.scheduleId)) {
5286
+ const { insertAfter, scheduleId } = pending[i];
5287
+ if (mountSchedulingWidget(insertAfter, notifyOnConnect, scheduleId)) {
4460
5288
  pending.splice(i, 1);
4461
5289
  }
4462
5290
  }
4463
5291
  }
4464
5292
  }
5293
+ function getLinkHref(el) {
5294
+ const anchor = el instanceof HTMLAnchorElement ? el : el.querySelector("a");
5295
+ return anchor?.getAttribute("href") ?? "";
5296
+ }
5297
+ function applyLinkHref(el, val) {
5298
+ const anchor = el instanceof HTMLAnchorElement ? el : el.querySelector("a");
5299
+ if (anchor) anchor.setAttribute("href", val);
5300
+ }
4465
5301
  function collectEditableNodes() {
4466
- return Array.from(document.querySelectorAll("[data-ohw-editable]")).map((el) => {
5302
+ const nodes = Array.from(document.querySelectorAll("[data-ohw-editable]")).map((el) => {
4467
5303
  if (el.dataset.ohwEditable === "image") {
4468
5304
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
4469
5305
  return { key: el.dataset.ohwKey ?? "", type: "image", text: img?.src ?? "" };
@@ -4473,12 +5309,52 @@ function collectEditableNodes() {
4473
5309
  const url = raw.replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "");
4474
5310
  return { key: el.dataset.ohwKey ?? "", type: "bg-image", text: url };
4475
5311
  }
5312
+ if (el.dataset.ohwEditable === "link") {
5313
+ return { key: el.dataset.ohwKey ?? "", type: "link", text: getLinkHref(el) };
5314
+ }
4476
5315
  return {
4477
5316
  key: el.dataset.ohwKey ?? "",
4478
5317
  type: el.dataset.ohwEditable ?? "text",
4479
5318
  text: el.dataset.ohwEditable === "plain" ? el.innerText ?? "" : el.innerHTML
4480
5319
  };
4481
5320
  });
5321
+ document.querySelectorAll("[data-ohw-href-key]").forEach((el) => {
5322
+ const key = el.getAttribute("data-ohw-href-key") ?? "";
5323
+ if (!key) return;
5324
+ nodes.push({ key, type: "link", text: getLinkHref(el) });
5325
+ });
5326
+ return nodes;
5327
+ }
5328
+ function applyLinkByKey(key, val) {
5329
+ document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
5330
+ if (el.dataset.ohwEditable === "link") applyLinkHref(el, val);
5331
+ });
5332
+ document.querySelectorAll(`[data-ohw-href-key="${key}"]`).forEach((el) => {
5333
+ applyLinkHref(el, val);
5334
+ });
5335
+ }
5336
+ function isInsideLinkEditor(target) {
5337
+ return Boolean(
5338
+ target.closest("[data-ohw-link-popover-root]") || target.closest("[data-ohw-link-modal-root]") || target.closest('[data-slot="popover-content"]')
5339
+ );
5340
+ }
5341
+ function getHrefKeyFromElement(el) {
5342
+ if (!el) return null;
5343
+ const anchor = el.closest("[data-ohw-href-key]");
5344
+ if (!anchor) return null;
5345
+ const key = anchor.getAttribute("data-ohw-href-key");
5346
+ if (!key) return null;
5347
+ return { anchor, key };
5348
+ }
5349
+ function titleCaseSectionId(id) {
5350
+ return id.split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
5351
+ }
5352
+ function collectSections() {
5353
+ return Array.from(document.querySelectorAll("[data-ohw-section]")).map((el) => {
5354
+ const id = el.getAttribute("data-ohw-section") ?? "";
5355
+ const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
5356
+ return { id, label };
5357
+ });
4482
5358
  }
4483
5359
  var FORCE_PSEUDO_STATES = [
4484
5360
  { pseudo: ":hover", state: "hover" },
@@ -4619,7 +5495,7 @@ var TOOLBAR_GROUPS = [
4619
5495
  ];
4620
5496
  function GlowFrame({ rect, elRef }) {
4621
5497
  const GAP = 6;
4622
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5498
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4623
5499
  "div",
4624
5500
  {
4625
5501
  ref: elRef,
@@ -4667,10 +5543,12 @@ function FloatingToolbar({
4667
5543
  parentScroll,
4668
5544
  elRef,
4669
5545
  onCommand,
4670
- activeCommands
5546
+ activeCommands,
5547
+ showEditLink,
5548
+ onEditLink
4671
5549
  }) {
4672
5550
  const { top, left, transform } = calcToolbarPos(rect, parentScroll);
4673
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5551
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4674
5552
  "div",
4675
5553
  {
4676
5554
  ref: elRef,
@@ -4693,56 +5571,97 @@ function FloatingToolbar({
4693
5571
  pointerEvents: "auto",
4694
5572
  whiteSpace: "nowrap"
4695
5573
  },
4696
- children: TOOLBAR_GROUPS.map((btns, gi) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react3.default.Fragment, { children: [
4697
- gi > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { display: "block", width: 1, height: 24, background: "#E7E5E4", flexShrink: 0 } }),
4698
- btns.map((btn) => {
4699
- const isActive = activeCommands.has(btn.cmd);
4700
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4701
- "button",
4702
- {
4703
- title: btn.title,
4704
- onMouseDown: (e) => {
4705
- e.preventDefault();
4706
- onCommand(btn.cmd);
4707
- },
4708
- onMouseEnter: (e) => {
4709
- if (!isActive) e.currentTarget.style.background = "#F5F5F4";
4710
- },
4711
- onMouseLeave: (e) => {
4712
- if (!isActive) e.currentTarget.style.background = "transparent";
4713
- },
4714
- style: {
4715
- display: "flex",
4716
- alignItems: "center",
4717
- justifyContent: "center",
4718
- border: "none",
4719
- background: isActive ? PRIMARY : "transparent",
4720
- borderRadius: 4,
4721
- cursor: "pointer",
4722
- color: isActive ? "#FFFFFF" : "#1C1917",
4723
- flexShrink: 0,
4724
- padding: 6
5574
+ onMouseDown: (e) => e.stopPropagation(),
5575
+ children: [
5576
+ TOOLBAR_GROUPS.map((btns, gi) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react5.default.Fragment, { children: [
5577
+ gi > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: { display: "block", width: 1, height: 24, background: "#E7E5E4", flexShrink: 0 } }),
5578
+ btns.map((btn) => {
5579
+ const isActive = activeCommands.has(btn.cmd);
5580
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5581
+ "button",
5582
+ {
5583
+ title: btn.title,
5584
+ onMouseDown: (e) => {
5585
+ e.preventDefault();
5586
+ onCommand(btn.cmd);
5587
+ },
5588
+ onMouseEnter: (e) => {
5589
+ if (!isActive) e.currentTarget.style.background = "#F5F5F4";
5590
+ },
5591
+ onMouseLeave: (e) => {
5592
+ if (!isActive) e.currentTarget.style.background = "transparent";
5593
+ },
5594
+ style: {
5595
+ display: "flex",
5596
+ alignItems: "center",
5597
+ justifyContent: "center",
5598
+ border: "none",
5599
+ background: isActive ? PRIMARY : "transparent",
5600
+ borderRadius: 4,
5601
+ cursor: "pointer",
5602
+ color: isActive ? "#FFFFFF" : "#1C1917",
5603
+ flexShrink: 0,
5604
+ padding: 6
5605
+ },
5606
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5607
+ "svg",
5608
+ {
5609
+ width: "16",
5610
+ height: "16",
5611
+ viewBox: "0 0 24 24",
5612
+ fill: "none",
5613
+ stroke: isActive ? "#FFFFFF" : "#1C1917",
5614
+ strokeWidth: "2.5",
5615
+ strokeLinecap: "round",
5616
+ strokeLinejoin: "round",
5617
+ style: isActive && gi > 0 ? { filter: "drop-shadow(0 0 0.5px #fff)" } : void 0,
5618
+ dangerouslySetInnerHTML: { __html: ICONS[btn.cmd] }
5619
+ }
5620
+ )
4725
5621
  },
4726
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4727
- "svg",
4728
- {
4729
- width: "16",
4730
- height: "16",
4731
- viewBox: "0 0 24 24",
4732
- fill: "none",
4733
- stroke: isActive ? "#FFFFFF" : "#1C1917",
4734
- strokeWidth: "2.5",
4735
- strokeLinecap: "round",
4736
- strokeLinejoin: "round",
4737
- style: isActive && gi > 0 ? { filter: "drop-shadow(0 0 0.5px #fff)" } : void 0,
4738
- dangerouslySetInnerHTML: { __html: ICONS[btn.cmd] }
4739
- }
4740
- )
5622
+ btn.cmd
5623
+ );
5624
+ })
5625
+ ] }, gi)),
5626
+ showEditLink ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5627
+ "button",
5628
+ {
5629
+ type: "button",
5630
+ title: "Edit link",
5631
+ onMouseDown: (e) => {
5632
+ e.preventDefault();
5633
+ e.stopPropagation();
5634
+ onEditLink?.();
5635
+ },
5636
+ onClick: (e) => {
5637
+ e.preventDefault();
5638
+ e.stopPropagation();
4741
5639
  },
4742
- btn.cmd
4743
- );
4744
- })
4745
- ] }, gi))
5640
+ onMouseEnter: (e) => {
5641
+ e.currentTarget.style.background = "#F5F5F4";
5642
+ },
5643
+ onMouseLeave: (e) => {
5644
+ e.currentTarget.style.background = "transparent";
5645
+ },
5646
+ style: {
5647
+ display: "flex",
5648
+ alignItems: "center",
5649
+ justifyContent: "center",
5650
+ border: "none",
5651
+ background: "transparent",
5652
+ borderRadius: 4,
5653
+ cursor: "pointer",
5654
+ color: "#1C1917",
5655
+ flexShrink: 0,
5656
+ padding: 6
5657
+ },
5658
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("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: [
5659
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }),
5660
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })
5661
+ ] })
5662
+ }
5663
+ ) : null
5664
+ ]
4746
5665
  }
4747
5666
  );
4748
5667
  }
@@ -4756,7 +5675,7 @@ function StateToggle({
4756
5675
  states,
4757
5676
  onStateChange
4758
5677
  }) {
4759
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5678
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4760
5679
  ToggleGroup,
4761
5680
  {
4762
5681
  "data-ohw-state-toggle": "",
@@ -4770,7 +5689,7 @@ function StateToggle({
4770
5689
  left: rect.right - 8,
4771
5690
  transform: "translateX(-100%)"
4772
5691
  },
4773
- children: states.map((state) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ToggleGroupItem, { value: state, size: "sm", children: state }, state))
5692
+ children: states.map((state) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ToggleGroupItem, { value: state, size: "sm", children: state }, state))
4774
5693
  }
4775
5694
  );
4776
5695
  }
@@ -4797,8 +5716,8 @@ function OhhwellsBridge() {
4797
5716
  const router = (0, import_navigation.useRouter)();
4798
5717
  const searchParams = (0, import_navigation.useSearchParams)();
4799
5718
  const isEditMode = isEditSessionActive();
4800
- const [bridgeRoot, setBridgeRoot] = (0, import_react3.useState)(null);
4801
- (0, import_react3.useEffect)(() => {
5719
+ const [bridgeRoot, setBridgeRoot] = (0, import_react5.useState)(null);
5720
+ (0, import_react5.useEffect)(() => {
4802
5721
  const figtreeFontId = "ohw-figtree-font";
4803
5722
  if (!document.getElementById(figtreeFontId)) {
4804
5723
  const preconnect1 = Object.assign(document.createElement("link"), { rel: "preconnect", href: "https://fonts.googleapis.com" });
@@ -4822,41 +5741,83 @@ function OhhwellsBridge() {
4822
5741
  }, []);
4823
5742
  const subdomainFromQuery = searchParams.get("subdomain");
4824
5743
  const subdomain = resolveSubdomain(subdomainFromQuery);
4825
- const postToParent = (0, import_react3.useCallback)((data) => {
5744
+ const postToParent = (0, import_react5.useCallback)((data) => {
4826
5745
  if (typeof window !== "undefined" && window.parent !== window) {
4827
5746
  window.parent.postMessage(data, "*");
4828
5747
  }
4829
5748
  }, []);
4830
- const [fetchState, setFetchState] = (0, import_react3.useState)("idle");
4831
- const autoSaveTimers = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
4832
- const activeElRef = (0, import_react3.useRef)(null);
4833
- const originalContentRef = (0, import_react3.useRef)(null);
4834
- const activeStateElRef = (0, import_react3.useRef)(null);
4835
- const parentScrollRef = (0, import_react3.useRef)(null);
4836
- const toolbarElRef = (0, import_react3.useRef)(null);
4837
- const glowElRef = (0, import_react3.useRef)(null);
4838
- const hoveredImageRef = (0, import_react3.useRef)(null);
4839
- const hoveredImageHasTextOverlapRef = (0, import_react3.useRef)(false);
4840
- const hoveredGapRef = (0, import_react3.useRef)(null);
4841
- const imageUnhoverTimerRef = (0, import_react3.useRef)(null);
4842
- const imageShowTimerRef = (0, import_react3.useRef)(null);
4843
- const editStylesRef = (0, import_react3.useRef)(null);
4844
- const activateRef = (0, import_react3.useRef)(() => {
5749
+ const [fetchState, setFetchState] = (0, import_react5.useState)("idle");
5750
+ const autoSaveTimers = (0, import_react5.useRef)(/* @__PURE__ */ new Map());
5751
+ const activeElRef = (0, import_react5.useRef)(null);
5752
+ const originalContentRef = (0, import_react5.useRef)(null);
5753
+ const activeStateElRef = (0, import_react5.useRef)(null);
5754
+ const parentScrollRef = (0, import_react5.useRef)(null);
5755
+ const toolbarElRef = (0, import_react5.useRef)(null);
5756
+ const glowElRef = (0, import_react5.useRef)(null);
5757
+ const hoveredImageRef = (0, import_react5.useRef)(null);
5758
+ const hoveredImageHasTextOverlapRef = (0, import_react5.useRef)(false);
5759
+ const hoveredGapRef = (0, import_react5.useRef)(null);
5760
+ const imageUnhoverTimerRef = (0, import_react5.useRef)(null);
5761
+ const imageShowTimerRef = (0, import_react5.useRef)(null);
5762
+ const editStylesRef = (0, import_react5.useRef)(null);
5763
+ const activateRef = (0, import_react5.useRef)(() => {
4845
5764
  });
4846
- const deactivateRef = (0, import_react3.useRef)(() => {
5765
+ const deactivateRef = (0, import_react5.useRef)(() => {
4847
5766
  });
4848
- const refreshActiveCommandsRef = (0, import_react3.useRef)(() => {
5767
+ const refreshActiveCommandsRef = (0, import_react5.useRef)(() => {
4849
5768
  });
4850
- const postToParentRef = (0, import_react3.useRef)(postToParent);
5769
+ const postToParentRef = (0, import_react5.useRef)(postToParent);
4851
5770
  postToParentRef.current = postToParent;
4852
- const sectionsLoadedRef = (0, import_react3.useRef)(false);
4853
- const pendingScheduleConfigRequests = (0, import_react3.useRef)([]);
4854
- const [toolbarRect, setToolbarRect] = (0, import_react3.useState)(null);
4855
- const [toggleState, setToggleState] = (0, import_react3.useState)(null);
4856
- const [maxBadge, setMaxBadge] = (0, import_react3.useState)(null);
4857
- const [activeCommands, setActiveCommands] = (0, import_react3.useState)(/* @__PURE__ */ new Set());
4858
- const [sectionGap, setSectionGap] = (0, import_react3.useState)(null);
4859
- (0, import_react3.useEffect)(() => {
5771
+ const [toolbarRect, setToolbarRect] = (0, import_react5.useState)(null);
5772
+ const [toggleState, setToggleState] = (0, import_react5.useState)(null);
5773
+ const [maxBadge, setMaxBadge] = (0, import_react5.useState)(null);
5774
+ const [activeCommands, setActiveCommands] = (0, import_react5.useState)(/* @__PURE__ */ new Set());
5775
+ const [toolbarShowEditLink, setToolbarShowEditLink] = (0, import_react5.useState)(false);
5776
+ const [linkPopover, setLinkPopover] = (0, import_react5.useState)(null);
5777
+ const [sitePages, setSitePages] = (0, import_react5.useState)([]);
5778
+ const [sectionsByPath, setSectionsByPath] = (0, import_react5.useState)({});
5779
+ const setLinkPopoverRef = (0, import_react5.useRef)(setLinkPopover);
5780
+ const linkPopoverPanelRef = (0, import_react5.useRef)(null);
5781
+ const linkPopoverOpenRef = (0, import_react5.useRef)(false);
5782
+ const linkPopoverGraceUntilRef = (0, import_react5.useRef)(0);
5783
+ setLinkPopoverRef.current = setLinkPopover;
5784
+ const bumpLinkPopoverGrace = () => {
5785
+ linkPopoverGraceUntilRef.current = Date.now() + 350;
5786
+ };
5787
+ (0, import_react5.useEffect)(() => {
5788
+ if (!linkPopover) return;
5789
+ if (hoveredImageRef.current) {
5790
+ hoveredImageRef.current = null;
5791
+ hoveredImageHasTextOverlapRef.current = false;
5792
+ }
5793
+ postToParent({ type: "ow:image-unhover" });
5794
+ }, [linkPopover, postToParent]);
5795
+ (0, import_react5.useEffect)(() => {
5796
+ if (!isEditMode) return;
5797
+ const useFixtures = shouldUseDevFixtures();
5798
+ if (useFixtures) {
5799
+ setSitePages(DEV_SITE_PAGES);
5800
+ setSectionsByPath(DEV_SECTIONS_BY_PATH);
5801
+ if (process.env.NODE_ENV === "development") {
5802
+ console.info("[ohhwells-bridge] Dev fixtures loaded (ohw-fixtures=1)");
5803
+ }
5804
+ }
5805
+ const onSitePages = (e) => {
5806
+ if (e.data?.type !== "ow:site-pages" || !Array.isArray(e.data.pages)) return;
5807
+ if (useFixtures) return;
5808
+ setSitePages(
5809
+ e.data.pages.map((p) => ({
5810
+ path: p.path,
5811
+ title: p.title
5812
+ }))
5813
+ );
5814
+ };
5815
+ window.addEventListener("message", onSitePages);
5816
+ if (!useFixtures) postToParent({ type: "ow:request-site-pages" });
5817
+ return () => window.removeEventListener("message", onSitePages);
5818
+ }, [isEditMode, postToParent]);
5819
+ const [sectionGap, setSectionGap] = (0, import_react5.useState)(null);
5820
+ (0, import_react5.useEffect)(() => {
4860
5821
  const update = () => {
4861
5822
  const el = activeElRef.current;
4862
5823
  if (el) setToolbarRect(el.getBoundingClientRect());
@@ -4880,33 +5841,10 @@ function OhhwellsBridge() {
4880
5841
  vvp.removeEventListener("resize", update);
4881
5842
  };
4882
5843
  }, []);
4883
- const refreshStateRules = (0, import_react3.useCallback)(() => {
5844
+ const refreshStateRules = (0, import_react5.useCallback)(() => {
4884
5845
  editStylesRef.current?.forceHover && (editStylesRef.current.forceHover.textContent = collectStateRules());
4885
5846
  }, []);
4886
- const processConfigRequest = (0, import_react3.useCallback)((insertAfterVal) => {
4887
- const tracker = getSectionsTracker();
4888
- let entries = [];
4889
- try {
4890
- entries = JSON.parse(tracker.textContent || "[]");
4891
- } catch {
4892
- }
4893
- const path = window.location.pathname;
4894
- const entry = entries.find(
4895
- (e) => e.type === "scheduling" && e.insertAfter === insertAfterVal && (!e.pagePath || e.pagePath === path)
4896
- );
4897
- if (entry) {
4898
- window.postMessage({ type: "ow:schedule-config", insertAfter: insertAfterVal, scheduleId: entry.scheduleId ?? null }, "*");
4899
- return;
4900
- }
4901
- const newEntry = { type: "scheduling", insertAfter: insertAfterVal, pagePath: path };
4902
- entries.push(newEntry);
4903
- tracker.textContent = JSON.stringify(entries);
4904
- if (isEditMode) {
4905
- postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent }] });
4906
- }
4907
- window.postMessage({ type: "ow:schedule-config", insertAfter: insertAfterVal, scheduleId: null }, "*");
4908
- }, [isEditMode]);
4909
- const deactivate = (0, import_react3.useCallback)(() => {
5847
+ const deactivate = (0, import_react5.useCallback)(() => {
4910
5848
  const el = activeElRef.current;
4911
5849
  if (!el) return;
4912
5850
  const key = el.dataset.ohwKey;
@@ -4929,9 +5867,10 @@ function OhhwellsBridge() {
4929
5867
  setToolbarRect(null);
4930
5868
  setMaxBadge(null);
4931
5869
  setActiveCommands(/* @__PURE__ */ new Set());
5870
+ setToolbarShowEditLink(false);
4932
5871
  postToParent({ type: "ow:exit-edit" });
4933
5872
  }, [postToParent]);
4934
- const activate = (0, import_react3.useCallback)((el) => {
5873
+ const activate = (0, import_react5.useCallback)((el) => {
4935
5874
  if (activeElRef.current === el) return;
4936
5875
  deactivate();
4937
5876
  if (hoveredImageRef.current) {
@@ -4944,12 +5883,13 @@ function OhhwellsBridge() {
4944
5883
  originalContentRef.current = el.innerHTML;
4945
5884
  el.focus();
4946
5885
  setToolbarRect(el.getBoundingClientRect());
5886
+ setToolbarShowEditLink(Boolean(getHrefKeyFromElement(el)));
4947
5887
  postToParent({ type: "ow:enter-edit", key: el.dataset.ohwKey });
4948
5888
  requestAnimationFrame(() => refreshActiveCommandsRef.current());
4949
5889
  }, [deactivate, postToParent]);
4950
5890
  activateRef.current = activate;
4951
5891
  deactivateRef.current = deactivate;
4952
- (0, import_react3.useLayoutEffect)(() => {
5892
+ (0, import_react5.useLayoutEffect)(() => {
4953
5893
  if (!subdomain || isEditMode) {
4954
5894
  setFetchState("done");
4955
5895
  return;
@@ -4971,14 +5911,15 @@ function OhhwellsBridge() {
4971
5911
  } else if (el.dataset.ohwEditable === "bg-image") {
4972
5912
  const next = `url('${val}')`;
4973
5913
  if (el.style.backgroundImage !== next) el.style.backgroundImage = next;
5914
+ } else if (el.dataset.ohwEditable === "link") {
5915
+ applyLinkHref(el, val);
4974
5916
  } else if (el.innerHTML !== val) {
4975
5917
  el.innerHTML = val;
4976
5918
  }
4977
5919
  });
5920
+ applyLinkByKey(key, val);
4978
5921
  }
4979
5922
  initSectionsFromContent(content, true);
4980
- sectionsLoadedRef.current = true;
4981
- pendingScheduleConfigRequests.current.splice(0).forEach(processConfigRequest);
4982
5923
  return imageLoads.length > 0 ? Promise.all(imageLoads).then(() => {
4983
5924
  }) : Promise.resolve();
4984
5925
  };
@@ -5003,7 +5944,7 @@ function OhhwellsBridge() {
5003
5944
  cancelled = true;
5004
5945
  };
5005
5946
  }, [subdomain, isEditMode, pathname]);
5006
- (0, import_react3.useEffect)(() => {
5947
+ (0, import_react5.useEffect)(() => {
5007
5948
  if (!subdomain || isEditMode) return;
5008
5949
  let debounceTimer = null;
5009
5950
  const applyFromCache = () => {
@@ -5019,10 +5960,13 @@ function OhhwellsBridge() {
5019
5960
  } else if (el.dataset.ohwEditable === "bg-image") {
5020
5961
  const next = `url('${val}')`;
5021
5962
  if (el.style.backgroundImage !== next) el.style.backgroundImage = next;
5963
+ } else if (el.dataset.ohwEditable === "link") {
5964
+ applyLinkHref(el, val);
5022
5965
  } else if (el.innerHTML !== val) {
5023
5966
  el.innerHTML = val;
5024
5967
  }
5025
5968
  });
5969
+ applyLinkByKey(key, val);
5026
5970
  }
5027
5971
  };
5028
5972
  const scheduleApply = () => {
@@ -5037,16 +5981,16 @@ function OhhwellsBridge() {
5037
5981
  if (debounceTimer) clearTimeout(debounceTimer);
5038
5982
  };
5039
5983
  }, [subdomain, isEditMode, pathname]);
5040
- (0, import_react3.useLayoutEffect)(() => {
5984
+ (0, import_react5.useLayoutEffect)(() => {
5041
5985
  const el = document.getElementById("ohw-loader");
5042
5986
  if (!el) return;
5043
5987
  const visible = Boolean(subdomain) && fetchState !== "done";
5044
5988
  el.style.display = visible ? "flex" : "none";
5045
5989
  }, [subdomain, fetchState]);
5046
- (0, import_react3.useEffect)(() => {
5990
+ (0, import_react5.useEffect)(() => {
5047
5991
  postToParent({ type: "ow:navigation", path: pathname });
5048
5992
  }, [pathname, postToParent]);
5049
- (0, import_react3.useEffect)(() => {
5993
+ (0, import_react5.useEffect)(() => {
5050
5994
  if (!isEditMode) return;
5051
5995
  const measure = () => {
5052
5996
  const h = document.body.scrollHeight;
@@ -5070,7 +6014,7 @@ function OhhwellsBridge() {
5070
6014
  window.removeEventListener("resize", handleResize);
5071
6015
  };
5072
6016
  }, [pathname, isEditMode, postToParent]);
5073
- (0, import_react3.useEffect)(() => {
6017
+ (0, import_react5.useEffect)(() => {
5074
6018
  if (!subdomainFromQuery || isEditMode) return;
5075
6019
  const handleClick = (e) => {
5076
6020
  const anchor = e.target.closest("a");
@@ -5086,7 +6030,7 @@ function OhhwellsBridge() {
5086
6030
  document.addEventListener("click", handleClick, true);
5087
6031
  return () => document.removeEventListener("click", handleClick, true);
5088
6032
  }, [subdomainFromQuery, isEditMode, router]);
5089
- (0, import_react3.useEffect)(() => {
6033
+ (0, import_react5.useEffect)(() => {
5090
6034
  if (!isEditMode) {
5091
6035
  editStylesRef.current?.base.remove();
5092
6036
  editStylesRef.current?.forceHover.remove();
@@ -5112,6 +6056,7 @@ function OhhwellsBridge() {
5112
6056
  [data-ohw-editable]:not([contenteditable]):not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"]) { cursor: text !important; }
5113
6057
  [data-ohw-editable="image"], [data-ohw-editable="image"] *,
5114
6058
  [data-ohw-editable="bg-image"], [data-ohw-editable="bg-image"] * { cursor: pointer !important; }
6059
+ [data-ohw-editable="link"], [data-ohw-editable="link"] * { cursor: pointer !important; }
5115
6060
  [data-ohw-hovered]:not([contenteditable]) {
5116
6061
  outline: 2px dashed ${PRIMARY} !important;
5117
6062
  outline-offset: 4px;
@@ -5149,8 +6094,21 @@ function OhhwellsBridge() {
5149
6094
  if (target.closest("[data-ohw-toolbar]")) return;
5150
6095
  if (target.closest("[data-ohw-state-toggle]")) return;
5151
6096
  if (target.closest("[data-ohw-max-badge]")) return;
6097
+ if (isInsideLinkEditor(target)) return;
5152
6098
  const editable = target.closest("[data-ohw-editable]");
5153
6099
  if (editable) {
6100
+ if (editable.dataset.ohwEditable === "link") {
6101
+ e.preventDefault();
6102
+ e.stopPropagation();
6103
+ deactivateRef.current();
6104
+ bumpLinkPopoverGrace();
6105
+ setLinkPopoverRef.current({
6106
+ key: editable.dataset.ohwKey ?? "",
6107
+ target: getLinkHref(editable),
6108
+ rect: editable.getBoundingClientRect()
6109
+ });
6110
+ return;
6111
+ }
5154
6112
  if (editable.dataset.ohwEditable === "image" || editable.dataset.ohwEditable === "bg-image") {
5155
6113
  e.preventDefault();
5156
6114
  e.stopPropagation();
@@ -5171,6 +6129,15 @@ function OhhwellsBridge() {
5171
6129
  }
5172
6130
  const anchor = target.closest("a");
5173
6131
  if (anchor) e.preventDefault();
6132
+ if (Date.now() < linkPopoverGraceUntilRef.current) return;
6133
+ if (linkPopoverOpenRef.current && activeElRef.current) {
6134
+ const hrefCtx = getHrefKeyFromElement(activeElRef.current);
6135
+ if (hrefCtx && (hrefCtx.anchor === target || hrefCtx.anchor.contains(target))) return;
6136
+ }
6137
+ if (linkPopoverOpenRef.current) {
6138
+ setLinkPopoverRef.current(null);
6139
+ return;
6140
+ }
5174
6141
  deactivateRef.current();
5175
6142
  };
5176
6143
  const handleMouseOver = (e) => {
@@ -5277,6 +6244,15 @@ function OhhwellsBridge() {
5277
6244
  return smallest;
5278
6245
  };
5279
6246
  const probeImageAt = (clientX, clientY, isDragOver = false, fromParentViewport = false) => {
6247
+ if (linkPopoverOpenRef.current) {
6248
+ if (hoveredImageRef.current) {
6249
+ hoveredImageRef.current = null;
6250
+ hoveredImageHasTextOverlapRef.current = false;
6251
+ resumeAnimTracks();
6252
+ postToParentRef.current({ type: "ow:image-unhover" });
6253
+ }
6254
+ return;
6255
+ }
5280
6256
  const toggleEl = document.querySelector("[data-ohw-state-toggle]");
5281
6257
  if (toggleEl) {
5282
6258
  const { x, y } = toProbeCoords(clientX, clientY, fromParentViewport);
@@ -5302,6 +6278,15 @@ function OhhwellsBridge() {
5302
6278
  }
5303
6279
  return;
5304
6280
  }
6281
+ if (topEl?.closest("[data-ohw-link-popover-root]") || topEl?.closest("[data-ohw-link-modal-root]")) {
6282
+ if (hoveredImageRef.current) {
6283
+ hoveredImageRef.current = null;
6284
+ hoveredImageHasTextOverlapRef.current = false;
6285
+ resumeAnimTracks();
6286
+ postToParentRef.current({ type: "ow:image-unhover" });
6287
+ }
6288
+ return;
6289
+ }
5305
6290
  const isStateCardImage = !!imgEl.closest("[data-ohw-editable-state]");
5306
6291
  const textEditable = Array.from(
5307
6292
  document.querySelectorAll('[data-ohw-editable]:not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"])')
@@ -5621,25 +6606,35 @@ function OhhwellsBridge() {
5621
6606
  if (img) img.src = val;
5622
6607
  } else if (el.dataset.ohwEditable === "bg-image") {
5623
6608
  el.style.backgroundImage = `url('${val}')`;
6609
+ } else if (el.dataset.ohwEditable === "link") {
6610
+ applyLinkHref(el, val);
5624
6611
  } else {
5625
6612
  el.innerHTML = val;
5626
6613
  }
5627
6614
  });
6615
+ applyLinkByKey(key, val);
5628
6616
  }
5629
6617
  if (sectionsJson) {
5630
6618
  initSectionsFromContent({ __ohw_sections: sectionsJson }, true);
5631
- sectionsLoadedRef.current = true;
5632
- pendingScheduleConfigRequests.current.splice(0).forEach(processConfigRequest);
5633
6619
  }
5634
6620
  postToParentRef.current({ type: "ow:hydrate-done" });
5635
6621
  };
5636
6622
  window.addEventListener("message", handleHydrate);
5637
6623
  const handleDeactivate = (e) => {
5638
6624
  if (e.data?.type !== "ow:deactivate") return;
6625
+ if (Date.now() < linkPopoverGraceUntilRef.current) return;
6626
+ if (linkPopoverOpenRef.current) {
6627
+ setLinkPopoverRef.current(null);
6628
+ return;
6629
+ }
5639
6630
  deactivateRef.current();
5640
6631
  };
5641
6632
  window.addEventListener("message", handleDeactivate);
5642
6633
  const handleKeyDown = (e) => {
6634
+ if (e.key === "Escape" && linkPopoverOpenRef.current) {
6635
+ setLinkPopoverRef.current(null);
6636
+ return;
6637
+ }
5643
6638
  if (e.key !== "Escape") return;
5644
6639
  const el = activeElRef.current;
5645
6640
  if (el && originalContentRef.current !== null) {
@@ -5893,23 +6888,7 @@ function OhhwellsBridge() {
5893
6888
  if (imageShowTimerRef.current) clearTimeout(imageShowTimerRef.current);
5894
6889
  };
5895
6890
  }, [isEditMode, refreshStateRules]);
5896
- (0, import_react3.useEffect)(() => {
5897
- const handler = (e) => {
5898
- if (e.data?.type !== "ow:request-schedule-config") return;
5899
- const insertAfterVal = e.data.insertAfter;
5900
- if (!insertAfterVal) return;
5901
- if (!sectionsLoadedRef.current) {
5902
- if (!pendingScheduleConfigRequests.current.includes(insertAfterVal)) {
5903
- pendingScheduleConfigRequests.current.push(insertAfterVal);
5904
- }
5905
- return;
5906
- }
5907
- processConfigRequest(insertAfterVal);
5908
- };
5909
- window.addEventListener("message", handler);
5910
- return () => window.removeEventListener("message", handler);
5911
- }, [processConfigRequest]);
5912
- (0, import_react3.useEffect)(() => {
6891
+ (0, import_react5.useEffect)(() => {
5913
6892
  if (!isEditMode) return;
5914
6893
  document.querySelectorAll("[data-ohw-active-state]").forEach((el) => {
5915
6894
  el.removeAttribute("data-ohw-active-state");
@@ -5924,20 +6903,26 @@ function OhhwellsBridge() {
5924
6903
  const raf = requestAnimationFrame(() => refreshStateRules());
5925
6904
  const timer = setTimeout(() => {
5926
6905
  refreshStateRules();
5927
- postToParent({ type: "ow:ready", version: "1", nodes: collectEditableNodes() });
6906
+ const sections = collectSections();
6907
+ setSectionsByPath((prev) => {
6908
+ const next = { ...prev, [pathname]: sections };
6909
+ return shouldUseDevFixtures() ? { ...DEV_SECTIONS_BY_PATH, ...next } : next;
6910
+ });
6911
+ postToParent({ type: "ow:ready", version: "1", path: pathname, nodes: collectEditableNodes(), sections });
6912
+ postToParent({ type: "ow:request-site-pages" });
5928
6913
  }, 150);
5929
6914
  return () => {
5930
6915
  cancelAnimationFrame(raf);
5931
6916
  clearTimeout(timer);
5932
6917
  };
5933
6918
  }, [pathname, isEditMode, refreshStateRules, postToParent]);
5934
- const handleCommand = (0, import_react3.useCallback)((cmd) => {
6919
+ const handleCommand = (0, import_react5.useCallback)((cmd) => {
5935
6920
  document.execCommand(cmd, false);
5936
6921
  activeElRef.current?.focus();
5937
6922
  if (activeElRef.current) setToolbarRect(activeElRef.current.getBoundingClientRect());
5938
6923
  refreshActiveCommandsRef.current();
5939
6924
  }, []);
5940
- const handleStateChange = (0, import_react3.useCallback)((state) => {
6925
+ const handleStateChange = (0, import_react5.useCallback)((state) => {
5941
6926
  if (!activeStateElRef.current) return;
5942
6927
  const el = activeStateElRef.current;
5943
6928
  if (state === "Default") {
@@ -5950,13 +6935,48 @@ function OhhwellsBridge() {
5950
6935
  }
5951
6936
  setToggleState((prev) => prev ? { ...prev, activeState: state } : null);
5952
6937
  }, [deactivate]);
6938
+ const closeLinkPopover = (0, import_react5.useCallback)(() => setLinkPopover(null), []);
6939
+ const openLinkPopoverForActive = (0, import_react5.useCallback)(() => {
6940
+ const hrefCtx = getHrefKeyFromElement(activeElRef.current);
6941
+ if (!hrefCtx) return;
6942
+ bumpLinkPopoverGrace();
6943
+ setLinkPopover({
6944
+ key: hrefCtx.key,
6945
+ target: getLinkHref(hrefCtx.anchor),
6946
+ rect: hrefCtx.anchor.getBoundingClientRect()
6947
+ });
6948
+ }, []);
6949
+ const handleLinkPopoverSubmit = (0, import_react5.useCallback)(
6950
+ (target) => {
6951
+ if (!linkPopover) return;
6952
+ const { key } = linkPopover;
6953
+ applyLinkByKey(key, target);
6954
+ postToParent({ type: "ow:change", nodes: [{ key, text: target }] });
6955
+ setLinkPopover(null);
6956
+ },
6957
+ [linkPopover, postToParent]
6958
+ );
6959
+ const showEditLink = toolbarShowEditLink;
6960
+ const currentSections = sectionsByPath[pathname] ?? [];
6961
+ linkPopoverOpenRef.current = linkPopover !== null;
5953
6962
  return bridgeRoot ? (0, import_react_dom2.createPortal)(
5954
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
5955
- toolbarRect && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
5956
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(GlowFrame, { rect: toolbarRect, elRef: glowElRef }),
5957
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FloatingToolbar, { rect: toolbarRect, parentScroll: parentScrollRef.current, elRef: toolbarElRef, onCommand: handleCommand, activeCommands })
6963
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
6964
+ toolbarRect && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
6965
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(GlowFrame, { rect: toolbarRect, elRef: glowElRef }),
6966
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
6967
+ FloatingToolbar,
6968
+ {
6969
+ rect: toolbarRect,
6970
+ parentScroll: parentScrollRef.current,
6971
+ elRef: toolbarElRef,
6972
+ onCommand: handleCommand,
6973
+ activeCommands,
6974
+ showEditLink,
6975
+ onEditLink: openLinkPopoverForActive
6976
+ }
6977
+ )
5958
6978
  ] }),
5959
- maxBadge && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
6979
+ maxBadge && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
5960
6980
  "div",
5961
6981
  {
5962
6982
  "data-ohw-max-badge": "",
@@ -5982,7 +7002,7 @@ function OhhwellsBridge() {
5982
7002
  ]
5983
7003
  }
5984
7004
  ),
5985
- toggleState && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
7005
+ toggleState && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5986
7006
  StateToggle,
5987
7007
  {
5988
7008
  rect: toggleState.rect,
@@ -5991,15 +7011,31 @@ function OhhwellsBridge() {
5991
7011
  onStateChange: handleStateChange
5992
7012
  }
5993
7013
  ),
5994
- sectionGap && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
7014
+ linkPopover ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
7015
+ LinkPopover,
7016
+ {
7017
+ rect: linkPopover.rect,
7018
+ parentScroll: parentScrollRef.current,
7019
+ panelRef: linkPopoverPanelRef,
7020
+ open: true,
7021
+ mode: "edit",
7022
+ pages: sitePages,
7023
+ sections: currentSections,
7024
+ sectionsByPath,
7025
+ initialTarget: linkPopover.target,
7026
+ onClose: closeLinkPopover,
7027
+ onSubmit: handleLinkPopoverSubmit
7028
+ }
7029
+ ) : null,
7030
+ sectionGap && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
5995
7031
  "div",
5996
7032
  {
5997
7033
  "data-ohw-section-insert-line": "",
5998
7034
  className: "fixed left-0 w-full z-2147483646 flex items-center pointer-events-none",
5999
7035
  style: { top: sectionGap.y, transform: "translateY(-50%)" },
6000
7036
  children: [
6001
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } }),
6002
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
7037
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } }),
7038
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
6003
7039
  Badge,
6004
7040
  {
6005
7041
  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",
@@ -6012,7 +7048,7 @@ function OhhwellsBridge() {
6012
7048
  children: "Add Section"
6013
7049
  }
6014
7050
  ),
6015
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } })
7051
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } })
6016
7052
  ]
6017
7053
  }
6018
7054
  )
@@ -6022,11 +7058,19 @@ function OhhwellsBridge() {
6022
7058
  }
6023
7059
  // Annotate the CommonJS export names for ESM import in node:
6024
7060
  0 && (module.exports = {
7061
+ LinkEditorPanel,
7062
+ LinkPopover,
6025
7063
  OhhwellsBridge,
6026
7064
  SchedulingWidget,
6027
7065
  Toggle,
6028
7066
  ToggleGroup,
6029
7067
  ToggleGroupItem,
6030
- toggleVariants
7068
+ buildTarget,
7069
+ filterAvailablePages,
7070
+ getEditModeInitialState,
7071
+ isValidUrl,
7072
+ parseTarget,
7073
+ toggleVariants,
7074
+ validateUrlInput
6031
7075
  });
6032
7076
  //# sourceMappingURL=index.cjs.map