@ohhwells/bridge 0.1.26 → 0.1.27-next.19

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" },
@@ -5284,6 +5378,7 @@ function applyLinkByKey(key, val) {
5284
5378
  document.querySelectorAll(`[data-ohw-href-key="${key}"]`).forEach((el) => {
5285
5379
  applyLinkHref(el, val);
5286
5380
  });
5381
+ window.dispatchEvent(new CustomEvent("ohw:link-href", { detail: { key, href: val } }));
5287
5382
  }
5288
5383
  function isInsideLinkEditor(target) {
5289
5384
  return Boolean(
@@ -5298,15 +5393,8 @@ function getHrefKeyFromElement(el) {
5298
5393
  if (!key) return null;
5299
5394
  return { anchor, key };
5300
5395
  }
5301
- function titleCaseSectionId(id) {
5302
- return id.split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
5303
- }
5304
5396
  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
- });
5397
+ return collectSectionsFromDom();
5310
5398
  }
5311
5399
  var FORCE_PSEUDO_STATES = [
5312
5400
  { pseudo: ":hover", state: "hover" },
@@ -5669,7 +5757,7 @@ function OhhwellsBridge() {
5669
5757
  const searchParams = useSearchParams();
5670
5758
  const isEditMode = isEditSessionActive();
5671
5759
  const [bridgeRoot, setBridgeRoot] = useState5(null);
5672
- useEffect4(() => {
5760
+ useEffect3(() => {
5673
5761
  const figtreeFontId = "ohw-figtree-font";
5674
5762
  if (!document.getElementById(figtreeFontId)) {
5675
5763
  const preconnect1 = Object.assign(document.createElement("link"), { rel: "preconnect", href: "https://fonts.googleapis.com" });
@@ -5728,6 +5816,7 @@ function OhhwellsBridge() {
5728
5816
  const [linkPopover, setLinkPopover] = useState5(null);
5729
5817
  const [sitePages, setSitePages] = useState5([]);
5730
5818
  const [sectionsByPath, setSectionsByPath] = useState5({});
5819
+ const sectionsPrefetchGenRef = useRef3(0);
5731
5820
  const setLinkPopoverRef = useRef3(setLinkPopover);
5732
5821
  const linkPopoverPanelRef = useRef3(null);
5733
5822
  const linkPopoverOpenRef = useRef3(false);
@@ -5736,7 +5825,28 @@ function OhhwellsBridge() {
5736
5825
  const bumpLinkPopoverGrace = () => {
5737
5826
  linkPopoverGraceUntilRef.current = Date.now() + 350;
5738
5827
  };
5739
- useEffect4(() => {
5828
+ const runSectionsPrefetch = useCallback2((pages) => {
5829
+ if (!isEditMode || shouldUseDevFixtures() || pages.length === 0) return;
5830
+ const gen = ++sectionsPrefetchGenRef.current;
5831
+ const paths = pages.map((p) => p.path);
5832
+ void prefetchSectionsForPaths(paths).then((fetched) => {
5833
+ if (gen !== sectionsPrefetchGenRef.current) return;
5834
+ if (process.env.NODE_ENV === "development") {
5835
+ console.info("[ohhwells-bridge] Sections prefetched for", Object.keys(fetched).join(", ") || "(none)");
5836
+ }
5837
+ setSectionsByPath(
5838
+ (prev) => mergePrefetchedSections(
5839
+ prev,
5840
+ fetched,
5841
+ typeof window !== "undefined" ? window.location.pathname : pathname,
5842
+ collectSections()
5843
+ )
5844
+ );
5845
+ });
5846
+ }, [isEditMode, pathname]);
5847
+ const runSectionsPrefetchRef = useRef3(runSectionsPrefetch);
5848
+ runSectionsPrefetchRef.current = runSectionsPrefetch;
5849
+ useEffect3(() => {
5740
5850
  if (!linkPopover) return;
5741
5851
  if (hoveredImageRef.current) {
5742
5852
  hoveredImageRef.current = null;
@@ -5744,7 +5854,7 @@ function OhhwellsBridge() {
5744
5854
  }
5745
5855
  postToParent({ type: "ow:image-unhover" });
5746
5856
  }, [linkPopover, postToParent]);
5747
- useEffect4(() => {
5857
+ useEffect3(() => {
5748
5858
  if (!isEditMode) return;
5749
5859
  const useFixtures = shouldUseDevFixtures();
5750
5860
  if (useFixtures) {
@@ -5757,19 +5867,26 @@ function OhhwellsBridge() {
5757
5867
  const onSitePages = (e) => {
5758
5868
  if (e.data?.type !== "ow:site-pages" || !Array.isArray(e.data.pages)) return;
5759
5869
  if (useFixtures) return;
5760
- setSitePages(
5761
- e.data.pages.map((p) => ({
5762
- path: p.path,
5763
- title: p.title
5764
- }))
5765
- );
5870
+ const mapped = e.data.pages.map((p) => ({
5871
+ path: p.path,
5872
+ title: p.title
5873
+ }));
5874
+ setSitePages(mapped);
5875
+ runSectionsPrefetchRef.current(mapped);
5766
5876
  };
5767
5877
  window.addEventListener("message", onSitePages);
5768
5878
  if (!useFixtures) postToParent({ type: "ow:request-site-pages" });
5769
5879
  return () => window.removeEventListener("message", onSitePages);
5770
5880
  }, [isEditMode, postToParent]);
5771
5881
  const [sectionGap, setSectionGap] = useState5(null);
5772
- useEffect4(() => {
5882
+ useEffect3(() => {
5883
+ if (!isEditMode || shouldUseDevFixtures()) return;
5884
+ void loadAllSectionsManifest().then((manifest) => {
5885
+ if (Object.keys(manifest).length === 0) return;
5886
+ setSectionsByPath((prev) => ({ ...manifest, ...prev }));
5887
+ });
5888
+ }, [isEditMode]);
5889
+ useEffect3(() => {
5773
5890
  const update = () => {
5774
5891
  const el = activeElRef.current;
5775
5892
  if (el) setToolbarRect(el.getBoundingClientRect());
@@ -5896,7 +6013,7 @@ function OhhwellsBridge() {
5896
6013
  cancelled = true;
5897
6014
  };
5898
6015
  }, [subdomain, isEditMode, pathname]);
5899
- useEffect4(() => {
6016
+ useEffect3(() => {
5900
6017
  if (!subdomain || isEditMode) return;
5901
6018
  let debounceTimer = null;
5902
6019
  const applyFromCache = () => {
@@ -5939,10 +6056,10 @@ function OhhwellsBridge() {
5939
6056
  const visible = Boolean(subdomain) && fetchState !== "done";
5940
6057
  el.style.display = visible ? "flex" : "none";
5941
6058
  }, [subdomain, fetchState]);
5942
- useEffect4(() => {
6059
+ useEffect3(() => {
5943
6060
  postToParent({ type: "ow:navigation", path: pathname });
5944
6061
  }, [pathname, postToParent]);
5945
- useEffect4(() => {
6062
+ useEffect3(() => {
5946
6063
  if (!isEditMode) return;
5947
6064
  const measure = () => {
5948
6065
  const h = document.body.scrollHeight;
@@ -5966,7 +6083,7 @@ function OhhwellsBridge() {
5966
6083
  window.removeEventListener("resize", handleResize);
5967
6084
  };
5968
6085
  }, [pathname, isEditMode, postToParent]);
5969
- useEffect4(() => {
6086
+ useEffect3(() => {
5970
6087
  if (!subdomainFromQuery || isEditMode) return;
5971
6088
  const handleClick = (e) => {
5972
6089
  const anchor = e.target.closest("a");
@@ -5982,7 +6099,7 @@ function OhhwellsBridge() {
5982
6099
  document.addEventListener("click", handleClick, true);
5983
6100
  return () => document.removeEventListener("click", handleClick, true);
5984
6101
  }, [subdomainFromQuery, isEditMode, router]);
5985
- useEffect4(() => {
6102
+ useEffect3(() => {
5986
6103
  if (!isEditMode) {
5987
6104
  editStylesRef.current?.base.remove();
5988
6105
  editStylesRef.current?.forceHover.remove();
@@ -6840,7 +6957,7 @@ function OhhwellsBridge() {
6840
6957
  if (imageShowTimerRef.current) clearTimeout(imageShowTimerRef.current);
6841
6958
  };
6842
6959
  }, [isEditMode, refreshStateRules]);
6843
- useEffect4(() => {
6960
+ useEffect3(() => {
6844
6961
  if (!isEditMode) return;
6845
6962
  document.querySelectorAll("[data-ohw-active-state]").forEach((el) => {
6846
6963
  el.removeAttribute("data-ohw-active-state");
@@ -6857,7 +6974,8 @@ function OhhwellsBridge() {
6857
6974
  refreshStateRules();
6858
6975
  const sections = collectSections();
6859
6976
  setSectionsByPath((prev) => {
6860
- const next = { ...prev, [pathname]: sections };
6977
+ const pathKey = normalizePath(pathname);
6978
+ const next = { ...prev, [pathKey]: sections };
6861
6979
  return shouldUseDevFixtures() ? { ...DEV_SECTIONS_BY_PATH, ...next } : next;
6862
6980
  });
6863
6981
  postToParent({ type: "ow:ready", version: "1", path: pathname, nodes: collectEditableNodes(), sections });
@@ -6868,6 +6986,12 @@ function OhhwellsBridge() {
6868
6986
  clearTimeout(timer);
6869
6987
  };
6870
6988
  }, [pathname, isEditMode, refreshStateRules, postToParent]);
6989
+ useEffect3(() => {
6990
+ scrollToHashSectionWhenReady();
6991
+ const onHashChange = () => scrollToHashSectionWhenReady();
6992
+ window.addEventListener("hashchange", onHashChange);
6993
+ return () => window.removeEventListener("hashchange", onHashChange);
6994
+ }, [pathname]);
6871
6995
  const handleCommand = useCallback2((cmd) => {
6872
6996
  document.execCommand(cmd, false);
6873
6997
  activeElRef.current?.focus();