@ohhwells/bridge 0.1.23 → 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
 
@@ -256,10 +264,10 @@ function buildTimezoneLabel(tz) {
256
264
  return tz;
257
265
  }
258
266
  }
259
- function EmptyState({ inEditor, insertAfter }) {
267
+ function EmptyState({ inEditor }) {
260
268
  const handleAddSchedule = () => {
261
269
  if (inEditor) {
262
- window.parent.postMessage({ type: "ow:scheduling-not-connected", insertAfter }, "*");
270
+ window.parent.postMessage({ type: "ow:scheduling-not-connected" }, "*");
263
271
  }
264
272
  };
265
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: [
@@ -484,21 +492,18 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
484
492
  const booked = getBookingsOnDate(cls, selectedDate);
485
493
  const available = cls.maxParticipants - booked;
486
494
  const isFull = available <= 0;
487
- const isPrivate = cls.maxParticipants === 1;
488
495
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
489
496
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex gap-4 py-4 sm:items-center sm:gap-[60px] box-border", children: [
490
497
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col gap-2 shrink-0 sm:contents", children: [
491
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col gap-2 sm:w-[120px] sm:shrink-0", children: [
492
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block", children: formatClassTime(cls) }),
493
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
494
- "span",
495
- {
496
- className: "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium w-fit",
497
- style: isPrivate ? { background: "#DDFBE3", color: "#199130", border: "1px solid #199130", fontWeight: 500 } : { background: "#EEEDFF", color: "#5953FF", border: "1px solid #5953FF", fontWeight: 500 },
498
- children: isPrivate ? "PRIVATE" : "GROUP"
499
- }
500
- )
501
- ] }),
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
+ ),
502
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: [
503
508
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
504
509
  available,
@@ -717,7 +722,6 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
717
722
  window.parent.postMessage({ type: "ow:scheduling-not-connected", insertAfter }, "*");
718
723
  }
719
724
  };
720
- if (!inEditor && !loading && !schedule) return null;
721
725
  const sectionId = `scheduling-${insertAfter}`;
722
726
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
723
727
  "section",
@@ -763,7 +767,7 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
763
767
  ] }),
764
768
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "w-full flex flex-col items-end gap-3", children: [
765
769
  timezoneLabel && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-sm font-normal text-(--color-accent,#A89B83)", children: timezoneLabel }),
766
- /* @__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)(
767
771
  ScheduleView,
768
772
  {
769
773
  schedule,
@@ -4219,8 +4223,844 @@ function isEditSessionActive() {
4219
4223
  return new URLSearchParams(q).get("mode") === "edit";
4220
4224
  }
4221
4225
 
4222
- // 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");
4223
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");
4224
5064
  var badgeVariants = cva(
4225
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",
4226
5066
  {
@@ -4238,11 +5078,11 @@ var badgeVariants = cva(
4238
5078
  }
4239
5079
  );
4240
5080
  function Badge({ className, variant, ...props }) {
4241
- 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 });
4242
5082
  }
4243
5083
 
4244
5084
  // src/OhhwellsBridge.tsx
4245
- var import_jsx_runtime6 = require("react/jsx-runtime");
5085
+ var import_jsx_runtime16 = require("react/jsx-runtime");
4246
5086
  var PRIMARY = "#0885FE";
4247
5087
  var IMAGE_FADE_MS = 300;
4248
5088
  function runOpacityFade(el, onDone) {
@@ -4382,10 +5222,9 @@ function initSectionsFromContent(content, removeExisting = false) {
4382
5222
  const inEditor = typeof window !== "undefined" && window.self !== window.top;
4383
5223
  if (inEditor) getSectionsTracker().textContent = raw;
4384
5224
  const pageEntries = getPageSchedulingEntries(raw);
4385
- const notifyForEntry = inEditor ? (e) => !e.scheduleId : false;
4386
- mountSchedulingEntries(pageEntries, notifyForEntry);
4387
- requestAnimationFrame(() => retryMissingSchedulingMounts(pageEntries, notifyForEntry));
4388
- setTimeout(() => retryMissingSchedulingMounts(pageEntries, notifyForEntry), 250);
5225
+ mountSchedulingEntries(pageEntries, false);
5226
+ requestAnimationFrame(() => retryMissingSchedulingMounts(pageEntries, false));
5227
+ setTimeout(() => retryMissingSchedulingMounts(pageEntries, false), 250);
4389
5228
  } catch {
4390
5229
  }
4391
5230
  }
@@ -4412,7 +5251,7 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
4412
5251
  const root = (0, import_client.createRoot)(container);
4413
5252
  (0, import_react_dom.flushSync)(() => {
4414
5253
  root.render(
4415
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5254
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4416
5255
  SchedulingWidget,
4417
5256
  {
4418
5257
  notifyOnConnect,
@@ -4444,16 +5283,23 @@ function mountSchedulingEntries(entries, notifyOnConnect = false) {
4444
5283
  const pending = entries.filter((e) => e.type === "scheduling").sort((a, b) => schedulingMountDepth(a.insertAfter) - schedulingMountDepth(b.insertAfter));
4445
5284
  for (let attempt = 0; attempt < pending.length + 1 && pending.length > 0; attempt++) {
4446
5285
  for (let i = pending.length - 1; i >= 0; i--) {
4447
- const entry = pending[i];
4448
- const shouldNotify = typeof notifyOnConnect === "function" ? notifyOnConnect(entry) : notifyOnConnect;
4449
- if (mountSchedulingWidget(entry.insertAfter, shouldNotify, entry.scheduleId)) {
5286
+ const { insertAfter, scheduleId } = pending[i];
5287
+ if (mountSchedulingWidget(insertAfter, notifyOnConnect, scheduleId)) {
4450
5288
  pending.splice(i, 1);
4451
5289
  }
4452
5290
  }
4453
5291
  }
4454
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
+ }
4455
5301
  function collectEditableNodes() {
4456
- return Array.from(document.querySelectorAll("[data-ohw-editable]")).map((el) => {
5302
+ const nodes = Array.from(document.querySelectorAll("[data-ohw-editable]")).map((el) => {
4457
5303
  if (el.dataset.ohwEditable === "image") {
4458
5304
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
4459
5305
  return { key: el.dataset.ohwKey ?? "", type: "image", text: img?.src ?? "" };
@@ -4463,12 +5309,52 @@ function collectEditableNodes() {
4463
5309
  const url = raw.replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "");
4464
5310
  return { key: el.dataset.ohwKey ?? "", type: "bg-image", text: url };
4465
5311
  }
5312
+ if (el.dataset.ohwEditable === "link") {
5313
+ return { key: el.dataset.ohwKey ?? "", type: "link", text: getLinkHref(el) };
5314
+ }
4466
5315
  return {
4467
5316
  key: el.dataset.ohwKey ?? "",
4468
5317
  type: el.dataset.ohwEditable ?? "text",
4469
5318
  text: el.dataset.ohwEditable === "plain" ? el.innerText ?? "" : el.innerHTML
4470
5319
  };
4471
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
+ });
4472
5358
  }
4473
5359
  var FORCE_PSEUDO_STATES = [
4474
5360
  { pseudo: ":hover", state: "hover" },
@@ -4609,7 +5495,7 @@ var TOOLBAR_GROUPS = [
4609
5495
  ];
4610
5496
  function GlowFrame({ rect, elRef }) {
4611
5497
  const GAP = 6;
4612
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5498
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4613
5499
  "div",
4614
5500
  {
4615
5501
  ref: elRef,
@@ -4657,10 +5543,12 @@ function FloatingToolbar({
4657
5543
  parentScroll,
4658
5544
  elRef,
4659
5545
  onCommand,
4660
- activeCommands
5546
+ activeCommands,
5547
+ showEditLink,
5548
+ onEditLink
4661
5549
  }) {
4662
5550
  const { top, left, transform } = calcToolbarPos(rect, parentScroll);
4663
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5551
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4664
5552
  "div",
4665
5553
  {
4666
5554
  ref: elRef,
@@ -4683,56 +5571,97 @@ function FloatingToolbar({
4683
5571
  pointerEvents: "auto",
4684
5572
  whiteSpace: "nowrap"
4685
5573
  },
4686
- children: TOOLBAR_GROUPS.map((btns, gi) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react3.default.Fragment, { children: [
4687
- gi > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { display: "block", width: 1, height: 24, background: "#E7E5E4", flexShrink: 0 } }),
4688
- btns.map((btn) => {
4689
- const isActive = activeCommands.has(btn.cmd);
4690
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4691
- "button",
4692
- {
4693
- title: btn.title,
4694
- onMouseDown: (e) => {
4695
- e.preventDefault();
4696
- onCommand(btn.cmd);
4697
- },
4698
- onMouseEnter: (e) => {
4699
- if (!isActive) e.currentTarget.style.background = "#F5F5F4";
4700
- },
4701
- onMouseLeave: (e) => {
4702
- if (!isActive) e.currentTarget.style.background = "transparent";
4703
- },
4704
- style: {
4705
- display: "flex",
4706
- alignItems: "center",
4707
- justifyContent: "center",
4708
- border: "none",
4709
- background: isActive ? PRIMARY : "transparent",
4710
- borderRadius: 4,
4711
- cursor: "pointer",
4712
- color: isActive ? "#FFFFFF" : "#1C1917",
4713
- flexShrink: 0,
4714
- 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
+ )
4715
5621
  },
4716
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4717
- "svg",
4718
- {
4719
- width: "16",
4720
- height: "16",
4721
- viewBox: "0 0 24 24",
4722
- fill: "none",
4723
- stroke: isActive ? "#FFFFFF" : "#1C1917",
4724
- strokeWidth: "2.5",
4725
- strokeLinecap: "round",
4726
- strokeLinejoin: "round",
4727
- style: isActive && gi > 0 ? { filter: "drop-shadow(0 0 0.5px #fff)" } : void 0,
4728
- dangerouslySetInnerHTML: { __html: ICONS[btn.cmd] }
4729
- }
4730
- )
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";
4731
5645
  },
4732
- btn.cmd
4733
- );
4734
- })
4735
- ] }, 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
+ ]
4736
5665
  }
4737
5666
  );
4738
5667
  }
@@ -4746,7 +5675,7 @@ function StateToggle({
4746
5675
  states,
4747
5676
  onStateChange
4748
5677
  }) {
4749
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5678
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4750
5679
  ToggleGroup,
4751
5680
  {
4752
5681
  "data-ohw-state-toggle": "",
@@ -4760,7 +5689,7 @@ function StateToggle({
4760
5689
  left: rect.right - 8,
4761
5690
  transform: "translateX(-100%)"
4762
5691
  },
4763
- 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))
4764
5693
  }
4765
5694
  );
4766
5695
  }
@@ -4787,8 +5716,8 @@ function OhhwellsBridge() {
4787
5716
  const router = (0, import_navigation.useRouter)();
4788
5717
  const searchParams = (0, import_navigation.useSearchParams)();
4789
5718
  const isEditMode = isEditSessionActive();
4790
- const [bridgeRoot, setBridgeRoot] = (0, import_react3.useState)(null);
4791
- (0, import_react3.useEffect)(() => {
5719
+ const [bridgeRoot, setBridgeRoot] = (0, import_react5.useState)(null);
5720
+ (0, import_react5.useEffect)(() => {
4792
5721
  const figtreeFontId = "ohw-figtree-font";
4793
5722
  if (!document.getElementById(figtreeFontId)) {
4794
5723
  const preconnect1 = Object.assign(document.createElement("link"), { rel: "preconnect", href: "https://fonts.googleapis.com" });
@@ -4812,39 +5741,75 @@ function OhhwellsBridge() {
4812
5741
  }, []);
4813
5742
  const subdomainFromQuery = searchParams.get("subdomain");
4814
5743
  const subdomain = resolveSubdomain(subdomainFromQuery);
4815
- const postToParent = (0, import_react3.useCallback)((data) => {
5744
+ const postToParent = (0, import_react5.useCallback)((data) => {
4816
5745
  if (typeof window !== "undefined" && window.parent !== window) {
4817
5746
  window.parent.postMessage(data, "*");
4818
5747
  }
4819
5748
  }, []);
4820
- const [fetchState, setFetchState] = (0, import_react3.useState)("idle");
4821
- const autoSaveTimers = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
4822
- const activeElRef = (0, import_react3.useRef)(null);
4823
- const originalContentRef = (0, import_react3.useRef)(null);
4824
- const activeStateElRef = (0, import_react3.useRef)(null);
4825
- const parentScrollRef = (0, import_react3.useRef)(null);
4826
- const toolbarElRef = (0, import_react3.useRef)(null);
4827
- const glowElRef = (0, import_react3.useRef)(null);
4828
- const hoveredImageRef = (0, import_react3.useRef)(null);
4829
- const hoveredImageHasTextOverlapRef = (0, import_react3.useRef)(false);
4830
- const hoveredGapRef = (0, import_react3.useRef)(null);
4831
- const imageUnhoverTimerRef = (0, import_react3.useRef)(null);
4832
- const imageShowTimerRef = (0, import_react3.useRef)(null);
4833
- const editStylesRef = (0, import_react3.useRef)(null);
4834
- 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)(() => {
4835
5764
  });
4836
- const deactivateRef = (0, import_react3.useRef)(() => {
5765
+ const deactivateRef = (0, import_react5.useRef)(() => {
4837
5766
  });
4838
- const refreshActiveCommandsRef = (0, import_react3.useRef)(() => {
5767
+ const refreshActiveCommandsRef = (0, import_react5.useRef)(() => {
4839
5768
  });
4840
- const postToParentRef = (0, import_react3.useRef)(postToParent);
5769
+ const postToParentRef = (0, import_react5.useRef)(postToParent);
4841
5770
  postToParentRef.current = postToParent;
4842
- const [toolbarRect, setToolbarRect] = (0, import_react3.useState)(null);
4843
- const [toggleState, setToggleState] = (0, import_react3.useState)(null);
4844
- const [maxBadge, setMaxBadge] = (0, import_react3.useState)(null);
4845
- const [activeCommands, setActiveCommands] = (0, import_react3.useState)(/* @__PURE__ */ new Set());
4846
- const [sectionGap, setSectionGap] = (0, import_react3.useState)(null);
4847
- (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)(() => {
4848
5813
  const update = () => {
4849
5814
  const el = activeElRef.current;
4850
5815
  if (el) setToolbarRect(el.getBoundingClientRect());
@@ -4868,10 +5833,10 @@ function OhhwellsBridge() {
4868
5833
  vvp.removeEventListener("resize", update);
4869
5834
  };
4870
5835
  }, []);
4871
- const refreshStateRules = (0, import_react3.useCallback)(() => {
5836
+ const refreshStateRules = (0, import_react5.useCallback)(() => {
4872
5837
  editStylesRef.current?.forceHover && (editStylesRef.current.forceHover.textContent = collectStateRules());
4873
5838
  }, []);
4874
- const deactivate = (0, import_react3.useCallback)(() => {
5839
+ const deactivate = (0, import_react5.useCallback)(() => {
4875
5840
  const el = activeElRef.current;
4876
5841
  if (!el) return;
4877
5842
  const key = el.dataset.ohwKey;
@@ -4894,9 +5859,10 @@ function OhhwellsBridge() {
4894
5859
  setToolbarRect(null);
4895
5860
  setMaxBadge(null);
4896
5861
  setActiveCommands(/* @__PURE__ */ new Set());
5862
+ setToolbarShowEditLink(false);
4897
5863
  postToParent({ type: "ow:exit-edit" });
4898
5864
  }, [postToParent]);
4899
- const activate = (0, import_react3.useCallback)((el) => {
5865
+ const activate = (0, import_react5.useCallback)((el) => {
4900
5866
  if (activeElRef.current === el) return;
4901
5867
  deactivate();
4902
5868
  if (hoveredImageRef.current) {
@@ -4909,12 +5875,13 @@ function OhhwellsBridge() {
4909
5875
  originalContentRef.current = el.innerHTML;
4910
5876
  el.focus();
4911
5877
  setToolbarRect(el.getBoundingClientRect());
5878
+ setToolbarShowEditLink(Boolean(getHrefKeyFromElement(el)));
4912
5879
  postToParent({ type: "ow:enter-edit", key: el.dataset.ohwKey });
4913
5880
  requestAnimationFrame(() => refreshActiveCommandsRef.current());
4914
5881
  }, [deactivate, postToParent]);
4915
5882
  activateRef.current = activate;
4916
5883
  deactivateRef.current = deactivate;
4917
- (0, import_react3.useLayoutEffect)(() => {
5884
+ (0, import_react5.useLayoutEffect)(() => {
4918
5885
  if (!subdomain || isEditMode) {
4919
5886
  setFetchState("done");
4920
5887
  return;
@@ -4936,10 +5903,13 @@ function OhhwellsBridge() {
4936
5903
  } else if (el.dataset.ohwEditable === "bg-image") {
4937
5904
  const next = `url('${val}')`;
4938
5905
  if (el.style.backgroundImage !== next) el.style.backgroundImage = next;
5906
+ } else if (el.dataset.ohwEditable === "link") {
5907
+ applyLinkHref(el, val);
4939
5908
  } else if (el.innerHTML !== val) {
4940
5909
  el.innerHTML = val;
4941
5910
  }
4942
5911
  });
5912
+ applyLinkByKey(key, val);
4943
5913
  }
4944
5914
  initSectionsFromContent(content, true);
4945
5915
  return imageLoads.length > 0 ? Promise.all(imageLoads).then(() => {
@@ -4966,7 +5936,7 @@ function OhhwellsBridge() {
4966
5936
  cancelled = true;
4967
5937
  };
4968
5938
  }, [subdomain, isEditMode, pathname]);
4969
- (0, import_react3.useEffect)(() => {
5939
+ (0, import_react5.useEffect)(() => {
4970
5940
  if (!subdomain || isEditMode) return;
4971
5941
  let debounceTimer = null;
4972
5942
  const applyFromCache = () => {
@@ -4982,10 +5952,13 @@ function OhhwellsBridge() {
4982
5952
  } else if (el.dataset.ohwEditable === "bg-image") {
4983
5953
  const next = `url('${val}')`;
4984
5954
  if (el.style.backgroundImage !== next) el.style.backgroundImage = next;
5955
+ } else if (el.dataset.ohwEditable === "link") {
5956
+ applyLinkHref(el, val);
4985
5957
  } else if (el.innerHTML !== val) {
4986
5958
  el.innerHTML = val;
4987
5959
  }
4988
5960
  });
5961
+ applyLinkByKey(key, val);
4989
5962
  }
4990
5963
  };
4991
5964
  const scheduleApply = () => {
@@ -5000,16 +5973,16 @@ function OhhwellsBridge() {
5000
5973
  if (debounceTimer) clearTimeout(debounceTimer);
5001
5974
  };
5002
5975
  }, [subdomain, isEditMode, pathname]);
5003
- (0, import_react3.useLayoutEffect)(() => {
5976
+ (0, import_react5.useLayoutEffect)(() => {
5004
5977
  const el = document.getElementById("ohw-loader");
5005
5978
  if (!el) return;
5006
5979
  const visible = Boolean(subdomain) && fetchState !== "done";
5007
5980
  el.style.display = visible ? "flex" : "none";
5008
5981
  }, [subdomain, fetchState]);
5009
- (0, import_react3.useEffect)(() => {
5982
+ (0, import_react5.useEffect)(() => {
5010
5983
  postToParent({ type: "ow:navigation", path: pathname });
5011
5984
  }, [pathname, postToParent]);
5012
- (0, import_react3.useEffect)(() => {
5985
+ (0, import_react5.useEffect)(() => {
5013
5986
  if (!isEditMode) return;
5014
5987
  const measure = () => {
5015
5988
  const h = document.body.scrollHeight;
@@ -5033,7 +6006,7 @@ function OhhwellsBridge() {
5033
6006
  window.removeEventListener("resize", handleResize);
5034
6007
  };
5035
6008
  }, [pathname, isEditMode, postToParent]);
5036
- (0, import_react3.useEffect)(() => {
6009
+ (0, import_react5.useEffect)(() => {
5037
6010
  if (!subdomainFromQuery || isEditMode) return;
5038
6011
  const handleClick = (e) => {
5039
6012
  const anchor = e.target.closest("a");
@@ -5049,7 +6022,7 @@ function OhhwellsBridge() {
5049
6022
  document.addEventListener("click", handleClick, true);
5050
6023
  return () => document.removeEventListener("click", handleClick, true);
5051
6024
  }, [subdomainFromQuery, isEditMode, router]);
5052
- (0, import_react3.useEffect)(() => {
6025
+ (0, import_react5.useEffect)(() => {
5053
6026
  if (!isEditMode) {
5054
6027
  editStylesRef.current?.base.remove();
5055
6028
  editStylesRef.current?.forceHover.remove();
@@ -5075,6 +6048,7 @@ function OhhwellsBridge() {
5075
6048
  [data-ohw-editable]:not([contenteditable]):not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"]) { cursor: text !important; }
5076
6049
  [data-ohw-editable="image"], [data-ohw-editable="image"] *,
5077
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; }
5078
6052
  [data-ohw-hovered]:not([contenteditable]) {
5079
6053
  outline: 2px dashed ${PRIMARY} !important;
5080
6054
  outline-offset: 4px;
@@ -5112,8 +6086,21 @@ function OhhwellsBridge() {
5112
6086
  if (target.closest("[data-ohw-toolbar]")) return;
5113
6087
  if (target.closest("[data-ohw-state-toggle]")) return;
5114
6088
  if (target.closest("[data-ohw-max-badge]")) return;
6089
+ if (isInsideLinkEditor(target)) return;
5115
6090
  const editable = target.closest("[data-ohw-editable]");
5116
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
+ }
5117
6104
  if (editable.dataset.ohwEditable === "image" || editable.dataset.ohwEditable === "bg-image") {
5118
6105
  e.preventDefault();
5119
6106
  e.stopPropagation();
@@ -5134,6 +6121,15 @@ function OhhwellsBridge() {
5134
6121
  }
5135
6122
  const anchor = target.closest("a");
5136
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
+ }
5137
6133
  deactivateRef.current();
5138
6134
  };
5139
6135
  const handleMouseOver = (e) => {
@@ -5584,10 +6580,13 @@ function OhhwellsBridge() {
5584
6580
  if (img) img.src = val;
5585
6581
  } else if (el.dataset.ohwEditable === "bg-image") {
5586
6582
  el.style.backgroundImage = `url('${val}')`;
6583
+ } else if (el.dataset.ohwEditable === "link") {
6584
+ applyLinkHref(el, val);
5587
6585
  } else {
5588
6586
  el.innerHTML = val;
5589
6587
  }
5590
6588
  });
6589
+ applyLinkByKey(key, val);
5591
6590
  }
5592
6591
  if (sectionsJson) {
5593
6592
  initSectionsFromContent({ __ohw_sections: sectionsJson }, true);
@@ -5597,10 +6596,19 @@ function OhhwellsBridge() {
5597
6596
  window.addEventListener("message", handleHydrate);
5598
6597
  const handleDeactivate = (e) => {
5599
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
+ }
5600
6604
  deactivateRef.current();
5601
6605
  };
5602
6606
  window.addEventListener("message", handleDeactivate);
5603
6607
  const handleKeyDown = (e) => {
6608
+ if (e.key === "Escape" && linkPopoverOpenRef.current) {
6609
+ setLinkPopoverRef.current(null);
6610
+ return;
6611
+ }
5604
6612
  if (e.key !== "Escape") return;
5605
6613
  const el = activeElRef.current;
5606
6614
  if (el && originalContentRef.current !== null) {
@@ -5854,7 +6862,7 @@ function OhhwellsBridge() {
5854
6862
  if (imageShowTimerRef.current) clearTimeout(imageShowTimerRef.current);
5855
6863
  };
5856
6864
  }, [isEditMode, refreshStateRules]);
5857
- (0, import_react3.useEffect)(() => {
6865
+ (0, import_react5.useEffect)(() => {
5858
6866
  if (!isEditMode) return;
5859
6867
  document.querySelectorAll("[data-ohw-active-state]").forEach((el) => {
5860
6868
  el.removeAttribute("data-ohw-active-state");
@@ -5869,20 +6877,26 @@ function OhhwellsBridge() {
5869
6877
  const raf = requestAnimationFrame(() => refreshStateRules());
5870
6878
  const timer = setTimeout(() => {
5871
6879
  refreshStateRules();
5872
- 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" });
5873
6887
  }, 150);
5874
6888
  return () => {
5875
6889
  cancelAnimationFrame(raf);
5876
6890
  clearTimeout(timer);
5877
6891
  };
5878
6892
  }, [pathname, isEditMode, refreshStateRules, postToParent]);
5879
- const handleCommand = (0, import_react3.useCallback)((cmd) => {
6893
+ const handleCommand = (0, import_react5.useCallback)((cmd) => {
5880
6894
  document.execCommand(cmd, false);
5881
6895
  activeElRef.current?.focus();
5882
6896
  if (activeElRef.current) setToolbarRect(activeElRef.current.getBoundingClientRect());
5883
6897
  refreshActiveCommandsRef.current();
5884
6898
  }, []);
5885
- const handleStateChange = (0, import_react3.useCallback)((state) => {
6899
+ const handleStateChange = (0, import_react5.useCallback)((state) => {
5886
6900
  if (!activeStateElRef.current) return;
5887
6901
  const el = activeStateElRef.current;
5888
6902
  if (state === "Default") {
@@ -5895,13 +6909,48 @@ function OhhwellsBridge() {
5895
6909
  }
5896
6910
  setToggleState((prev) => prev ? { ...prev, activeState: state } : null);
5897
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;
5898
6936
  return bridgeRoot ? (0, import_react_dom2.createPortal)(
5899
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
5900
- toolbarRect && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
5901
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(GlowFrame, { rect: toolbarRect, elRef: glowElRef }),
5902
- /* @__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
+ )
5903
6952
  ] }),
5904
- maxBadge && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
6953
+ maxBadge && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
5905
6954
  "div",
5906
6955
  {
5907
6956
  "data-ohw-max-badge": "",
@@ -5927,7 +6976,7 @@ function OhhwellsBridge() {
5927
6976
  ]
5928
6977
  }
5929
6978
  ),
5930
- toggleState && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
6979
+ toggleState && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5931
6980
  StateToggle,
5932
6981
  {
5933
6982
  rect: toggleState.rect,
@@ -5936,15 +6985,31 @@ function OhhwellsBridge() {
5936
6985
  onStateChange: handleStateChange
5937
6986
  }
5938
6987
  ),
5939
- 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)(
5940
7005
  "div",
5941
7006
  {
5942
7007
  "data-ohw-section-insert-line": "",
5943
7008
  className: "fixed left-0 w-full z-2147483646 flex items-center pointer-events-none",
5944
7009
  style: { top: sectionGap.y, transform: "translateY(-50%)" },
5945
7010
  children: [
5946
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } }),
5947
- /* @__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)(
5948
7013
  Badge,
5949
7014
  {
5950
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",
@@ -5957,7 +7022,7 @@ function OhhwellsBridge() {
5957
7022
  children: "Add Section"
5958
7023
  }
5959
7024
  ),
5960
- /* @__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 } })
5961
7026
  ]
5962
7027
  }
5963
7028
  )
@@ -5967,11 +7032,19 @@ function OhhwellsBridge() {
5967
7032
  }
5968
7033
  // Annotate the CommonJS export names for ESM import in node:
5969
7034
  0 && (module.exports = {
7035
+ LinkEditorPanel,
7036
+ LinkPopover,
5970
7037
  OhhwellsBridge,
5971
7038
  SchedulingWidget,
5972
7039
  Toggle,
5973
7040
  ToggleGroup,
5974
7041
  ToggleGroupItem,
5975
- toggleVariants
7042
+ buildTarget,
7043
+ filterAvailablePages,
7044
+ getEditModeInitialState,
7045
+ isValidUrl,
7046
+ parseTarget,
7047
+ toggleVariants,
7048
+ validateUrlInput
5976
7049
  });
5977
7050
  //# sourceMappingURL=index.cjs.map