@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.cjs CHANGED
@@ -4223,6 +4223,252 @@ function isEditSessionActive() {
4223
4223
  return new URLSearchParams(q).get("mode") === "edit";
4224
4224
  }
4225
4225
 
4226
+ // src/lib/sections.ts
4227
+ function titleCaseSectionId(id) {
4228
+ return id.split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
4229
+ }
4230
+ function parseSectionsFromRoot(root) {
4231
+ const seen = /* @__PURE__ */ new Set();
4232
+ const sections = [];
4233
+ for (const el of root.querySelectorAll("[data-ohw-section]")) {
4234
+ const id = el.getAttribute("data-ohw-section") ?? "";
4235
+ if (!id || seen.has(id)) continue;
4236
+ seen.add(id);
4237
+ const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
4238
+ sections.push({ id, label });
4239
+ }
4240
+ return sections;
4241
+ }
4242
+ function collectSectionsFromDom() {
4243
+ if (typeof document === "undefined") return [];
4244
+ return parseSectionsFromRoot(document);
4245
+ }
4246
+ function parseSectionsFromHtml(html) {
4247
+ const doc = new DOMParser().parseFromString(html, "text/html");
4248
+ return parseSectionsFromRoot(doc);
4249
+ }
4250
+
4251
+ // src/ui/link-modal/linkModal.utils.ts
4252
+ function parseTarget(target) {
4253
+ if (!target) return { pageRoute: "", sectionId: null };
4254
+ const hashIndex = target.indexOf("#");
4255
+ if (hashIndex === -1) return { pageRoute: target, sectionId: null };
4256
+ return {
4257
+ pageRoute: target.slice(0, hashIndex),
4258
+ sectionId: target.slice(hashIndex + 1) || null
4259
+ };
4260
+ }
4261
+ function normalizePath(path) {
4262
+ const trimmed = path.trim();
4263
+ if (!trimmed) return "/";
4264
+ try {
4265
+ if (/^https?:\/\//i.test(trimmed)) {
4266
+ return new URL(trimmed).pathname || "/";
4267
+ }
4268
+ } catch {
4269
+ }
4270
+ const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
4271
+ if (withSlash.length > 1 && withSlash.endsWith("/")) return withSlash.slice(0, -1);
4272
+ return withSlash;
4273
+ }
4274
+ function findPageByPath(pages, path) {
4275
+ const normalized = normalizePath(path);
4276
+ return pages.find((p) => normalizePath(p.path) === normalized);
4277
+ }
4278
+ function inferPageTitleFromPath(path) {
4279
+ const normalized = normalizePath(path);
4280
+ if (normalized === "/") return "Home";
4281
+ const segment = normalized.split("/").filter(Boolean).pop() ?? "";
4282
+ if (!segment) return "Home";
4283
+ return segment.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
4284
+ }
4285
+ function isInternalPath(route) {
4286
+ const trimmed = route.trim();
4287
+ if (!trimmed) return false;
4288
+ if (/^https?:\/\//i.test(trimmed)) return false;
4289
+ if (trimmed.startsWith("/")) return true;
4290
+ if (trimmed.includes(" ") || trimmed.includes(".")) return false;
4291
+ return true;
4292
+ }
4293
+ function resolvePage(route, pages) {
4294
+ const found = findPageByPath(pages, route);
4295
+ if (found) return { path: found.path, title: found.title };
4296
+ return { path: normalizePath(route), title: inferPageTitleFromPath(route) };
4297
+ }
4298
+ function getSectionsForPath(sectionsByPath, pageRoute) {
4299
+ if (!sectionsByPath) return [];
4300
+ const normalized = normalizePath(pageRoute);
4301
+ return sectionsByPath[normalized] ?? sectionsByPath[pageRoute] ?? [];
4302
+ }
4303
+ function buildTarget(page, section) {
4304
+ if (section) return `${page.path}#${section.id}`;
4305
+ return page.path;
4306
+ }
4307
+ function isValidUrl(urlString) {
4308
+ try {
4309
+ const url = new URL(urlString);
4310
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
4311
+ const hostname = url.hostname;
4312
+ if (!hostname || hostname.length === 0) return false;
4313
+ if (hostname.includes(" ")) return false;
4314
+ return hostname.includes(".") || hostname === "localhost";
4315
+ } catch {
4316
+ return false;
4317
+ }
4318
+ }
4319
+ function normalizeUrl(value) {
4320
+ const trimmed = value.trim();
4321
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
4322
+ return `https://${trimmed}`;
4323
+ }
4324
+ function validateUrlInput(value, pages, allPages) {
4325
+ if (!value.trim()) return "";
4326
+ const filtered = pages.filter((p) => p.title.toLowerCase().startsWith(value.toLowerCase()));
4327
+ if (filtered.length > 0) return "";
4328
+ const matchingPage = allPages.find((p) => p.title.toLowerCase() === value.toLowerCase());
4329
+ if (matchingPage) return "";
4330
+ if (!isValidUrl(normalizeUrl(value))) {
4331
+ return "Please enter a valid URL (e.g., https://example.com)";
4332
+ }
4333
+ return "";
4334
+ }
4335
+ function getEditModeInitialState(target, pages, sections) {
4336
+ const empty = {
4337
+ searchValue: "",
4338
+ selectedPage: null,
4339
+ selectedSection: null,
4340
+ step: "input"
4341
+ };
4342
+ if (!target) return empty;
4343
+ if (isValidUrl(normalizeUrl(target))) {
4344
+ return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
4345
+ }
4346
+ const { pageRoute, sectionId } = parseTarget(target);
4347
+ if (sectionId) {
4348
+ if (isInternalPath(pageRoute)) {
4349
+ const page = resolvePage(pageRoute, pages);
4350
+ const sec = sections.find((s) => s.id === sectionId);
4351
+ return {
4352
+ searchValue: page.title,
4353
+ selectedPage: page,
4354
+ selectedSection: sec ? { id: sec.id, label: sec.label } : null,
4355
+ step: "input"
4356
+ };
4357
+ }
4358
+ return { searchValue: pageRoute || target, selectedPage: null, selectedSection: null, step: "input" };
4359
+ }
4360
+ if (isInternalPath(pageRoute)) {
4361
+ const page = resolvePage(pageRoute, pages);
4362
+ return {
4363
+ searchValue: page.title,
4364
+ selectedPage: page,
4365
+ selectedSection: null,
4366
+ step: "input"
4367
+ };
4368
+ }
4369
+ return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
4370
+ }
4371
+ function filterAvailablePages(pages, existingTargets) {
4372
+ const taken = new Set(existingTargets);
4373
+ return pages.filter((p) => !taken.has(p.path));
4374
+ }
4375
+
4376
+ // src/lib/prefetch-sections.ts
4377
+ function buildPageFetchUrl(path) {
4378
+ const normalized = normalizePath(path);
4379
+ const search = readPreservedSearch();
4380
+ return `${window.location.origin}${normalized}${search}`;
4381
+ }
4382
+ async function loadAllSectionsManifest() {
4383
+ const manifest = await loadSectionsManifest();
4384
+ return manifest ?? {};
4385
+ }
4386
+ async function loadSectionsManifest() {
4387
+ try {
4388
+ const res = await fetch(`${window.location.origin}/ohw-sections.json`, {
4389
+ credentials: "same-origin",
4390
+ cache: "no-store"
4391
+ });
4392
+ if (!res.ok) return null;
4393
+ const data = await res.json();
4394
+ return data && typeof data === "object" ? data : null;
4395
+ } catch {
4396
+ return null;
4397
+ }
4398
+ }
4399
+ async function prefetchSectionsFromHtml(paths) {
4400
+ const pairs = await Promise.all(
4401
+ paths.map(async (path) => {
4402
+ try {
4403
+ const res = await fetch(buildPageFetchUrl(path), {
4404
+ credentials: "same-origin",
4405
+ cache: "no-store",
4406
+ headers: { Accept: "text/html" }
4407
+ });
4408
+ if (!res.ok) return [path, []];
4409
+ const html = await res.text();
4410
+ return [path, parseSectionsFromHtml(html)];
4411
+ } catch {
4412
+ return [path, []];
4413
+ }
4414
+ })
4415
+ );
4416
+ const out = {};
4417
+ for (const [path, sections] of pairs) {
4418
+ if (sections.length > 0) out[path] = sections;
4419
+ }
4420
+ return out;
4421
+ }
4422
+ async function prefetchSectionsForPaths(paths) {
4423
+ const unique = [...new Set(paths.map((p) => normalizePath(p)))];
4424
+ const out = {};
4425
+ const manifest = await loadSectionsManifest();
4426
+ if (manifest) {
4427
+ const keys = unique.length > 0 ? unique : Object.keys(manifest);
4428
+ for (const path of keys) {
4429
+ const sections = manifest[path];
4430
+ if (sections?.length) out[path] = sections;
4431
+ }
4432
+ }
4433
+ const missing = unique.filter((path) => !out[path]?.length);
4434
+ if (missing.length > 0) {
4435
+ Object.assign(out, await prefetchSectionsFromHtml(missing));
4436
+ }
4437
+ return out;
4438
+ }
4439
+ function mergePrefetchedSections(prev, fetched, livePath, liveSections) {
4440
+ const next = { ...prev, ...fetched };
4441
+ if (liveSections.length > 0) next[normalizePath(livePath)] = liveSections;
4442
+ return next;
4443
+ }
4444
+
4445
+ // src/lib/scroll-to-section.ts
4446
+ var MAX_ATTEMPTS = 12;
4447
+ var RETRY_MS = 100;
4448
+ function getHashSectionId() {
4449
+ if (typeof window === "undefined") return null;
4450
+ const id = window.location.hash.replace(/^#/, "").trim();
4451
+ return id || null;
4452
+ }
4453
+ function scrollToOhwSection(sectionId, behavior = "smooth") {
4454
+ if (!sectionId || typeof document === "undefined") return false;
4455
+ const el = document.querySelector(`[data-ohw-section="${CSS.escape(sectionId)}"]`);
4456
+ if (!el) return false;
4457
+ el.scrollIntoView({ behavior, block: "start" });
4458
+ return true;
4459
+ }
4460
+ function scrollToHashSectionWhenReady(behavior = "smooth") {
4461
+ const id = getHashSectionId();
4462
+ if (!id) return;
4463
+ let attempt = 0;
4464
+ const tick = () => {
4465
+ if (scrollToOhwSection(id, behavior)) return;
4466
+ attempt += 1;
4467
+ if (attempt < MAX_ATTEMPTS) window.setTimeout(tick, RETRY_MS);
4468
+ };
4469
+ tick();
4470
+ }
4471
+
4226
4472
  // src/ui/link-modal/calcPopoverPos.ts
4227
4473
  function calcPopoverPos(rect, parentScroll, approxW = 483, approxH = 320) {
4228
4474
  const GAP = 8;
@@ -4481,12 +4727,7 @@ function UrlOrPageInput({
4481
4727
  }) {
4482
4728
  const inputId = (0, import_react3.useId)();
4483
4729
  const inputRef = (0, import_react3.useRef)(null);
4484
- const [isEditing, setIsEditing] = (0, import_react3.useState)(false);
4485
4730
  const [isFocused, setIsFocused] = (0, import_react3.useState)(false);
4486
- const showFilledDisplay = Boolean(selectedPage) && !isEditing && !readOnly;
4487
- (0, import_react3.useEffect)(() => {
4488
- if (selectedPage) setIsEditing(false);
4489
- }, [selectedPage]);
4490
4731
  const openDropdown = () => {
4491
4732
  if (readOnly || filteredPages.length === 0) return;
4492
4733
  onDropdownOpenChange(true);
@@ -4497,9 +4738,10 @@ function UrlOrPageInput({
4497
4738
  if (readOnly || filteredPages.length === 0) return;
4498
4739
  onDropdownOpenChange(!dropdownOpen);
4499
4740
  };
4500
- const enterEditMode = () => {
4501
- if (readOnly) return;
4502
- setIsEditing(true);
4741
+ const clearSelection = (e) => {
4742
+ e.preventDefault();
4743
+ e.stopPropagation();
4744
+ onInputChange("");
4503
4745
  requestAnimationFrame(() => inputRef.current?.focus());
4504
4746
  };
4505
4747
  const fieldClassName = cn(
@@ -4509,62 +4751,41 @@ function UrlOrPageInput({
4509
4751
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex w-full flex-col gap-2 p-0", children: [
4510
4752
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Label, { htmlFor: inputId, className: cn(urlError && "text-destructive"), children: "Destination" }),
4511
4753
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(Popover, { open: dropdownOpen && !readOnly, onOpenChange: onDropdownOpenChange, children: [
4512
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PopoverTrigger, { asChild: true, children: showFilledDisplay ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
4513
- "button",
4514
- {
4515
- type: "button",
4516
- "data-ohw-link-field": true,
4517
- className: cn(fieldClassName, "m-0 cursor-pointer border-solid text-left"),
4518
- onClick: openDropdown,
4519
- onFocus: () => setIsFocused(true),
4520
- onBlur: () => setIsFocused(false),
4521
- onKeyDown: (e) => {
4522
- if (e.key === "Backspace" || e.key === "Delete" || e.key.length === 1) {
4523
- enterEditMode();
4524
- }
4525
- },
4526
- children: [
4527
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex shrink-0 items-center pr-2", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(IconFile, { className: "text-foreground", "aria-hidden": true }) }),
4528
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "min-w-0 flex-1 truncate text-sm font-normal text-foreground", children: selectedPage.title }),
4529
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(FieldChevron, { onClick: toggleDropdown })
4530
- ]
4531
- }
4532
- ) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
4533
- "div",
4534
- {
4535
- "data-ohw-link-field": true,
4536
- className: fieldClassName,
4537
- onMouseDown: (e) => {
4538
- if (readOnly) return;
4539
- if (e.target === e.currentTarget) enterEditMode();
4540
- },
4541
- children: [
4542
- selectedPage ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex shrink-0 items-center pr-2", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(IconFile, { className: "text-foreground", "aria-hidden": true }) }) : null,
4543
- readOnly ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "min-w-0 flex-1 truncate text-sm leading-5 text-foreground", children: selectedPage?.title ?? value }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
4544
- Input,
4545
- {
4546
- ref: inputRef,
4547
- id: inputId,
4548
- value,
4549
- onChange: (e) => onInputChange(e.target.value),
4550
- onFocus: () => {
4551
- setIsFocused(true);
4552
- setIsEditing(true);
4553
- openDropdown();
4554
- },
4555
- onBlur: () => setIsFocused(false),
4556
- placeholder,
4557
- className: cn(
4558
- "min-w-0 flex-1 truncate border-0 bg-transparent p-0 text-sm font-normal leading-5 shadow-none outline-none ring-0",
4559
- value ? "text-foreground" : "text-muted-foreground placeholder:text-muted-foreground"
4560
- )
4561
- }
4562
- ),
4563
- !readOnly ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(FieldChevron, { onClick: toggleDropdown }) : null
4564
- ]
4565
- }
4566
- ) }),
4567
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PopoverContent, { "data-ohw-link-popover-root": "", onOpenAutoFocus: (e) => e.preventDefault(), children: filteredPages.map((page) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
4754
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { "data-ohw-link-field": true, className: fieldClassName, children: [
4755
+ selectedPage ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex shrink-0 items-center pr-2", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(IconFile, { className: "text-foreground", "aria-hidden": true }) }) : null,
4756
+ readOnly ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "min-w-0 flex-1 truncate text-sm leading-5 text-foreground", children: selectedPage?.title ?? value }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
4757
+ Input,
4758
+ {
4759
+ ref: inputRef,
4760
+ id: inputId,
4761
+ value,
4762
+ onChange: (e) => onInputChange(e.target.value),
4763
+ onFocus: () => {
4764
+ setIsFocused(true);
4765
+ if (!selectedPage) openDropdown();
4766
+ },
4767
+ onBlur: () => setIsFocused(false),
4768
+ placeholder,
4769
+ className: cn(
4770
+ "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",
4771
+ value ? "text-foreground" : "text-muted-foreground placeholder:text-muted-foreground"
4772
+ )
4773
+ }
4774
+ ),
4775
+ selectedPage && !readOnly ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
4776
+ "button",
4777
+ {
4778
+ type: "button",
4779
+ 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",
4780
+ onMouseDown: clearSelection,
4781
+ "aria-label": "Clear selected page",
4782
+ tabIndex: -1,
4783
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(IconX, { "aria-hidden": true })
4784
+ }
4785
+ ) : null,
4786
+ !readOnly ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(FieldChevron, { onClick: toggleDropdown }) : null
4787
+ ] }) }),
4788
+ filteredPages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PopoverContent, { "data-ohw-link-popover-root": "", onOpenAutoFocus: (e) => e.preventDefault(), children: filteredPages.map((page) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
4568
4789
  "button",
4569
4790
  {
4570
4791
  type: "button",
@@ -4584,138 +4805,11 @@ function UrlOrPageInput({
4584
4805
 
4585
4806
  // src/ui/link-modal/useLinkModalState.ts
4586
4807
  var import_react4 = require("react");
4587
-
4588
- // src/ui/link-modal/linkModal.utils.ts
4589
- function parseTarget(target) {
4590
- if (!target) return { pageRoute: "", sectionId: null };
4591
- const hashIndex = target.indexOf("#");
4592
- if (hashIndex === -1) return { pageRoute: target, sectionId: null };
4593
- return {
4594
- pageRoute: target.slice(0, hashIndex),
4595
- sectionId: target.slice(hashIndex + 1) || null
4596
- };
4597
- }
4598
- function normalizePath(path) {
4599
- const trimmed = path.trim();
4600
- if (!trimmed) return "/";
4601
- try {
4602
- if (/^https?:\/\//i.test(trimmed)) {
4603
- return new URL(trimmed).pathname || "/";
4604
- }
4605
- } catch {
4606
- }
4607
- const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
4608
- if (withSlash.length > 1 && withSlash.endsWith("/")) return withSlash.slice(0, -1);
4609
- return withSlash;
4610
- }
4611
- function findPageByPath(pages, path) {
4612
- const normalized = normalizePath(path);
4613
- return pages.find((p) => normalizePath(p.path) === normalized);
4614
- }
4615
- function inferPageTitleFromPath(path) {
4616
- const normalized = normalizePath(path);
4617
- if (normalized === "/") return "Home";
4618
- const segment = normalized.split("/").filter(Boolean).pop() ?? "";
4619
- if (!segment) return "Home";
4620
- return segment.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
4621
- }
4622
- function isInternalPath(route) {
4623
- const trimmed = route.trim();
4624
- if (!trimmed) return false;
4625
- if (/^https?:\/\//i.test(trimmed)) return false;
4626
- if (trimmed.startsWith("/")) return true;
4627
- if (trimmed.includes(" ") || trimmed.includes(".")) return false;
4628
- return true;
4629
- }
4630
- function resolvePage(route, pages) {
4631
- const found = findPageByPath(pages, route);
4632
- if (found) return { path: found.path, title: found.title };
4633
- return { path: normalizePath(route), title: inferPageTitleFromPath(route) };
4634
- }
4635
- function getSectionsForPath(sectionsByPath, pageRoute, fallback) {
4636
- if (!sectionsByPath) return fallback;
4637
- const normalized = normalizePath(pageRoute);
4638
- return sectionsByPath[normalized] ?? sectionsByPath[pageRoute] ?? fallback;
4639
- }
4640
- function buildTarget(page, section) {
4641
- if (section) return `${page.path}#${section.id}`;
4642
- return page.path;
4643
- }
4644
- function isValidUrl(urlString) {
4645
- try {
4646
- const url = new URL(urlString);
4647
- if (url.protocol !== "http:" && url.protocol !== "https:") return false;
4648
- const hostname = url.hostname;
4649
- if (!hostname || hostname.length === 0) return false;
4650
- if (hostname.includes(" ")) return false;
4651
- return hostname.includes(".") || hostname === "localhost";
4652
- } catch {
4653
- return false;
4654
- }
4655
- }
4656
- function normalizeUrl(value) {
4657
- const trimmed = value.trim();
4658
- if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
4659
- return `https://${trimmed}`;
4660
- }
4661
- function validateUrlInput(value, pages, allPages) {
4662
- if (!value.trim()) return "";
4663
- const filtered = pages.filter((p) => p.title.toLowerCase().startsWith(value.toLowerCase()));
4664
- if (filtered.length > 0) return "";
4665
- const matchingPage = allPages.find((p) => p.title.toLowerCase() === value.toLowerCase());
4666
- if (matchingPage) return "";
4667
- if (!isValidUrl(normalizeUrl(value))) {
4668
- return "Please enter a valid URL (e.g., https://example.com)";
4669
- }
4670
- return "";
4671
- }
4672
- function getEditModeInitialState(target, pages, sections) {
4673
- const empty = {
4674
- searchValue: "",
4675
- selectedPage: null,
4676
- selectedSection: null,
4677
- step: "input"
4678
- };
4679
- if (!target) return empty;
4680
- if (isValidUrl(normalizeUrl(target))) {
4681
- return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
4682
- }
4683
- const { pageRoute, sectionId } = parseTarget(target);
4684
- if (sectionId) {
4685
- if (isInternalPath(pageRoute)) {
4686
- const page = resolvePage(pageRoute, pages);
4687
- const sec = sections.find((s) => s.id === sectionId);
4688
- return {
4689
- searchValue: page.title,
4690
- selectedPage: page,
4691
- selectedSection: sec ? { id: sec.id, label: sec.label } : null,
4692
- step: "input"
4693
- };
4694
- }
4695
- return { searchValue: pageRoute || target, selectedPage: null, selectedSection: null, step: "input" };
4696
- }
4697
- if (isInternalPath(pageRoute)) {
4698
- const page = resolvePage(pageRoute, pages);
4699
- return {
4700
- searchValue: page.title,
4701
- selectedPage: page,
4702
- selectedSection: null,
4703
- step: "input"
4704
- };
4705
- }
4706
- return { searchValue: target, selectedPage: null, selectedSection: null, step: "input" };
4707
- }
4708
- function filterAvailablePages(pages, existingTargets) {
4709
- const taken = new Set(existingTargets);
4710
- return pages.filter((p) => !taken.has(p.path));
4711
- }
4712
-
4713
- // src/ui/link-modal/useLinkModalState.ts
4714
4808
  function useLinkModalState({
4715
4809
  open,
4716
4810
  mode,
4717
4811
  pages,
4718
- sections,
4812
+ sections: _sections,
4719
4813
  sectionsByPath,
4720
4814
  initialTarget,
4721
4815
  existingTargets,
@@ -4744,7 +4838,7 @@ function useLinkModalState({
4744
4838
  if (!open) return;
4745
4839
  if (mode === "edit" && initialTarget) {
4746
4840
  const { pageRoute } = parseTarget(initialTarget);
4747
- const targetSections = getSectionsForPath(sectionsByPath, pageRoute, sections);
4841
+ const targetSections = getSectionsForPath(sectionsByPath, pageRoute);
4748
4842
  const init = getEditModeInitialState(initialTarget, pages, targetSections);
4749
4843
  setSearchValue(init.searchValue);
4750
4844
  setSelectedPage(init.selectedPage);
@@ -4755,21 +4849,19 @@ function useLinkModalState({
4755
4849
  }
4756
4850
  setDropdownOpen(false);
4757
4851
  setUrlError("");
4758
- }, [open, mode, initialTarget, pages, sections, sectionsByPath, reset]);
4852
+ }, [open, mode, initialTarget, pages, sectionsByPath, reset]);
4759
4853
  const filteredPages = (0, import_react4.useMemo)(() => {
4760
4854
  if (!searchValue.trim()) return availablePages;
4761
4855
  return availablePages.filter((p) => p.title.toLowerCase().startsWith(searchValue.toLowerCase()));
4762
4856
  }, [availablePages, searchValue]);
4763
4857
  const activeSections = (0, import_react4.useMemo)(() => {
4764
- if (selectedPage && sectionsByPath) {
4765
- return getSectionsForPath(sectionsByPath, selectedPage.path, sections);
4766
- }
4767
- return sections;
4768
- }, [selectedPage, sectionsByPath, sections]);
4858
+ if (!selectedPage) return [];
4859
+ return getSectionsForPath(sectionsByPath, selectedPage.path);
4860
+ }, [selectedPage, sectionsByPath]);
4769
4861
  const showBreadcrumb = step === "confirmed" && selectedPage && selectedSection;
4770
4862
  const showSectionPicker = step === "sectionPicker" && selectedPage;
4771
4863
  const showSectionRow = mode === "edit" && selectedPage && selectedSection && step === "input";
4772
- const showChooseSection = step === "input" && selectedPage && !selectedSection && !showSectionRow;
4864
+ const showChooseSection = step === "input" && selectedPage && !selectedSection && !showSectionRow && activeSections.length > 0;
4773
4865
  const handleInputChange = (value) => {
4774
4866
  setSearchValue(value);
4775
4867
  setSelectedPage(null);
@@ -5033,11 +5125,13 @@ var DEV_SECTIONS_BY_PATH = {
5033
5125
  ],
5034
5126
  "/classes": [{ id: "class-library", label: "Class Library" }],
5035
5127
  "/pricing": [
5128
+ { id: "page-header", label: "Page Header" },
5129
+ { id: "free-class-cta", label: "Free Class" },
5036
5130
  { id: "benefits-marquee", label: "Benefits" },
5037
5131
  { id: "monthly-memberships", label: "Memberships" },
5038
- { id: "package-matcher", label: "Packages" },
5039
5132
  { id: "specialty-programs", label: "Specialty Programs" },
5040
- { id: "free-class-cta", label: "Free Class" }
5133
+ { id: "package-matcher", label: "Packages" },
5134
+ { id: "wordmark", label: "Wordmark" }
5041
5135
  ],
5042
5136
  "/studio": [
5043
5137
  { id: "studio-intro", label: "Studio Intro" },
@@ -5346,15 +5440,8 @@ function getHrefKeyFromElement(el) {
5346
5440
  if (!key) return null;
5347
5441
  return { anchor, key };
5348
5442
  }
5349
- function titleCaseSectionId(id) {
5350
- return id.split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
5351
- }
5352
5443
  function collectSections() {
5353
- return Array.from(document.querySelectorAll("[data-ohw-section]")).map((el) => {
5354
- const id = el.getAttribute("data-ohw-section") ?? "";
5355
- const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
5356
- return { id, label };
5357
- });
5444
+ return collectSectionsFromDom();
5358
5445
  }
5359
5446
  var FORCE_PSEUDO_STATES = [
5360
5447
  { pseudo: ":hover", state: "hover" },
@@ -5776,6 +5863,7 @@ function OhhwellsBridge() {
5776
5863
  const [linkPopover, setLinkPopover] = (0, import_react5.useState)(null);
5777
5864
  const [sitePages, setSitePages] = (0, import_react5.useState)([]);
5778
5865
  const [sectionsByPath, setSectionsByPath] = (0, import_react5.useState)({});
5866
+ const sectionsPrefetchGenRef = (0, import_react5.useRef)(0);
5779
5867
  const setLinkPopoverRef = (0, import_react5.useRef)(setLinkPopover);
5780
5868
  const linkPopoverPanelRef = (0, import_react5.useRef)(null);
5781
5869
  const linkPopoverOpenRef = (0, import_react5.useRef)(false);
@@ -5784,6 +5872,35 @@ function OhhwellsBridge() {
5784
5872
  const bumpLinkPopoverGrace = () => {
5785
5873
  linkPopoverGraceUntilRef.current = Date.now() + 350;
5786
5874
  };
5875
+ const runSectionsPrefetch = (0, import_react5.useCallback)((pages) => {
5876
+ if (!isEditMode || shouldUseDevFixtures() || pages.length === 0) return;
5877
+ const gen = ++sectionsPrefetchGenRef.current;
5878
+ const paths = pages.map((p) => p.path);
5879
+ void prefetchSectionsForPaths(paths).then((fetched) => {
5880
+ if (gen !== sectionsPrefetchGenRef.current) return;
5881
+ if (process.env.NODE_ENV === "development") {
5882
+ console.info("[ohhwells-bridge] Sections prefetched for", Object.keys(fetched).join(", ") || "(none)");
5883
+ }
5884
+ setSectionsByPath(
5885
+ (prev) => mergePrefetchedSections(
5886
+ prev,
5887
+ fetched,
5888
+ typeof window !== "undefined" ? window.location.pathname : pathname,
5889
+ collectSections()
5890
+ )
5891
+ );
5892
+ });
5893
+ }, [isEditMode, pathname]);
5894
+ const runSectionsPrefetchRef = (0, import_react5.useRef)(runSectionsPrefetch);
5895
+ runSectionsPrefetchRef.current = runSectionsPrefetch;
5896
+ (0, import_react5.useEffect)(() => {
5897
+ if (!linkPopover) return;
5898
+ if (hoveredImageRef.current) {
5899
+ hoveredImageRef.current = null;
5900
+ hoveredImageHasTextOverlapRef.current = false;
5901
+ }
5902
+ postToParent({ type: "ow:image-unhover" });
5903
+ }, [linkPopover, postToParent]);
5787
5904
  (0, import_react5.useEffect)(() => {
5788
5905
  if (!isEditMode) return;
5789
5906
  const useFixtures = shouldUseDevFixtures();
@@ -5797,18 +5914,25 @@ function OhhwellsBridge() {
5797
5914
  const onSitePages = (e) => {
5798
5915
  if (e.data?.type !== "ow:site-pages" || !Array.isArray(e.data.pages)) return;
5799
5916
  if (useFixtures) return;
5800
- setSitePages(
5801
- e.data.pages.map((p) => ({
5802
- path: p.path,
5803
- title: p.title
5804
- }))
5805
- );
5917
+ const mapped = e.data.pages.map((p) => ({
5918
+ path: p.path,
5919
+ title: p.title
5920
+ }));
5921
+ setSitePages(mapped);
5922
+ runSectionsPrefetchRef.current(mapped);
5806
5923
  };
5807
5924
  window.addEventListener("message", onSitePages);
5808
5925
  if (!useFixtures) postToParent({ type: "ow:request-site-pages" });
5809
5926
  return () => window.removeEventListener("message", onSitePages);
5810
5927
  }, [isEditMode, postToParent]);
5811
5928
  const [sectionGap, setSectionGap] = (0, import_react5.useState)(null);
5929
+ (0, import_react5.useEffect)(() => {
5930
+ if (!isEditMode || shouldUseDevFixtures()) return;
5931
+ void loadAllSectionsManifest().then((manifest) => {
5932
+ if (Object.keys(manifest).length === 0) return;
5933
+ setSectionsByPath((prev) => ({ ...manifest, ...prev }));
5934
+ });
5935
+ }, [isEditMode]);
5812
5936
  (0, import_react5.useEffect)(() => {
5813
5937
  const update = () => {
5814
5938
  const el = activeElRef.current;
@@ -6236,6 +6360,15 @@ function OhhwellsBridge() {
6236
6360
  return smallest;
6237
6361
  };
6238
6362
  const probeImageAt = (clientX, clientY, isDragOver = false, fromParentViewport = false) => {
6363
+ if (linkPopoverOpenRef.current) {
6364
+ if (hoveredImageRef.current) {
6365
+ hoveredImageRef.current = null;
6366
+ hoveredImageHasTextOverlapRef.current = false;
6367
+ resumeAnimTracks();
6368
+ postToParentRef.current({ type: "ow:image-unhover" });
6369
+ }
6370
+ return;
6371
+ }
6239
6372
  const toggleEl = document.querySelector("[data-ohw-state-toggle]");
6240
6373
  if (toggleEl) {
6241
6374
  const { x, y } = toProbeCoords(clientX, clientY, fromParentViewport);
@@ -6261,6 +6394,15 @@ function OhhwellsBridge() {
6261
6394
  }
6262
6395
  return;
6263
6396
  }
6397
+ if (topEl?.closest("[data-ohw-link-popover-root]") || topEl?.closest("[data-ohw-link-modal-root]")) {
6398
+ if (hoveredImageRef.current) {
6399
+ hoveredImageRef.current = null;
6400
+ hoveredImageHasTextOverlapRef.current = false;
6401
+ resumeAnimTracks();
6402
+ postToParentRef.current({ type: "ow:image-unhover" });
6403
+ }
6404
+ return;
6405
+ }
6264
6406
  const isStateCardImage = !!imgEl.closest("[data-ohw-editable-state]");
6265
6407
  const textEditable = Array.from(
6266
6408
  document.querySelectorAll('[data-ohw-editable]:not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"])')
@@ -6879,7 +7021,8 @@ function OhhwellsBridge() {
6879
7021
  refreshStateRules();
6880
7022
  const sections = collectSections();
6881
7023
  setSectionsByPath((prev) => {
6882
- const next = { ...prev, [pathname]: sections };
7024
+ const pathKey = normalizePath(pathname);
7025
+ const next = { ...prev, [pathKey]: sections };
6883
7026
  return shouldUseDevFixtures() ? { ...DEV_SECTIONS_BY_PATH, ...next } : next;
6884
7027
  });
6885
7028
  postToParent({ type: "ow:ready", version: "1", path: pathname, nodes: collectEditableNodes(), sections });
@@ -6890,6 +7033,12 @@ function OhhwellsBridge() {
6890
7033
  clearTimeout(timer);
6891
7034
  };
6892
7035
  }, [pathname, isEditMode, refreshStateRules, postToParent]);
7036
+ (0, import_react5.useEffect)(() => {
7037
+ scrollToHashSectionWhenReady();
7038
+ const onHashChange = () => scrollToHashSectionWhenReady();
7039
+ window.addEventListener("hashchange", onHashChange);
7040
+ return () => window.removeEventListener("hashchange", onHashChange);
7041
+ }, [pathname]);
6893
7042
  const handleCommand = (0, import_react5.useCallback)((cmd) => {
6894
7043
  document.execCommand(cmd, false);
6895
7044
  activeElRef.current?.focus();