@ohhwells/bridge 0.1.132 → 0.1.133

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.js CHANGED
@@ -4067,18 +4067,25 @@ function FadeSlideshow({ node, ctx, path }) {
4067
4067
  (dir) => onInteract(stepViableSlide(index, failed, count, dir, loop)),
4068
4068
  [count, failed, index, loop, onInteract]
4069
4069
  );
4070
+ const prefetchRef = React.useRef(prefetch);
4071
+ React.useEffect(() => {
4072
+ prefetchRef.current = prefetch;
4073
+ });
4074
+ React.useEffect(() => {
4075
+ if (autoplay) hasInteractedRef.current = false;
4076
+ }, [autoplay]);
4070
4077
  React.useEffect(() => {
4071
4078
  if (!autoplay) return;
4072
4079
  const id = window.setInterval(() => {
4073
4080
  if (hasInteractedRef.current || hoveredRef.current) return;
4074
4081
  setIndex((i) => loop ? (i + 1) % count : Math.min(count - 1, i + 1));
4075
4082
  }, intervalMs);
4076
- const warmId = window.setTimeout(() => prefetch(1 % count), Math.min(1500, intervalMs / 2));
4083
+ const warmId = window.setTimeout(() => prefetchRef.current(1 % count), Math.min(1500, intervalMs / 2));
4077
4084
  return () => {
4078
4085
  window.clearInterval(id);
4079
4086
  window.clearTimeout(warmId);
4080
4087
  };
4081
- }, [autoplay, count, intervalMs, loop, prefetch]);
4088
+ }, [autoplay, count, intervalMs, loop]);
4082
4089
  const hasMovedRef = React.useRef(false);
4083
4090
  React.useEffect(() => {
4084
4091
  if (!hasMovedRef.current) {
@@ -13283,17 +13290,9 @@ var Button = React7.forwardRef(
13283
13290
  );
13284
13291
  Button.displayName = "Button";
13285
13292
 
13286
- // src/ui/MediaOverlay.tsx
13287
- import { Fragment as Fragment4, jsx as jsx16, jsxs as jsxs7 } from "react/jsx-runtime";
13293
+ // src/ui/media-skeleton.ts
13288
13294
  var MEDIA_UPLOAD_FADE_MS = 300;
13289
- var VIDEO_SETTINGS_BAR_INSET = 8;
13290
- var OVERLAY_BUTTON_STYLE = {
13291
- pointerEvents: "auto",
13292
- fontFamily: "Inter, sans-serif",
13293
- fontSize: 12,
13294
- color: "#000"
13295
- };
13296
- var SKELETON_CSS = `
13295
+ var MEDIA_SKELETON_CSS = `
13297
13296
  @keyframes ohw-media-shimmer {
13298
13297
  0% { transform: translateX(-100%); }
13299
13298
  100% { transform: translateX(100%); }
@@ -13310,6 +13309,16 @@ var SKELETON_CSS = `
13310
13309
  animation: ohw-media-shimmer 1.4s infinite;
13311
13310
  }
13312
13311
  `;
13312
+
13313
+ // src/ui/MediaOverlay.tsx
13314
+ import { Fragment as Fragment4, jsx as jsx16, jsxs as jsxs7 } from "react/jsx-runtime";
13315
+ var VIDEO_SETTINGS_BAR_INSET = 8;
13316
+ var OVERLAY_BUTTON_STYLE = {
13317
+ pointerEvents: "auto",
13318
+ fontFamily: "Inter, sans-serif",
13319
+ fontSize: 12,
13320
+ color: "#000"
13321
+ };
13313
13322
  var REPLACE_BUTTON_INSET = 12;
13314
13323
  function replaceButtonMode(image, button) {
13315
13324
  if (!button.fullWidth || !button.height) return "none";
@@ -13377,7 +13386,7 @@ function MediaOverlay({
13377
13386
  "data-ohw-media-skeleton": "",
13378
13387
  "aria-hidden": true,
13379
13388
  style: { ...box, pointerEvents: "none" },
13380
- children: /* @__PURE__ */ jsx16("style", { children: SKELETON_CSS })
13389
+ children: /* @__PURE__ */ jsx16("style", { children: MEDIA_SKELETON_CSS })
13381
13390
  }
13382
13391
  );
13383
13392
  }
@@ -15807,6 +15816,17 @@ function setCarouselSlideAsset(tree, index, assetId) {
15807
15816
  return true;
15808
15817
  });
15809
15818
  }
15819
+ function carouselAutoplay(tree) {
15820
+ return findCarouselBlock(tree)?.slots?.autoplay === true;
15821
+ }
15822
+ function setCarouselAutoplay(tree, on) {
15823
+ return withCarousel(tree, (block) => {
15824
+ const slots = { ...block.slots, autoplay: on };
15825
+ if (!on) delete slots.interval;
15826
+ block.slots = slots;
15827
+ return true;
15828
+ });
15829
+ }
15810
15830
 
15811
15831
  // src/ui/ai-section/AiFadeCarouselOverlay.tsx
15812
15832
  import { Fragment as Fragment9, jsx as jsx22, jsxs as jsxs13 } from "react/jsx-runtime";
@@ -15920,10 +15940,85 @@ function Pill4({ label, onClick, disabled }) {
15920
15940
  }
15921
15941
  );
15922
15942
  }
15943
+ function TogglePill({
15944
+ label,
15945
+ on,
15946
+ onToggle,
15947
+ disabled
15948
+ }) {
15949
+ return /* @__PURE__ */ jsxs13(
15950
+ "button",
15951
+ {
15952
+ type: "button",
15953
+ role: "switch",
15954
+ "aria-checked": on,
15955
+ onClick: onToggle,
15956
+ disabled,
15957
+ title: on ? `${label} on \u2014 click to turn off` : `${label} off \u2014 click to turn on`,
15958
+ style: {
15959
+ height: 28,
15960
+ padding: "0 8px 0 10px",
15961
+ display: "inline-flex",
15962
+ alignItems: "center",
15963
+ gap: 8,
15964
+ borderRadius: 999,
15965
+ border: "none",
15966
+ cursor: disabled ? "default" : "pointer",
15967
+ background: "#ffffff",
15968
+ color: OHW_BLUE_DARK4,
15969
+ opacity: disabled ? 0.4 : 1,
15970
+ fontSize: 12,
15971
+ lineHeight: "16px",
15972
+ fontFamily: "Figtree, ui-sans-serif, system-ui, sans-serif",
15973
+ whiteSpace: "nowrap",
15974
+ flex: "0 0 auto"
15975
+ },
15976
+ children: [
15977
+ label,
15978
+ /* @__PURE__ */ jsx22(
15979
+ "span",
15980
+ {
15981
+ "aria-hidden": true,
15982
+ style: {
15983
+ position: "relative",
15984
+ width: 26,
15985
+ height: 15,
15986
+ borderRadius: 999,
15987
+ flex: "0 0 auto",
15988
+ // Off is a grey track, not a washed-out blue: the pill's own white background already
15989
+ // carries the brand colour in the text, and a pale blue track next to it read as
15990
+ // "disabled control" rather than "switch that is off".
15991
+ background: on ? OHW_BLUE_DARK4 : "#CBD5E1",
15992
+ transition: "background 120ms ease"
15993
+ },
15994
+ children: /* @__PURE__ */ jsx22(
15995
+ "span",
15996
+ {
15997
+ style: {
15998
+ position: "absolute",
15999
+ top: 2,
16000
+ left: on ? 13 : 2,
16001
+ width: 11,
16002
+ height: 11,
16003
+ borderRadius: "50%",
16004
+ background: "#ffffff",
16005
+ boxShadow: "0 1px 2px rgba(0,0,0,0.3)",
16006
+ transition: "left 120ms ease"
16007
+ }
16008
+ }
16009
+ )
16010
+ }
16011
+ )
16012
+ ]
16013
+ }
16014
+ );
16015
+ }
15923
16016
  function CarouselSlideToolbar({
15924
16017
  pending,
15925
16018
  error,
15926
- onChangeImage
16019
+ autoplay,
16020
+ onChangeImage,
16021
+ onToggleAutoplay
15927
16022
  }) {
15928
16023
  return /* @__PURE__ */ jsxs13(
15929
16024
  "div",
@@ -15940,11 +16035,18 @@ function CarouselSlideToolbar({
15940
16035
  padding: 8,
15941
16036
  borderRadius: 12,
15942
16037
  boxShadow: "0 8px 24px rgba(0,0,0,0.25)",
15943
- pointerEvents: pending ? "none" : "auto",
15944
- opacity: pending ? 0.6 : 1
16038
+ // Not dimmed while pending, only inert. The toolbar used to drop to 0.6 opacity for the
16039
+ // length of the validate round-trip, which on a local backend is short enough that the
16040
+ // autoplay toggle just flashed — a dim and an un-dim with nothing in between. Progress is
16041
+ // reported where the change is: the switch moves at once (optimistically), and an image
16042
+ // swap raises the skeleton over the slide.
16043
+ pointerEvents: pending ? "none" : "auto"
15945
16044
  },
15946
16045
  children: [
15947
- /* @__PURE__ */ jsx22(Pill4, { label: "Change Image", onClick: onChangeImage }),
16046
+ /* @__PURE__ */ jsxs13("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [
16047
+ /* @__PURE__ */ jsx22(Pill4, { label: "Change Image", onClick: onChangeImage }),
16048
+ /* @__PURE__ */ jsx22(TogglePill, { label: "Autoplay", on: autoplay, onToggle: onToggleAutoplay })
16049
+ ] }),
15948
16050
  error && /* @__PURE__ */ jsx22("div", { style: { color: "#FCA5A5", fontSize: 11, maxWidth: 220, textAlign: "center" }, children: error })
15949
16051
  ]
15950
16052
  }
@@ -15960,8 +16062,24 @@ function AiFadeCarouselOverlay({
15960
16062
  const [active, setActive] = useState10(null);
15961
16063
  const [pending, setPending] = useState10(false);
15962
16064
  const [error, setError] = useState10(null);
16065
+ const [autoplayOverride, setAutoplayOverride] = useState10(null);
16066
+ const [swapping, setSwapping] = useState10(null);
16067
+ const skeletonRef = useRef8(null);
15963
16068
  const activeRef = useRef8(null);
15964
16069
  activeRef.current = active;
16070
+ useEffect8(() => {
16071
+ setAutoplayOverride(null);
16072
+ }, [active?.sectionId, active?.index]);
16073
+ useEffect8(() => {
16074
+ if (!swapping?.fadingOut || !skeletonRef.current) return;
16075
+ const anim = skeletonRef.current.animate([{ opacity: 1 }, { opacity: 0 }], {
16076
+ duration: MEDIA_UPLOAD_FADE_MS,
16077
+ easing: "ease-out",
16078
+ fill: "forwards"
16079
+ });
16080
+ anim.finished.then(() => setSwapping(null)).catch(() => setSwapping(null));
16081
+ return () => anim.cancel();
16082
+ }, [swapping?.fadingOut]);
15965
16083
  useEffect8(() => {
15966
16084
  const onMove = (e) => {
15967
16085
  const t = e.target;
@@ -16102,6 +16220,27 @@ function AiFadeCarouselOverlay({
16102
16220
  children: /* @__PURE__ */ jsx22(Star4, { size: 14, style: { pointerEvents: "none" } })
16103
16221
  }
16104
16222
  ),
16223
+ swapping && /* @__PURE__ */ jsx22(
16224
+ "div",
16225
+ {
16226
+ ref: skeletonRef,
16227
+ "data-ohw-bridge": "",
16228
+ "data-ohw-media-skeleton": "",
16229
+ "aria-hidden": true,
16230
+ style: {
16231
+ position: "fixed",
16232
+ top: swapping.rect.top,
16233
+ left: swapping.rect.left,
16234
+ width: swapping.rect.width,
16235
+ height: swapping.rect.height,
16236
+ // Under the toolbar (2147483647) so its controls stay visible and clickable over the
16237
+ // slide being swapped, and inert so it cannot eat the click that dismisses them.
16238
+ zIndex: 2147483646,
16239
+ pointerEvents: "none"
16240
+ },
16241
+ children: /* @__PURE__ */ jsx22("style", { children: MEDIA_SKELETON_CSS })
16242
+ }
16243
+ ),
16105
16244
  activeItemRect && active && activeEntry && /* @__PURE__ */ jsx22(
16106
16245
  ItemInteractionLayer,
16107
16246
  {
@@ -16112,14 +16251,36 @@ function AiFadeCarouselOverlay({
16112
16251
  {
16113
16252
  pending,
16114
16253
  error,
16254
+ autoplay: autoplayOverride ?? carouselAutoplay(activeEntry.tree),
16255
+ onToggleAutoplay: () => {
16256
+ const next = !(autoplayOverride ?? carouselAutoplay(activeEntry.tree));
16257
+ setAutoplayOverride(next);
16258
+ void runMutation(active.sectionId, (tree) => setCarouselAutoplay(tree, next)).then(
16259
+ (ok) => {
16260
+ if (!ok) setAutoplayOverride(null);
16261
+ }
16262
+ );
16263
+ },
16115
16264
  onChangeImage: () => {
16116
- void requestAssetPick2(postToParent2).then((picked) => {
16265
+ void requestAssetPick2(postToParent2).then(async (picked) => {
16117
16266
  if (!picked) return;
16118
- void runMutation(
16267
+ const slideRect = readRect5(active);
16268
+ if (slideRect) setSwapping({ rect: slideRect, fadingOut: false });
16269
+ const ok = await runMutation(
16119
16270
  active.sectionId,
16120
16271
  (tree) => setCarouselSlideAsset(tree, active.index, picked.assetId),
16121
16272
  { [picked.assetId]: picked.url }
16122
16273
  );
16274
+ if (!ok) {
16275
+ setSwapping(null);
16276
+ return;
16277
+ }
16278
+ const preload = new Image();
16279
+ const done = () => setSwapping((s) => s ? { ...s, fadingOut: true } : s);
16280
+ preload.onload = done;
16281
+ preload.onerror = done;
16282
+ preload.src = picked.url;
16283
+ if (preload.complete) done();
16123
16284
  });
16124
16285
  }
16125
16286
  }
@@ -30067,7 +30228,7 @@ function OhhwellsBridge() {
30067
30228
  postToParent2({
30068
30229
  type: "ow:ready",
30069
30230
  version: "1",
30070
- bridgeVersion: "0.1.130",
30231
+ bridgeVersion: "0.1.132",
30071
30232
  path: pathname,
30072
30233
  nodes: collectEditableNodes(editContentRef.current),
30073
30234
  sections