@ohhwells/bridge 0.1.25 → 0.1.27-next.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  // src/OhhwellsBridge.tsx
4
- import React5, { useCallback as useCallback2, useEffect as useEffect4, useLayoutEffect, useRef as useRef3, useState as useState5 } from "react";
4
+ import React5, { useCallback as useCallback2, useEffect as useEffect3, useLayoutEffect, useRef as useRef3, useState as useState5 } from "react";
5
5
  import { createRoot } from "react-dom/client";
6
6
  import { flushSync } from "react-dom";
7
7
 
@@ -4175,6 +4175,252 @@ function isEditSessionActive() {
4175
4175
  return new URLSearchParams(q).get("mode") === "edit";
4176
4176
  }
4177
4177
 
4178
+ // src/lib/sections.ts
4179
+ function titleCaseSectionId(id) {
4180
+ return id.split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
4181
+ }
4182
+ function parseSectionsFromRoot(root) {
4183
+ const seen = /* @__PURE__ */ new Set();
4184
+ const sections = [];
4185
+ for (const el of root.querySelectorAll("[data-ohw-section]")) {
4186
+ const id = el.getAttribute("data-ohw-section") ?? "";
4187
+ if (!id || seen.has(id)) continue;
4188
+ seen.add(id);
4189
+ const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
4190
+ sections.push({ id, label });
4191
+ }
4192
+ return sections;
4193
+ }
4194
+ function collectSectionsFromDom() {
4195
+ if (typeof document === "undefined") return [];
4196
+ return parseSectionsFromRoot(document);
4197
+ }
4198
+ function parseSectionsFromHtml(html) {
4199
+ const doc = new DOMParser().parseFromString(html, "text/html");
4200
+ return parseSectionsFromRoot(doc);
4201
+ }
4202
+
4203
+ // src/ui/link-modal/linkModal.utils.ts
4204
+ function parseTarget(target) {
4205
+ if (!target) return { pageRoute: "", sectionId: null };
4206
+ const hashIndex = target.indexOf("#");
4207
+ if (hashIndex === -1) return { pageRoute: target, sectionId: null };
4208
+ return {
4209
+ pageRoute: target.slice(0, hashIndex),
4210
+ sectionId: target.slice(hashIndex + 1) || null
4211
+ };
4212
+ }
4213
+ function normalizePath(path) {
4214
+ const trimmed = path.trim();
4215
+ if (!trimmed) return "/";
4216
+ try {
4217
+ if (/^https?:\/\//i.test(trimmed)) {
4218
+ return new URL(trimmed).pathname || "/";
4219
+ }
4220
+ } catch {
4221
+ }
4222
+ const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
4223
+ if (withSlash.length > 1 && withSlash.endsWith("/")) return withSlash.slice(0, -1);
4224
+ return withSlash;
4225
+ }
4226
+ function findPageByPath(pages, path) {
4227
+ const normalized = normalizePath(path);
4228
+ return pages.find((p) => normalizePath(p.path) === normalized);
4229
+ }
4230
+ function inferPageTitleFromPath(path) {
4231
+ const normalized = normalizePath(path);
4232
+ if (normalized === "/") return "Home";
4233
+ const segment = normalized.split("/").filter(Boolean).pop() ?? "";
4234
+ if (!segment) return "Home";
4235
+ return segment.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
4236
+ }
4237
+ function isInternalPath(route) {
4238
+ const trimmed = route.trim();
4239
+ if (!trimmed) return false;
4240
+ if (/^https?:\/\//i.test(trimmed)) return false;
4241
+ if (trimmed.startsWith("/")) return true;
4242
+ if (trimmed.includes(" ") || trimmed.includes(".")) return false;
4243
+ return true;
4244
+ }
4245
+ function resolvePage(route, pages) {
4246
+ const found = findPageByPath(pages, route);
4247
+ if (found) return { path: found.path, title: found.title };
4248
+ return { path: normalizePath(route), title: inferPageTitleFromPath(route) };
4249
+ }
4250
+ function getSectionsForPath(sectionsByPath, pageRoute) {
4251
+ if (!sectionsByPath) return [];
4252
+ const normalized = normalizePath(pageRoute);
4253
+ return sectionsByPath[normalized] ?? sectionsByPath[pageRoute] ?? [];
4254
+ }
4255
+ function buildTarget(page, section) {
4256
+ if (section) return `${page.path}#${section.id}`;
4257
+ return page.path;
4258
+ }
4259
+ function isValidUrl(urlString) {
4260
+ try {
4261
+ const url = new URL(urlString);
4262
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
4263
+ const hostname = url.hostname;
4264
+ if (!hostname || hostname.length === 0) return false;
4265
+ if (hostname.includes(" ")) return false;
4266
+ return hostname.includes(".") || hostname === "localhost";
4267
+ } catch {
4268
+ return false;
4269
+ }
4270
+ }
4271
+ function normalizeUrl(value) {
4272
+ const trimmed = value.trim();
4273
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
4274
+ return `https://${trimmed}`;
4275
+ }
4276
+ function validateUrlInput(value, pages, allPages) {
4277
+ if (!value.trim()) return "";
4278
+ const filtered = pages.filter((p) => p.title.toLowerCase().startsWith(value.toLowerCase()));
4279
+ if (filtered.length > 0) return "";
4280
+ const matchingPage = allPages.find((p) => p.title.toLowerCase() === value.toLowerCase());
4281
+ if (matchingPage) return "";
4282
+ if (!isValidUrl(normalizeUrl(value))) {
4283
+ return "Please enter a valid URL (e.g., https://example.com)";
4284
+ }
4285
+ return "";
4286
+ }
4287
+ function getEditModeInitialState(target, pages, sections) {
4288
+ const empty = {
4289
+ searchValue: "",
4290
+ selectedPage: null,
4291
+ selectedSection: null,
4292
+ step: "input"
4293
+ };
4294
+ if (!target) return empty;
4295
+ if (isValidUrl(normalizeUrl(target))) {
4296
+ return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
4297
+ }
4298
+ const { pageRoute, sectionId } = parseTarget(target);
4299
+ if (sectionId) {
4300
+ if (isInternalPath(pageRoute)) {
4301
+ const page = resolvePage(pageRoute, pages);
4302
+ const sec = sections.find((s) => s.id === sectionId);
4303
+ return {
4304
+ searchValue: page.title,
4305
+ selectedPage: page,
4306
+ selectedSection: sec ? { id: sec.id, label: sec.label } : null,
4307
+ step: "input"
4308
+ };
4309
+ }
4310
+ return { searchValue: pageRoute || target, selectedPage: null, selectedSection: null, step: "input" };
4311
+ }
4312
+ if (isInternalPath(pageRoute)) {
4313
+ const page = resolvePage(pageRoute, pages);
4314
+ return {
4315
+ searchValue: page.title,
4316
+ selectedPage: page,
4317
+ selectedSection: null,
4318
+ step: "input"
4319
+ };
4320
+ }
4321
+ return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
4322
+ }
4323
+ function filterAvailablePages(pages, existingTargets) {
4324
+ const taken = new Set(existingTargets);
4325
+ return pages.filter((p) => !taken.has(p.path));
4326
+ }
4327
+
4328
+ // src/lib/prefetch-sections.ts
4329
+ function buildPageFetchUrl(path) {
4330
+ const normalized = normalizePath(path);
4331
+ const search = readPreservedSearch();
4332
+ return `${window.location.origin}${normalized}${search}`;
4333
+ }
4334
+ async function loadAllSectionsManifest() {
4335
+ const manifest = await loadSectionsManifest();
4336
+ return manifest ?? {};
4337
+ }
4338
+ async function loadSectionsManifest() {
4339
+ try {
4340
+ const res = await fetch(`${window.location.origin}/ohw-sections.json`, {
4341
+ credentials: "same-origin",
4342
+ cache: "no-store"
4343
+ });
4344
+ if (!res.ok) return null;
4345
+ const data = await res.json();
4346
+ return data && typeof data === "object" ? data : null;
4347
+ } catch {
4348
+ return null;
4349
+ }
4350
+ }
4351
+ async function prefetchSectionsFromHtml(paths) {
4352
+ const pairs = await Promise.all(
4353
+ paths.map(async (path) => {
4354
+ try {
4355
+ const res = await fetch(buildPageFetchUrl(path), {
4356
+ credentials: "same-origin",
4357
+ cache: "no-store",
4358
+ headers: { Accept: "text/html" }
4359
+ });
4360
+ if (!res.ok) return [path, []];
4361
+ const html = await res.text();
4362
+ return [path, parseSectionsFromHtml(html)];
4363
+ } catch {
4364
+ return [path, []];
4365
+ }
4366
+ })
4367
+ );
4368
+ const out = {};
4369
+ for (const [path, sections] of pairs) {
4370
+ if (sections.length > 0) out[path] = sections;
4371
+ }
4372
+ return out;
4373
+ }
4374
+ async function prefetchSectionsForPaths(paths) {
4375
+ const unique = [...new Set(paths.map((p) => normalizePath(p)))];
4376
+ const out = {};
4377
+ const manifest = await loadSectionsManifest();
4378
+ if (manifest) {
4379
+ const keys = unique.length > 0 ? unique : Object.keys(manifest);
4380
+ for (const path of keys) {
4381
+ const sections = manifest[path];
4382
+ if (sections?.length) out[path] = sections;
4383
+ }
4384
+ }
4385
+ const missing = unique.filter((path) => !out[path]?.length);
4386
+ if (missing.length > 0) {
4387
+ Object.assign(out, await prefetchSectionsFromHtml(missing));
4388
+ }
4389
+ return out;
4390
+ }
4391
+ function mergePrefetchedSections(prev, fetched, livePath, liveSections) {
4392
+ const next = { ...prev, ...fetched };
4393
+ if (liveSections.length > 0) next[normalizePath(livePath)] = liveSections;
4394
+ return next;
4395
+ }
4396
+
4397
+ // src/lib/scroll-to-section.ts
4398
+ var MAX_ATTEMPTS = 12;
4399
+ var RETRY_MS = 100;
4400
+ function getHashSectionId() {
4401
+ if (typeof window === "undefined") return null;
4402
+ const id = window.location.hash.replace(/^#/, "").trim();
4403
+ return id || null;
4404
+ }
4405
+ function scrollToOhwSection(sectionId, behavior = "smooth") {
4406
+ if (!sectionId || typeof document === "undefined") return false;
4407
+ const el = document.querySelector(`[data-ohw-section="${CSS.escape(sectionId)}"]`);
4408
+ if (!el) return false;
4409
+ el.scrollIntoView({ behavior, block: "start" });
4410
+ return true;
4411
+ }
4412
+ function scrollToHashSectionWhenReady(behavior = "smooth") {
4413
+ const id = getHashSectionId();
4414
+ if (!id) return;
4415
+ let attempt = 0;
4416
+ const tick = () => {
4417
+ if (scrollToOhwSection(id, behavior)) return;
4418
+ attempt += 1;
4419
+ if (attempt < MAX_ATTEMPTS) window.setTimeout(tick, RETRY_MS);
4420
+ };
4421
+ tick();
4422
+ }
4423
+
4178
4424
  // src/ui/link-modal/calcPopoverPos.ts
4179
4425
  function calcPopoverPos(rect, parentScroll, approxW = 483, approxH = 320) {
4180
4426
  const GAP = 8;
@@ -4336,7 +4582,7 @@ function SectionPickerList({ sections, onSelect }) {
4336
4582
  }
4337
4583
 
4338
4584
  // src/ui/link-modal/UrlOrPageInput.tsx
4339
- import { useEffect as useEffect2, useId, useRef as useRef2, useState as useState3 } from "react";
4585
+ import { useId, useRef as useRef2, useState as useState3 } from "react";
4340
4586
 
4341
4587
  // src/ui/input.tsx
4342
4588
  import * as React4 from "react";
@@ -4433,12 +4679,7 @@ function UrlOrPageInput({
4433
4679
  }) {
4434
4680
  const inputId = useId();
4435
4681
  const inputRef = useRef2(null);
4436
- const [isEditing, setIsEditing] = useState3(false);
4437
4682
  const [isFocused, setIsFocused] = useState3(false);
4438
- const showFilledDisplay = Boolean(selectedPage) && !isEditing && !readOnly;
4439
- useEffect2(() => {
4440
- if (selectedPage) setIsEditing(false);
4441
- }, [selectedPage]);
4442
4683
  const openDropdown = () => {
4443
4684
  if (readOnly || filteredPages.length === 0) return;
4444
4685
  onDropdownOpenChange(true);
@@ -4449,9 +4690,10 @@ function UrlOrPageInput({
4449
4690
  if (readOnly || filteredPages.length === 0) return;
4450
4691
  onDropdownOpenChange(!dropdownOpen);
4451
4692
  };
4452
- const enterEditMode = () => {
4453
- if (readOnly) return;
4454
- setIsEditing(true);
4693
+ const clearSelection = (e) => {
4694
+ e.preventDefault();
4695
+ e.stopPropagation();
4696
+ onInputChange("");
4455
4697
  requestAnimationFrame(() => inputRef.current?.focus());
4456
4698
  };
4457
4699
  const fieldClassName = cn(
@@ -4461,62 +4703,41 @@ function UrlOrPageInput({
4461
4703
  return /* @__PURE__ */ jsxs6("div", { className: "flex w-full flex-col gap-2 p-0", children: [
4462
4704
  /* @__PURE__ */ jsx12(Label, { htmlFor: inputId, className: cn(urlError && "text-destructive"), children: "Destination" }),
4463
4705
  /* @__PURE__ */ jsxs6(Popover, { open: dropdownOpen && !readOnly, onOpenChange: onDropdownOpenChange, children: [
4464
- /* @__PURE__ */ jsx12(PopoverTrigger, { asChild: true, children: showFilledDisplay ? /* @__PURE__ */ jsxs6(
4465
- "button",
4466
- {
4467
- type: "button",
4468
- "data-ohw-link-field": true,
4469
- className: cn(fieldClassName, "m-0 cursor-pointer border-solid text-left"),
4470
- onClick: openDropdown,
4471
- onFocus: () => setIsFocused(true),
4472
- onBlur: () => setIsFocused(false),
4473
- onKeyDown: (e) => {
4474
- if (e.key === "Backspace" || e.key === "Delete" || e.key.length === 1) {
4475
- enterEditMode();
4476
- }
4477
- },
4478
- children: [
4479
- /* @__PURE__ */ jsx12("div", { className: "flex shrink-0 items-center pr-2", children: /* @__PURE__ */ jsx12(IconFile, { className: "text-foreground", "aria-hidden": true }) }),
4480
- /* @__PURE__ */ jsx12("span", { className: "min-w-0 flex-1 truncate text-sm font-normal text-foreground", children: selectedPage.title }),
4481
- /* @__PURE__ */ jsx12(FieldChevron, { onClick: toggleDropdown })
4482
- ]
4483
- }
4484
- ) : /* @__PURE__ */ jsxs6(
4485
- "div",
4486
- {
4487
- "data-ohw-link-field": true,
4488
- className: fieldClassName,
4489
- onMouseDown: (e) => {
4490
- if (readOnly) return;
4491
- if (e.target === e.currentTarget) enterEditMode();
4492
- },
4493
- children: [
4494
- selectedPage ? /* @__PURE__ */ jsx12("div", { className: "flex shrink-0 items-center pr-2", children: /* @__PURE__ */ jsx12(IconFile, { className: "text-foreground", "aria-hidden": true }) }) : null,
4495
- readOnly ? /* @__PURE__ */ jsx12("span", { className: "min-w-0 flex-1 truncate text-sm leading-5 text-foreground", children: selectedPage?.title ?? value }) : /* @__PURE__ */ jsx12(
4496
- Input,
4497
- {
4498
- ref: inputRef,
4499
- id: inputId,
4500
- value,
4501
- onChange: (e) => onInputChange(e.target.value),
4502
- onFocus: () => {
4503
- setIsFocused(true);
4504
- setIsEditing(true);
4505
- openDropdown();
4506
- },
4507
- onBlur: () => setIsFocused(false),
4508
- placeholder,
4509
- className: cn(
4510
- "min-w-0 flex-1 truncate border-0 bg-transparent p-0 text-sm font-normal leading-5 shadow-none outline-none ring-0",
4511
- value ? "text-foreground" : "text-muted-foreground placeholder:text-muted-foreground"
4512
- )
4513
- }
4514
- ),
4515
- !readOnly ? /* @__PURE__ */ jsx12(FieldChevron, { onClick: toggleDropdown }) : null
4516
- ]
4517
- }
4518
- ) }),
4519
- /* @__PURE__ */ jsx12(PopoverContent, { "data-ohw-link-popover-root": "", onOpenAutoFocus: (e) => e.preventDefault(), children: filteredPages.map((page) => /* @__PURE__ */ jsxs6(
4706
+ /* @__PURE__ */ jsx12(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs6("div", { "data-ohw-link-field": true, className: fieldClassName, children: [
4707
+ selectedPage ? /* @__PURE__ */ jsx12("div", { className: "flex shrink-0 items-center pr-2", children: /* @__PURE__ */ jsx12(IconFile, { className: "text-foreground", "aria-hidden": true }) }) : null,
4708
+ readOnly ? /* @__PURE__ */ jsx12("span", { className: "min-w-0 flex-1 truncate text-sm leading-5 text-foreground", children: selectedPage?.title ?? value }) : /* @__PURE__ */ jsx12(
4709
+ Input,
4710
+ {
4711
+ ref: inputRef,
4712
+ id: inputId,
4713
+ value,
4714
+ onChange: (e) => onInputChange(e.target.value),
4715
+ onFocus: () => {
4716
+ setIsFocused(true);
4717
+ if (!selectedPage) openDropdown();
4718
+ },
4719
+ onBlur: () => setIsFocused(false),
4720
+ placeholder,
4721
+ className: cn(
4722
+ "min-w-0 flex-1 truncate border-0 bg-transparent p-0 text-sm font-normal leading-5 shadow-none outline-none ring-0 caret-foreground",
4723
+ value ? "text-foreground" : "text-muted-foreground placeholder:text-muted-foreground"
4724
+ )
4725
+ }
4726
+ ),
4727
+ selectedPage && !readOnly ? /* @__PURE__ */ jsx12(
4728
+ "button",
4729
+ {
4730
+ type: "button",
4731
+ className: "flex shrink-0 items-center justify-center rounded-sm p-0.5 text-muted-foreground outline-none hover:bg-muted/50 hover:text-foreground",
4732
+ onMouseDown: clearSelection,
4733
+ "aria-label": "Clear selected page",
4734
+ tabIndex: -1,
4735
+ children: /* @__PURE__ */ jsx12(IconX, { "aria-hidden": true })
4736
+ }
4737
+ ) : null,
4738
+ !readOnly ? /* @__PURE__ */ jsx12(FieldChevron, { onClick: toggleDropdown }) : null
4739
+ ] }) }),
4740
+ filteredPages.length > 0 && /* @__PURE__ */ jsx12(PopoverContent, { "data-ohw-link-popover-root": "", onOpenAutoFocus: (e) => e.preventDefault(), children: filteredPages.map((page) => /* @__PURE__ */ jsxs6(
4520
4741
  "button",
4521
4742
  {
4522
4743
  type: "button",
@@ -4535,139 +4756,12 @@ function UrlOrPageInput({
4535
4756
  }
4536
4757
 
4537
4758
  // src/ui/link-modal/useLinkModalState.ts
4538
- import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useState as useState4 } from "react";
4539
-
4540
- // src/ui/link-modal/linkModal.utils.ts
4541
- function parseTarget(target) {
4542
- if (!target) return { pageRoute: "", sectionId: null };
4543
- const hashIndex = target.indexOf("#");
4544
- if (hashIndex === -1) return { pageRoute: target, sectionId: null };
4545
- return {
4546
- pageRoute: target.slice(0, hashIndex),
4547
- sectionId: target.slice(hashIndex + 1) || null
4548
- };
4549
- }
4550
- function normalizePath(path) {
4551
- const trimmed = path.trim();
4552
- if (!trimmed) return "/";
4553
- try {
4554
- if (/^https?:\/\//i.test(trimmed)) {
4555
- return new URL(trimmed).pathname || "/";
4556
- }
4557
- } catch {
4558
- }
4559
- const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
4560
- if (withSlash.length > 1 && withSlash.endsWith("/")) return withSlash.slice(0, -1);
4561
- return withSlash;
4562
- }
4563
- function findPageByPath(pages, path) {
4564
- const normalized = normalizePath(path);
4565
- return pages.find((p) => normalizePath(p.path) === normalized);
4566
- }
4567
- function inferPageTitleFromPath(path) {
4568
- const normalized = normalizePath(path);
4569
- if (normalized === "/") return "Home";
4570
- const segment = normalized.split("/").filter(Boolean).pop() ?? "";
4571
- if (!segment) return "Home";
4572
- return segment.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
4573
- }
4574
- function isInternalPath(route) {
4575
- const trimmed = route.trim();
4576
- if (!trimmed) return false;
4577
- if (/^https?:\/\//i.test(trimmed)) return false;
4578
- if (trimmed.startsWith("/")) return true;
4579
- if (trimmed.includes(" ") || trimmed.includes(".")) return false;
4580
- return true;
4581
- }
4582
- function resolvePage(route, pages) {
4583
- const found = findPageByPath(pages, route);
4584
- if (found) return { path: found.path, title: found.title };
4585
- return { path: normalizePath(route), title: inferPageTitleFromPath(route) };
4586
- }
4587
- function getSectionsForPath(sectionsByPath, pageRoute, fallback) {
4588
- if (!sectionsByPath) return fallback;
4589
- const normalized = normalizePath(pageRoute);
4590
- return sectionsByPath[normalized] ?? sectionsByPath[pageRoute] ?? fallback;
4591
- }
4592
- function buildTarget(page, section) {
4593
- if (section) return `${page.path}#${section.id}`;
4594
- return page.path;
4595
- }
4596
- function isValidUrl(urlString) {
4597
- try {
4598
- const url = new URL(urlString);
4599
- if (url.protocol !== "http:" && url.protocol !== "https:") return false;
4600
- const hostname = url.hostname;
4601
- if (!hostname || hostname.length === 0) return false;
4602
- if (hostname.includes(" ")) return false;
4603
- return hostname.includes(".") || hostname === "localhost";
4604
- } catch {
4605
- return false;
4606
- }
4607
- }
4608
- function normalizeUrl(value) {
4609
- const trimmed = value.trim();
4610
- if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
4611
- return `https://${trimmed}`;
4612
- }
4613
- function validateUrlInput(value, pages, allPages) {
4614
- if (!value.trim()) return "";
4615
- const filtered = pages.filter((p) => p.title.toLowerCase().startsWith(value.toLowerCase()));
4616
- if (filtered.length > 0) return "";
4617
- const matchingPage = allPages.find((p) => p.title.toLowerCase() === value.toLowerCase());
4618
- if (matchingPage) return "";
4619
- if (!isValidUrl(normalizeUrl(value))) {
4620
- return "Please enter a valid URL (e.g., https://example.com)";
4621
- }
4622
- return "";
4623
- }
4624
- function getEditModeInitialState(target, pages, sections) {
4625
- const empty = {
4626
- searchValue: "",
4627
- selectedPage: null,
4628
- selectedSection: null,
4629
- step: "input"
4630
- };
4631
- if (!target) return empty;
4632
- if (isValidUrl(normalizeUrl(target))) {
4633
- return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
4634
- }
4635
- const { pageRoute, sectionId } = parseTarget(target);
4636
- if (sectionId) {
4637
- if (isInternalPath(pageRoute)) {
4638
- const page = resolvePage(pageRoute, pages);
4639
- const sec = sections.find((s) => s.id === sectionId);
4640
- return {
4641
- searchValue: page.title,
4642
- selectedPage: page,
4643
- selectedSection: sec ? { id: sec.id, label: sec.label } : null,
4644
- step: "input"
4645
- };
4646
- }
4647
- return { searchValue: pageRoute || target, selectedPage: null, selectedSection: null, step: "input" };
4648
- }
4649
- if (isInternalPath(pageRoute)) {
4650
- const page = resolvePage(pageRoute, pages);
4651
- return {
4652
- searchValue: page.title,
4653
- selectedPage: page,
4654
- selectedSection: null,
4655
- step: "input"
4656
- };
4657
- }
4658
- return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
4659
- }
4660
- function filterAvailablePages(pages, existingTargets) {
4661
- const taken = new Set(existingTargets);
4662
- return pages.filter((p) => !taken.has(p.path));
4663
- }
4664
-
4665
- // src/ui/link-modal/useLinkModalState.ts
4759
+ import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useState as useState4 } from "react";
4666
4760
  function useLinkModalState({
4667
4761
  open,
4668
4762
  mode,
4669
4763
  pages,
4670
- sections,
4764
+ sections: _sections,
4671
4765
  sectionsByPath,
4672
4766
  initialTarget,
4673
4767
  existingTargets,
@@ -4692,11 +4786,11 @@ function useLinkModalState({
4692
4786
  setDropdownOpen(false);
4693
4787
  setUrlError("");
4694
4788
  }, []);
4695
- useEffect3(() => {
4789
+ useEffect2(() => {
4696
4790
  if (!open) return;
4697
4791
  if (mode === "edit" && initialTarget) {
4698
4792
  const { pageRoute } = parseTarget(initialTarget);
4699
- const targetSections = getSectionsForPath(sectionsByPath, pageRoute, sections);
4793
+ const targetSections = getSectionsForPath(sectionsByPath, pageRoute);
4700
4794
  const init = getEditModeInitialState(initialTarget, pages, targetSections);
4701
4795
  setSearchValue(init.searchValue);
4702
4796
  setSelectedPage(init.selectedPage);
@@ -4707,21 +4801,19 @@ function useLinkModalState({
4707
4801
  }
4708
4802
  setDropdownOpen(false);
4709
4803
  setUrlError("");
4710
- }, [open, mode, initialTarget, pages, sections, sectionsByPath, reset]);
4804
+ }, [open, mode, initialTarget, pages, sectionsByPath, reset]);
4711
4805
  const filteredPages = useMemo2(() => {
4712
4806
  if (!searchValue.trim()) return availablePages;
4713
4807
  return availablePages.filter((p) => p.title.toLowerCase().startsWith(searchValue.toLowerCase()));
4714
4808
  }, [availablePages, searchValue]);
4715
4809
  const activeSections = useMemo2(() => {
4716
- if (selectedPage && sectionsByPath) {
4717
- return getSectionsForPath(sectionsByPath, selectedPage.path, sections);
4718
- }
4719
- return sections;
4720
- }, [selectedPage, sectionsByPath, sections]);
4810
+ if (!selectedPage) return [];
4811
+ return getSectionsForPath(sectionsByPath, selectedPage.path);
4812
+ }, [selectedPage, sectionsByPath]);
4721
4813
  const showBreadcrumb = step === "confirmed" && selectedPage && selectedSection;
4722
4814
  const showSectionPicker = step === "sectionPicker" && selectedPage;
4723
4815
  const showSectionRow = mode === "edit" && selectedPage && selectedSection && step === "input";
4724
- const showChooseSection = step === "input" && selectedPage && !selectedSection && !showSectionRow;
4816
+ const showChooseSection = step === "input" && selectedPage && !selectedSection && !showSectionRow && activeSections.length > 0;
4725
4817
  const handleInputChange = (value) => {
4726
4818
  setSearchValue(value);
4727
4819
  setSelectedPage(null);
@@ -4985,11 +5077,13 @@ var DEV_SECTIONS_BY_PATH = {
4985
5077
  ],
4986
5078
  "/classes": [{ id: "class-library", label: "Class Library" }],
4987
5079
  "/pricing": [
5080
+ { id: "page-header", label: "Page Header" },
5081
+ { id: "free-class-cta", label: "Free Class" },
4988
5082
  { id: "benefits-marquee", label: "Benefits" },
4989
5083
  { id: "monthly-memberships", label: "Memberships" },
4990
- { id: "package-matcher", label: "Packages" },
4991
5084
  { id: "specialty-programs", label: "Specialty Programs" },
4992
- { id: "free-class-cta", label: "Free Class" }
5085
+ { id: "package-matcher", label: "Packages" },
5086
+ { id: "wordmark", label: "Wordmark" }
4993
5087
  ],
4994
5088
  "/studio": [
4995
5089
  { id: "studio-intro", label: "Studio Intro" },
@@ -5298,15 +5392,8 @@ function getHrefKeyFromElement(el) {
5298
5392
  if (!key) return null;
5299
5393
  return { anchor, key };
5300
5394
  }
5301
- function titleCaseSectionId(id) {
5302
- return id.split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
5303
- }
5304
5395
  function collectSections() {
5305
- return Array.from(document.querySelectorAll("[data-ohw-section]")).map((el) => {
5306
- const id = el.getAttribute("data-ohw-section") ?? "";
5307
- const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
5308
- return { id, label };
5309
- });
5396
+ return collectSectionsFromDom();
5310
5397
  }
5311
5398
  var FORCE_PSEUDO_STATES = [
5312
5399
  { pseudo: ":hover", state: "hover" },
@@ -5669,7 +5756,7 @@ function OhhwellsBridge() {
5669
5756
  const searchParams = useSearchParams();
5670
5757
  const isEditMode = isEditSessionActive();
5671
5758
  const [bridgeRoot, setBridgeRoot] = useState5(null);
5672
- useEffect4(() => {
5759
+ useEffect3(() => {
5673
5760
  const figtreeFontId = "ohw-figtree-font";
5674
5761
  if (!document.getElementById(figtreeFontId)) {
5675
5762
  const preconnect1 = Object.assign(document.createElement("link"), { rel: "preconnect", href: "https://fonts.googleapis.com" });
@@ -5728,6 +5815,7 @@ function OhhwellsBridge() {
5728
5815
  const [linkPopover, setLinkPopover] = useState5(null);
5729
5816
  const [sitePages, setSitePages] = useState5([]);
5730
5817
  const [sectionsByPath, setSectionsByPath] = useState5({});
5818
+ const sectionsPrefetchGenRef = useRef3(0);
5731
5819
  const setLinkPopoverRef = useRef3(setLinkPopover);
5732
5820
  const linkPopoverPanelRef = useRef3(null);
5733
5821
  const linkPopoverOpenRef = useRef3(false);
@@ -5736,7 +5824,36 @@ function OhhwellsBridge() {
5736
5824
  const bumpLinkPopoverGrace = () => {
5737
5825
  linkPopoverGraceUntilRef.current = Date.now() + 350;
5738
5826
  };
5739
- useEffect4(() => {
5827
+ const runSectionsPrefetch = useCallback2((pages) => {
5828
+ if (!isEditMode || shouldUseDevFixtures() || pages.length === 0) return;
5829
+ const gen = ++sectionsPrefetchGenRef.current;
5830
+ const paths = pages.map((p) => p.path);
5831
+ void prefetchSectionsForPaths(paths).then((fetched) => {
5832
+ if (gen !== sectionsPrefetchGenRef.current) return;
5833
+ if (process.env.NODE_ENV === "development") {
5834
+ console.info("[ohhwells-bridge] Sections prefetched for", Object.keys(fetched).join(", ") || "(none)");
5835
+ }
5836
+ setSectionsByPath(
5837
+ (prev) => mergePrefetchedSections(
5838
+ prev,
5839
+ fetched,
5840
+ typeof window !== "undefined" ? window.location.pathname : pathname,
5841
+ collectSections()
5842
+ )
5843
+ );
5844
+ });
5845
+ }, [isEditMode, pathname]);
5846
+ const runSectionsPrefetchRef = useRef3(runSectionsPrefetch);
5847
+ runSectionsPrefetchRef.current = runSectionsPrefetch;
5848
+ useEffect3(() => {
5849
+ if (!linkPopover) return;
5850
+ if (hoveredImageRef.current) {
5851
+ hoveredImageRef.current = null;
5852
+ hoveredImageHasTextOverlapRef.current = false;
5853
+ }
5854
+ postToParent({ type: "ow:image-unhover" });
5855
+ }, [linkPopover, postToParent]);
5856
+ useEffect3(() => {
5740
5857
  if (!isEditMode) return;
5741
5858
  const useFixtures = shouldUseDevFixtures();
5742
5859
  if (useFixtures) {
@@ -5749,19 +5866,26 @@ function OhhwellsBridge() {
5749
5866
  const onSitePages = (e) => {
5750
5867
  if (e.data?.type !== "ow:site-pages" || !Array.isArray(e.data.pages)) return;
5751
5868
  if (useFixtures) return;
5752
- setSitePages(
5753
- e.data.pages.map((p) => ({
5754
- path: p.path,
5755
- title: p.title
5756
- }))
5757
- );
5869
+ const mapped = e.data.pages.map((p) => ({
5870
+ path: p.path,
5871
+ title: p.title
5872
+ }));
5873
+ setSitePages(mapped);
5874
+ runSectionsPrefetchRef.current(mapped);
5758
5875
  };
5759
5876
  window.addEventListener("message", onSitePages);
5760
5877
  if (!useFixtures) postToParent({ type: "ow:request-site-pages" });
5761
5878
  return () => window.removeEventListener("message", onSitePages);
5762
5879
  }, [isEditMode, postToParent]);
5763
5880
  const [sectionGap, setSectionGap] = useState5(null);
5764
- useEffect4(() => {
5881
+ useEffect3(() => {
5882
+ if (!isEditMode || shouldUseDevFixtures()) return;
5883
+ void loadAllSectionsManifest().then((manifest) => {
5884
+ if (Object.keys(manifest).length === 0) return;
5885
+ setSectionsByPath((prev) => ({ ...manifest, ...prev }));
5886
+ });
5887
+ }, [isEditMode]);
5888
+ useEffect3(() => {
5765
5889
  const update = () => {
5766
5890
  const el = activeElRef.current;
5767
5891
  if (el) setToolbarRect(el.getBoundingClientRect());
@@ -5888,7 +6012,7 @@ function OhhwellsBridge() {
5888
6012
  cancelled = true;
5889
6013
  };
5890
6014
  }, [subdomain, isEditMode, pathname]);
5891
- useEffect4(() => {
6015
+ useEffect3(() => {
5892
6016
  if (!subdomain || isEditMode) return;
5893
6017
  let debounceTimer = null;
5894
6018
  const applyFromCache = () => {
@@ -5931,10 +6055,10 @@ function OhhwellsBridge() {
5931
6055
  const visible = Boolean(subdomain) && fetchState !== "done";
5932
6056
  el.style.display = visible ? "flex" : "none";
5933
6057
  }, [subdomain, fetchState]);
5934
- useEffect4(() => {
6058
+ useEffect3(() => {
5935
6059
  postToParent({ type: "ow:navigation", path: pathname });
5936
6060
  }, [pathname, postToParent]);
5937
- useEffect4(() => {
6061
+ useEffect3(() => {
5938
6062
  if (!isEditMode) return;
5939
6063
  const measure = () => {
5940
6064
  const h = document.body.scrollHeight;
@@ -5958,7 +6082,7 @@ function OhhwellsBridge() {
5958
6082
  window.removeEventListener("resize", handleResize);
5959
6083
  };
5960
6084
  }, [pathname, isEditMode, postToParent]);
5961
- useEffect4(() => {
6085
+ useEffect3(() => {
5962
6086
  if (!subdomainFromQuery || isEditMode) return;
5963
6087
  const handleClick = (e) => {
5964
6088
  const anchor = e.target.closest("a");
@@ -5974,7 +6098,7 @@ function OhhwellsBridge() {
5974
6098
  document.addEventListener("click", handleClick, true);
5975
6099
  return () => document.removeEventListener("click", handleClick, true);
5976
6100
  }, [subdomainFromQuery, isEditMode, router]);
5977
- useEffect4(() => {
6101
+ useEffect3(() => {
5978
6102
  if (!isEditMode) {
5979
6103
  editStylesRef.current?.base.remove();
5980
6104
  editStylesRef.current?.forceHover.remove();
@@ -6188,6 +6312,15 @@ function OhhwellsBridge() {
6188
6312
  return smallest;
6189
6313
  };
6190
6314
  const probeImageAt = (clientX, clientY, isDragOver = false, fromParentViewport = false) => {
6315
+ if (linkPopoverOpenRef.current) {
6316
+ if (hoveredImageRef.current) {
6317
+ hoveredImageRef.current = null;
6318
+ hoveredImageHasTextOverlapRef.current = false;
6319
+ resumeAnimTracks();
6320
+ postToParentRef.current({ type: "ow:image-unhover" });
6321
+ }
6322
+ return;
6323
+ }
6191
6324
  const toggleEl = document.querySelector("[data-ohw-state-toggle]");
6192
6325
  if (toggleEl) {
6193
6326
  const { x, y } = toProbeCoords(clientX, clientY, fromParentViewport);
@@ -6213,6 +6346,15 @@ function OhhwellsBridge() {
6213
6346
  }
6214
6347
  return;
6215
6348
  }
6349
+ if (topEl?.closest("[data-ohw-link-popover-root]") || topEl?.closest("[data-ohw-link-modal-root]")) {
6350
+ if (hoveredImageRef.current) {
6351
+ hoveredImageRef.current = null;
6352
+ hoveredImageHasTextOverlapRef.current = false;
6353
+ resumeAnimTracks();
6354
+ postToParentRef.current({ type: "ow:image-unhover" });
6355
+ }
6356
+ return;
6357
+ }
6216
6358
  const isStateCardImage = !!imgEl.closest("[data-ohw-editable-state]");
6217
6359
  const textEditable = Array.from(
6218
6360
  document.querySelectorAll('[data-ohw-editable]:not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"])')
@@ -6814,7 +6956,7 @@ function OhhwellsBridge() {
6814
6956
  if (imageShowTimerRef.current) clearTimeout(imageShowTimerRef.current);
6815
6957
  };
6816
6958
  }, [isEditMode, refreshStateRules]);
6817
- useEffect4(() => {
6959
+ useEffect3(() => {
6818
6960
  if (!isEditMode) return;
6819
6961
  document.querySelectorAll("[data-ohw-active-state]").forEach((el) => {
6820
6962
  el.removeAttribute("data-ohw-active-state");
@@ -6831,7 +6973,8 @@ function OhhwellsBridge() {
6831
6973
  refreshStateRules();
6832
6974
  const sections = collectSections();
6833
6975
  setSectionsByPath((prev) => {
6834
- const next = { ...prev, [pathname]: sections };
6976
+ const pathKey = normalizePath(pathname);
6977
+ const next = { ...prev, [pathKey]: sections };
6835
6978
  return shouldUseDevFixtures() ? { ...DEV_SECTIONS_BY_PATH, ...next } : next;
6836
6979
  });
6837
6980
  postToParent({ type: "ow:ready", version: "1", path: pathname, nodes: collectEditableNodes(), sections });
@@ -6842,6 +6985,12 @@ function OhhwellsBridge() {
6842
6985
  clearTimeout(timer);
6843
6986
  };
6844
6987
  }, [pathname, isEditMode, refreshStateRules, postToParent]);
6988
+ useEffect3(() => {
6989
+ scrollToHashSectionWhenReady();
6990
+ const onHashChange = () => scrollToHashSectionWhenReady();
6991
+ window.addEventListener("hashchange", onHashChange);
6992
+ return () => window.removeEventListener("hashchange", onHashChange);
6993
+ }, [pathname]);
6845
6994
  const handleCommand = useCallback2((cmd) => {
6846
6995
  document.execCommand(cmd, false);
6847
6996
  activeElRef.current?.focus();