@ohhwells/bridge 0.1.24 → 0.1.25

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();
5639
+ },
5640
+ onMouseEnter: (e) => {
5641
+ e.currentTarget.style.background = "#F5F5F4";
5642
+ },
5643
+ onMouseLeave: (e) => {
5644
+ e.currentTarget.style.background = "transparent";
4741
5645
  },
4742
- btn.cmd
4743
- );
4744
- })
4745
- ] }, gi))
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,75 @@ 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 (!isEditMode) return;
5789
+ const useFixtures = shouldUseDevFixtures();
5790
+ if (useFixtures) {
5791
+ setSitePages(DEV_SITE_PAGES);
5792
+ setSectionsByPath(DEV_SECTIONS_BY_PATH);
5793
+ if (process.env.NODE_ENV === "development") {
5794
+ console.info("[ohhwells-bridge] Dev fixtures loaded (ohw-fixtures=1)");
5795
+ }
5796
+ }
5797
+ const onSitePages = (e) => {
5798
+ if (e.data?.type !== "ow:site-pages" || !Array.isArray(e.data.pages)) return;
5799
+ if (useFixtures) return;
5800
+ setSitePages(
5801
+ e.data.pages.map((p) => ({
5802
+ path: p.path,
5803
+ title: p.title
5804
+ }))
5805
+ );
5806
+ };
5807
+ window.addEventListener("message", onSitePages);
5808
+ if (!useFixtures) postToParent({ type: "ow:request-site-pages" });
5809
+ return () => window.removeEventListener("message", onSitePages);
5810
+ }, [isEditMode, postToParent]);
5811
+ const [sectionGap, setSectionGap] = (0, import_react5.useState)(null);
5812
+ (0, import_react5.useEffect)(() => {
4860
5813
  const update = () => {
4861
5814
  const el = activeElRef.current;
4862
5815
  if (el) setToolbarRect(el.getBoundingClientRect());
@@ -4880,33 +5833,10 @@ function OhhwellsBridge() {
4880
5833
  vvp.removeEventListener("resize", update);
4881
5834
  };
4882
5835
  }, []);
4883
- const refreshStateRules = (0, import_react3.useCallback)(() => {
5836
+ const refreshStateRules = (0, import_react5.useCallback)(() => {
4884
5837
  editStylesRef.current?.forceHover && (editStylesRef.current.forceHover.textContent = collectStateRules());
4885
5838
  }, []);
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)(() => {
5839
+ const deactivate = (0, import_react5.useCallback)(() => {
4910
5840
  const el = activeElRef.current;
4911
5841
  if (!el) return;
4912
5842
  const key = el.dataset.ohwKey;
@@ -4929,9 +5859,10 @@ function OhhwellsBridge() {
4929
5859
  setToolbarRect(null);
4930
5860
  setMaxBadge(null);
4931
5861
  setActiveCommands(/* @__PURE__ */ new Set());
5862
+ setToolbarShowEditLink(false);
4932
5863
  postToParent({ type: "ow:exit-edit" });
4933
5864
  }, [postToParent]);
4934
- const activate = (0, import_react3.useCallback)((el) => {
5865
+ const activate = (0, import_react5.useCallback)((el) => {
4935
5866
  if (activeElRef.current === el) return;
4936
5867
  deactivate();
4937
5868
  if (hoveredImageRef.current) {
@@ -4944,12 +5875,13 @@ function OhhwellsBridge() {
4944
5875
  originalContentRef.current = el.innerHTML;
4945
5876
  el.focus();
4946
5877
  setToolbarRect(el.getBoundingClientRect());
5878
+ setToolbarShowEditLink(Boolean(getHrefKeyFromElement(el)));
4947
5879
  postToParent({ type: "ow:enter-edit", key: el.dataset.ohwKey });
4948
5880
  requestAnimationFrame(() => refreshActiveCommandsRef.current());
4949
5881
  }, [deactivate, postToParent]);
4950
5882
  activateRef.current = activate;
4951
5883
  deactivateRef.current = deactivate;
4952
- (0, import_react3.useLayoutEffect)(() => {
5884
+ (0, import_react5.useLayoutEffect)(() => {
4953
5885
  if (!subdomain || isEditMode) {
4954
5886
  setFetchState("done");
4955
5887
  return;
@@ -4971,14 +5903,15 @@ function OhhwellsBridge() {
4971
5903
  } else if (el.dataset.ohwEditable === "bg-image") {
4972
5904
  const next = `url('${val}')`;
4973
5905
  if (el.style.backgroundImage !== next) el.style.backgroundImage = next;
5906
+ } else if (el.dataset.ohwEditable === "link") {
5907
+ applyLinkHref(el, val);
4974
5908
  } else if (el.innerHTML !== val) {
4975
5909
  el.innerHTML = val;
4976
5910
  }
4977
5911
  });
5912
+ applyLinkByKey(key, val);
4978
5913
  }
4979
5914
  initSectionsFromContent(content, true);
4980
- sectionsLoadedRef.current = true;
4981
- pendingScheduleConfigRequests.current.splice(0).forEach(processConfigRequest);
4982
5915
  return imageLoads.length > 0 ? Promise.all(imageLoads).then(() => {
4983
5916
  }) : Promise.resolve();
4984
5917
  };
@@ -5003,7 +5936,7 @@ function OhhwellsBridge() {
5003
5936
  cancelled = true;
5004
5937
  };
5005
5938
  }, [subdomain, isEditMode, pathname]);
5006
- (0, import_react3.useEffect)(() => {
5939
+ (0, import_react5.useEffect)(() => {
5007
5940
  if (!subdomain || isEditMode) return;
5008
5941
  let debounceTimer = null;
5009
5942
  const applyFromCache = () => {
@@ -5019,10 +5952,13 @@ function OhhwellsBridge() {
5019
5952
  } else if (el.dataset.ohwEditable === "bg-image") {
5020
5953
  const next = `url('${val}')`;
5021
5954
  if (el.style.backgroundImage !== next) el.style.backgroundImage = next;
5955
+ } else if (el.dataset.ohwEditable === "link") {
5956
+ applyLinkHref(el, val);
5022
5957
  } else if (el.innerHTML !== val) {
5023
5958
  el.innerHTML = val;
5024
5959
  }
5025
5960
  });
5961
+ applyLinkByKey(key, val);
5026
5962
  }
5027
5963
  };
5028
5964
  const scheduleApply = () => {
@@ -5037,16 +5973,16 @@ function OhhwellsBridge() {
5037
5973
  if (debounceTimer) clearTimeout(debounceTimer);
5038
5974
  };
5039
5975
  }, [subdomain, isEditMode, pathname]);
5040
- (0, import_react3.useLayoutEffect)(() => {
5976
+ (0, import_react5.useLayoutEffect)(() => {
5041
5977
  const el = document.getElementById("ohw-loader");
5042
5978
  if (!el) return;
5043
5979
  const visible = Boolean(subdomain) && fetchState !== "done";
5044
5980
  el.style.display = visible ? "flex" : "none";
5045
5981
  }, [subdomain, fetchState]);
5046
- (0, import_react3.useEffect)(() => {
5982
+ (0, import_react5.useEffect)(() => {
5047
5983
  postToParent({ type: "ow:navigation", path: pathname });
5048
5984
  }, [pathname, postToParent]);
5049
- (0, import_react3.useEffect)(() => {
5985
+ (0, import_react5.useEffect)(() => {
5050
5986
  if (!isEditMode) return;
5051
5987
  const measure = () => {
5052
5988
  const h = document.body.scrollHeight;
@@ -5070,7 +6006,7 @@ function OhhwellsBridge() {
5070
6006
  window.removeEventListener("resize", handleResize);
5071
6007
  };
5072
6008
  }, [pathname, isEditMode, postToParent]);
5073
- (0, import_react3.useEffect)(() => {
6009
+ (0, import_react5.useEffect)(() => {
5074
6010
  if (!subdomainFromQuery || isEditMode) return;
5075
6011
  const handleClick = (e) => {
5076
6012
  const anchor = e.target.closest("a");
@@ -5086,7 +6022,7 @@ function OhhwellsBridge() {
5086
6022
  document.addEventListener("click", handleClick, true);
5087
6023
  return () => document.removeEventListener("click", handleClick, true);
5088
6024
  }, [subdomainFromQuery, isEditMode, router]);
5089
- (0, import_react3.useEffect)(() => {
6025
+ (0, import_react5.useEffect)(() => {
5090
6026
  if (!isEditMode) {
5091
6027
  editStylesRef.current?.base.remove();
5092
6028
  editStylesRef.current?.forceHover.remove();
@@ -5112,6 +6048,7 @@ function OhhwellsBridge() {
5112
6048
  [data-ohw-editable]:not([contenteditable]):not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"]) { cursor: text !important; }
5113
6049
  [data-ohw-editable="image"], [data-ohw-editable="image"] *,
5114
6050
  [data-ohw-editable="bg-image"], [data-ohw-editable="bg-image"] * { cursor: pointer !important; }
6051
+ [data-ohw-editable="link"], [data-ohw-editable="link"] * { cursor: pointer !important; }
5115
6052
  [data-ohw-hovered]:not([contenteditable]) {
5116
6053
  outline: 2px dashed ${PRIMARY} !important;
5117
6054
  outline-offset: 4px;
@@ -5149,8 +6086,21 @@ function OhhwellsBridge() {
5149
6086
  if (target.closest("[data-ohw-toolbar]")) return;
5150
6087
  if (target.closest("[data-ohw-state-toggle]")) return;
5151
6088
  if (target.closest("[data-ohw-max-badge]")) return;
6089
+ if (isInsideLinkEditor(target)) return;
5152
6090
  const editable = target.closest("[data-ohw-editable]");
5153
6091
  if (editable) {
6092
+ if (editable.dataset.ohwEditable === "link") {
6093
+ e.preventDefault();
6094
+ e.stopPropagation();
6095
+ deactivateRef.current();
6096
+ bumpLinkPopoverGrace();
6097
+ setLinkPopoverRef.current({
6098
+ key: editable.dataset.ohwKey ?? "",
6099
+ target: getLinkHref(editable),
6100
+ rect: editable.getBoundingClientRect()
6101
+ });
6102
+ return;
6103
+ }
5154
6104
  if (editable.dataset.ohwEditable === "image" || editable.dataset.ohwEditable === "bg-image") {
5155
6105
  e.preventDefault();
5156
6106
  e.stopPropagation();
@@ -5171,6 +6121,15 @@ function OhhwellsBridge() {
5171
6121
  }
5172
6122
  const anchor = target.closest("a");
5173
6123
  if (anchor) e.preventDefault();
6124
+ if (Date.now() < linkPopoverGraceUntilRef.current) return;
6125
+ if (linkPopoverOpenRef.current && activeElRef.current) {
6126
+ const hrefCtx = getHrefKeyFromElement(activeElRef.current);
6127
+ if (hrefCtx && (hrefCtx.anchor === target || hrefCtx.anchor.contains(target))) return;
6128
+ }
6129
+ if (linkPopoverOpenRef.current) {
6130
+ setLinkPopoverRef.current(null);
6131
+ return;
6132
+ }
5174
6133
  deactivateRef.current();
5175
6134
  };
5176
6135
  const handleMouseOver = (e) => {
@@ -5621,25 +6580,35 @@ function OhhwellsBridge() {
5621
6580
  if (img) img.src = val;
5622
6581
  } else if (el.dataset.ohwEditable === "bg-image") {
5623
6582
  el.style.backgroundImage = `url('${val}')`;
6583
+ } else if (el.dataset.ohwEditable === "link") {
6584
+ applyLinkHref(el, val);
5624
6585
  } else {
5625
6586
  el.innerHTML = val;
5626
6587
  }
5627
6588
  });
6589
+ applyLinkByKey(key, val);
5628
6590
  }
5629
6591
  if (sectionsJson) {
5630
6592
  initSectionsFromContent({ __ohw_sections: sectionsJson }, true);
5631
- sectionsLoadedRef.current = true;
5632
- pendingScheduleConfigRequests.current.splice(0).forEach(processConfigRequest);
5633
6593
  }
5634
6594
  postToParentRef.current({ type: "ow:hydrate-done" });
5635
6595
  };
5636
6596
  window.addEventListener("message", handleHydrate);
5637
6597
  const handleDeactivate = (e) => {
5638
6598
  if (e.data?.type !== "ow:deactivate") return;
6599
+ if (Date.now() < linkPopoverGraceUntilRef.current) return;
6600
+ if (linkPopoverOpenRef.current) {
6601
+ setLinkPopoverRef.current(null);
6602
+ return;
6603
+ }
5639
6604
  deactivateRef.current();
5640
6605
  };
5641
6606
  window.addEventListener("message", handleDeactivate);
5642
6607
  const handleKeyDown = (e) => {
6608
+ if (e.key === "Escape" && linkPopoverOpenRef.current) {
6609
+ setLinkPopoverRef.current(null);
6610
+ return;
6611
+ }
5643
6612
  if (e.key !== "Escape") return;
5644
6613
  const el = activeElRef.current;
5645
6614
  if (el && originalContentRef.current !== null) {
@@ -5893,23 +6862,7 @@ function OhhwellsBridge() {
5893
6862
  if (imageShowTimerRef.current) clearTimeout(imageShowTimerRef.current);
5894
6863
  };
5895
6864
  }, [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)(() => {
6865
+ (0, import_react5.useEffect)(() => {
5913
6866
  if (!isEditMode) return;
5914
6867
  document.querySelectorAll("[data-ohw-active-state]").forEach((el) => {
5915
6868
  el.removeAttribute("data-ohw-active-state");
@@ -5924,20 +6877,26 @@ function OhhwellsBridge() {
5924
6877
  const raf = requestAnimationFrame(() => refreshStateRules());
5925
6878
  const timer = setTimeout(() => {
5926
6879
  refreshStateRules();
5927
- postToParent({ type: "ow:ready", version: "1", nodes: collectEditableNodes() });
6880
+ const sections = collectSections();
6881
+ setSectionsByPath((prev) => {
6882
+ const next = { ...prev, [pathname]: sections };
6883
+ return shouldUseDevFixtures() ? { ...DEV_SECTIONS_BY_PATH, ...next } : next;
6884
+ });
6885
+ postToParent({ type: "ow:ready", version: "1", path: pathname, nodes: collectEditableNodes(), sections });
6886
+ postToParent({ type: "ow:request-site-pages" });
5928
6887
  }, 150);
5929
6888
  return () => {
5930
6889
  cancelAnimationFrame(raf);
5931
6890
  clearTimeout(timer);
5932
6891
  };
5933
6892
  }, [pathname, isEditMode, refreshStateRules, postToParent]);
5934
- const handleCommand = (0, import_react3.useCallback)((cmd) => {
6893
+ const handleCommand = (0, import_react5.useCallback)((cmd) => {
5935
6894
  document.execCommand(cmd, false);
5936
6895
  activeElRef.current?.focus();
5937
6896
  if (activeElRef.current) setToolbarRect(activeElRef.current.getBoundingClientRect());
5938
6897
  refreshActiveCommandsRef.current();
5939
6898
  }, []);
5940
- const handleStateChange = (0, import_react3.useCallback)((state) => {
6899
+ const handleStateChange = (0, import_react5.useCallback)((state) => {
5941
6900
  if (!activeStateElRef.current) return;
5942
6901
  const el = activeStateElRef.current;
5943
6902
  if (state === "Default") {
@@ -5950,13 +6909,48 @@ function OhhwellsBridge() {
5950
6909
  }
5951
6910
  setToggleState((prev) => prev ? { ...prev, activeState: state } : null);
5952
6911
  }, [deactivate]);
6912
+ const closeLinkPopover = (0, import_react5.useCallback)(() => setLinkPopover(null), []);
6913
+ const openLinkPopoverForActive = (0, import_react5.useCallback)(() => {
6914
+ const hrefCtx = getHrefKeyFromElement(activeElRef.current);
6915
+ if (!hrefCtx) return;
6916
+ bumpLinkPopoverGrace();
6917
+ setLinkPopover({
6918
+ key: hrefCtx.key,
6919
+ target: getLinkHref(hrefCtx.anchor),
6920
+ rect: hrefCtx.anchor.getBoundingClientRect()
6921
+ });
6922
+ }, []);
6923
+ const handleLinkPopoverSubmit = (0, import_react5.useCallback)(
6924
+ (target) => {
6925
+ if (!linkPopover) return;
6926
+ const { key } = linkPopover;
6927
+ applyLinkByKey(key, target);
6928
+ postToParent({ type: "ow:change", nodes: [{ key, text: target }] });
6929
+ setLinkPopover(null);
6930
+ },
6931
+ [linkPopover, postToParent]
6932
+ );
6933
+ const showEditLink = toolbarShowEditLink;
6934
+ const currentSections = sectionsByPath[pathname] ?? [];
6935
+ linkPopoverOpenRef.current = linkPopover !== null;
5953
6936
  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 })
6937
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
6938
+ toolbarRect && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
6939
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(GlowFrame, { rect: toolbarRect, elRef: glowElRef }),
6940
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
6941
+ FloatingToolbar,
6942
+ {
6943
+ rect: toolbarRect,
6944
+ parentScroll: parentScrollRef.current,
6945
+ elRef: toolbarElRef,
6946
+ onCommand: handleCommand,
6947
+ activeCommands,
6948
+ showEditLink,
6949
+ onEditLink: openLinkPopoverForActive
6950
+ }
6951
+ )
5958
6952
  ] }),
5959
- maxBadge && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
6953
+ maxBadge && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
5960
6954
  "div",
5961
6955
  {
5962
6956
  "data-ohw-max-badge": "",
@@ -5982,7 +6976,7 @@ function OhhwellsBridge() {
5982
6976
  ]
5983
6977
  }
5984
6978
  ),
5985
- toggleState && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
6979
+ toggleState && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5986
6980
  StateToggle,
5987
6981
  {
5988
6982
  rect: toggleState.rect,
@@ -5991,15 +6985,31 @@ function OhhwellsBridge() {
5991
6985
  onStateChange: handleStateChange
5992
6986
  }
5993
6987
  ),
5994
- sectionGap && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
6988
+ linkPopover ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
6989
+ LinkPopover,
6990
+ {
6991
+ rect: linkPopover.rect,
6992
+ parentScroll: parentScrollRef.current,
6993
+ panelRef: linkPopoverPanelRef,
6994
+ open: true,
6995
+ mode: "edit",
6996
+ pages: sitePages,
6997
+ sections: currentSections,
6998
+ sectionsByPath,
6999
+ initialTarget: linkPopover.target,
7000
+ onClose: closeLinkPopover,
7001
+ onSubmit: handleLinkPopoverSubmit
7002
+ }
7003
+ ) : null,
7004
+ sectionGap && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
5995
7005
  "div",
5996
7006
  {
5997
7007
  "data-ohw-section-insert-line": "",
5998
7008
  className: "fixed left-0 w-full z-2147483646 flex items-center pointer-events-none",
5999
7009
  style: { top: sectionGap.y, transform: "translateY(-50%)" },
6000
7010
  children: [
6001
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } }),
6002
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
7011
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } }),
7012
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
6003
7013
  Badge,
6004
7014
  {
6005
7015
  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 +7022,7 @@ function OhhwellsBridge() {
6012
7022
  children: "Add Section"
6013
7023
  }
6014
7024
  ),
6015
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } })
7025
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } })
6016
7026
  ]
6017
7027
  }
6018
7028
  )
@@ -6022,11 +7032,19 @@ function OhhwellsBridge() {
6022
7032
  }
6023
7033
  // Annotate the CommonJS export names for ESM import in node:
6024
7034
  0 && (module.exports = {
7035
+ LinkEditorPanel,
7036
+ LinkPopover,
6025
7037
  OhhwellsBridge,
6026
7038
  SchedulingWidget,
6027
7039
  Toggle,
6028
7040
  ToggleGroup,
6029
7041
  ToggleGroupItem,
6030
- toggleVariants
7042
+ buildTarget,
7043
+ filterAvailablePages,
7044
+ getEditModeInitialState,
7045
+ isValidUrl,
7046
+ parseTarget,
7047
+ toggleVariants,
7048
+ validateUrlInput
6031
7049
  });
6032
7050
  //# sourceMappingURL=index.cjs.map