@ohhwells/bridge 0.1.54-next.157 → 0.1.55-next.158

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -245,6 +245,76 @@ function applyBrandChrome(content) {
245
245
  }
246
246
  }
247
247
 
248
+ // src/lib/brand-kit.ts
249
+ var BRAND_KIT_KEY = "__ohw_brand";
250
+ var BRAND_VAR_PREFIX = "--ohw-brand-";
251
+ var BRAND_VAR_NAMES = ["primary", "accent", "light", "dark", "surface", "border", "muted"].map(
252
+ (role) => `${BRAND_VAR_PREFIX}${role}`
253
+ );
254
+ var FONT_VARS = { heading: ["--font-heading", "--font-display"], body: ["--font-body"] };
255
+ var BRAND_FONT_LINK_ID = "ohw-brand-fonts";
256
+ function brandColorVars(kit) {
257
+ const { dark, primary, accent, light } = kit.palette;
258
+ const mix = (a, aPct, b) => `color-mix(in srgb, ${a} ${aPct}%, ${b})`;
259
+ return {
260
+ [`${BRAND_VAR_PREFIX}primary`]: primary,
261
+ [`${BRAND_VAR_PREFIX}accent`]: accent,
262
+ [`${BRAND_VAR_PREFIX}light`]: light,
263
+ [`${BRAND_VAR_PREFIX}dark`]: dark,
264
+ [`${BRAND_VAR_PREFIX}surface`]: mix(light, 95, dark),
265
+ [`${BRAND_VAR_PREFIX}border`]: mix(light, 85, dark),
266
+ [`${BRAND_VAR_PREFIX}muted`]: mix(dark, 62, light)
267
+ };
268
+ }
269
+ function parseBrandKit(raw) {
270
+ if (!raw) return null;
271
+ try {
272
+ const parsed = JSON.parse(raw);
273
+ const p = parsed?.palette;
274
+ const f = parsed?.fonts;
275
+ if (!p || !f || typeof p.dark !== "string" || typeof p.primary !== "string" || typeof p.accent !== "string" || typeof p.light !== "string" || typeof f.heading !== "string" || typeof f.body !== "string") {
276
+ return null;
277
+ }
278
+ return {
279
+ palette: { dark: p.dark, primary: p.primary, accent: p.accent, light: p.light },
280
+ fonts: { heading: f.heading, body: f.body }
281
+ };
282
+ } catch {
283
+ return null;
284
+ }
285
+ }
286
+ function familyOf(stack) {
287
+ const first = stack.split(",")[0]?.trim() ?? "";
288
+ return first.replace(/^['"]|['"]$/g, "");
289
+ }
290
+ function loadBrandFonts(families) {
291
+ const unique = [...new Set(families.map((f) => f.trim()).filter(Boolean))];
292
+ if (unique.length === 0) return;
293
+ const spec = unique.map((f) => `${f.replace(/ /g, "+")}:400,500,600,700`).join("|");
294
+ const href = `https://fonts.googleapis.com/css?family=${spec}&display=swap`;
295
+ let link = document.getElementById(BRAND_FONT_LINK_ID);
296
+ if (!link) {
297
+ link = document.createElement("link");
298
+ link.id = BRAND_FONT_LINK_ID;
299
+ link.rel = "stylesheet";
300
+ document.head.appendChild(link);
301
+ }
302
+ if (link.href !== href) link.href = href;
303
+ }
304
+ function applyBrandToDom(kit) {
305
+ const root = document.documentElement;
306
+ if (!kit) {
307
+ for (const name of BRAND_VAR_NAMES) root.style.removeProperty(name);
308
+ for (const name of [...FONT_VARS.heading, ...FONT_VARS.body]) root.style.removeProperty(name);
309
+ document.getElementById(BRAND_FONT_LINK_ID)?.remove();
310
+ return;
311
+ }
312
+ for (const [name, value] of Object.entries(brandColorVars(kit))) root.style.setProperty(name, value);
313
+ for (const name of FONT_VARS.heading) root.style.setProperty(name, kit.fonts.heading);
314
+ for (const name of FONT_VARS.body) root.style.setProperty(name, kit.fonts.body);
315
+ loadBrandFonts([familyOf(kit.fonts.heading), familyOf(kit.fonts.body)]);
316
+ }
317
+
248
318
  // src/ui/ai-tree/aiSectionsManager.tsx
249
319
  var import_react_dom = require("react-dom");
250
320
  var import_client = require("react-dom/client");
@@ -1331,17 +1401,34 @@ var import_jsx_runtime2 = require("react/jsx-runtime");
1331
1401
  var CONTAINER_ATTR = "data-ohw-ai-generated";
1332
1402
  var REPLACED_ATTR = "data-ohw-ai-replaced-by";
1333
1403
  var REMOVED_ATTR = "data-ohw-ai-removed";
1404
+ function readRootVar(name) {
1405
+ if (typeof document === "undefined") return "";
1406
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
1407
+ }
1408
+ function deriveBrandOverride() {
1409
+ const dark = readRootVar("--ohw-brand-dark");
1410
+ const primary = readRootVar("--ohw-brand-primary");
1411
+ const light = readRootVar("--ohw-brand-light");
1412
+ if (!dark || !primary || !light) return null;
1413
+ const accent = readRootVar("--ohw-brand-accent");
1414
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1415
+ const body = readRootVar("--font-body");
1416
+ return {
1417
+ palette: { dark, primary, accent: accent || dark, light },
1418
+ fonts: {
1419
+ heading: heading || AI_DEFAULT_BRAND.fonts.heading,
1420
+ body: body || AI_DEFAULT_BRAND.fonts.body
1421
+ }
1422
+ };
1423
+ }
1334
1424
  function deriveTemplateBrand() {
1335
- if (typeof document === "undefined") return null;
1336
- const cs = getComputedStyle(document.documentElement);
1337
- const read = (name) => cs.getPropertyValue(name).trim();
1338
- const dark = read("--color-dark");
1339
- const primary = read("--color-primary");
1340
- const light = read("--color-light");
1425
+ const dark = readRootVar("--color-dark");
1426
+ const primary = readRootVar("--color-primary");
1427
+ const light = readRootVar("--color-light");
1341
1428
  if (!dark || !primary || !light) return null;
1342
- const accent = read("--color-accent");
1343
- const heading = read("--font-heading") || read("--font-display");
1344
- const body = read("--font-body");
1429
+ const accent = readRootVar("--color-accent");
1430
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1431
+ const body = readRootVar("--font-body");
1345
1432
  return {
1346
1433
  palette: { dark, primary, accent: accent || dark, light },
1347
1434
  fonts: {
@@ -1433,7 +1520,9 @@ function syncReplacedOriginals(state) {
1433
1520
  }
1434
1521
  function applyAiSectionsToDom(state, options) {
1435
1522
  if (typeof document === "undefined") return;
1523
+ const brandOverride = deriveBrandOverride();
1436
1524
  const templateBrand = deriveTemplateBrand();
1525
+ const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
1437
1526
  const activeIds = new Set(state.sections.map((entry) => entry.id));
1438
1527
  for (const [id, section] of mounted) {
1439
1528
  if (!activeIds.has(id)) {
@@ -1443,7 +1532,7 @@ function applyAiSectionsToDom(state, options) {
1443
1532
  }
1444
1533
  }
1445
1534
  for (const entry of state.sections) {
1446
- const serialized = JSON.stringify(entry);
1535
+ const serialized = JSON.stringify(entry) + brandKey;
1447
1536
  const existing = mounted.get(entry.id);
1448
1537
  if (existing && existing.serialized === serialized && existing.container.isConnected) {
1449
1538
  continue;
@@ -1467,7 +1556,7 @@ function applyAiSectionsToDom(state, options) {
1467
1556
  AiTreeRenderer,
1468
1557
  {
1469
1558
  tree: entry.tree,
1470
- brand: entry.brand ?? options?.brand ?? templateBrand ?? void 0,
1559
+ brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
1471
1560
  resolveMedia,
1472
1561
  editKeyPrefix: `ai.${entry.id}`
1473
1562
  }
@@ -14102,6 +14191,7 @@ function OhhwellsBridge() {
14102
14191
  const addNavAfterAnchorRef = (0, import_react16.useRef)(null);
14103
14192
  const editContentRef = (0, import_react16.useRef)({});
14104
14193
  const aiSectionsRef = (0, import_react16.useRef)("");
14194
+ const brandKitRef = (0, import_react16.useRef)("");
14105
14195
  const pendingDeleteUndoRef = (0, import_react16.useRef)(null);
14106
14196
  const [floatingPanel, setFloatingPanel] = (0, import_react16.useState)(null);
14107
14197
  const floatingPanelOpenRef = (0, import_react16.useRef)(false);
@@ -15348,12 +15438,17 @@ function OhhwellsBridge() {
15348
15438
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
15349
15439
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
15350
15440
  }
15441
+ if (typeof content[BRAND_KIT_KEY] === "string") {
15442
+ brandKitRef.current = content[BRAND_KIT_KEY];
15443
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
15444
+ }
15351
15445
  applyBrandChrome(content);
15352
15446
  for (const [key, val] of Object.entries(content)) {
15353
15447
  if (key === "__ohw_sections") continue;
15354
15448
  if (key === AI_SECTIONS_KEY) continue;
15355
15449
  if (key === LOGO_PLACEHOLDER_KEY) continue;
15356
15450
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
15451
+ if (key === BRAND_KIT_KEY) continue;
15357
15452
  if (BRAND_CHROME_KEYS.has(key)) continue;
15358
15453
  if (applyVideoSettingNode(key, val)) continue;
15359
15454
  if (applyCarouselNode(key, val)) continue;
@@ -15436,10 +15531,14 @@ function OhhwellsBridge() {
15436
15531
  observer?.disconnect();
15437
15532
  try {
15438
15533
  applyBrandChrome(content);
15534
+ if (typeof content[BRAND_KIT_KEY] === "string") {
15535
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
15536
+ }
15439
15537
  for (const [key, val] of Object.entries(content)) {
15440
15538
  if (key === "__ohw_sections") continue;
15441
15539
  if (key === LOGO_PLACEHOLDER_KEY) continue;
15442
15540
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
15541
+ if (key === BRAND_KIT_KEY) continue;
15443
15542
  if (BRAND_CHROME_KEYS.has(key)) continue;
15444
15543
  if (applyVideoSettingNode(key, val)) continue;
15445
15544
  if (applyCarouselNode(key, val)) continue;
@@ -15580,26 +15679,31 @@ function OhhwellsBridge() {
15580
15679
  }, [isEditMode, pathname, subdomain, fetchState, resyncSelectedNavigationItem]);
15581
15680
  (0, import_react16.useEffect)(() => {
15582
15681
  if (!isEditMode) return;
15682
+ let lastPosted = 0;
15583
15683
  const measure = () => {
15584
15684
  const h = document.body.scrollHeight;
15585
- if (h > 50) postToParent2({ type: "ow:height", height: h });
15685
+ if (h > 50 && Math.abs(h - lastPosted) > 1) {
15686
+ lastPosted = h;
15687
+ postToParent2({ type: "ow:height", height: h });
15688
+ }
15689
+ };
15690
+ let raf = null;
15691
+ const schedule = () => {
15692
+ if (raf != null) return;
15693
+ raf = requestAnimationFrame(() => {
15694
+ raf = null;
15695
+ measure();
15696
+ });
15586
15697
  };
15587
15698
  const t1 = setTimeout(measure, 50);
15588
15699
  const t2 = setTimeout(measure, 500);
15589
- let lastWidth = window.innerWidth;
15590
- let resizeTimer = null;
15591
- const handleResize = () => {
15592
- if (window.innerWidth === lastWidth) return;
15593
- lastWidth = window.innerWidth;
15594
- if (resizeTimer) clearTimeout(resizeTimer);
15595
- resizeTimer = setTimeout(measure, 150);
15596
- };
15597
- window.addEventListener("resize", handleResize);
15700
+ const ro = new ResizeObserver(schedule);
15701
+ ro.observe(document.body);
15598
15702
  return () => {
15599
15703
  clearTimeout(t1);
15600
15704
  clearTimeout(t2);
15601
- if (resizeTimer) clearTimeout(resizeTimer);
15602
- window.removeEventListener("resize", handleResize);
15705
+ if (raf != null) cancelAnimationFrame(raf);
15706
+ ro.disconnect();
15603
15707
  };
15604
15708
  }, [pathname, isEditMode, postToParent2]);
15605
15709
  (0, import_react16.useEffect)(() => {
@@ -16996,6 +17100,10 @@ function OhhwellsBridge() {
16996
17100
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
16997
17101
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
16998
17102
  }
17103
+ if (typeof content[BRAND_KIT_KEY] === "string") {
17104
+ brandKitRef.current = content[BRAND_KIT_KEY];
17105
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
17106
+ }
16999
17107
  applyBrandChrome(content);
17000
17108
  let sectionsJson = null;
17001
17109
  for (const [key, val] of Object.entries(content)) {
@@ -17006,6 +17114,7 @@ function OhhwellsBridge() {
17006
17114
  if (key === AI_SECTIONS_KEY) continue;
17007
17115
  if (key === LOGO_PLACEHOLDER_KEY) continue;
17008
17116
  if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
17117
+ if (key === BRAND_KIT_KEY) continue;
17009
17118
  if (BRAND_CHROME_KEYS.has(key)) continue;
17010
17119
  if (applyVideoSettingNode(key, val)) continue;
17011
17120
  if (applyCarouselNode(key, val)) continue;
@@ -17150,6 +17259,17 @@ function OhhwellsBridge() {
17150
17259
  postAiSectionsChanged();
17151
17260
  };
17152
17261
  window.addEventListener("message", handleAiSetSections);
17262
+ const handleAiSetBrand = (e) => {
17263
+ if (e.data?.type !== "ow:ai-set-brand") return;
17264
+ const value = typeof e.data.value === "string" ? e.data.value : "";
17265
+ const previous = brandKitRef.current;
17266
+ brandKitRef.current = value;
17267
+ applyBrandToDom(parseBrandKit(value));
17268
+ if (aiSectionsRef.current) applyAiSectionsToDom(parseAiSectionsState(aiSectionsRef.current));
17269
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: BRAND_KIT_KEY, text: value }] });
17270
+ postToParentRef.current({ type: "ow:ai-brand-applied", previous, value });
17271
+ };
17272
+ window.addEventListener("message", handleAiSetBrand);
17153
17273
  const handleDeactivate = (e) => {
17154
17274
  if (e.data?.type !== "ow:deactivate") return;
17155
17275
  if (Date.now() < linkPopoverGraceUntilRef.current) return;
@@ -17398,6 +17518,9 @@ function OhhwellsBridge() {
17398
17518
  if (aiSectionsRef.current) {
17399
17519
  nodes.push({ key: AI_SECTIONS_KEY, type: "sections", text: aiSectionsRef.current });
17400
17520
  }
17521
+ if (brandKitRef.current) {
17522
+ nodes.push({ key: BRAND_KIT_KEY, type: "brand", text: brandKitRef.current });
17523
+ }
17401
17524
  postToParentRef.current({ type: "ow:save-result", nodes });
17402
17525
  };
17403
17526
  const handleInsertSection = (e) => {
@@ -17800,6 +17923,7 @@ function OhhwellsBridge() {
17800
17923
  window.removeEventListener("message", handleAiApplyTree);
17801
17924
  window.removeEventListener("message", handleAiDeleteSection);
17802
17925
  window.removeEventListener("message", handleAiSetSections);
17926
+ window.removeEventListener("message", handleAiSetBrand);
17803
17927
  window.removeEventListener("message", handleDeactivate);
17804
17928
  window.removeEventListener("message", handleToastAction);
17805
17929
  window.removeEventListener("message", handleUiEscape);
@@ -18003,7 +18127,7 @@ function OhhwellsBridge() {
18003
18127
  postToParent2({
18004
18128
  type: "ow:ready",
18005
18129
  version: "1",
18006
- bridgeVersion: "0.1.54",
18130
+ bridgeVersion: "0.1.55",
18007
18131
  path: pathname,
18008
18132
  nodes: collectEditableNodes(editContentRef.current),
18009
18133
  sections