@bendyline/squisq-react 2.11.6 → 2.11.8

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 CHANGED
@@ -38,24 +38,25 @@ state rather than crashing.
38
38
 
39
39
  ## Components
40
40
 
41
- | Component | Description |
42
- | ---------------------- | ------------------------------------------------------------------- |
43
- | `DocPlayer` | Main document player with timed playback, audio sync, and controls |
44
- | `LinearDocView` | Scroll-based linear rendering of all blocks |
45
- | `BlockRenderer` | SVG-based renderer for a single block |
46
- | `MarkdownRenderer` | Renders Squisq markdown as a visual document |
47
- | `DocPlayerWithSidebar` | DocPlayer with a sidebar navigation panel |
48
- | `CaptionOverlay` | Timed caption/subtitle overlay |
49
- | `DocProgressBar` | Playback progress indicator |
50
- | `DocControlsOverlay` | Floating playback controls |
51
- | `DocControlsBottom` | Bottom-bar playback controls |
52
- | `DocControlsSidebar` | Sidebar navigation controls |
53
- | `DocControlsSlideshow` | Slideshow-style navigation controls |
54
- | `SocialCaptionOverlay` | Large centered word-by-word (TikTok/Reels-style) captions |
55
- | `InlineVideoPlayer` | Native `<video>` wrapper resolving `src`/`poster` via MediaContext |
56
- | `InlineAudioPlayer` | Native `<audio>` wrapper resolving `src` via MediaContext |
57
- | `MediaClipLayer` | Hidden `<audio>`/`<video>` elements for timed media clips |
58
- | `JsonView` | Read-only viewer for JSON values bound to a Squisq-annotated schema |
41
+ | Component | Description |
42
+ | ----------------------- | ------------------------------------------------------------------- |
43
+ | `DocPlayer` | Main document player with timed playback, audio sync, and controls |
44
+ | `LinearDocView` | Scroll-based linear rendering of all blocks |
45
+ | `BlockRenderer` | SVG-based renderer for a single block |
46
+ | `MarkdownRenderer` | Renders Squisq markdown as a visual document |
47
+ | `DocPlayerWithSidebar` | DocPlayer with a sidebar navigation panel |
48
+ | `CaptionOverlay` | Timed caption/subtitle overlay |
49
+ | `DocProgressBar` | Playback progress indicator |
50
+ | `DocControlsOverlay` | Floating playback controls |
51
+ | `DocControlsBottom` | Bottom-bar playback controls |
52
+ | `DocControlsSidebar` | Sidebar navigation controls |
53
+ | `DocControlsSlideshow` | Slideshow-style navigation controls |
54
+ | `SocialCaptionOverlay` | Large centered word-by-word (TikTok/Reels-style) captions |
55
+ | `InlineVideoPlayer` | Native `<video>` wrapper resolving `src`/`poster` via MediaContext |
56
+ | `InlineAudioPlayer` | Native `<audio>` wrapper resolving `src` via MediaContext |
57
+ | `AnimatedImageControls` | Play/pause controls over an animated GIF/WebP/APNG image |
58
+ | `MediaClipLayer` | Hidden `<audio>`/`<video>` elements for timed media clips |
59
+ | `JsonView` | Read-only viewer for JSON values bound to a Squisq-annotated schema |
59
60
 
60
61
  ### Fenced-code copy control
61
62
 
@@ -57,13 +57,198 @@ function InlineAudioPlayer({
57
57
  return /* @__PURE__ */ jsx2("span", { className: `squisq-inline-audio-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx2("audio", { src: resolvedSrc, controls, preload }) });
58
58
  }
59
59
 
60
+ // src/animatedImage/AnimatedImageControls.tsx
61
+ import { useLayoutEffect, useRef } from "react";
62
+ import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
63
+ var FORMAT_LABELS = { gif: "GIF", webp: "WEBP", png: "APNG" };
64
+ function AnimatedImageControls({ playback, imageRef }) {
65
+ const canvasRef = useRef(null);
66
+ const { info, status, displaySrc } = playback;
67
+ const paused = status === "paused";
68
+ const animated = info?.animated ?? false;
69
+ useLayoutEffect(() => {
70
+ if (!paused || !animated) return;
71
+ const image = imageRef.current;
72
+ const canvas = canvasRef.current;
73
+ if (!image || !canvas) return;
74
+ const draw = () => {
75
+ if (!image.naturalWidth || !image.naturalHeight) return;
76
+ canvas.width = image.naturalWidth;
77
+ canvas.height = image.naturalHeight;
78
+ canvas.getContext("2d")?.drawImage(image, 0, 0);
79
+ };
80
+ if (image.complete && image.naturalWidth) {
81
+ draw();
82
+ return;
83
+ }
84
+ image.addEventListener("load", draw, { once: true });
85
+ return () => image.removeEventListener("load", draw);
86
+ }, [paused, animated, displaySrc, imageRef]);
87
+ if (!info || !animated) return null;
88
+ const label = status === "playing" ? "Pause animation" : status === "ended" ? "Replay animation" : "Play animation";
89
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
90
+ paused && /* @__PURE__ */ jsx3(
91
+ "canvas",
92
+ {
93
+ ref: canvasRef,
94
+ className: "squisq-animated-image-still",
95
+ "aria-hidden": "true",
96
+ style: {
97
+ position: "absolute",
98
+ inset: 0,
99
+ width: "100%",
100
+ height: "100%",
101
+ pointerEvents: "none"
102
+ }
103
+ }
104
+ ),
105
+ /* @__PURE__ */ jsxs(
106
+ "button",
107
+ {
108
+ type: "button",
109
+ className: "squisq-animated-image-toggle",
110
+ "data-status": status,
111
+ "aria-label": label,
112
+ title: label,
113
+ style: { position: "absolute", top: 8, left: 8 },
114
+ onMouseDown: (event) => event.preventDefault(),
115
+ onClick: (event) => {
116
+ event.preventDefault();
117
+ event.stopPropagation();
118
+ if (status === "playing") playback.pause();
119
+ else playback.play();
120
+ },
121
+ children: [
122
+ /* @__PURE__ */ jsx3(StatusGlyph, { status }),
123
+ /* @__PURE__ */ jsx3("span", { className: "squisq-animated-image-format", children: FORMAT_LABELS[info.format] })
124
+ ]
125
+ }
126
+ )
127
+ ] });
128
+ }
129
+ function StatusGlyph({ status }) {
130
+ const path = status === "playing" ? "M2.5 1.5h2.75v9H2.5zM6.75 1.5H9.5v9H6.75z" : status === "ended" ? "M6 1.5V0L3.5 2.25 6 4.5V3a3 3 0 1 1-3 3H1.5A4.5 4.5 0 1 0 6 1.5z" : "M3 1.5v9l7.5-4.5z";
131
+ return /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 12 12", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx3("path", { d: path }) });
132
+ }
133
+
134
+ // src/animatedImage/useAnimatedImage.ts
135
+ import { useCallback, useEffect, useRef as useRef2, useState } from "react";
136
+ import {
137
+ inspectAnimatedImage,
138
+ mayBeAnimatedImage
139
+ } from "@bendyline/squisq/imageEdit";
140
+ var INSPECTION_CACHE_LIMIT = 64;
141
+ var inspections = /* @__PURE__ */ new Map();
142
+ function inspectUrl(url) {
143
+ const cached = inspections.get(url);
144
+ if (cached) return cached;
145
+ const pending = fetch(url).then((response) => response.ok ? response.arrayBuffer() : null).then((buffer) => buffer ? inspectAnimatedImage(buffer) : null).catch(() => null);
146
+ if (!/^data:/i.test(url)) {
147
+ inspections.set(url, pending);
148
+ if (inspections.size > INSPECTION_CACHE_LIMIT) {
149
+ const oldest = inspections.keys().next().value;
150
+ if (oldest !== void 0) inspections.delete(oldest);
151
+ }
152
+ }
153
+ return pending;
154
+ }
155
+ function inspectableUrl(url) {
156
+ if (/^(?:blob|data):/i.test(url)) return url;
157
+ if (typeof document === "undefined") return null;
158
+ try {
159
+ const parsed = new URL(url, document.baseURI);
160
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
161
+ return parsed.origin === window.location.origin ? parsed.href : null;
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+ function prefersReducedMotion() {
167
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
168
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
169
+ }
170
+ function initialStatus() {
171
+ return prefersReducedMotion() ? "paused" : "playing";
172
+ }
173
+ function useAnimatedImage(src, resolvedUrl) {
174
+ const [info, setInfo] = useState(null);
175
+ const [status, setStatus] = useState(initialStatus);
176
+ const [replayUrl, setReplayUrl] = useState(null);
177
+ const [run, setRun] = useState(0);
178
+ const generationRef = useRef2(0);
179
+ const replayUrlRef = useRef2(null);
180
+ const swapReplayUrl = useCallback((next) => {
181
+ if (replayUrlRef.current) URL.revokeObjectURL(replayUrlRef.current);
182
+ replayUrlRef.current = next;
183
+ setReplayUrl(next);
184
+ }, []);
185
+ useEffect(() => {
186
+ setInfo(null);
187
+ const url = resolvedUrl && mayBeAnimatedImage(src) ? inspectableUrl(resolvedUrl) : null;
188
+ if (!url) return;
189
+ let cancelled = false;
190
+ void inspectUrl(url).then((next) => {
191
+ if (!cancelled) setInfo(next);
192
+ });
193
+ return () => {
194
+ cancelled = true;
195
+ };
196
+ }, [src, resolvedUrl]);
197
+ useEffect(() => {
198
+ generationRef.current++;
199
+ swapReplayUrl(null);
200
+ setStatus(initialStatus());
201
+ }, [resolvedUrl, swapReplayUrl]);
202
+ useEffect(
203
+ () => () => {
204
+ if (replayUrlRef.current) URL.revokeObjectURL(replayUrlRef.current);
205
+ },
206
+ []
207
+ );
208
+ useEffect(() => {
209
+ if (status !== "playing" || !info?.animated || info.playCount === 0) return;
210
+ const timer = setTimeout(() => setStatus("ended"), info.durationMs * info.playCount);
211
+ return () => clearTimeout(timer);
212
+ }, [status, info, run]);
213
+ const play = useCallback(() => {
214
+ const generation = ++generationRef.current;
215
+ const start = (url) => {
216
+ if (generation !== generationRef.current) {
217
+ if (url) URL.revokeObjectURL(url);
218
+ return;
219
+ }
220
+ if (url) swapReplayUrl(url);
221
+ setStatus("playing");
222
+ setRun((n) => n + 1);
223
+ };
224
+ fetch(replayUrl ?? resolvedUrl).then((response) => response.blob()).then(
225
+ (blob) => start(URL.createObjectURL(blob)),
226
+ // Unreadable now: resume the image as it is rather than stay stuck.
227
+ () => start(null)
228
+ );
229
+ }, [replayUrl, resolvedUrl, swapReplayUrl]);
230
+ const pause = useCallback(() => {
231
+ generationRef.current++;
232
+ setStatus("paused");
233
+ }, []);
234
+ return {
235
+ info,
236
+ animated: info?.animated ?? false,
237
+ displaySrc: replayUrl ?? resolvedUrl,
238
+ status,
239
+ play,
240
+ pause
241
+ };
242
+ }
243
+
60
244
  // src/MarkdownRenderer.tsx
61
- import { Component, Fragment, useEffect, useRef, useState } from "react";
245
+ import { Component, Fragment as Fragment2, useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
246
+ import { mayBeAnimatedImage as mayBeAnimatedImage2 } from "@bendyline/squisq/imageEdit";
62
247
  import {
63
248
  sanitizeHtmlNodes,
64
249
  sanitizeUrl
65
250
  } from "@bendyline/squisq/markdown";
66
- import { jsx as jsx3, jsxs } from "react/jsx-runtime";
251
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
67
252
  var DEFAULT_CTX = { htmlPolicy: "sanitize" };
68
253
  var CODE_COPY_LABELS = {
69
254
  idle: "Copy",
@@ -86,15 +271,15 @@ function CodeBlock({
86
271
  language,
87
272
  ctx
88
273
  }) {
89
- const [status, setStatus] = useState("idle");
90
- const resetTimer = useRef();
91
- useEffect(
274
+ const [status, setStatus] = useState2("idle");
275
+ const resetTimer = useRef3();
276
+ useEffect2(
92
277
  () => () => {
93
278
  if (resetTimer.current !== void 0) clearTimeout(resetTimer.current);
94
279
  },
95
280
  []
96
281
  );
97
- const pre = /* @__PURE__ */ jsx3("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx3("code", { className: language ? `language-${language}` : void 0, children: value }) });
282
+ const pre = /* @__PURE__ */ jsx4("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx4("code", { className: language ? `language-${language}` : void 0, children: value }) });
98
283
  if (!ctx.showCodeCopyButton) return pre;
99
284
  const handleCopy = async () => {
100
285
  if (status === "copying") return;
@@ -108,9 +293,9 @@ function CodeBlock({
108
293
  if (resetTimer.current !== void 0) clearTimeout(resetTimer.current);
109
294
  resetTimer.current = setTimeout(() => setStatus("idle"), 1600);
110
295
  };
111
- return /* @__PURE__ */ jsxs("div", { className: "squisq-md-code-frame", children: [
296
+ return /* @__PURE__ */ jsxs2("div", { className: "squisq-md-code-frame", children: [
112
297
  pre,
113
- /* @__PURE__ */ jsx3(
298
+ /* @__PURE__ */ jsx4(
114
299
  "button",
115
300
  {
116
301
  type: "button",
@@ -143,7 +328,7 @@ function HostFence({
143
328
  }) {
144
329
  const renderer = ctx.fenceRenderers?.[lang];
145
330
  if (!renderer) return null;
146
- return /* @__PURE__ */ jsx3("div", { className: `squisq-md-fence-widget squisq-md-fence-widget-${lang}`, "data-fence-lang": lang, children: renderer({
331
+ return /* @__PURE__ */ jsx4("div", { className: `squisq-md-fence-widget squisq-md-fence-widget-${lang}`, "data-fence-lang": lang, children: renderer({
147
332
  lang,
148
333
  ...node.meta != null ? { meta: node.meta } : {},
149
334
  value: node.value,
@@ -157,37 +342,37 @@ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
157
342
  switch (node.type) {
158
343
  case "text": {
159
344
  if (!node.value.includes("\n")) {
160
- return /* @__PURE__ */ jsx3(Fragment, { children: node.value }, key);
345
+ return /* @__PURE__ */ jsx4(Fragment2, { children: node.value }, key);
161
346
  }
162
347
  const parts = node.value.split("\n");
163
- return /* @__PURE__ */ jsx3(Fragment, { children: parts.map((part, j) => /* @__PURE__ */ jsxs(Fragment, { children: [
164
- j > 0 && /* @__PURE__ */ jsx3("br", {}),
348
+ return /* @__PURE__ */ jsx4(Fragment2, { children: parts.map((part, j) => /* @__PURE__ */ jsxs2(Fragment2, { children: [
349
+ j > 0 && /* @__PURE__ */ jsx4("br", {}),
165
350
  part
166
351
  ] }, j)) }, key);
167
352
  }
168
353
  case "emphasis":
169
- return /* @__PURE__ */ jsx3("em", { className: "squisq-md-em", children: renderInline(node.children, key, ctx) }, key);
354
+ return /* @__PURE__ */ jsx4("em", { className: "squisq-md-em", children: renderInline(node.children, key, ctx) }, key);
170
355
  case "strong":
171
- return /* @__PURE__ */ jsx3("strong", { className: "squisq-md-strong", children: renderInline(node.children, key, ctx) }, key);
356
+ return /* @__PURE__ */ jsx4("strong", { className: "squisq-md-strong", children: renderInline(node.children, key, ctx) }, key);
172
357
  case "delete":
173
- return /* @__PURE__ */ jsx3("del", { className: "squisq-md-del", children: renderInline(node.children, key, ctx) }, key);
358
+ return /* @__PURE__ */ jsx4("del", { className: "squisq-md-del", children: renderInline(node.children, key, ctx) }, key);
174
359
  case "superscript":
175
- return /* @__PURE__ */ jsx3("sup", { className: "squisq-md-sup", children: renderInline(node.children, key, ctx) }, key);
360
+ return /* @__PURE__ */ jsx4("sup", { className: "squisq-md-sup", children: renderInline(node.children, key, ctx) }, key);
176
361
  case "subscript":
177
- return /* @__PURE__ */ jsx3("sub", { className: "squisq-md-sub", children: renderInline(node.children, key, ctx) }, key);
362
+ return /* @__PURE__ */ jsx4("sub", { className: "squisq-md-sub", children: renderInline(node.children, key, ctx) }, key);
178
363
  case "inlineCode":
179
- return /* @__PURE__ */ jsx3("code", { className: "squisq-md-inline-code", children: node.value }, key);
364
+ return /* @__PURE__ */ jsx4("code", { className: "squisq-md-inline-code", children: node.value }, key);
180
365
  case "link": {
181
366
  if (ctx.accessoryPaths?.has(node.url)) {
182
- return /* @__PURE__ */ jsxs(
367
+ return /* @__PURE__ */ jsxs2(
183
368
  "span",
184
369
  {
185
370
  className: "squisq-md-attachment",
186
371
  "data-attachment-path": node.url,
187
372
  title: node.title ?? `Attachment: ${node.url}`,
188
373
  children: [
189
- /* @__PURE__ */ jsx3("i", { className: "fa-solid fa-paperclip", "aria-hidden": "true" }),
190
- /* @__PURE__ */ jsx3("span", { className: "squisq-md-attachment-label", children: renderInline(node.children, key, ctx) })
374
+ /* @__PURE__ */ jsx4("i", { className: "fa-solid fa-paperclip", "aria-hidden": "true" }),
375
+ /* @__PURE__ */ jsx4("span", { className: "squisq-md-attachment-label", children: renderInline(node.children, key, ctx) })
191
376
  ]
192
377
  },
193
378
  key
@@ -195,9 +380,9 @@ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
195
380
  }
196
381
  const href = sanitizeUrl(node.url, "link", { extraLinkSchemes: ctx.linkSchemes });
197
382
  if (!href) {
198
- return /* @__PURE__ */ jsx3("span", { className: "squisq-md-link squisq-md-link--blocked", children: renderInline(node.children, key, ctx) }, key);
383
+ return /* @__PURE__ */ jsx4("span", { className: "squisq-md-link squisq-md-link--blocked", children: renderInline(node.children, key, ctx) }, key);
199
384
  }
200
- return /* @__PURE__ */ jsx3(
385
+ return /* @__PURE__ */ jsx4(
201
386
  "a",
202
387
  {
203
388
  className: "squisq-md-link",
@@ -211,13 +396,13 @@ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
211
396
  );
212
397
  }
213
398
  case "image":
214
- return /* @__PURE__ */ jsx3(MdImage, { src: node.url, alt: node.alt ?? "", title: node.title ?? void 0 }, key);
399
+ return /* @__PURE__ */ jsx4(MdImage, { src: node.url, alt: node.alt ?? "", title: node.title ?? void 0 }, key);
215
400
  case "break":
216
- return /* @__PURE__ */ jsx3("br", {}, key);
401
+ return /* @__PURE__ */ jsx4("br", {}, key);
217
402
  case "inlineMath":
218
- return /* @__PURE__ */ jsx3("code", { className: "squisq-md-inline-math", children: node.value }, key);
403
+ return /* @__PURE__ */ jsx4("code", { className: "squisq-md-inline-math", children: node.value }, key);
219
404
  case "inlineIcon":
220
- return /* @__PURE__ */ jsx3(
405
+ return /* @__PURE__ */ jsx4(
221
406
  "i",
222
407
  {
223
408
  className: `fa-${node.family} fa-${node.name}`,
@@ -228,25 +413,25 @@ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
228
413
  );
229
414
  case "htmlInline":
230
415
  if (ctx.htmlPolicy === "strip") return null;
231
- return /* @__PURE__ */ jsx3("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`, ctx) }, key);
416
+ return /* @__PURE__ */ jsx4("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`, ctx) }, key);
232
417
  case "footnoteReference":
233
- return /* @__PURE__ */ jsx3("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs("a", { href: `#fn-${node.identifier}`, children: [
418
+ return /* @__PURE__ */ jsx4("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs2("a", { href: `#fn-${node.identifier}`, children: [
234
419
  "[",
235
420
  node.label ?? node.identifier,
236
421
  "]"
237
422
  ] }) }, key);
238
423
  case "linkReference":
239
- return /* @__PURE__ */ jsx3("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key, ctx) }, key);
424
+ return /* @__PURE__ */ jsx4("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key, ctx) }, key);
240
425
  case "imageReference":
241
- return /* @__PURE__ */ jsxs("span", { className: "squisq-md-image-ref", children: [
426
+ return /* @__PURE__ */ jsxs2("span", { className: "squisq-md-image-ref", children: [
242
427
  "[",
243
428
  node.alt ?? node.identifier,
244
429
  "]"
245
430
  ] }, key);
246
431
  case "textDirective":
247
- return /* @__PURE__ */ jsx3("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key, ctx) }, key);
432
+ return /* @__PURE__ */ jsx4("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key, ctx) }, key);
248
433
  case "mention":
249
- return /* @__PURE__ */ jsxs(
434
+ return /* @__PURE__ */ jsxs2(
250
435
  "span",
251
436
  {
252
437
  className: "squisq-md-mention mention",
@@ -269,32 +454,32 @@ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
269
454
  function renderBlock(node, key, ctx = DEFAULT_CTX) {
270
455
  switch (node.type) {
271
456
  case "paragraph":
272
- return /* @__PURE__ */ jsx3("p", { className: "squisq-md-p", children: renderInline(node.children, key, ctx) }, key);
457
+ return /* @__PURE__ */ jsx4("p", { className: "squisq-md-p", children: renderInline(node.children, key, ctx) }, key);
273
458
  case "heading": {
274
459
  const Tag = `h${node.depth}`;
275
- return /* @__PURE__ */ jsx3(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key, ctx) }, key);
460
+ return /* @__PURE__ */ jsx4(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key, ctx) }, key);
276
461
  }
277
462
  case "blockquote":
278
- return /* @__PURE__ */ jsx3("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key, ctx) }, key);
463
+ return /* @__PURE__ */ jsx4("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key, ctx) }, key);
279
464
  case "list":
280
465
  if (node.ordered) {
281
- return /* @__PURE__ */ jsx3("ol", { className: "squisq-md-list squisq-md-ol", start: node.start ?? void 0, children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
466
+ return /* @__PURE__ */ jsx4("ol", { className: "squisq-md-list squisq-md-ol", start: node.start ?? void 0, children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
282
467
  }
283
- return /* @__PURE__ */ jsx3("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
468
+ return /* @__PURE__ */ jsx4("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
284
469
  case "code": {
285
470
  const fenceLang = node.lang?.trim().toLowerCase();
286
471
  if (fenceLang && ctx.fenceRenderers?.[fenceLang]) {
287
- return /* @__PURE__ */ jsx3(
472
+ return /* @__PURE__ */ jsx4(
288
473
  FenceWidgetBoundary,
289
474
  {
290
- fallback: /* @__PURE__ */ jsx3(CodeBlock, { value: node.value, language: node.lang, ctx }),
291
- children: /* @__PURE__ */ jsx3(HostFence, { node, lang: fenceLang, ctx })
475
+ fallback: /* @__PURE__ */ jsx4(CodeBlock, { value: node.value, language: node.lang, ctx }),
476
+ children: /* @__PURE__ */ jsx4(HostFence, { node, lang: fenceLang, ctx })
292
477
  },
293
478
  key
294
479
  );
295
480
  }
296
481
  if (fenceLang === "mermaid") {
297
- return /* @__PURE__ */ jsx3(
482
+ return /* @__PURE__ */ jsx4(
298
483
  MermaidDiagram,
299
484
  {
300
485
  source: node.value,
@@ -305,39 +490,39 @@ function renderBlock(node, key, ctx = DEFAULT_CTX) {
305
490
  key
306
491
  );
307
492
  }
308
- return /* @__PURE__ */ jsx3(CodeBlock, { value: node.value, language: node.lang, ctx }, key);
493
+ return /* @__PURE__ */ jsx4(CodeBlock, { value: node.value, language: node.lang, ctx }, key);
309
494
  }
310
495
  case "thematicBreak":
311
- return /* @__PURE__ */ jsx3("hr", { className: "squisq-md-hr" }, key);
496
+ return /* @__PURE__ */ jsx4("hr", { className: "squisq-md-hr" }, key);
312
497
  case "table":
313
498
  return renderTable(node.children, node.align, key, ctx);
314
499
  case "htmlBlock":
315
500
  if (ctx.htmlPolicy === "strip") return null;
316
- return /* @__PURE__ */ jsx3("div", { className: "squisq-md-html-block", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`, ctx) }, key);
501
+ return /* @__PURE__ */ jsx4("div", { className: "squisq-md-html-block", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`, ctx) }, key);
317
502
  case "math":
318
- return /* @__PURE__ */ jsx3("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx3("code", { children: node.value }) }, key);
503
+ return /* @__PURE__ */ jsx4("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx4("code", { children: node.value }) }, key);
319
504
  case "definition":
320
505
  return null;
321
506
  case "footnoteDefinition":
322
- return /* @__PURE__ */ jsxs("div", { className: "squisq-md-footnote-def", id: `fn-${node.identifier}`, children: [
323
- /* @__PURE__ */ jsx3("sup", { children: node.label ?? node.identifier }),
507
+ return /* @__PURE__ */ jsxs2("div", { className: "squisq-md-footnote-def", id: `fn-${node.identifier}`, children: [
508
+ /* @__PURE__ */ jsx4("sup", { children: node.label ?? node.identifier }),
324
509
  renderBlocks(node.children, key, ctx)
325
510
  ] }, key);
326
511
  case "containerDirective":
327
- return /* @__PURE__ */ jsxs(
512
+ return /* @__PURE__ */ jsxs2(
328
513
  "div",
329
514
  {
330
515
  className: `squisq-md-directive squisq-md-directive-${node.name}`,
331
516
  "data-directive": node.name,
332
517
  children: [
333
- node.label && /* @__PURE__ */ jsx3("div", { className: "squisq-md-directive-label", children: node.label }),
518
+ node.label && /* @__PURE__ */ jsx4("div", { className: "squisq-md-directive-label", children: node.label }),
334
519
  renderBlocks(node.children, key, ctx)
335
520
  ]
336
521
  },
337
522
  key
338
523
  );
339
524
  case "leafDirective":
340
- return /* @__PURE__ */ jsx3(
525
+ return /* @__PURE__ */ jsx4(
341
526
  "div",
342
527
  {
343
528
  className: `squisq-md-directive squisq-md-directive-${node.name}`,
@@ -347,11 +532,11 @@ function renderBlock(node, key, ctx = DEFAULT_CTX) {
347
532
  key
348
533
  );
349
534
  case "definitionList":
350
- return /* @__PURE__ */ jsx3("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
535
+ return /* @__PURE__ */ jsx4("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
351
536
  if (child.type === "definitionTerm") {
352
- return /* @__PURE__ */ jsx3("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`, ctx) }, `${key}dt${i}`);
537
+ return /* @__PURE__ */ jsx4("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`, ctx) }, `${key}dt${i}`);
353
538
  }
354
- return /* @__PURE__ */ jsx3("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`, ctx) }, `${key}dd${i}`);
539
+ return /* @__PURE__ */ jsx4("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`, ctx) }, `${key}dd${i}`);
355
540
  }) }, key);
356
541
  default:
357
542
  return null;
@@ -359,15 +544,15 @@ function renderBlock(node, key, ctx = DEFAULT_CTX) {
359
544
  }
360
545
  function renderListItem(item, key, ctx = DEFAULT_CTX) {
361
546
  const isTask = item.checked !== null && item.checked !== void 0;
362
- return /* @__PURE__ */ jsxs("li", { className: `squisq-md-li${isTask ? " squisq-md-task" : ""}`, children: [
363
- isTask && /* @__PURE__ */ jsx3("input", { type: "checkbox", checked: !!item.checked, readOnly: true, className: "squisq-md-checkbox" }),
547
+ return /* @__PURE__ */ jsxs2("li", { className: `squisq-md-li${isTask ? " squisq-md-task" : ""}`, children: [
548
+ isTask && /* @__PURE__ */ jsx4("input", { type: "checkbox", checked: !!item.checked, readOnly: true, className: "squisq-md-checkbox" }),
364
549
  renderBlocks(item.children, key, ctx)
365
550
  ] }, key);
366
551
  }
367
552
  function renderTable(rows, align, key, ctx = DEFAULT_CTX) {
368
553
  const [headerRow, ...bodyRows] = rows;
369
- return /* @__PURE__ */ jsxs("table", { className: "squisq-md-table", children: [
370
- headerRow && /* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsx3("tr", { children: headerRow.children.map((cell, ci) => /* @__PURE__ */ jsx3(
554
+ return /* @__PURE__ */ jsxs2("table", { className: "squisq-md-table", children: [
555
+ headerRow && /* @__PURE__ */ jsx4("thead", { children: /* @__PURE__ */ jsx4("tr", { children: headerRow.children.map((cell, ci) => /* @__PURE__ */ jsx4(
371
556
  "th",
372
557
  {
373
558
  className: "squisq-md-th",
@@ -376,7 +561,7 @@ function renderTable(rows, align, key, ctx = DEFAULT_CTX) {
376
561
  },
377
562
  `${key}th${ci}`
378
563
  )) }) }),
379
- bodyRows.length > 0 && /* @__PURE__ */ jsx3("tbody", { children: bodyRows.map((row, ri) => /* @__PURE__ */ jsx3("tr", { children: row.children.map((cell, ci) => /* @__PURE__ */ jsx3(
564
+ bodyRows.length > 0 && /* @__PURE__ */ jsx4("tbody", { children: bodyRows.map((row, ri) => /* @__PURE__ */ jsx4("tr", { children: row.children.map((cell, ci) => /* @__PURE__ */ jsx4(
380
565
  "td",
381
566
  {
382
567
  className: "squisq-md-td",
@@ -393,8 +578,34 @@ function renderBlocks(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
393
578
  function MdImage({ src, alt, title }) {
394
579
  const safeSrc = sanitizeUrl(src, "media");
395
580
  const resolved = useMediaUrl(safeSrc ?? "", ".");
581
+ const imageRef = useRef3(null);
582
+ const playback = useAnimatedImage(safeSrc ?? "", resolved);
396
583
  if (!safeSrc) return null;
397
- return /* @__PURE__ */ jsx3("img", { className: "squisq-md-image", src: resolved, alt, title });
584
+ if (!mayBeAnimatedImage2(safeSrc)) {
585
+ return /* @__PURE__ */ jsx4("img", { className: "squisq-md-image", src: resolved, alt, title });
586
+ }
587
+ return /* @__PURE__ */ jsxs2(
588
+ "span",
589
+ {
590
+ className: "squisq-animated-image",
591
+ "data-animated": playback.animated ? "true" : void 0,
592
+ style: { position: "relative", display: "inline-block", maxWidth: "100%" },
593
+ children: [
594
+ /* @__PURE__ */ jsx4(
595
+ "img",
596
+ {
597
+ ref: imageRef,
598
+ className: "squisq-md-image",
599
+ src: playback.displaySrc,
600
+ alt,
601
+ title,
602
+ style: { display: "block", maxWidth: "100%" }
603
+ }
604
+ ),
605
+ /* @__PURE__ */ jsx4(AnimatedImageControls, { playback, imageRef })
606
+ ]
607
+ }
608
+ );
398
609
  }
399
610
  function resolveHtmlNodes(nodes, htmlPolicy) {
400
611
  if (htmlPolicy === "strip") return [];
@@ -466,7 +677,7 @@ function renderHtmlElement(el, key, ctx) {
466
677
  if (tagName === "video") {
467
678
  const src = sanitizeUrl(el.attributes.src ?? "", "media") ?? "";
468
679
  const poster = sanitizeUrl(el.attributes.poster ?? "", "media") ?? void 0;
469
- return /* @__PURE__ */ jsx3(
680
+ return /* @__PURE__ */ jsx4(
470
681
  InlineVideoPlayer,
471
682
  {
472
683
  src,
@@ -481,7 +692,7 @@ function renderHtmlElement(el, key, ctx) {
481
692
  }
482
693
  if (tagName === "audio") {
483
694
  const src = sanitizeUrl(el.attributes.src ?? "", "media") ?? "";
484
- return /* @__PURE__ */ jsx3(
695
+ return /* @__PURE__ */ jsx4(
485
696
  InlineAudioPlayer,
486
697
  {
487
698
  src,
@@ -494,9 +705,9 @@ function renderHtmlElement(el, key, ctx) {
494
705
  const Tag = tagName;
495
706
  const props = reactPropsFromAttrs(el.attributes, ctx);
496
707
  if (el.selfClosing) {
497
- return /* @__PURE__ */ jsx3(Tag, { ...props }, key);
708
+ return /* @__PURE__ */ jsx4(Tag, { ...props }, key);
498
709
  }
499
- return /* @__PURE__ */ jsx3(Tag, { ...props, children: renderHtmlNodes(el.children, `${key}c`, ctx) }, key);
710
+ return /* @__PURE__ */ jsx4(Tag, { ...props, children: renderHtmlNodes(el.children, `${key}c`, ctx) }, key);
500
711
  }
501
712
  function renderHtmlNodes(nodes, keyPrefix, ctx = DEFAULT_CTX) {
502
713
  return nodes.map((node, i) => {
@@ -505,7 +716,7 @@ function renderHtmlNodes(nodes, keyPrefix, ctx = DEFAULT_CTX) {
505
716
  case "htmlElement":
506
717
  return renderHtmlElement(node, key, ctx);
507
718
  case "htmlText":
508
- return /* @__PURE__ */ jsx3(Fragment, { children: node.value }, key);
719
+ return /* @__PURE__ */ jsx4(Fragment2, { children: node.value }, key);
509
720
  case "htmlComment":
510
721
  return null;
511
722
  default:
@@ -527,8 +738,8 @@ function MarkdownRenderer({
527
738
  }) {
528
739
  const ambientFenceRenderers = useFenceRenderers();
529
740
  const mediaProvider = useMediaProvider();
530
- const [accessoryPaths, setAccessoryPaths] = useState(NO_ACCESSORY_PATHS);
531
- useEffect(() => {
741
+ const [accessoryPaths, setAccessoryPaths] = useState2(NO_ACCESSORY_PATHS);
742
+ useEffect2(() => {
532
743
  if (!mediaProvider) {
533
744
  setAccessoryPaths(clearAccessoryPaths);
534
745
  return;
@@ -547,7 +758,7 @@ function MarkdownRenderer({
547
758
  };
548
759
  }, [mediaProvider]);
549
760
  if (!nodes || nodes.length === 0) return null;
550
- return /* @__PURE__ */ jsx3("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", {
761
+ return /* @__PURE__ */ jsx4("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", {
551
762
  htmlPolicy,
552
763
  linkSchemes,
553
764
  theme,
@@ -563,5 +774,7 @@ export {
563
774
  useFenceRenderers,
564
775
  InlineVideoPlayer,
565
776
  InlineAudioPlayer,
777
+ AnimatedImageControls,
778
+ useAnimatedImage,
566
779
  MarkdownRenderer
567
780
  };
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-TT6ENR6T.js";
4
4
  import {
5
5
  MarkdownRenderer
6
- } from "./chunk-ZY5N3JPM.js";
6
+ } from "./chunk-6PUMLLJH.js";
7
7
 
8
8
  // src/jsonView/useJsonViewTokens.ts
9
9
  import { useMemo } from "react";