@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/README.md +304 -304
- package/dist/index.cjs +341 -217
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +355 -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" },
|
|
@@ -5332,6 +5426,7 @@ function applyLinkByKey(key, val) {
|
|
|
5332
5426
|
document.querySelectorAll(`[data-ohw-href-key="${key}"]`).forEach((el) => {
|
|
5333
5427
|
applyLinkHref(el, val);
|
|
5334
5428
|
});
|
|
5429
|
+
window.dispatchEvent(new CustomEvent("ohw:link-href", { detail: { key, href: val } }));
|
|
5335
5430
|
}
|
|
5336
5431
|
function isInsideLinkEditor(target) {
|
|
5337
5432
|
return Boolean(
|
|
@@ -5346,15 +5441,8 @@ function getHrefKeyFromElement(el) {
|
|
|
5346
5441
|
if (!key) return null;
|
|
5347
5442
|
return { anchor, key };
|
|
5348
5443
|
}
|
|
5349
|
-
function titleCaseSectionId(id) {
|
|
5350
|
-
return id.split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
5351
|
-
}
|
|
5352
5444
|
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
|
-
});
|
|
5445
|
+
return collectSectionsFromDom();
|
|
5358
5446
|
}
|
|
5359
5447
|
var FORCE_PSEUDO_STATES = [
|
|
5360
5448
|
{ pseudo: ":hover", state: "hover" },
|
|
@@ -5776,6 +5864,7 @@ function OhhwellsBridge() {
|
|
|
5776
5864
|
const [linkPopover, setLinkPopover] = (0, import_react5.useState)(null);
|
|
5777
5865
|
const [sitePages, setSitePages] = (0, import_react5.useState)([]);
|
|
5778
5866
|
const [sectionsByPath, setSectionsByPath] = (0, import_react5.useState)({});
|
|
5867
|
+
const sectionsPrefetchGenRef = (0, import_react5.useRef)(0);
|
|
5779
5868
|
const setLinkPopoverRef = (0, import_react5.useRef)(setLinkPopover);
|
|
5780
5869
|
const linkPopoverPanelRef = (0, import_react5.useRef)(null);
|
|
5781
5870
|
const linkPopoverOpenRef = (0, import_react5.useRef)(false);
|
|
@@ -5784,6 +5873,27 @@ function OhhwellsBridge() {
|
|
|
5784
5873
|
const bumpLinkPopoverGrace = () => {
|
|
5785
5874
|
linkPopoverGraceUntilRef.current = Date.now() + 350;
|
|
5786
5875
|
};
|
|
5876
|
+
const runSectionsPrefetch = (0, import_react5.useCallback)((pages) => {
|
|
5877
|
+
if (!isEditMode || shouldUseDevFixtures() || pages.length === 0) return;
|
|
5878
|
+
const gen = ++sectionsPrefetchGenRef.current;
|
|
5879
|
+
const paths = pages.map((p) => p.path);
|
|
5880
|
+
void prefetchSectionsForPaths(paths).then((fetched) => {
|
|
5881
|
+
if (gen !== sectionsPrefetchGenRef.current) return;
|
|
5882
|
+
if (process.env.NODE_ENV === "development") {
|
|
5883
|
+
console.info("[ohhwells-bridge] Sections prefetched for", Object.keys(fetched).join(", ") || "(none)");
|
|
5884
|
+
}
|
|
5885
|
+
setSectionsByPath(
|
|
5886
|
+
(prev) => mergePrefetchedSections(
|
|
5887
|
+
prev,
|
|
5888
|
+
fetched,
|
|
5889
|
+
typeof window !== "undefined" ? window.location.pathname : pathname,
|
|
5890
|
+
collectSections()
|
|
5891
|
+
)
|
|
5892
|
+
);
|
|
5893
|
+
});
|
|
5894
|
+
}, [isEditMode, pathname]);
|
|
5895
|
+
const runSectionsPrefetchRef = (0, import_react5.useRef)(runSectionsPrefetch);
|
|
5896
|
+
runSectionsPrefetchRef.current = runSectionsPrefetch;
|
|
5787
5897
|
(0, import_react5.useEffect)(() => {
|
|
5788
5898
|
if (!linkPopover) return;
|
|
5789
5899
|
if (hoveredImageRef.current) {
|
|
@@ -5805,18 +5915,25 @@ function OhhwellsBridge() {
|
|
|
5805
5915
|
const onSitePages = (e) => {
|
|
5806
5916
|
if (e.data?.type !== "ow:site-pages" || !Array.isArray(e.data.pages)) return;
|
|
5807
5917
|
if (useFixtures) return;
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
);
|
|
5918
|
+
const mapped = e.data.pages.map((p) => ({
|
|
5919
|
+
path: p.path,
|
|
5920
|
+
title: p.title
|
|
5921
|
+
}));
|
|
5922
|
+
setSitePages(mapped);
|
|
5923
|
+
runSectionsPrefetchRef.current(mapped);
|
|
5814
5924
|
};
|
|
5815
5925
|
window.addEventListener("message", onSitePages);
|
|
5816
5926
|
if (!useFixtures) postToParent({ type: "ow:request-site-pages" });
|
|
5817
5927
|
return () => window.removeEventListener("message", onSitePages);
|
|
5818
5928
|
}, [isEditMode, postToParent]);
|
|
5819
5929
|
const [sectionGap, setSectionGap] = (0, import_react5.useState)(null);
|
|
5930
|
+
(0, import_react5.useEffect)(() => {
|
|
5931
|
+
if (!isEditMode || shouldUseDevFixtures()) return;
|
|
5932
|
+
void loadAllSectionsManifest().then((manifest) => {
|
|
5933
|
+
if (Object.keys(manifest).length === 0) return;
|
|
5934
|
+
setSectionsByPath((prev) => ({ ...manifest, ...prev }));
|
|
5935
|
+
});
|
|
5936
|
+
}, [isEditMode]);
|
|
5820
5937
|
(0, import_react5.useEffect)(() => {
|
|
5821
5938
|
const update = () => {
|
|
5822
5939
|
const el = activeElRef.current;
|
|
@@ -6905,7 +7022,8 @@ function OhhwellsBridge() {
|
|
|
6905
7022
|
refreshStateRules();
|
|
6906
7023
|
const sections = collectSections();
|
|
6907
7024
|
setSectionsByPath((prev) => {
|
|
6908
|
-
const
|
|
7025
|
+
const pathKey = normalizePath(pathname);
|
|
7026
|
+
const next = { ...prev, [pathKey]: sections };
|
|
6909
7027
|
return shouldUseDevFixtures() ? { ...DEV_SECTIONS_BY_PATH, ...next } : next;
|
|
6910
7028
|
});
|
|
6911
7029
|
postToParent({ type: "ow:ready", version: "1", path: pathname, nodes: collectEditableNodes(), sections });
|
|
@@ -6916,6 +7034,12 @@ function OhhwellsBridge() {
|
|
|
6916
7034
|
clearTimeout(timer);
|
|
6917
7035
|
};
|
|
6918
7036
|
}, [pathname, isEditMode, refreshStateRules, postToParent]);
|
|
7037
|
+
(0, import_react5.useEffect)(() => {
|
|
7038
|
+
scrollToHashSectionWhenReady();
|
|
7039
|
+
const onHashChange = () => scrollToHashSectionWhenReady();
|
|
7040
|
+
window.addEventListener("hashchange", onHashChange);
|
|
7041
|
+
return () => window.removeEventListener("hashchange", onHashChange);
|
|
7042
|
+
}, [pathname]);
|
|
6919
7043
|
const handleCommand = (0, import_react5.useCallback)((cmd) => {
|
|
6920
7044
|
document.execCommand(cmd, false);
|
|
6921
7045
|
activeElRef.current?.focus();
|