@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.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
|
|
4501
|
-
|
|
4502
|
-
|
|
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:
|
|
4513
|
-
"
|
|
4514
|
-
{
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
}
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
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
|
|
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,
|
|
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
|
|
4765
|
-
|
|
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: "
|
|
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
|
|
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,27 @@ 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;
|
|
5787
5896
|
(0, import_react5.useEffect)(() => {
|
|
5788
5897
|
if (!linkPopover) return;
|
|
5789
5898
|
if (hoveredImageRef.current) {
|
|
@@ -5805,18 +5914,25 @@ function OhhwellsBridge() {
|
|
|
5805
5914
|
const onSitePages = (e) => {
|
|
5806
5915
|
if (e.data?.type !== "ow:site-pages" || !Array.isArray(e.data.pages)) return;
|
|
5807
5916
|
if (useFixtures) return;
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
);
|
|
5917
|
+
const mapped = e.data.pages.map((p) => ({
|
|
5918
|
+
path: p.path,
|
|
5919
|
+
title: p.title
|
|
5920
|
+
}));
|
|
5921
|
+
setSitePages(mapped);
|
|
5922
|
+
runSectionsPrefetchRef.current(mapped);
|
|
5814
5923
|
};
|
|
5815
5924
|
window.addEventListener("message", onSitePages);
|
|
5816
5925
|
if (!useFixtures) postToParent({ type: "ow:request-site-pages" });
|
|
5817
5926
|
return () => window.removeEventListener("message", onSitePages);
|
|
5818
5927
|
}, [isEditMode, postToParent]);
|
|
5819
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]);
|
|
5820
5936
|
(0, import_react5.useEffect)(() => {
|
|
5821
5937
|
const update = () => {
|
|
5822
5938
|
const el = activeElRef.current;
|
|
@@ -6905,7 +7021,8 @@ function OhhwellsBridge() {
|
|
|
6905
7021
|
refreshStateRules();
|
|
6906
7022
|
const sections = collectSections();
|
|
6907
7023
|
setSectionsByPath((prev) => {
|
|
6908
|
-
const
|
|
7024
|
+
const pathKey = normalizePath(pathname);
|
|
7025
|
+
const next = { ...prev, [pathKey]: sections };
|
|
6909
7026
|
return shouldUseDevFixtures() ? { ...DEV_SECTIONS_BY_PATH, ...next } : next;
|
|
6910
7027
|
});
|
|
6911
7028
|
postToParent({ type: "ow:ready", version: "1", path: pathname, nodes: collectEditableNodes(), sections });
|
|
@@ -6916,6 +7033,12 @@ function OhhwellsBridge() {
|
|
|
6916
7033
|
clearTimeout(timer);
|
|
6917
7034
|
};
|
|
6918
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]);
|
|
6919
7042
|
const handleCommand = (0, import_react5.useCallback)((cmd) => {
|
|
6920
7043
|
document.execCommand(cmd, false);
|
|
6921
7044
|
activeElRef.current?.focus();
|