@ohhwells/bridge 0.1.132 → 0.1.134

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
@@ -4159,18 +4159,25 @@ function FadeSlideshow({ node, ctx, path }) {
4159
4159
  (dir) => onInteract(stepViableSlide(index, failed, count, dir, loop)),
4160
4160
  [count, failed, index, loop, onInteract]
4161
4161
  );
4162
+ const prefetchRef = import_react.default.useRef(prefetch);
4163
+ import_react.default.useEffect(() => {
4164
+ prefetchRef.current = prefetch;
4165
+ });
4166
+ import_react.default.useEffect(() => {
4167
+ if (autoplay) hasInteractedRef.current = false;
4168
+ }, [autoplay]);
4162
4169
  import_react.default.useEffect(() => {
4163
4170
  if (!autoplay) return;
4164
4171
  const id = window.setInterval(() => {
4165
4172
  if (hasInteractedRef.current || hoveredRef.current) return;
4166
4173
  setIndex((i) => loop ? (i + 1) % count : Math.min(count - 1, i + 1));
4167
4174
  }, intervalMs);
4168
- const warmId = window.setTimeout(() => prefetch(1 % count), Math.min(1500, intervalMs / 2));
4175
+ const warmId = window.setTimeout(() => prefetchRef.current(1 % count), Math.min(1500, intervalMs / 2));
4169
4176
  return () => {
4170
4177
  window.clearInterval(id);
4171
4178
  window.clearTimeout(warmId);
4172
4179
  };
4173
- }, [autoplay, count, intervalMs, loop, prefetch]);
4180
+ }, [autoplay, count, intervalMs, loop]);
4174
4181
  const hasMovedRef = import_react.default.useRef(false);
4175
4182
  import_react.default.useEffect(() => {
4176
4183
  if (!hasMovedRef.current) {
@@ -13375,17 +13382,9 @@ var Button = React7.forwardRef(
13375
13382
  );
13376
13383
  Button.displayName = "Button";
13377
13384
 
13378
- // src/ui/MediaOverlay.tsx
13379
- var import_jsx_runtime16 = require("react/jsx-runtime");
13385
+ // src/ui/media-skeleton.ts
13380
13386
  var MEDIA_UPLOAD_FADE_MS = 300;
13381
- var VIDEO_SETTINGS_BAR_INSET = 8;
13382
- var OVERLAY_BUTTON_STYLE = {
13383
- pointerEvents: "auto",
13384
- fontFamily: "Inter, sans-serif",
13385
- fontSize: 12,
13386
- color: "#000"
13387
- };
13388
- var SKELETON_CSS = `
13387
+ var MEDIA_SKELETON_CSS = `
13389
13388
  @keyframes ohw-media-shimmer {
13390
13389
  0% { transform: translateX(-100%); }
13391
13390
  100% { transform: translateX(100%); }
@@ -13402,6 +13401,16 @@ var SKELETON_CSS = `
13402
13401
  animation: ohw-media-shimmer 1.4s infinite;
13403
13402
  }
13404
13403
  `;
13404
+
13405
+ // src/ui/MediaOverlay.tsx
13406
+ var import_jsx_runtime16 = require("react/jsx-runtime");
13407
+ var VIDEO_SETTINGS_BAR_INSET = 8;
13408
+ var OVERLAY_BUTTON_STYLE = {
13409
+ pointerEvents: "auto",
13410
+ fontFamily: "Inter, sans-serif",
13411
+ fontSize: 12,
13412
+ color: "#000"
13413
+ };
13405
13414
  var REPLACE_BUTTON_INSET = 12;
13406
13415
  function replaceButtonMode(image, button) {
13407
13416
  if (!button.fullWidth || !button.height) return "none";
@@ -13469,7 +13478,7 @@ function MediaOverlay({
13469
13478
  "data-ohw-media-skeleton": "",
13470
13479
  "aria-hidden": true,
13471
13480
  style: { ...box, pointerEvents: "none" },
13472
- children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("style", { children: SKELETON_CSS })
13481
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("style", { children: MEDIA_SKELETON_CSS })
13473
13482
  }
13474
13483
  );
13475
13484
  }
@@ -15899,6 +15908,17 @@ function setCarouselSlideAsset(tree, index, assetId) {
15899
15908
  return true;
15900
15909
  });
15901
15910
  }
15911
+ function carouselAutoplay(tree) {
15912
+ return findCarouselBlock(tree)?.slots?.autoplay === true;
15913
+ }
15914
+ function setCarouselAutoplay(tree, on) {
15915
+ return withCarousel(tree, (block) => {
15916
+ const slots = { ...block.slots, autoplay: on };
15917
+ if (!on) delete slots.interval;
15918
+ block.slots = slots;
15919
+ return true;
15920
+ });
15921
+ }
15902
15922
 
15903
15923
  // src/ui/ai-section/AiFadeCarouselOverlay.tsx
15904
15924
  var import_jsx_runtime22 = require("react/jsx-runtime");
@@ -16012,10 +16032,85 @@ function Pill4({ label, onClick, disabled }) {
16012
16032
  }
16013
16033
  );
16014
16034
  }
16035
+ function TogglePill({
16036
+ label,
16037
+ on,
16038
+ onToggle,
16039
+ disabled
16040
+ }) {
16041
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
16042
+ "button",
16043
+ {
16044
+ type: "button",
16045
+ role: "switch",
16046
+ "aria-checked": on,
16047
+ onClick: onToggle,
16048
+ disabled,
16049
+ title: on ? `${label} on \u2014 click to turn off` : `${label} off \u2014 click to turn on`,
16050
+ style: {
16051
+ height: 28,
16052
+ padding: "0 8px 0 10px",
16053
+ display: "inline-flex",
16054
+ alignItems: "center",
16055
+ gap: 8,
16056
+ borderRadius: 999,
16057
+ border: "none",
16058
+ cursor: disabled ? "default" : "pointer",
16059
+ background: "#ffffff",
16060
+ color: OHW_BLUE_DARK4,
16061
+ opacity: disabled ? 0.4 : 1,
16062
+ fontSize: 12,
16063
+ lineHeight: "16px",
16064
+ fontFamily: "Figtree, ui-sans-serif, system-ui, sans-serif",
16065
+ whiteSpace: "nowrap",
16066
+ flex: "0 0 auto"
16067
+ },
16068
+ children: [
16069
+ label,
16070
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
16071
+ "span",
16072
+ {
16073
+ "aria-hidden": true,
16074
+ style: {
16075
+ position: "relative",
16076
+ width: 26,
16077
+ height: 15,
16078
+ borderRadius: 999,
16079
+ flex: "0 0 auto",
16080
+ // Off is a grey track, not a washed-out blue: the pill's own white background already
16081
+ // carries the brand colour in the text, and a pale blue track next to it read as
16082
+ // "disabled control" rather than "switch that is off".
16083
+ background: on ? OHW_BLUE_DARK4 : "#CBD5E1",
16084
+ transition: "background 120ms ease"
16085
+ },
16086
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
16087
+ "span",
16088
+ {
16089
+ style: {
16090
+ position: "absolute",
16091
+ top: 2,
16092
+ left: on ? 13 : 2,
16093
+ width: 11,
16094
+ height: 11,
16095
+ borderRadius: "50%",
16096
+ background: "#ffffff",
16097
+ boxShadow: "0 1px 2px rgba(0,0,0,0.3)",
16098
+ transition: "left 120ms ease"
16099
+ }
16100
+ }
16101
+ )
16102
+ }
16103
+ )
16104
+ ]
16105
+ }
16106
+ );
16107
+ }
16015
16108
  function CarouselSlideToolbar({
16016
16109
  pending,
16017
16110
  error,
16018
- onChangeImage
16111
+ autoplay,
16112
+ onChangeImage,
16113
+ onToggleAutoplay
16019
16114
  }) {
16020
16115
  return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
16021
16116
  "div",
@@ -16032,11 +16127,18 @@ function CarouselSlideToolbar({
16032
16127
  padding: 8,
16033
16128
  borderRadius: 12,
16034
16129
  boxShadow: "0 8px 24px rgba(0,0,0,0.25)",
16035
- pointerEvents: pending ? "none" : "auto",
16036
- opacity: pending ? 0.6 : 1
16130
+ // Not dimmed while pending, only inert. The toolbar used to drop to 0.6 opacity for the
16131
+ // length of the validate round-trip, which on a local backend is short enough that the
16132
+ // autoplay toggle just flashed — a dim and an un-dim with nothing in between. Progress is
16133
+ // reported where the change is: the switch moves at once (optimistically), and an image
16134
+ // swap raises the skeleton over the slide.
16135
+ pointerEvents: pending ? "none" : "auto"
16037
16136
  },
16038
16137
  children: [
16039
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Pill4, { label: "Change Image", onClick: onChangeImage }),
16138
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [
16139
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Pill4, { label: "Change Image", onClick: onChangeImage }),
16140
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(TogglePill, { label: "Autoplay", on: autoplay, onToggle: onToggleAutoplay })
16141
+ ] }),
16040
16142
  error && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { style: { color: "#FCA5A5", fontSize: 11, maxWidth: 220, textAlign: "center" }, children: error })
16041
16143
  ]
16042
16144
  }
@@ -16052,8 +16154,24 @@ function AiFadeCarouselOverlay({
16052
16154
  const [active, setActive] = (0, import_react12.useState)(null);
16053
16155
  const [pending, setPending] = (0, import_react12.useState)(false);
16054
16156
  const [error, setError] = (0, import_react12.useState)(null);
16157
+ const [autoplayOverride, setAutoplayOverride] = (0, import_react12.useState)(null);
16158
+ const [swapping, setSwapping] = (0, import_react12.useState)(null);
16159
+ const skeletonRef = (0, import_react12.useRef)(null);
16055
16160
  const activeRef = (0, import_react12.useRef)(null);
16056
16161
  activeRef.current = active;
16162
+ (0, import_react12.useEffect)(() => {
16163
+ setAutoplayOverride(null);
16164
+ }, [active?.sectionId, active?.index]);
16165
+ (0, import_react12.useEffect)(() => {
16166
+ if (!swapping?.fadingOut || !skeletonRef.current) return;
16167
+ const anim = skeletonRef.current.animate([{ opacity: 1 }, { opacity: 0 }], {
16168
+ duration: MEDIA_UPLOAD_FADE_MS,
16169
+ easing: "ease-out",
16170
+ fill: "forwards"
16171
+ });
16172
+ anim.finished.then(() => setSwapping(null)).catch(() => setSwapping(null));
16173
+ return () => anim.cancel();
16174
+ }, [swapping?.fadingOut]);
16057
16175
  (0, import_react12.useEffect)(() => {
16058
16176
  const onMove = (e) => {
16059
16177
  const t = e.target;
@@ -16194,6 +16312,27 @@ function AiFadeCarouselOverlay({
16194
16312
  children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_lucide_react11.Star, { size: 14, style: { pointerEvents: "none" } })
16195
16313
  }
16196
16314
  ),
16315
+ swapping && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
16316
+ "div",
16317
+ {
16318
+ ref: skeletonRef,
16319
+ "data-ohw-bridge": "",
16320
+ "data-ohw-media-skeleton": "",
16321
+ "aria-hidden": true,
16322
+ style: {
16323
+ position: "fixed",
16324
+ top: swapping.rect.top,
16325
+ left: swapping.rect.left,
16326
+ width: swapping.rect.width,
16327
+ height: swapping.rect.height,
16328
+ // Under the toolbar (2147483647) so its controls stay visible and clickable over the
16329
+ // slide being swapped, and inert so it cannot eat the click that dismisses them.
16330
+ zIndex: 2147483646,
16331
+ pointerEvents: "none"
16332
+ },
16333
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("style", { children: MEDIA_SKELETON_CSS })
16334
+ }
16335
+ ),
16197
16336
  activeItemRect && active && activeEntry && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
16198
16337
  ItemInteractionLayer,
16199
16338
  {
@@ -16204,14 +16343,36 @@ function AiFadeCarouselOverlay({
16204
16343
  {
16205
16344
  pending,
16206
16345
  error,
16346
+ autoplay: autoplayOverride ?? carouselAutoplay(activeEntry.tree),
16347
+ onToggleAutoplay: () => {
16348
+ const next = !(autoplayOverride ?? carouselAutoplay(activeEntry.tree));
16349
+ setAutoplayOverride(next);
16350
+ void runMutation(active.sectionId, (tree) => setCarouselAutoplay(tree, next)).then(
16351
+ (ok) => {
16352
+ if (!ok) setAutoplayOverride(null);
16353
+ }
16354
+ );
16355
+ },
16207
16356
  onChangeImage: () => {
16208
- void requestAssetPick2(postToParent2).then((picked) => {
16357
+ void requestAssetPick2(postToParent2).then(async (picked) => {
16209
16358
  if (!picked) return;
16210
- void runMutation(
16359
+ const slideRect = readRect5(active);
16360
+ if (slideRect) setSwapping({ rect: slideRect, fadingOut: false });
16361
+ const ok = await runMutation(
16211
16362
  active.sectionId,
16212
16363
  (tree) => setCarouselSlideAsset(tree, active.index, picked.assetId),
16213
16364
  { [picked.assetId]: picked.url }
16214
16365
  );
16366
+ if (!ok) {
16367
+ setSwapping(null);
16368
+ return;
16369
+ }
16370
+ const preload = new Image();
16371
+ const done = () => setSwapping((s) => s ? { ...s, fadingOut: true } : s);
16372
+ preload.onload = done;
16373
+ preload.onerror = done;
16374
+ preload.src = picked.url;
16375
+ if (preload.complete) done();
16215
16376
  });
16216
16377
  }
16217
16378
  }
@@ -30153,7 +30314,7 @@ function OhhwellsBridge() {
30153
30314
  postToParent2({
30154
30315
  type: "ow:ready",
30155
30316
  version: "1",
30156
- bridgeVersion: "0.1.130",
30317
+ bridgeVersion: "0.1.132",
30157
30318
  path: pathname,
30158
30319
  nodes: collectEditableNodes(editContentRef.current),
30159
30320
  sections