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