@ohhwells/bridge 0.1.45 → 0.1.46-next.109

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
@@ -115,6 +115,7 @@ var import_react3 = require("react");
115
115
  var import_react2 = require("react");
116
116
  var import_radix_ui = require("radix-ui");
117
117
  var import_jsx_runtime = require("react/jsx-runtime");
118
+ var isValidEmail = (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
118
119
  function Spinner() {
119
120
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
120
121
  "svg",
@@ -148,12 +149,17 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
148
149
  const [error, setError] = (0, import_react2.useState)(null);
149
150
  const isBook = title === "Confirm your spot";
150
151
  const handleSubmit = async () => {
151
- if (!email.trim()) return;
152
+ const trimmed = email.trim();
153
+ if (!trimmed) return;
154
+ if (!isValidEmail(trimmed)) {
155
+ setError("Please enter a valid email address.");
156
+ return;
157
+ }
152
158
  setLoading(true);
153
159
  setError(null);
154
160
  try {
155
- await onSubmit(email.trim());
156
- setSubmittedEmail(email.trim());
161
+ await onSubmit(trimmed);
162
+ setSubmittedEmail(trimmed);
157
163
  setSuccess(true);
158
164
  } catch (e) {
159
165
  setError(e instanceof Error ? e.message : "Something went wrong. Please try again.");
@@ -228,7 +234,10 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
228
234
  {
229
235
  type: "email",
230
236
  value: email,
231
- onChange: (e) => setEmail(e.target.value),
237
+ onChange: (e) => {
238
+ setEmail(e.target.value);
239
+ if (error) setError(null);
240
+ },
232
241
  onKeyDown: handleKeyDown,
233
242
  placeholder: "hello@gmail.com",
234
243
  autoFocus: true,
@@ -285,12 +294,20 @@ function formatClassTime(cls) {
285
294
  const em = endTotal % 60;
286
295
  return `${h}:${cls.startTime.minutes}-${eh}:${String(em).padStart(2, "0")}`;
287
296
  }
297
+ function formatBookingLabel(cls) {
298
+ const price = cls.price;
299
+ const isFree = price === null || price === void 0 || `${price}` === "" || `${price}` === "0";
300
+ return isFree ? "Book for free" : `Book for ${cls.currency ?? ""} ${price}`.trim();
301
+ }
288
302
  function getBookingsOnDate(cls, date) {
289
303
  if (!cls.bookings?.length) return 0;
290
- const ds = date.toISOString().split("T")[0];
304
+ const target = new Date(date);
305
+ target.setHours(0, 0, 0, 0);
291
306
  return cls.bookings.filter((b) => {
292
307
  try {
293
- return new Date(b.classDate).toISOString().split("T")[0] === ds;
308
+ const bookingDate = new Date(b.classDate);
309
+ bookingDate.setHours(0, 0, 0, 0);
310
+ return bookingDate.getTime() === target.getTime();
294
311
  } catch {
295
312
  return false;
296
313
  }
@@ -555,7 +572,7 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
555
572
  }
556
573
  )
557
574
  ] }),
558
- /* @__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: [
575
+ cls.showAvailability !== false && /* @__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: [
559
576
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
560
577
  available,
561
578
  "/",
@@ -567,9 +584,17 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
567
584
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-1 flex-col gap-2 min-w-0 sm:contents", children: [
568
585
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex-1 flex flex-col gap-2 min-w-0", children: [
569
586
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block truncate", children: cls.name }),
570
- cls.hostName && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-base font-normal text-(--color-dark,#200C02) opacity-80 block truncate", children: cls.hostName })
587
+ cls.hostName && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-base font-normal text-(--color-dark,#200C02) opacity-80 block truncate", children: cls.hostName }),
588
+ cls.description && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
589
+ "span",
590
+ {
591
+ className: "font-body text-base font-normal text-(--color-dark,#200C02) opacity-80 block",
592
+ style: { wordBreak: "break-word" },
593
+ children: cls.description
594
+ }
595
+ )
571
596
  ] }),
572
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "hidden sm:flex w-14 shrink-0 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: [
597
+ cls.showAvailability !== false && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "hidden sm:flex w-14 shrink-0 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: [
573
598
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
574
599
  available,
575
600
  "/",
@@ -594,7 +619,7 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
594
619
  if (!cls.id) return;
595
620
  onOpenModal?.(isFull ? "waitlist" : "book", cls.id, selectedDate);
596
621
  },
597
- children: isFull ? "Join Waitlist" : "Book Now"
622
+ children: isFull ? "Join Waitlist" : formatBookingLabel(cls)
598
623
  }
599
624
  )
600
625
  ] })
@@ -636,6 +661,17 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
636
661
  const [isHovered, setIsHovered] = (0, import_react3.useState)(false);
637
662
  const [modalState, setModalState] = (0, import_react3.useState)(null);
638
663
  const switchScheduleIdRef = (0, import_react3.useRef)(null);
664
+ const liveScheduleIdRef = (0, import_react3.useRef)(null);
665
+ const refetchLiveSchedule = (0, import_react3.useCallback)(async () => {
666
+ const id = liveScheduleIdRef.current;
667
+ if (!id) return;
668
+ try {
669
+ const res = await fetch(`${API_URL}/api/schedule/id/${id}`);
670
+ const data = await res.json();
671
+ if (data?.id) setSchedule(data);
672
+ } catch {
673
+ }
674
+ }, []);
639
675
  const dates = (0, import_react3.useMemo)(() => {
640
676
  const today = /* @__PURE__ */ new Date();
641
677
  today.setHours(0, 0, 0, 0);
@@ -655,6 +691,18 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
655
691
  }
656
692
  }
657
693
  }, [schedule, dates]);
694
+ (0, import_react3.useEffect)(() => {
695
+ if (typeof window === "undefined") return;
696
+ const onVisible = () => {
697
+ if (!document.hidden) void refetchLiveSchedule();
698
+ };
699
+ window.addEventListener("focus", refetchLiveSchedule);
700
+ document.addEventListener("visibilitychange", onVisible);
701
+ return () => {
702
+ window.removeEventListener("focus", refetchLiveSchedule);
703
+ document.removeEventListener("visibilitychange", onVisible);
704
+ };
705
+ }, [refetchLiveSchedule]);
658
706
  (0, import_react3.useEffect)(() => {
659
707
  if (typeof window === "undefined") return;
660
708
  const isInEditor = window.self !== window.top;
@@ -716,7 +764,7 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
716
764
  setLoading(false);
717
765
  return;
718
766
  }
719
- ;
767
+ liveScheduleIdRef.current = effectiveId;
720
768
  (async () => {
721
769
  try {
722
770
  const res = await fetch(`${API_URL}/api/schedule/id/${effectiveId}`);
@@ -773,18 +821,14 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
773
821
  const data = await res.json().catch(() => ({}));
774
822
  throw new Error(data?.message ?? "Something went wrong. Please try again.");
775
823
  }
824
+ await refetchLiveSchedule();
776
825
  };
777
826
  const handleReplaceSchedule = () => {
778
827
  setIsHovered(false);
779
- if (schedule) {
780
- window.parent.postMessage({
781
- type: "ow:schedule-connected",
782
- schedule: { id: schedule.id, name: schedule.name },
783
- insertAfter
784
- }, "*");
785
- } else {
786
- window.parent.postMessage({ type: "ow:scheduling-not-connected", insertAfter }, "*");
787
- }
828
+ window.parent.postMessage(
829
+ { type: "ow:scheduling-not-connected", insertAfter, currentScheduleId: schedule?.id },
830
+ "*"
831
+ );
788
832
  };
789
833
  if (!inEditor && !loading && !schedule) return null;
790
834
  const sectionId = `scheduling-${insertAfter}`;
@@ -827,7 +871,19 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
827
871
  ),
828
872
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "max-w-[1280px] mx-auto flex flex-col items-center gap-16", children: [
829
873
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col items-center gap-4", children: [
830
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { className: "font-display text-4xl sm:text-5xl font-bold text-center m-0 text-(--color-dark,#200C02)", children: schedule?.name ?? "Book an appointment" }),
874
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
875
+ "h2",
876
+ {
877
+ className: "font-display text-center m-0 text-(--color-dark,#200C02)",
878
+ style: {
879
+ fontSize: "var(--fs-section-h2, clamp(40px, 4.4vw, 64px))",
880
+ fontWeight: "var(--font-weight-heading, 400)",
881
+ lineHeight: 1.05,
882
+ letterSpacing: "-0.02em"
883
+ },
884
+ children: schedule?.name ?? "Book an appointment"
885
+ }
886
+ ),
831
887
  schedule?.description && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "font-body text-body text-center m-0 text-(--color-accent,#A89B83)", children: schedule.description })
832
888
  ] }),
833
889
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "w-full flex flex-col items-end gap-3", children: [
@@ -4887,7 +4943,11 @@ function MediaOverlay({
4887
4943
  variant: "outline",
4888
4944
  size: "sm",
4889
4945
  className: "gap-1.5 cursor-pointer hover:bg-background",
4890
- style: OVERLAY_BUTTON_STYLE,
4946
+ style: {
4947
+ ...OVERLAY_BUTTON_STYLE,
4948
+ // Don't let Replace steal clicks when text (e.g. nav) sits on top of this media.
4949
+ pointerEvents: hover.hasTextOverlap ? "none" : "auto"
4950
+ },
4891
4951
  onMouseDown: (e) => e.preventDefault(),
4892
4952
  onClick: (e) => {
4893
4953
  e.stopPropagation();
@@ -6661,6 +6721,8 @@ function applyNavOrderToContainer(container, order) {
6661
6721
  if (current.length === expected.length && current.every((key, i) => key === expected[i])) {
6662
6722
  return;
6663
6723
  }
6724
+ const lastCurrentLinkEl = current.length > 0 ? findCounterpartByHrefKey(current[current.length - 1], container) : null;
6725
+ const anchor = lastCurrentLinkEl?.parentElement === container ? lastCurrentLinkEl.nextElementSibling : null;
6664
6726
  const seen = /* @__PURE__ */ new Set();
6665
6727
  for (const hrefKey of desired) {
6666
6728
  if (seen.has(hrefKey)) continue;
@@ -6675,7 +6737,9 @@ function applyNavOrderToContainer(container, order) {
6675
6737
  orderedEls.push(el);
6676
6738
  }
6677
6739
  for (const el of orderedEls) {
6678
- if (container.lastElementChild !== el) {
6740
+ if (anchor) {
6741
+ if (el.nextSibling !== anchor) container.insertBefore(el, anchor);
6742
+ } else if (container.lastElementChild !== el) {
6679
6743
  container.appendChild(el);
6680
6744
  }
6681
6745
  }
@@ -7007,7 +7071,195 @@ function parseFooterOrder(content) {
7007
7071
  return null;
7008
7072
  }
7009
7073
  }
7074
+ var FOOTER_HEADING_RE = /^footer-(\d+)-heading$/;
7075
+ var FOOTER_LABEL_RE = /^footer-(\d+)-(\d+)-label$/;
7076
+ var DEFAULT_FOOTER_COLUMN_HEADING = "New";
7077
+ var DEFAULT_FOOTER_COLUMN_LINK_LABEL = "New link";
7078
+ var DEFAULT_FOOTER_COLUMN_LINK_HREF = "";
7079
+ function isFooterLinksContainer(el) {
7080
+ return el.hasAttribute("data-ohw-footer-links") || el.classList.contains("rb-footer-links");
7081
+ }
7082
+ function getFooterColumnIndicesInDom() {
7083
+ const indices = /* @__PURE__ */ new Set();
7084
+ for (const col of listFooterColumns()) {
7085
+ const attr = col.getAttribute("data-ohw-footer-col");
7086
+ if (attr != null) {
7087
+ const n = parseInt(attr, 10);
7088
+ if (Number.isFinite(n)) indices.add(n);
7089
+ }
7090
+ for (const link of listFooterLinksInColumn(col)) {
7091
+ const parsed = parseFooterHrefKey(link.getAttribute("data-ohw-href-key"));
7092
+ if (parsed) indices.add(parsed.col);
7093
+ }
7094
+ const heading = col.querySelector('[data-ohw-key^="footer-"][data-ohw-key$="-heading"]');
7095
+ const headingKey = heading?.getAttribute("data-ohw-key");
7096
+ if (headingKey) {
7097
+ const match = headingKey.match(FOOTER_HEADING_RE);
7098
+ if (match) indices.add(parseInt(match[1], 10));
7099
+ }
7100
+ }
7101
+ return [...indices].sort((a, b) => a - b);
7102
+ }
7103
+ function getNextFooterColumnIndex() {
7104
+ const indices = getFooterColumnIndicesInDom();
7105
+ if (indices.length === 0) return 0;
7106
+ return Math.max(...indices) + 1;
7107
+ }
7108
+ function cloneFooterLinkShell(template) {
7109
+ const anchor = document.createElement("a");
7110
+ if (template) {
7111
+ anchor.style.cssText = template.style.cssText;
7112
+ if (template.className) anchor.className = template.className;
7113
+ }
7114
+ anchor.style.cursor = "pointer";
7115
+ anchor.style.textDecoration = "none";
7116
+ return anchor;
7117
+ }
7118
+ function buildFooterHeading(colIndex, text) {
7119
+ const heading = document.createElement("p");
7120
+ heading.setAttribute("data-ohw-editable", "text");
7121
+ heading.setAttribute("data-ohw-key", `footer-${colIndex}-heading`);
7122
+ heading.textContent = text;
7123
+ heading.style.cssText = [
7124
+ "color: var(--espresso, #3d312b)",
7125
+ "font-size: 14px",
7126
+ "font-weight: 800",
7127
+ "opacity: 0.82",
7128
+ "margin: 0 0 6px",
7129
+ "padding: 0",
7130
+ "line-height: 1.2"
7131
+ ].join(";");
7132
+ return heading;
7133
+ }
7134
+ function buildFooterLink(colIndex, itemIndex, href, label, template) {
7135
+ const anchor = cloneFooterLinkShell(template);
7136
+ anchor.setAttribute("href", href);
7137
+ anchor.setAttribute("data-ohw-href-key", `footer-${colIndex}-${itemIndex}-href`);
7138
+ const span = document.createElement("span");
7139
+ span.setAttribute("data-ohw-editable", "text");
7140
+ span.setAttribute("data-ohw-key", `footer-${colIndex}-${itemIndex}-label`);
7141
+ span.textContent = label;
7142
+ anchor.appendChild(span);
7143
+ return anchor;
7144
+ }
7145
+ function footerColumnExistsInDom(colIndex) {
7146
+ return document.querySelector(`[data-ohw-footer-col="${colIndex}"]`) !== null || document.querySelector(`[data-ohw-href-key^="footer-${colIndex}-"]`) !== null || document.querySelector(`[data-ohw-key="footer-${colIndex}-heading"]`) !== null;
7147
+ }
7148
+ function getFooterLinkTemplate() {
7149
+ for (const col of listFooterColumns()) {
7150
+ const link = listFooterLinksInColumn(col)[0];
7151
+ if (link) return link;
7152
+ }
7153
+ return null;
7154
+ }
7155
+ function getFooterColumnTemplate() {
7156
+ return listFooterColumns()[0] ?? null;
7157
+ }
7158
+ function insertFooterColumnDom(colIndex, heading, items) {
7159
+ const container = getFooterLinksContainer();
7160
+ if (!container) return null;
7161
+ const colTemplate = getFooterColumnTemplate();
7162
+ const linkTemplate = getFooterLinkTemplate();
7163
+ const column = document.createElement("div");
7164
+ if (colTemplate?.className) column.className = colTemplate.className;
7165
+ else column.className = "rb-footer-link-col";
7166
+ column.setAttribute("data-ohw-footer-col", String(colIndex));
7167
+ if (colTemplate) {
7168
+ const gap = getComputedStyle(colTemplate).gap;
7169
+ if (gap && gap !== "normal") column.style.gap = gap;
7170
+ column.style.display = getComputedStyle(colTemplate).display || "flex";
7171
+ column.style.flexDirection = "column";
7172
+ }
7173
+ column.appendChild(buildFooterHeading(colIndex, heading));
7174
+ let firstLink = null;
7175
+ items.forEach((item, itemIndex) => {
7176
+ const link = buildFooterLink(colIndex, itemIndex, item.href, item.label, linkTemplate);
7177
+ column.appendChild(link);
7178
+ if (!firstLink) firstLink = link;
7179
+ });
7180
+ container.appendChild(column);
7181
+ return { column, firstLink };
7182
+ }
7183
+ function insertFooterColumn(heading = DEFAULT_FOOTER_COLUMN_HEADING, linkLabel = DEFAULT_FOOTER_COLUMN_LINK_LABEL, linkHref = DEFAULT_FOOTER_COLUMN_LINK_HREF) {
7184
+ const colIndex = getNextFooterColumnIndex();
7185
+ const inserted = insertFooterColumnDom(colIndex, heading, [{ href: linkHref, label: linkLabel }]);
7186
+ if (!inserted?.firstLink) throw new Error("Failed to insert footer column");
7187
+ syncFooterColumnIndices(listFooterColumns());
7188
+ return {
7189
+ column: inserted.column,
7190
+ firstLink: inserted.firstLink,
7191
+ colIndex,
7192
+ headingKey: `footer-${colIndex}-heading`,
7193
+ hrefKey: `footer-${colIndex}-0-href`,
7194
+ labelKey: `footer-${colIndex}-0-label`,
7195
+ heading,
7196
+ label: linkLabel,
7197
+ href: linkHref,
7198
+ order: getFooterOrderFromDom()
7199
+ };
7200
+ }
7201
+ function collectFooterColumnIndicesFromContent(content) {
7202
+ const indices = /* @__PURE__ */ new Set();
7203
+ for (const key of Object.keys(content)) {
7204
+ const href = parseFooterHrefKey(key);
7205
+ if (href) indices.add(href.col);
7206
+ const heading = key.match(FOOTER_HEADING_RE);
7207
+ if (heading) indices.add(parseInt(heading[1], 10));
7208
+ const label = key.match(FOOTER_LABEL_RE);
7209
+ if (label) indices.add(parseInt(label[1], 10));
7210
+ }
7211
+ const order = parseFooterOrder(content);
7212
+ if (order) {
7213
+ for (const col of order) {
7214
+ for (const hrefKey of col) {
7215
+ const parsed = parseFooterHrefKey(hrefKey);
7216
+ if (parsed) indices.add(parsed.col);
7217
+ }
7218
+ }
7219
+ }
7220
+ return [...indices].sort((a, b) => a - b);
7221
+ }
7222
+ function collectFooterItemsForColumn(content, colIndex) {
7223
+ const itemIndices = /* @__PURE__ */ new Set();
7224
+ for (const key of Object.keys(content)) {
7225
+ const href = parseFooterHrefKey(key);
7226
+ if (href?.col === colIndex) itemIndices.add(href.item);
7227
+ const label = key.match(FOOTER_LABEL_RE);
7228
+ if (label && parseInt(label[1], 10) === colIndex) {
7229
+ itemIndices.add(parseInt(label[2], 10));
7230
+ }
7231
+ }
7232
+ const sorted = [...itemIndices].sort((a, b) => a - b);
7233
+ if (sorted.length === 0) {
7234
+ return [
7235
+ {
7236
+ href: content[`footer-${colIndex}-0-href`] ?? DEFAULT_FOOTER_COLUMN_LINK_HREF,
7237
+ label: content[`footer-${colIndex}-0-label`] ?? DEFAULT_FOOTER_COLUMN_LINK_LABEL
7238
+ }
7239
+ ];
7240
+ }
7241
+ return sorted.map((itemIndex) => ({
7242
+ href: content[`footer-${colIndex}-${itemIndex}-href`] ?? DEFAULT_FOOTER_COLUMN_LINK_HREF,
7243
+ label: content[`footer-${colIndex}-${itemIndex}-label`] ?? DEFAULT_FOOTER_COLUMN_LINK_LABEL
7244
+ }));
7245
+ }
7246
+ function reconcileFooterColumnsFromContent(content) {
7247
+ const indices = collectFooterColumnIndicesFromContent(content);
7248
+ for (const colIndex of indices) {
7249
+ if (footerColumnExistsInDom(colIndex)) continue;
7250
+ const heading = content[`footer-${colIndex}-heading`] ?? DEFAULT_FOOTER_COLUMN_HEADING;
7251
+ const items = collectFooterItemsForColumn(content, colIndex);
7252
+ const hasPayload = Boolean(content[`footer-${colIndex}-heading`]) || items.some(
7253
+ (_, i) => content[`footer-${colIndex}-${i}-href`] !== void 0 || content[`footer-${colIndex}-${i}-label`] !== void 0
7254
+ ) || (parseFooterOrder(content)?.some(
7255
+ (col) => col.some((k) => parseFooterHrefKey(k)?.col === colIndex)
7256
+ ) ?? false);
7257
+ if (!hasPayload) continue;
7258
+ insertFooterColumnDom(colIndex, heading, items);
7259
+ }
7260
+ }
7010
7261
  function reconcileFooterOrderFromContent(content) {
7262
+ reconcileFooterColumnsFromContent(content);
7011
7263
  const order = parseFooterOrder(content);
7012
7264
  if (!order?.length) return;
7013
7265
  const current = getFooterOrderFromDom();
@@ -7017,6 +7269,31 @@ function reconcileFooterOrderFromContent(content) {
7017
7269
  }
7018
7270
  applyFooterOrder(order);
7019
7271
  }
7272
+ function resolveFooterLinksContainerSelectionTarget(target, clientX, clientY, isPointOverItem) {
7273
+ const container = getFooterLinksContainer();
7274
+ if (!container) return null;
7275
+ const inContainer = target === container || container.contains(target) && Boolean(target.closest("[data-ohw-footer-links], .rb-footer-links"));
7276
+ if (!inContainer && target !== container) {
7277
+ const r2 = container.getBoundingClientRect();
7278
+ const over = clientX >= r2.left && clientX <= r2.right && clientY >= r2.top && clientY <= r2.bottom;
7279
+ if (!over) return null;
7280
+ }
7281
+ if (isPointOverItem(container, clientX, clientY)) return null;
7282
+ const column = target.closest("[data-ohw-footer-col], [data-ohw-footer-column]") ?? null;
7283
+ if (column && container.contains(column)) {
7284
+ if (target === column || column.contains(target)) return null;
7285
+ }
7286
+ if (target === container || container.contains(target) || inContainer) {
7287
+ if (target === container || isFooterLinksContainer(target)) return container;
7288
+ if (!column) return container;
7289
+ }
7290
+ return null;
7291
+ }
7292
+ function isRowLayoutColumn(links) {
7293
+ if (links.length < 2) return false;
7294
+ const firstTop = links[0].getBoundingClientRect().top;
7295
+ return links.every((link) => Math.abs(link.getBoundingClientRect().top - firstTop) < 4);
7296
+ }
7020
7297
  function buildLinkDropSlots(column, columnIndex) {
7021
7298
  const links = listFooterLinksInColumn(column);
7022
7299
  const colRect = column.getBoundingClientRect();
@@ -7034,6 +7311,32 @@ function buildLinkDropSlots(column, columnIndex) {
7034
7311
  });
7035
7312
  return slots;
7036
7313
  }
7314
+ if (isRowLayoutColumn(links)) {
7315
+ for (let i = 0; i <= links.length; i++) {
7316
+ let left;
7317
+ if (i === 0) {
7318
+ left = links[0].getBoundingClientRect().left - barThickness / 2;
7319
+ } else if (i === links.length) {
7320
+ const last = links[links.length - 1].getBoundingClientRect();
7321
+ left = last.right - barThickness / 2;
7322
+ } else {
7323
+ const prev = links[i - 1].getBoundingClientRect();
7324
+ const next = links[i].getBoundingClientRect();
7325
+ left = (prev.right + next.left) / 2 - barThickness / 2;
7326
+ }
7327
+ const heightRef = i === 0 ? links[0].getBoundingClientRect() : i === links.length ? links[links.length - 1].getBoundingClientRect() : links[i].getBoundingClientRect();
7328
+ slots.push({
7329
+ insertIndex: i,
7330
+ columnIndex,
7331
+ left,
7332
+ top: heightRef.top,
7333
+ width: barThickness,
7334
+ height: Math.max(heightRef.height, colRect.height * 0.8),
7335
+ direction: "vertical"
7336
+ });
7337
+ }
7338
+ return slots;
7339
+ }
7037
7340
  for (let i = 0; i <= links.length; i++) {
7038
7341
  let top;
7039
7342
  if (i === 0) {
@@ -7117,8 +7420,7 @@ function hitTestLinkDropSlot(clientX, clientY, draggedHrefKey) {
7117
7420
  return true;
7118
7421
  });
7119
7422
  for (const slot of slots) {
7120
- const cy = slot.top + slot.height / 2;
7121
- const dist = Math.abs(clientY - cy) + (inColX ? 0 : 1e3);
7423
+ const dist = slot.direction === "vertical" ? Math.abs(clientX - (slot.left + slot.width / 2)) : Math.abs(clientY - (slot.top + slot.height / 2));
7122
7424
  if (!best || dist < best.dist) best = { slot, dist };
7123
7425
  }
7124
7426
  }
@@ -7142,6 +7444,30 @@ function hitTestColumnDropSlot(clientX, _clientY) {
7142
7444
  return best?.slot ?? null;
7143
7445
  }
7144
7446
 
7447
+ // src/lib/add-footer-column.ts
7448
+ function buildFooterColumnEditContentPatch(result) {
7449
+ return {
7450
+ [result.headingKey]: result.heading,
7451
+ [result.hrefKey]: result.href,
7452
+ [result.labelKey]: result.label,
7453
+ [FOOTER_ORDER_KEY]: JSON.stringify(result.order)
7454
+ };
7455
+ }
7456
+ function addFooterColumnWithPersist({
7457
+ postToParent: postToParent2
7458
+ }) {
7459
+ const result = insertFooterColumn();
7460
+ const patch = buildFooterColumnEditContentPatch(result);
7461
+ setStoredLinkHref(result.hrefKey, result.href);
7462
+ postToParent2({
7463
+ type: "ow:change",
7464
+ nodes: Object.entries(patch).map(([key, text]) => ({ key, text }))
7465
+ });
7466
+ postToParent2({ type: "ow:toast", title: "Item added", toastType: "success" });
7467
+ enforceLinkHrefs();
7468
+ return result;
7469
+ }
7470
+
7145
7471
  // src/lib/item-drag-interaction.ts
7146
7472
  function disableNativeHrefDrag(el) {
7147
7473
  if (el.draggable) el.draggable = false;
@@ -7162,9 +7488,10 @@ function lockItemDuringDrag() {
7162
7488
  clearTextSelection();
7163
7489
  }
7164
7490
  function unlockItemDragInteraction() {
7491
+ const wasDragging = document.documentElement.hasAttribute("data-ohw-item-dragging");
7165
7492
  document.documentElement.removeAttribute("data-ohw-footer-press-drag");
7166
7493
  document.documentElement.removeAttribute("data-ohw-item-dragging");
7167
- clearTextSelection();
7494
+ if (wasDragging) clearTextSelection();
7168
7495
  }
7169
7496
  var armFooterPressDrag = armItemPressDrag;
7170
7497
  var lockFooterDuringDrag = lockItemDuringDrag;
@@ -7541,7 +7868,6 @@ function useNavItemDrag({
7541
7868
  const hrefKey = anchor?.getAttribute("data-ohw-href-key") ?? null;
7542
7869
  if (!anchor || !isNavbarHrefKey(hrefKey)) return;
7543
7870
  if (isNavbarButton3(anchor) || isDragHandleDisabled2(anchor)) return;
7544
- armItemPressDrag();
7545
7871
  navPointerDragRef.current = {
7546
7872
  el: anchor,
7547
7873
  startX: e.clientX,
@@ -7568,6 +7894,7 @@ function useNavItemDrag({
7568
7894
  if (dx * dx + dy * dy < THRESHOLD * THRESHOLD) return;
7569
7895
  e.preventDefault();
7570
7896
  pending.started = true;
7897
+ armItemPressDrag();
7571
7898
  clearTextSelection();
7572
7899
  try {
7573
7900
  document.body.setPointerCapture(pending.pointerId);
@@ -7595,7 +7922,8 @@ function useNavItemDrag({
7595
7922
  }
7596
7923
  } catch {
7597
7924
  }
7598
- if (!pending?.started) {
7925
+ if (!pending) return;
7926
+ if (!pending.started) {
7599
7927
  unlockItemDragInteraction();
7600
7928
  return;
7601
7929
  }
@@ -7647,7 +7975,6 @@ function useNavItemDrag({
7647
7975
  const hrefKey = selected.getAttribute("data-ohw-href-key");
7648
7976
  if (!hrefKey || !isNavbarHrefKey(hrefKey)) return false;
7649
7977
  if (isNavbarButton3(selected) || isDragHandleDisabled2(selected)) return false;
7650
- armItemPressDrag();
7651
7978
  navPointerDragRef.current = {
7652
7979
  el: selected,
7653
7980
  startX: clientX,
@@ -7673,15 +8000,60 @@ function useNavItemDrag({
7673
8000
  };
7674
8001
  }
7675
8002
 
7676
- // src/ui/navbar-container-chrome.tsx
8003
+ // src/ui/footer-container-chrome.tsx
7677
8004
  var import_lucide_react10 = require("lucide-react");
7678
8005
  var import_jsx_runtime21 = require("react/jsx-runtime");
8006
+ function FooterContainerChrome({ rect, onAdd }) {
8007
+ const chromeGap = 6;
8008
+ const buttonMargin = 7;
8009
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(TooltipProvider, { delayDuration: 0, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
8010
+ "div",
8011
+ {
8012
+ "data-ohw-footer-container-chrome": "",
8013
+ "data-ohw-bridge": "",
8014
+ className: "pointer-events-none fixed z-[2147483647]",
8015
+ style: {
8016
+ top: rect.top - chromeGap,
8017
+ left: rect.left - chromeGap,
8018
+ width: rect.width + chromeGap * 2,
8019
+ height: rect.height + chromeGap * 2
8020
+ },
8021
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Tooltip, { children: [
8022
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
8023
+ "button",
8024
+ {
8025
+ type: "button",
8026
+ "data-ohw-footer-add-button": "",
8027
+ className: "pointer-events-auto absolute left-1/2 flex size-7 -translate-x-1/2 -translate-y-full items-center justify-center rounded-[10px] border border-border bg-background p-0.5 shadow-sm transition-colors hover:bg-muted/80",
8028
+ style: { top: chromeGap - buttonMargin },
8029
+ "aria-label": "Add item",
8030
+ onMouseDown: (e) => {
8031
+ e.preventDefault();
8032
+ e.stopPropagation();
8033
+ },
8034
+ onClick: (e) => {
8035
+ e.preventDefault();
8036
+ e.stopPropagation();
8037
+ onAdd();
8038
+ },
8039
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react10.Plus, { className: "size-4 shrink-0 text-foreground", "aria-hidden": true })
8040
+ }
8041
+ ) }),
8042
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(TooltipContent, { side: "bottom", sideOffset: 9, children: "Add item" })
8043
+ ] })
8044
+ }
8045
+ ) });
8046
+ }
8047
+
8048
+ // src/ui/navbar-container-chrome.tsx
8049
+ var import_lucide_react11 = require("lucide-react");
8050
+ var import_jsx_runtime22 = require("react/jsx-runtime");
7679
8051
  function NavbarContainerChrome({
7680
8052
  rect,
7681
8053
  onAdd
7682
8054
  }) {
7683
8055
  const chromeGap = 6;
7684
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
8056
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
7685
8057
  "div",
7686
8058
  {
7687
8059
  "data-ohw-navbar-container-chrome": "",
@@ -7693,7 +8065,7 @@ function NavbarContainerChrome({
7693
8065
  width: rect.width + chromeGap * 2,
7694
8066
  height: rect.height + chromeGap * 2
7695
8067
  },
7696
- children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
8068
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
7697
8069
  "button",
7698
8070
  {
7699
8071
  type: "button",
@@ -7710,7 +8082,7 @@ function NavbarContainerChrome({
7710
8082
  e.stopPropagation();
7711
8083
  onAdd();
7712
8084
  },
7713
- children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react10.Plus, { className: "size-4 shrink-0 text-foreground", "aria-hidden": true })
8085
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react11.Plus, { className: "size-4 shrink-0 text-foreground", "aria-hidden": true })
7714
8086
  }
7715
8087
  )
7716
8088
  }
@@ -7719,7 +8091,7 @@ function NavbarContainerChrome({
7719
8091
 
7720
8092
  // src/ui/drop-indicator.tsx
7721
8093
  var React10 = __toESM(require("react"), 1);
7722
- var import_jsx_runtime22 = require("react/jsx-runtime");
8094
+ var import_jsx_runtime23 = require("react/jsx-runtime");
7723
8095
  var dropIndicatorVariants = cva(
7724
8096
  "ov-gap-line pointer-events-none shrink-0 transition-opacity duration-150",
7725
8097
  {
@@ -7743,7 +8115,7 @@ var dropIndicatorVariants = cva(
7743
8115
  );
7744
8116
  var DropIndicator = React10.forwardRef(
7745
8117
  ({ className, direction, state, ...props }, ref) => {
7746
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
8118
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
7747
8119
  "div",
7748
8120
  {
7749
8121
  ref,
@@ -7760,7 +8132,7 @@ var DropIndicator = React10.forwardRef(
7760
8132
  DropIndicator.displayName = "DropIndicator";
7761
8133
 
7762
8134
  // src/ui/badge.tsx
7763
- var import_jsx_runtime23 = require("react/jsx-runtime");
8135
+ var import_jsx_runtime24 = require("react/jsx-runtime");
7764
8136
  var badgeVariants = cva(
7765
8137
  "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",
7766
8138
  {
@@ -7778,12 +8150,12 @@ var badgeVariants = cva(
7778
8150
  }
7779
8151
  );
7780
8152
  function Badge({ className, variant, ...props }) {
7781
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: cn(badgeVariants({ variant }), className), ...props });
8153
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: cn(badgeVariants({ variant }), className), ...props });
7782
8154
  }
7783
8155
 
7784
8156
  // src/OhhwellsBridge.tsx
7785
- var import_lucide_react11 = require("lucide-react");
7786
- var import_jsx_runtime24 = require("react/jsx-runtime");
8157
+ var import_lucide_react12 = require("lucide-react");
8158
+ var import_jsx_runtime25 = require("react/jsx-runtime");
7787
8159
  var PRIMARY2 = "#0885FE";
7788
8160
  var IMAGE_FADE_MS = 300;
7789
8161
  function runOpacityFade(el, onDone) {
@@ -7962,7 +8334,7 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
7962
8334
  const root = (0, import_client.createRoot)(container);
7963
8335
  (0, import_react_dom2.flushSync)(() => {
7964
8336
  root.render(
7965
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
8337
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
7966
8338
  SchedulingWidget,
7967
8339
  {
7968
8340
  notifyOnConnect,
@@ -8163,7 +8535,7 @@ function applyLinkByKey(key, val) {
8163
8535
  }
8164
8536
  function isInsideLinkEditor(target) {
8165
8537
  return Boolean(
8166
- target.closest("[data-ohw-link-popover-root]") || target.closest("[data-ohw-link-modal-root]") || target.closest("[data-ohw-link-page-dropdown]") || target.closest("[data-ohw-section-picker]") || target.closest("[data-ohw-navbar-container-chrome]") || target.closest("[data-ohw-navbar-add-button]") || target.closest('[data-slot="popover-content"]') || target.closest('[data-slot="dialog-content"]') || target.closest('[data-slot="dialog-overlay"]')
8538
+ target.closest("[data-ohw-link-popover-root]") || target.closest("[data-ohw-link-modal-root]") || target.closest("[data-ohw-link-page-dropdown]") || target.closest("[data-ohw-section-picker]") || target.closest("[data-ohw-navbar-container-chrome]") || target.closest("[data-ohw-navbar-add-button]") || target.closest("[data-ohw-footer-container-chrome]") || target.closest("[data-ohw-footer-add-button]") || target.closest('[data-slot="popover-content"]') || target.closest('[data-slot="dialog-content"]') || target.closest('[data-slot="dialog-overlay"]')
8167
8539
  );
8168
8540
  }
8169
8541
  function getHrefKeyFromElement(el) {
@@ -8244,7 +8616,7 @@ function isInferredFooterGroup(el) {
8244
8616
  return countFooterNavItems(el) >= 2;
8245
8617
  }
8246
8618
  function isNavigationContainer(el) {
8247
- return el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isNavigationRoot(el) || isInferredFooterGroup(el);
8619
+ return el.hasAttribute("data-ohw-nav-container") || isFooterLinksContainer(el) || el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isNavigationRoot(el) || isInferredFooterGroup(el);
8248
8620
  }
8249
8621
  function isNavbarLinksContainer(el) {
8250
8622
  return el.hasAttribute("data-ohw-nav-container");
@@ -8263,6 +8635,9 @@ function isPointOverNavigation(x, y) {
8263
8635
  const roots = /* @__PURE__ */ new Set();
8264
8636
  const navRoot = document.querySelector("[data-ohw-nav-root]") ?? document.querySelector("nav");
8265
8637
  if (navRoot) roots.add(navRoot);
8638
+ document.querySelectorAll("[data-ohw-nav-drawer], [data-ohw-nav-container]").forEach((el) => {
8639
+ roots.add(el);
8640
+ });
8266
8641
  const footer = document.querySelector("footer");
8267
8642
  if (footer) roots.add(footer);
8268
8643
  for (const root of roots) {
@@ -8291,6 +8666,12 @@ function isPointOverNavItem(container, x, y) {
8291
8666
  return x >= itemRect.left && x <= itemRect.right && y >= itemRect.top && y <= itemRect.bottom;
8292
8667
  });
8293
8668
  }
8669
+ function isPointOverFooterColumn(x, y) {
8670
+ return listFooterColumns().some((col) => {
8671
+ const r2 = col.getBoundingClientRect();
8672
+ return x >= r2.left && x <= r2.right && y >= r2.top && y <= r2.bottom;
8673
+ });
8674
+ }
8294
8675
  function resolveNavContainerSelectionTarget(target, clientX, clientY) {
8295
8676
  if (getNavigationItemAnchor(target)) return null;
8296
8677
  if (target.closest('[data-ohw-role="navbar-button"]')) return null;
@@ -8321,7 +8702,10 @@ function getNavigationSelectionParent(el) {
8321
8702
  if (footerGroup) return footerGroup;
8322
8703
  return getNavigationRoot(el);
8323
8704
  }
8324
- if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isInferredFooterGroup(el)) {
8705
+ if (!isFooterLinksContainer(el) && (el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isInferredFooterGroup(el))) {
8706
+ return getFooterLinksContainer();
8707
+ }
8708
+ if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isFooterLinksContainer(el) || isInferredFooterGroup(el)) {
8325
8709
  return getNavigationRoot(el);
8326
8710
  }
8327
8711
  return null;
@@ -8492,9 +8876,16 @@ function sanitizeHtml(html) {
8492
8876
  if (!SAFE_TAGS.has(el.tagName)) {
8493
8877
  parent.replaceChild(document.createTextNode(el.textContent ?? ""), el);
8494
8878
  } else {
8495
- const textAlign = el.style?.textAlign || el.getAttribute("align") || "";
8879
+ const htmlEl = el;
8880
+ const textAlign = htmlEl.style?.textAlign || el.getAttribute("align") || "";
8881
+ const fontWeight = htmlEl.style?.fontWeight || "";
8882
+ const fontStyle = htmlEl.style?.fontStyle || "";
8883
+ const textDecorationLine = htmlEl.style?.textDecorationLine || htmlEl.style?.textDecoration || "";
8496
8884
  for (const attr of Array.from(el.attributes)) el.removeAttribute(attr.name);
8497
- if (textAlign) el.style.textAlign = textAlign;
8885
+ if (textAlign) htmlEl.style.textAlign = textAlign;
8886
+ if (fontWeight) htmlEl.style.fontWeight = fontWeight;
8887
+ if (fontStyle) htmlEl.style.fontStyle = fontStyle;
8888
+ if (textDecorationLine) htmlEl.style.textDecorationLine = textDecorationLine;
8498
8889
  walk(el);
8499
8890
  }
8500
8891
  }
@@ -8541,7 +8932,7 @@ function EditGlowChrome({
8541
8932
  hideHandle = false
8542
8933
  }) {
8543
8934
  const GAP = SELECTION_CHROME_GAP2;
8544
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
8935
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
8545
8936
  "div",
8546
8937
  {
8547
8938
  ref: elRef,
@@ -8556,7 +8947,7 @@ function EditGlowChrome({
8556
8947
  zIndex: 2147483646
8557
8948
  },
8558
8949
  children: [
8559
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
8950
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
8560
8951
  "div",
8561
8952
  {
8562
8953
  style: {
@@ -8569,7 +8960,7 @@ function EditGlowChrome({
8569
8960
  }
8570
8961
  }
8571
8962
  ),
8572
- reorderHrefKey && !hideHandle && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
8963
+ reorderHrefKey && !hideHandle && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
8573
8964
  "div",
8574
8965
  {
8575
8966
  "data-ohw-drag-handle-container": "",
@@ -8581,7 +8972,7 @@ function EditGlowChrome({
8581
8972
  transform: "translate(calc(-100% - 7px), -50%)",
8582
8973
  pointerEvents: dragDisabled ? "none" : "auto"
8583
8974
  },
8584
- children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
8975
+ children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
8585
8976
  DragHandle,
8586
8977
  {
8587
8978
  "aria-label": `Reorder ${reorderHrefKey}`,
@@ -8682,6 +9073,79 @@ function resolveItemInteractionState(rect, parentScroll) {
8682
9073
  const { transform } = calcToolbarPos(rect, parentScroll, 120);
8683
9074
  return transform.includes("translateY(-100%)") ? "active-top" : "active-bottom";
8684
9075
  }
9076
+ function resolveSelectionStartElement(sel) {
9077
+ if (!sel || sel.rangeCount === 0) return null;
9078
+ const range = sel.getRangeAt(0);
9079
+ let startNode = range.startContainer;
9080
+ if (startNode.nodeType === Node.ELEMENT_NODE) {
9081
+ const el = startNode;
9082
+ startNode = el.childNodes[range.startOffset] ?? el.childNodes[range.startOffset - 1] ?? el;
9083
+ }
9084
+ return startNode.nodeType === Node.TEXT_NODE ? startNode.parentElement : startNode;
9085
+ }
9086
+ function detectActiveFormats(startEl, activeRoot) {
9087
+ const formats = /* @__PURE__ */ new Set();
9088
+ const cs = getComputedStyle(startEl);
9089
+ const weight = parseInt(cs.fontWeight, 10);
9090
+ let hasBold = cs.fontWeight === "bold" || !Number.isNaN(weight) && weight >= 600;
9091
+ let hasItalic = cs.fontStyle === "italic" || cs.fontStyle === "oblique";
9092
+ let hasUnderline = false;
9093
+ let hasStrike = false;
9094
+ let hasUl = false;
9095
+ let hasOl = false;
9096
+ for (let el = startEl; el; el = el.parentElement) {
9097
+ if (el.tagName === "B" || el.tagName === "STRONG") hasBold = true;
9098
+ if (el.tagName === "I" || el.tagName === "EM") hasItalic = true;
9099
+ const decoration = getComputedStyle(el).textDecorationLine;
9100
+ if (decoration.includes("underline") || el.tagName === "U" || el.tagName === "INS") hasUnderline = true;
9101
+ if (decoration.includes("line-through") || el.tagName === "S" || el.tagName === "STRIKE" || el.tagName === "DEL") hasStrike = true;
9102
+ if (el.tagName === "UL") hasUl = true;
9103
+ if (el.tagName === "OL") hasOl = true;
9104
+ if (el === activeRoot) break;
9105
+ }
9106
+ if (hasBold) formats.add("bold");
9107
+ if (hasItalic) formats.add("italic");
9108
+ if (hasUnderline) formats.add("underline");
9109
+ if (hasStrike) formats.add("strikeThrough");
9110
+ if (hasUl) formats.add("insertUnorderedList");
9111
+ if (hasOl) formats.add("insertOrderedList");
9112
+ const block = startEl.closest("div, p, h1, h2, h3, h4, h5, h6, li, td, th") ?? startEl;
9113
+ const align = getComputedStyle(block).textAlign;
9114
+ if (align === "center") formats.add("justifyCenter");
9115
+ else if (align === "right" || align === "end") formats.add("justifyRight");
9116
+ else formats.add("justifyLeft");
9117
+ return formats;
9118
+ }
9119
+ function setsEqual(a, b) {
9120
+ if (a.size !== b.size) return false;
9121
+ for (const item of a) {
9122
+ if (!b.has(item)) return false;
9123
+ }
9124
+ return true;
9125
+ }
9126
+ function textOffsetInRoot(root, node, offset) {
9127
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
9128
+ let total = 0;
9129
+ let current;
9130
+ while (current = walker.nextNode()) {
9131
+ if (current === node) return total + offset;
9132
+ total += (current.textContent ?? "").length;
9133
+ }
9134
+ return total;
9135
+ }
9136
+ function pointAtTextOffset(root, targetOffset) {
9137
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
9138
+ let total = 0;
9139
+ let current;
9140
+ let last = null;
9141
+ while (current = walker.nextNode()) {
9142
+ const len = (current.textContent ?? "").length;
9143
+ if (total + len >= targetOffset) return { node: current, offset: targetOffset - total };
9144
+ total += len;
9145
+ last = current;
9146
+ }
9147
+ return last ? { node: last, offset: (last.textContent ?? "").length } : null;
9148
+ }
8685
9149
  function FloatingToolbar({
8686
9150
  rect,
8687
9151
  parentScroll,
@@ -8718,7 +9182,7 @@ function FloatingToolbar({
8718
9182
  return () => ro.disconnect();
8719
9183
  }, [showEditLink, activeCommands]);
8720
9184
  const { top, left, transform } = calcToolbarPos(rect, parentScroll, measuredW);
8721
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
9185
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
8722
9186
  "div",
8723
9187
  {
8724
9188
  ref: setRefs,
@@ -8730,12 +9194,12 @@ function FloatingToolbar({
8730
9194
  zIndex: 2147483647,
8731
9195
  pointerEvents: "auto"
8732
9196
  },
8733
- children: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(CustomToolbar, { children: [
8734
- TOOLBAR_GROUPS.map((btns, gi) => /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_react9.default.Fragment, { children: [
8735
- gi > 0 && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(CustomToolbarDivider, {}),
9197
+ children: /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(CustomToolbar, { children: [
9198
+ TOOLBAR_GROUPS.map((btns, gi) => /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_react9.default.Fragment, { children: [
9199
+ gi > 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(CustomToolbarDivider, {}),
8736
9200
  btns.map((btn) => {
8737
9201
  const isActive = activeCommands.has(btn.cmd);
8738
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
9202
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
8739
9203
  CustomToolbarButton,
8740
9204
  {
8741
9205
  title: btn.title,
@@ -8744,7 +9208,7 @@ function FloatingToolbar({
8744
9208
  e.preventDefault();
8745
9209
  onCommand(btn.cmd);
8746
9210
  },
8747
- children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
9211
+ children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
8748
9212
  "svg",
8749
9213
  {
8750
9214
  width: "16",
@@ -8765,7 +9229,7 @@ function FloatingToolbar({
8765
9229
  );
8766
9230
  })
8767
9231
  ] }, gi)),
8768
- showEditLink ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
9232
+ showEditLink ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
8769
9233
  CustomToolbarButton,
8770
9234
  {
8771
9235
  type: "button",
@@ -8779,7 +9243,7 @@ function FloatingToolbar({
8779
9243
  e.preventDefault();
8780
9244
  e.stopPropagation();
8781
9245
  },
8782
- children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(import_lucide_react11.Link, { className: "size-4 shrink-0", "aria-hidden": true })
9246
+ children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react12.Link, { className: "size-4 shrink-0", "aria-hidden": true })
8783
9247
  }
8784
9248
  ) : null
8785
9249
  ] })
@@ -8796,7 +9260,7 @@ function StateToggle({
8796
9260
  states,
8797
9261
  onStateChange
8798
9262
  }) {
8799
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
9263
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
8800
9264
  ToggleGroup,
8801
9265
  {
8802
9266
  "data-ohw-state-toggle": "",
@@ -8810,7 +9274,7 @@ function StateToggle({
8810
9274
  left: rect.right - 8,
8811
9275
  transform: "translateX(-100%)"
8812
9276
  },
8813
- children: states.map((state) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ToggleGroupItem, { value: state, size: "sm", children: state }, state))
9277
+ children: states.map((state) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ToggleGroupItem, { value: state, size: "sm", children: state }, state))
8814
9278
  }
8815
9279
  );
8816
9280
  }
@@ -8874,6 +9338,7 @@ function OhhwellsBridge() {
8874
9338
  const [fetchState, setFetchState] = (0, import_react9.useState)("idle");
8875
9339
  const autoSaveTimers = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
8876
9340
  const activeElRef = (0, import_react9.useRef)(null);
9341
+ const pointerHeldRef = (0, import_react9.useRef)(false);
8877
9342
  const selectedElRef = (0, import_react9.useRef)(null);
8878
9343
  const selectedHrefKeyRef = (0, import_react9.useRef)(null);
8879
9344
  const selectedFooterColAttrRef = (0, import_react9.useRef)(null);
@@ -9058,6 +9523,7 @@ function OhhwellsBridge() {
9058
9523
  document.addEventListener("touchmove", preventBackgroundScroll, scrollOpts);
9059
9524
  postToParent2({ type: "ow:image-unhover" });
9060
9525
  return () => {
9526
+ document.documentElement.removeAttribute("data-ohw-link-popover-open");
9061
9527
  postToParent2({ type: "ow:link-modal-lock", locked: false });
9062
9528
  html.style.overflow = prevHtmlOverflow;
9063
9529
  body.style.overflow = prevBodyOverflow;
@@ -9304,6 +9770,18 @@ function OhhwellsBridge() {
9304
9770
  intent: "add-nav"
9305
9771
  });
9306
9772
  }, []);
9773
+ const handleAddFooterColumn = (0, import_react9.useCallback)(() => {
9774
+ deselectRef.current();
9775
+ deactivateRef.current();
9776
+ const result = addFooterColumnWithPersist({ postToParent: postToParent2 });
9777
+ editContentRef.current = {
9778
+ ...editContentRef.current,
9779
+ ...buildFooterColumnEditContentPatch(result)
9780
+ };
9781
+ requestAnimationFrame(() => {
9782
+ selectRef.current(result.firstLink);
9783
+ });
9784
+ }, [postToParent2]);
9307
9785
  const clearFooterDragVisuals = (0, import_react9.useCallback)(() => {
9308
9786
  footerDragRef.current = null;
9309
9787
  setSiblingHintRects([]);
@@ -9559,7 +10037,6 @@ function OhhwellsBridge() {
9559
10037
  if (e.button !== 0) return;
9560
10038
  const selected = selectedElRef.current;
9561
10039
  if (!selected) return;
9562
- armFooterPressDrag();
9563
10040
  const hrefKey = selected.getAttribute("data-ohw-href-key");
9564
10041
  if (hrefKey && isFooterHrefKey(hrefKey)) {
9565
10042
  footerPointerDragRef.current = {
@@ -9629,7 +10106,7 @@ function OhhwellsBridge() {
9629
10106
  const selectFrame = (0, import_react9.useCallback)((el) => {
9630
10107
  if (!isNavigationContainer(el)) return;
9631
10108
  if (activeElRef.current) deactivate();
9632
- const isFooterColumn = el.hasAttribute("data-ohw-footer-col") || Boolean(el.closest("footer") && isInferredFooterGroup(el));
10109
+ const isFooterColumn = !isFooterLinksContainer(el) && (el.hasAttribute("data-ohw-footer-col") || Boolean(el.closest("footer") && isInferredFooterGroup(el)));
9633
10110
  selectedElRef.current = el;
9634
10111
  selectedHrefKeyRef.current = null;
9635
10112
  selectedFooterColAttrRef.current = isFooterColumn ? el.getAttribute("data-ohw-footer-col") ?? String(listFooterColumns().indexOf(el)) : null;
@@ -9669,7 +10146,10 @@ function OhhwellsBridge() {
9669
10146
  setToolbarVariant("rich-text");
9670
10147
  siblingHintElRef.current = null;
9671
10148
  setSiblingHintRect(null);
10149
+ setSiblingHintRects([]);
9672
10150
  setIsItemDragging(false);
10151
+ const preActivationSelection = window.getSelection();
10152
+ const preservedRange = preActivationSelection && preActivationSelection.rangeCount > 0 && !preActivationSelection.isCollapsed && el.contains(preActivationSelection.getRangeAt(0).commonAncestorContainer) ? preActivationSelection.getRangeAt(0).cloneRange() : null;
9673
10153
  el.setAttribute("contenteditable", "true");
9674
10154
  el.setAttribute("data-ohw-editing", "");
9675
10155
  el.removeAttribute("data-ohw-hovered");
@@ -9677,7 +10157,11 @@ function OhhwellsBridge() {
9677
10157
  activeElRef.current = el;
9678
10158
  originalContentRef.current = el.innerHTML;
9679
10159
  el.focus({ preventScroll: true });
9680
- if (options?.caretX !== void 0 && options?.caretY !== void 0) {
10160
+ if (preservedRange) {
10161
+ const selection = window.getSelection();
10162
+ selection?.removeAllRanges();
10163
+ selection?.addRange(preservedRange);
10164
+ } else if (options?.caretX !== void 0 && options?.caretY !== void 0) {
9681
10165
  const { caretX, caretY } = options;
9682
10166
  placeCaretAtPoint(el, caretX, caretY);
9683
10167
  requestAnimationFrame(() => {
@@ -10133,7 +10617,9 @@ function OhhwellsBridge() {
10133
10617
  if (inActive || inActiveNav) {
10134
10618
  e.preventDefault();
10135
10619
  e.stopPropagation();
10136
- if (e.detail < 2) {
10620
+ const selection = window.getSelection();
10621
+ const hasRangeSelection = Boolean(selection && !selection.isCollapsed);
10622
+ if (e.detail < 2 && !hasRangeSelection) {
10137
10623
  placeCaretAtPoint(active, e.clientX, e.clientY);
10138
10624
  refreshActiveCommandsRef.current();
10139
10625
  }
@@ -10176,7 +10662,7 @@ function OhhwellsBridge() {
10176
10662
  }
10177
10663
  e.preventDefault();
10178
10664
  e.stopPropagation();
10179
- activateRef.current(editable);
10665
+ activateRef.current(editable, { caretX: e.clientX, caretY: e.clientY });
10180
10666
  return;
10181
10667
  }
10182
10668
  const hrefAnchor = getNavigationItemAnchor(target);
@@ -10203,6 +10689,18 @@ function OhhwellsBridge() {
10203
10689
  selectFrameRef.current(footerColClick);
10204
10690
  return;
10205
10691
  }
10692
+ const footerLinksToSelect = resolveFooterLinksContainerSelectionTarget(
10693
+ target,
10694
+ e.clientX,
10695
+ e.clientY,
10696
+ isPointOverNavItem
10697
+ );
10698
+ if (footerLinksToSelect) {
10699
+ e.preventDefault();
10700
+ e.stopPropagation();
10701
+ selectFrameRef.current(footerLinksToSelect);
10702
+ return;
10703
+ }
10206
10704
  const navContainerToSelect = resolveNavContainerSelectionTarget(target, e.clientX, e.clientY);
10207
10705
  if (navContainerToSelect) {
10208
10706
  e.preventDefault();
@@ -10261,8 +10759,8 @@ function OhhwellsBridge() {
10261
10759
  return;
10262
10760
  }
10263
10761
  const navLabel = getNavigationLabelEditable(target);
10264
- if (!navLabel) return;
10265
- const { editable } = navLabel;
10762
+ const editable = navLabel?.editable ?? target.closest('[data-ohw-editable="text"], [data-ohw-editable="plain"]');
10763
+ if (!editable || isMediaEditable(editable) || editable.dataset.ohwEditable === "link") return;
10266
10764
  e.preventDefault();
10267
10765
  e.stopPropagation();
10268
10766
  if (activeElRef.current !== editable) {
@@ -10283,16 +10781,38 @@ function OhhwellsBridge() {
10283
10781
  setHoveredNavContainerRect(null);
10284
10782
  return;
10285
10783
  }
10286
- if (toolbarVariantRef.current !== "select-frame") {
10287
- const navContainer = target.closest("[data-ohw-nav-container]");
10288
- if (navContainer && !getNavigationItemAnchor(target)) {
10289
- hoveredNavContainerRef.current = navContainer;
10290
- setHoveredNavContainerRect(navContainer.getBoundingClientRect());
10291
- hoveredItemElRef.current = null;
10292
- setHoveredItemRect(null);
10293
- return;
10784
+ {
10785
+ const selected2 = selectedElRef.current;
10786
+ const selectedIsFooterColumn = Boolean(selected2) && !isFooterLinksContainer(selected2) && (selected2.hasAttribute("data-ohw-footer-col") || selected2.hasAttribute("data-ohw-footer-column") || Boolean(selected2.closest("footer") && isInferredFooterGroup(selected2)));
10787
+ const allowNavContainerHover = toolbarVariantRef.current !== "select-frame";
10788
+ const allowFooterLinksHover = toolbarVariantRef.current !== "select-frame" || selectedIsFooterColumn;
10789
+ if (allowNavContainerHover) {
10790
+ const navContainer = target.closest("[data-ohw-nav-container]");
10791
+ if (navContainer && !getNavigationItemAnchor(target)) {
10792
+ hoveredNavContainerRef.current = navContainer;
10793
+ setHoveredNavContainerRect(navContainer.getBoundingClientRect());
10794
+ hoveredItemElRef.current = null;
10795
+ setHoveredItemRect(null);
10796
+ return;
10797
+ }
10294
10798
  }
10295
- if (!target.closest("[data-ohw-nav-container]")) {
10799
+ if (allowFooterLinksHover) {
10800
+ const navContainer = target.closest("[data-ohw-nav-container]");
10801
+ if (!navContainer) {
10802
+ const footerLinks = target.closest("[data-ohw-footer-links], .rb-footer-links") ?? null;
10803
+ if (footerLinks && selected2 !== footerLinks && !getNavigationItemAnchor(target) && !target.closest("[data-ohw-footer-col], [data-ohw-footer-column]")) {
10804
+ hoveredNavContainerRef.current = footerLinks;
10805
+ setHoveredNavContainerRect(footerLinks.getBoundingClientRect());
10806
+ hoveredItemElRef.current = null;
10807
+ setHoveredItemRect(null);
10808
+ return;
10809
+ }
10810
+ if (!footerLinks) {
10811
+ hoveredNavContainerRef.current = null;
10812
+ setHoveredNavContainerRect(null);
10813
+ }
10814
+ }
10815
+ } else if (toolbarVariantRef.current === "select-frame") {
10296
10816
  hoveredNavContainerRef.current = null;
10297
10817
  setHoveredNavContainerRect(null);
10298
10818
  }
@@ -10334,9 +10854,9 @@ function OhhwellsBridge() {
10334
10854
  };
10335
10855
  const handleMouseOut = (e) => {
10336
10856
  const target = e.target;
10337
- if (target.closest("[data-ohw-nav-container], [data-ohw-footer-col], [data-ohw-footer-column]")) {
10857
+ if (target.closest("[data-ohw-nav-container], [data-ohw-footer-col], [data-ohw-footer-column], [data-ohw-footer-links], .rb-footer-links")) {
10338
10858
  const related2 = e.relatedTarget instanceof Element ? e.relatedTarget : null;
10339
- if (related2?.closest("[data-ohw-nav-container], [data-ohw-footer-col], [data-ohw-footer-column], [data-ohw-navbar-container-chrome], [data-ohw-navbar-add-button]")) {
10859
+ if (related2?.closest("[data-ohw-nav-container], [data-ohw-footer-col], [data-ohw-footer-column], [data-ohw-footer-links], .rb-footer-links, [data-ohw-navbar-container-chrome], [data-ohw-navbar-add-button], [data-ohw-footer-container-chrome], [data-ohw-footer-add-button]")) {
10340
10860
  return;
10341
10861
  }
10342
10862
  hoveredNavContainerRef.current = null;
@@ -10431,6 +10951,15 @@ function OhhwellsBridge() {
10431
10951
  if (track) track.setAttribute("data-ohw-hover-paused", "");
10432
10952
  };
10433
10953
  const clearImageHover = () => setMediaHover(null);
10954
+ const dismissImageHover = () => {
10955
+ if (hoveredImageRef.current) {
10956
+ hoveredImageRef.current = null;
10957
+ hoveredImageHasTextOverlapRef.current = false;
10958
+ resumeAnimTracks();
10959
+ postToParentRef.current({ type: "ow:image-unhover" });
10960
+ }
10961
+ clearImageHover();
10962
+ };
10434
10963
  const findImageAtPoint = (clientX, clientY, fromParentViewport) => {
10435
10964
  const { x, y } = toProbeCoords(clientX, clientY, fromParentViewport);
10436
10965
  const images = Array.from(document.querySelectorAll(MEDIA_SELECTOR));
@@ -10474,16 +11003,13 @@ function OhhwellsBridge() {
10474
11003
  }
10475
11004
  return smallest;
10476
11005
  };
10477
- const dismissImageHover = () => {
10478
- if (hoveredImageRef.current) {
10479
- hoveredImageRef.current = null;
10480
- hoveredImageHasTextOverlapRef.current = false;
10481
- resumeAnimTracks();
10482
- postToParentRef.current({ type: "ow:image-unhover" });
10483
- }
10484
- };
10485
11006
  const probeNavigationHoverAt = (x, y) => {
10486
- if (toolbarVariantRef.current === "select-frame") {
11007
+ const selected = selectedElRef.current;
11008
+ const selectedIsFooterColumn = Boolean(selected) && !isFooterLinksContainer(selected) && (selected.hasAttribute("data-ohw-footer-col") || selected.hasAttribute("data-ohw-footer-column") || Boolean(selected.closest("footer") && isInferredFooterGroup(selected)));
11009
+ const selectedIsFooterLinks = Boolean(selected && isFooterLinksContainer(selected));
11010
+ const allowNavContainerHover = toolbarVariantRef.current !== "select-frame";
11011
+ const allowFooterLinksHover = (toolbarVariantRef.current !== "select-frame" || selectedIsFooterColumn) && !selectedIsFooterLinks;
11012
+ if (toolbarVariantRef.current === "select-frame" && !allowFooterLinksHover) {
10487
11013
  hoveredNavContainerRef.current = null;
10488
11014
  setHoveredNavContainerRect(null);
10489
11015
  }
@@ -10505,7 +11031,6 @@ function OhhwellsBridge() {
10505
11031
  if (navItemHit) {
10506
11032
  hoveredNavContainerRef.current = null;
10507
11033
  setHoveredNavContainerRect(null);
10508
- const selected = selectedElRef.current;
10509
11034
  if (selected !== navItemHit) {
10510
11035
  clearHrefKeyHover(navItemHit);
10511
11036
  hoveredItemElRef.current = navItemHit;
@@ -10517,7 +11042,7 @@ function OhhwellsBridge() {
10517
11042
  return;
10518
11043
  }
10519
11044
  }
10520
- if (toolbarVariantRef.current !== "select-frame") {
11045
+ if (allowNavContainerHover) {
10521
11046
  for (const container of navContainers) {
10522
11047
  const containerRect = container.getBoundingClientRect();
10523
11048
  const overContainer = x >= containerRect.left && x <= containerRect.right && y >= containerRect.top && y <= containerRect.bottom;
@@ -10529,12 +11054,23 @@ function OhhwellsBridge() {
10529
11054
  setHoveredItemRect(null);
10530
11055
  return;
10531
11056
  }
10532
- hoveredNavContainerRef.current = null;
10533
- setHoveredNavContainerRect(null);
10534
- } else {
10535
- hoveredNavContainerRef.current = null;
10536
- setHoveredNavContainerRect(null);
10537
11057
  }
11058
+ if (allowFooterLinksHover) {
11059
+ const footerLinks = getFooterLinksContainer();
11060
+ if (footerLinks) {
11061
+ const containerRect = footerLinks.getBoundingClientRect();
11062
+ const overContainer = x >= containerRect.left && x <= containerRect.right && y >= containerRect.top && y <= containerRect.bottom;
11063
+ if (overContainer && !isPointOverNavItem(footerLinks, x, y) && !isPointOverFooterColumn(x, y)) {
11064
+ hoveredNavContainerRef.current = footerLinks;
11065
+ setHoveredNavContainerRect(footerLinks.getBoundingClientRect());
11066
+ hoveredItemElRef.current = null;
11067
+ setHoveredItemRect(null);
11068
+ return;
11069
+ }
11070
+ }
11071
+ }
11072
+ hoveredNavContainerRef.current = null;
11073
+ setHoveredNavContainerRect(null);
10538
11074
  for (const column of footerColumns) {
10539
11075
  const containerRect = column.getBoundingClientRect();
10540
11076
  const overContainer = x >= containerRect.left && x <= containerRect.right && y >= containerRect.top && y <= containerRect.bottom;
@@ -11041,6 +11577,7 @@ function OhhwellsBridge() {
11041
11577
  }
11042
11578
  editContentRef.current = { ...editContentRef.current, ...content };
11043
11579
  reconcileNavbarItemsFromContent(editContentRef.current);
11580
+ reconcileFooterOrderFromContent(editContentRef.current);
11044
11581
  syncNavigationDragCursorAttrs();
11045
11582
  enforceLinkHrefs();
11046
11583
  postToParentRef.current({ type: "ow:hydrate-done" });
@@ -11095,6 +11632,11 @@ function OhhwellsBridge() {
11095
11632
  }
11096
11633
  if (selectedElRef.current) {
11097
11634
  if (toolbarVariantRef.current === "select-frame") {
11635
+ const parent2 = getNavigationSelectionParent(selectedElRef.current);
11636
+ if (parent2 && isNavigationContainer(parent2) && isFooterLinksContainer(parent2) && !isFooterLinksContainer(selectedElRef.current)) {
11637
+ selectFrameRef.current(parent2);
11638
+ return;
11639
+ }
11098
11640
  deselectRef.current();
11099
11641
  return;
11100
11642
  }
@@ -11110,13 +11652,10 @@ function OhhwellsBridge() {
11110
11652
  const handleKeyDown = (e) => {
11111
11653
  if (e.key === "Escape" && document.querySelector("[data-ohw-section-picker]")) return;
11112
11654
  if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a" && activeElRef.current) {
11113
- const navAnchor = getNavigationItemAnchor(activeElRef.current);
11114
- if (navAnchor) {
11115
- e.preventDefault();
11116
- selectAllTextInEditable(activeElRef.current);
11117
- refreshActiveCommandsRef.current();
11118
- return;
11119
- }
11655
+ e.preventDefault();
11656
+ selectAllTextInEditable(activeElRef.current);
11657
+ refreshActiveCommandsRef.current();
11658
+ return;
11120
11659
  }
11121
11660
  if (e.key === "Escape" && linkPopoverOpenRef.current) {
11122
11661
  setLinkPopoverRef.current(null);
@@ -11124,6 +11663,12 @@ function OhhwellsBridge() {
11124
11663
  }
11125
11664
  if (e.key === "Escape" && selectedElRef.current && !activeElRef.current) {
11126
11665
  if (toolbarVariantRef.current === "select-frame") {
11666
+ const parent2 = getNavigationSelectionParent(selectedElRef.current);
11667
+ if (parent2 && isNavigationContainer(parent2) && isFooterLinksContainer(parent2) && !isFooterLinksContainer(selectedElRef.current)) {
11668
+ e.preventDefault();
11669
+ selectFrameRef.current(parent2);
11670
+ return;
11671
+ }
11127
11672
  deselectRef.current();
11128
11673
  return;
11129
11674
  }
@@ -11286,30 +11831,30 @@ function OhhwellsBridge() {
11286
11831
  const h = document.documentElement.scrollHeight;
11287
11832
  if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
11288
11833
  };
11834
+ let selectionChangeRaf = null;
11835
+ const runSelectionChange = () => {
11836
+ selectionChangeRaf = null;
11837
+ const activeRoot = activeElRef.current;
11838
+ if (!activeRoot) return;
11839
+ const startEl = resolveSelectionStartElement(window.getSelection());
11840
+ const next = startEl ? detectActiveFormats(startEl, activeRoot) : /* @__PURE__ */ new Set();
11841
+ setActiveCommands((prev) => setsEqual(prev, next) ? prev : next);
11842
+ };
11289
11843
  const handleSelectionChange = () => {
11290
- if (!activeElRef.current) return;
11291
- const next = /* @__PURE__ */ new Set();
11292
- for (const cmd of ["bold", "italic", "underline", "strikeThrough", "insertUnorderedList", "insertOrderedList"]) {
11293
- try {
11294
- if (document.queryCommandState(cmd)) next.add(cmd);
11295
- } catch {
11296
- }
11297
- }
11298
- const sel = window.getSelection();
11299
- const anchor = sel?.anchorNode;
11300
- if (anchor) {
11301
- const el = anchor.nodeType === Node.TEXT_NODE ? anchor.parentElement : anchor;
11302
- const block = el?.closest("div, p, h1, h2, h3, h4, h5, h6, li, td, th") ?? el;
11303
- if (block) {
11304
- const align = getComputedStyle(block).textAlign;
11305
- if (align === "center") next.add("justifyCenter");
11306
- else if (align === "right" || align === "end") next.add("justifyRight");
11307
- else next.add("justifyLeft");
11308
- }
11309
- }
11310
- setActiveCommands(next);
11844
+ if (pointerHeldRef.current) return;
11845
+ if (selectionChangeRaf !== null) return;
11846
+ selectionChangeRaf = requestAnimationFrame(runSelectionChange);
11847
+ };
11848
+ const markPointerHeld = (e) => {
11849
+ if (e.button !== 0) return;
11850
+ pointerHeldRef.current = true;
11311
11851
  };
11312
- refreshActiveCommandsRef.current = handleSelectionChange;
11852
+ const markPointerReleased = () => {
11853
+ if (!pointerHeldRef.current) return;
11854
+ pointerHeldRef.current = false;
11855
+ handleSelectionChange();
11856
+ };
11857
+ refreshActiveCommandsRef.current = runSelectionChange;
11313
11858
  const handleDocMouseLeave = () => {
11314
11859
  hoveredImageRef.current = null;
11315
11860
  setMediaHover(null);
@@ -11549,7 +12094,6 @@ function OhhwellsBridge() {
11549
12094
  const anchor = getNavigationItemAnchor(target);
11550
12095
  const hrefKey = anchor?.getAttribute("data-ohw-href-key") ?? null;
11551
12096
  if (anchor && isFooterHrefKey(hrefKey)) {
11552
- armFooterPressDrag();
11553
12097
  footerPointerDragRef.current = {
11554
12098
  el: anchor,
11555
12099
  kind: "link",
@@ -11563,7 +12107,6 @@ function OhhwellsBridge() {
11563
12107
  }
11564
12108
  const col = target.closest("[data-ohw-footer-col]");
11565
12109
  if (col && !getNavigationItemAnchor(target)) {
11566
- armFooterPressDrag();
11567
12110
  footerPointerDragRef.current = {
11568
12111
  el: col,
11569
12112
  kind: "column",
@@ -11592,6 +12135,7 @@ function OhhwellsBridge() {
11592
12135
  if (dx * dx + dy * dy < THRESHOLD * THRESHOLD) return;
11593
12136
  e.preventDefault();
11594
12137
  pending.started = true;
12138
+ armFooterPressDrag();
11595
12139
  clearTextSelection();
11596
12140
  try {
11597
12141
  document.body.setPointerCapture(pending.pointerId);
@@ -11645,7 +12189,8 @@ function OhhwellsBridge() {
11645
12189
  }
11646
12190
  } catch {
11647
12191
  }
11648
- if (!pending?.started) {
12192
+ if (!pending) return;
12193
+ if (!pending.started) {
11649
12194
  unlockFooterDragInteraction();
11650
12195
  return;
11651
12196
  }
@@ -11738,9 +12283,39 @@ function OhhwellsBridge() {
11738
12283
  return () => window.removeEventListener("hashchange", onHashChange);
11739
12284
  }, [pathname]);
11740
12285
  const handleCommand = (0, import_react9.useCallback)((cmd) => {
12286
+ const el = activeElRef.current;
12287
+ const selBefore = window.getSelection();
12288
+ let savedOffsets = null;
12289
+ if (el && selBefore && selBefore.rangeCount > 0 && !selBefore.isCollapsed) {
12290
+ const r2 = selBefore.getRangeAt(0);
12291
+ if (el.contains(r2.commonAncestorContainer)) {
12292
+ savedOffsets = {
12293
+ start: textOffsetInRoot(el, r2.startContainer, r2.startOffset),
12294
+ end: textOffsetInRoot(el, r2.endContainer, r2.endOffset)
12295
+ };
12296
+ }
12297
+ }
12298
+ document.execCommand("styleWithCSS", false, true);
11741
12299
  document.execCommand(cmd, false);
11742
- activeElRef.current?.focus();
11743
- if (activeElRef.current) setToolbarRect(getEditMeasureEl(activeElRef.current).getBoundingClientRect());
12300
+ if (el && document.activeElement !== el) {
12301
+ el.focus();
12302
+ }
12303
+ const selAfter = window.getSelection();
12304
+ if (el && savedOffsets && selAfter?.isCollapsed) {
12305
+ const start = pointAtTextOffset(el, savedOffsets.start);
12306
+ const end = pointAtTextOffset(el, savedOffsets.end);
12307
+ if (start && end) {
12308
+ try {
12309
+ const range = document.createRange();
12310
+ range.setStart(start.node, start.offset);
12311
+ range.setEnd(end.node, end.offset);
12312
+ selAfter.removeAllRanges();
12313
+ selAfter.addRange(range);
12314
+ } catch {
12315
+ }
12316
+ }
12317
+ }
12318
+ if (el) setToolbarRect(getEditMeasureEl(el).getBoundingClientRect());
11744
12319
  refreshActiveCommandsRef.current();
11745
12320
  }, []);
11746
12321
  const handleStateChange = (0, import_react9.useCallback)((state) => {
@@ -11880,9 +12455,9 @@ function OhhwellsBridge() {
11880
12455
  [postToParent2]
11881
12456
  );
11882
12457
  return bridgeRoot ? (0, import_react_dom3.createPortal)(
11883
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
11884
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { ref: attachVisibleViewport, "data-ohw-visible-viewport": "", "aria-hidden": true }),
11885
- Object.entries(uploadingRects).map(([key, { rect, fadingOut }]) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
12458
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
12459
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { ref: attachVisibleViewport, "data-ohw-visible-viewport": "", "aria-hidden": true }),
12460
+ Object.entries(uploadingRects).map(([key, { rect, fadingOut }]) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
11886
12461
  MediaOverlay,
11887
12462
  {
11888
12463
  hover: { key, rect, elementType: "image", isDragOver: false, hasTextOverlap: false },
@@ -11893,7 +12468,7 @@ function OhhwellsBridge() {
11893
12468
  },
11894
12469
  `uploading-${key}`
11895
12470
  )),
11896
- mediaHover && !(mediaHover.key in uploadingRects) && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
12471
+ mediaHover && !(mediaHover.key in uploadingRects) && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
11897
12472
  MediaOverlay,
11898
12473
  {
11899
12474
  hover: mediaHover,
@@ -11902,10 +12477,10 @@ function OhhwellsBridge() {
11902
12477
  onVideoSettingsChange: handleVideoSettingsChange
11903
12478
  }
11904
12479
  ),
11905
- siblingHintRect && !linkPopover && !isItemDragging && siblingHintRects.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ItemInteractionLayer, { rect: siblingHintRect, state: "sibling-hint" }),
11906
- siblingHintRects.length > 0 && !linkPopover && !isItemDragging && siblingHintRects.map((rect, i) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ItemInteractionLayer, { rect, state: "sibling-hint" }, `sibling-hint-${i}`)),
11907
- isItemDragging && draggedItemRect && selectedElRef.current !== footerDragRef.current?.draggedEl && selectedElRef.current !== navDragRef.current?.draggedEl && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ItemInteractionLayer, { rect: draggedItemRect, state: "dragging" }),
11908
- isItemDragging && footerDropSlots.map((slot, i) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
12480
+ siblingHintRect && !linkPopover && !isItemDragging && siblingHintRects.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ItemInteractionLayer, { rect: siblingHintRect, state: "sibling-hint" }),
12481
+ siblingHintRects.length > 0 && !linkPopover && !isItemDragging && siblingHintRects.map((rect, i) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ItemInteractionLayer, { rect, state: "sibling-hint" }, `sibling-hint-${i}`)),
12482
+ isItemDragging && draggedItemRect && selectedElRef.current !== footerDragRef.current?.draggedEl && selectedElRef.current !== navDragRef.current?.draggedEl && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ItemInteractionLayer, { rect: draggedItemRect, state: "dragging" }),
12483
+ isItemDragging && footerDropSlots.map((slot, i) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
11909
12484
  "div",
11910
12485
  {
11911
12486
  className: "pointer-events-none fixed z-2147483646",
@@ -11915,7 +12490,7 @@ function OhhwellsBridge() {
11915
12490
  width: slot.width,
11916
12491
  height: slot.height
11917
12492
  },
11918
- children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
12493
+ children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
11919
12494
  DropIndicator,
11920
12495
  {
11921
12496
  direction: slot.direction,
@@ -11926,7 +12501,7 @@ function OhhwellsBridge() {
11926
12501
  },
11927
12502
  `footer-drop-${slot.direction}-${slot.columnIndex}-${slot.insertIndex}-${i}`
11928
12503
  )),
11929
- isItemDragging && navDropSlots.map((slot, i) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
12504
+ isItemDragging && navDropSlots.map((slot, i) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
11930
12505
  "div",
11931
12506
  {
11932
12507
  className: "pointer-events-none fixed z-2147483646",
@@ -11936,7 +12511,7 @@ function OhhwellsBridge() {
11936
12511
  width: slot.width,
11937
12512
  height: slot.height
11938
12513
  },
11939
- children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
12514
+ children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
11940
12515
  DropIndicator,
11941
12516
  {
11942
12517
  direction: slot.direction,
@@ -11947,10 +12522,11 @@ function OhhwellsBridge() {
11947
12522
  },
11948
12523
  `nav-drop-${slot.direction}-${slot.parentId ?? "root"}-${slot.insertIndex}-${i}`
11949
12524
  )),
11950
- hoveredNavContainerRect && toolbarVariant !== "select-frame" && !linkPopover && !isItemDragging && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ItemInteractionLayer, { rect: hoveredNavContainerRect, state: "hover" }),
11951
- hoveredItemRect && !linkPopover && !hoveredNavContainerRect && !isItemDragging && hoveredItemElRef.current !== selectedElRef.current && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ItemInteractionLayer, { rect: hoveredItemRect, state: "hover" }),
11952
- toolbarVariant === "select-frame" && toolbarRect && !linkPopover && !isItemDragging && !isFooterFrameSelection && selectedElRef.current && isNavbarLinksContainer(selectedElRef.current) && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(NavbarContainerChrome, { rect: toolbarRect, onAdd: handleAddTopLevelNavItem }),
11953
- toolbarRect && !linkPopover && (toolbarVariant === "link-action" || toolbarVariant === "select-frame") && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
12525
+ hoveredNavContainerRect && (toolbarVariant !== "select-frame" || isFooterFrameSelection) && !linkPopover && !isItemDragging && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ItemInteractionLayer, { rect: hoveredNavContainerRect, state: "hover" }),
12526
+ hoveredItemRect && !linkPopover && !hoveredNavContainerRect && !isItemDragging && hoveredItemElRef.current !== selectedElRef.current && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ItemInteractionLayer, { rect: hoveredItemRect, state: "hover" }),
12527
+ toolbarVariant === "select-frame" && toolbarRect && !linkPopover && !isItemDragging && !isFooterFrameSelection && selectedElRef.current && isNavbarLinksContainer(selectedElRef.current) && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(NavbarContainerChrome, { rect: toolbarRect, onAdd: handleAddTopLevelNavItem }),
12528
+ toolbarVariant === "select-frame" && toolbarRect && !linkPopover && !isItemDragging && !isFooterFrameSelection && selectedElRef.current && isFooterLinksContainer(selectedElRef.current) && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(FooterContainerChrome, { rect: toolbarRect, onAdd: handleAddFooterColumn }),
12529
+ toolbarRect && !linkPopover && (toolbarVariant === "link-action" || toolbarVariant === "select-frame") && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
11954
12530
  ItemInteractionLayer,
11955
12531
  {
11956
12532
  rect: isItemDragging && draggedItemRect && (footerDragRef.current?.wasSelected || navDragRef.current?.wasSelected) ? draggedItemRect : toolbarRect,
@@ -11964,7 +12540,7 @@ function OhhwellsBridge() {
11964
12540
  onItemPointerDown: handleItemChromePointerDown,
11965
12541
  onItemClick: handleItemChromeClick,
11966
12542
  itemDragSurface: !isFooterFrameSelection,
11967
- toolbar: toolbarVariant === "link-action" && !isItemDragging ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
12543
+ toolbar: toolbarVariant === "link-action" && !isItemDragging ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
11968
12544
  ItemActionToolbar,
11969
12545
  {
11970
12546
  onEditLink: openLinkPopoverForSelected,
@@ -11977,8 +12553,8 @@ function OhhwellsBridge() {
11977
12553
  ) : void 0
11978
12554
  }
11979
12555
  ),
11980
- toolbarRect && toolbarVariant === "rich-text" && !linkPopover && /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
11981
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
12556
+ toolbarRect && toolbarVariant === "rich-text" && !linkPopover && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
12557
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
11982
12558
  EditGlowChrome,
11983
12559
  {
11984
12560
  rect: toolbarRect,
@@ -11988,7 +12564,7 @@ function OhhwellsBridge() {
11988
12564
  hideHandle: isItemDragging
11989
12565
  }
11990
12566
  ),
11991
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
12567
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
11992
12568
  FloatingToolbar,
11993
12569
  {
11994
12570
  rect: toolbarRect,
@@ -12001,7 +12577,7 @@ function OhhwellsBridge() {
12001
12577
  }
12002
12578
  )
12003
12579
  ] }),
12004
- maxBadge && /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
12580
+ maxBadge && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
12005
12581
  "div",
12006
12582
  {
12007
12583
  "data-ohw-max-badge": "",
@@ -12027,7 +12603,7 @@ function OhhwellsBridge() {
12027
12603
  ]
12028
12604
  }
12029
12605
  ),
12030
- toggleState && !linkPopover && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
12606
+ toggleState && !linkPopover && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
12031
12607
  StateToggle,
12032
12608
  {
12033
12609
  rect: toggleState.rect,
@@ -12036,15 +12612,15 @@ function OhhwellsBridge() {
12036
12612
  onStateChange: handleStateChange
12037
12613
  }
12038
12614
  ),
12039
- sectionGap && !linkPopover && /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
12615
+ sectionGap && !linkPopover && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
12040
12616
  "div",
12041
12617
  {
12042
12618
  "data-ohw-section-insert-line": "",
12043
12619
  className: "fixed left-0 w-full z-2147483646 flex items-center pointer-events-none",
12044
12620
  style: { top: sectionGap.y, transform: "translateY(-50%)" },
12045
12621
  children: [
12046
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } }),
12047
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
12622
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } }),
12623
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
12048
12624
  Badge,
12049
12625
  {
12050
12626
  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",
@@ -12061,11 +12637,11 @@ function OhhwellsBridge() {
12061
12637
  children: "Add Section"
12062
12638
  }
12063
12639
  ),
12064
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } })
12640
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } })
12065
12641
  ]
12066
12642
  }
12067
12643
  ),
12068
- linkPopover && dialogPortalContainer ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
12644
+ linkPopover && dialogPortalContainer ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
12069
12645
  LinkPopover,
12070
12646
  {
12071
12647
  panelRef: linkPopoverPanelRef,