@ohhwells/bridge 0.1.26 → 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/README.md +304 -304
- package/dist/index.cjs +340 -217
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +354 -231
- package/dist/index.js.map +1 -1
- package/dist/styles.css +14 -12
- package/package.json +48 -48
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
|
|
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 {
|
|
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
|
|
4453
|
-
|
|
4454
|
-
|
|
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:
|
|
4465
|
-
"
|
|
4466
|
-
{
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
}
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
4717
|
-
|
|
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: "
|
|
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
|
|
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
|
-
|
|
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,28 @@ function OhhwellsBridge() {
|
|
|
5736
5824
|
const bumpLinkPopoverGrace = () => {
|
|
5737
5825
|
linkPopoverGraceUntilRef.current = Date.now() + 350;
|
|
5738
5826
|
};
|
|
5739
|
-
|
|
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(() => {
|
|
5740
5849
|
if (!linkPopover) return;
|
|
5741
5850
|
if (hoveredImageRef.current) {
|
|
5742
5851
|
hoveredImageRef.current = null;
|
|
@@ -5744,7 +5853,7 @@ function OhhwellsBridge() {
|
|
|
5744
5853
|
}
|
|
5745
5854
|
postToParent({ type: "ow:image-unhover" });
|
|
5746
5855
|
}, [linkPopover, postToParent]);
|
|
5747
|
-
|
|
5856
|
+
useEffect3(() => {
|
|
5748
5857
|
if (!isEditMode) return;
|
|
5749
5858
|
const useFixtures = shouldUseDevFixtures();
|
|
5750
5859
|
if (useFixtures) {
|
|
@@ -5757,19 +5866,26 @@ function OhhwellsBridge() {
|
|
|
5757
5866
|
const onSitePages = (e) => {
|
|
5758
5867
|
if (e.data?.type !== "ow:site-pages" || !Array.isArray(e.data.pages)) return;
|
|
5759
5868
|
if (useFixtures) return;
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
);
|
|
5869
|
+
const mapped = e.data.pages.map((p) => ({
|
|
5870
|
+
path: p.path,
|
|
5871
|
+
title: p.title
|
|
5872
|
+
}));
|
|
5873
|
+
setSitePages(mapped);
|
|
5874
|
+
runSectionsPrefetchRef.current(mapped);
|
|
5766
5875
|
};
|
|
5767
5876
|
window.addEventListener("message", onSitePages);
|
|
5768
5877
|
if (!useFixtures) postToParent({ type: "ow:request-site-pages" });
|
|
5769
5878
|
return () => window.removeEventListener("message", onSitePages);
|
|
5770
5879
|
}, [isEditMode, postToParent]);
|
|
5771
5880
|
const [sectionGap, setSectionGap] = useState5(null);
|
|
5772
|
-
|
|
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(() => {
|
|
5773
5889
|
const update = () => {
|
|
5774
5890
|
const el = activeElRef.current;
|
|
5775
5891
|
if (el) setToolbarRect(el.getBoundingClientRect());
|
|
@@ -5896,7 +6012,7 @@ function OhhwellsBridge() {
|
|
|
5896
6012
|
cancelled = true;
|
|
5897
6013
|
};
|
|
5898
6014
|
}, [subdomain, isEditMode, pathname]);
|
|
5899
|
-
|
|
6015
|
+
useEffect3(() => {
|
|
5900
6016
|
if (!subdomain || isEditMode) return;
|
|
5901
6017
|
let debounceTimer = null;
|
|
5902
6018
|
const applyFromCache = () => {
|
|
@@ -5939,10 +6055,10 @@ function OhhwellsBridge() {
|
|
|
5939
6055
|
const visible = Boolean(subdomain) && fetchState !== "done";
|
|
5940
6056
|
el.style.display = visible ? "flex" : "none";
|
|
5941
6057
|
}, [subdomain, fetchState]);
|
|
5942
|
-
|
|
6058
|
+
useEffect3(() => {
|
|
5943
6059
|
postToParent({ type: "ow:navigation", path: pathname });
|
|
5944
6060
|
}, [pathname, postToParent]);
|
|
5945
|
-
|
|
6061
|
+
useEffect3(() => {
|
|
5946
6062
|
if (!isEditMode) return;
|
|
5947
6063
|
const measure = () => {
|
|
5948
6064
|
const h = document.body.scrollHeight;
|
|
@@ -5966,7 +6082,7 @@ function OhhwellsBridge() {
|
|
|
5966
6082
|
window.removeEventListener("resize", handleResize);
|
|
5967
6083
|
};
|
|
5968
6084
|
}, [pathname, isEditMode, postToParent]);
|
|
5969
|
-
|
|
6085
|
+
useEffect3(() => {
|
|
5970
6086
|
if (!subdomainFromQuery || isEditMode) return;
|
|
5971
6087
|
const handleClick = (e) => {
|
|
5972
6088
|
const anchor = e.target.closest("a");
|
|
@@ -5982,7 +6098,7 @@ function OhhwellsBridge() {
|
|
|
5982
6098
|
document.addEventListener("click", handleClick, true);
|
|
5983
6099
|
return () => document.removeEventListener("click", handleClick, true);
|
|
5984
6100
|
}, [subdomainFromQuery, isEditMode, router]);
|
|
5985
|
-
|
|
6101
|
+
useEffect3(() => {
|
|
5986
6102
|
if (!isEditMode) {
|
|
5987
6103
|
editStylesRef.current?.base.remove();
|
|
5988
6104
|
editStylesRef.current?.forceHover.remove();
|
|
@@ -6840,7 +6956,7 @@ function OhhwellsBridge() {
|
|
|
6840
6956
|
if (imageShowTimerRef.current) clearTimeout(imageShowTimerRef.current);
|
|
6841
6957
|
};
|
|
6842
6958
|
}, [isEditMode, refreshStateRules]);
|
|
6843
|
-
|
|
6959
|
+
useEffect3(() => {
|
|
6844
6960
|
if (!isEditMode) return;
|
|
6845
6961
|
document.querySelectorAll("[data-ohw-active-state]").forEach((el) => {
|
|
6846
6962
|
el.removeAttribute("data-ohw-active-state");
|
|
@@ -6857,7 +6973,8 @@ function OhhwellsBridge() {
|
|
|
6857
6973
|
refreshStateRules();
|
|
6858
6974
|
const sections = collectSections();
|
|
6859
6975
|
setSectionsByPath((prev) => {
|
|
6860
|
-
const
|
|
6976
|
+
const pathKey = normalizePath(pathname);
|
|
6977
|
+
const next = { ...prev, [pathKey]: sections };
|
|
6861
6978
|
return shouldUseDevFixtures() ? { ...DEV_SECTIONS_BY_PATH, ...next } : next;
|
|
6862
6979
|
});
|
|
6863
6980
|
postToParent({ type: "ow:ready", version: "1", path: pathname, nodes: collectEditableNodes(), sections });
|
|
@@ -6868,6 +6985,12 @@ function OhhwellsBridge() {
|
|
|
6868
6985
|
clearTimeout(timer);
|
|
6869
6986
|
};
|
|
6870
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]);
|
|
6871
6994
|
const handleCommand = useCallback2((cmd) => {
|
|
6872
6995
|
document.execCommand(cmd, false);
|
|
6873
6996
|
activeElRef.current?.focus();
|