@bendyline/squisq-editor-react 2.4.5 → 2.4.6

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
@@ -69,7 +69,14 @@ and block-at-a-time / timeline editing primitives (`useBlockNavigator`,
69
69
  (`versioningPrunePolicy`, default keep-last-50); a Version History panel
70
70
  appears in the toolbar.
71
71
  - **Recording** — with a `mediaProvider` wired, a record button appears in the
72
- toolbar (`allowRecording`, default `true`).
72
+ toolbar (`allowRecording`, default `true`). The recorder's Advanced device
73
+ settings expando discovers microphones/cameras, shows only capture
74
+ constraints advertised by the browser, separates camera and screen
75
+ constraints for dual recordings, and exposes MediaRecorder codec, bitrate,
76
+ and keyframe hints. Preferred constraints use the closest available setting;
77
+ Required microphone/camera constraints fail when the selected input cannot
78
+ satisfy them (screen settings remain preferences, as required by
79
+ `getDisplayMedia`).
73
80
  - **Presentation mode** — every Use view can fill the Squisq control, open a
74
81
  synchronized audience window, or take over the entire screen. Slideshow/video
75
82
  playback, Page/Document scrolling, and the Narrate surface remain linked to
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RecorderModal
3
- } from "./chunk-PKGBWNUQ.js";
3
+ } from "./chunk-ZSQN6IO7.js";
4
4
 
5
5
  // src/recorder/RecorderButton.tsx
6
6
  import { useCallback, useState } from "react";
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  markdownToTiptap,
8
8
  tiptapToMarkdown
9
- } from "./chunk-WLJ623UZ.js";
9
+ } from "./chunk-K2WJYVS4.js";
10
10
  import {
11
11
  ImageEditor,
12
12
  ImageViewer,
@@ -15,13 +15,13 @@ import {
15
15
  import {
16
16
  RecorderPanel,
17
17
  useModalDialog
18
- } from "./chunk-PKGBWNUQ.js";
18
+ } from "./chunk-ZSQN6IO7.js";
19
19
  import {
20
20
  Icon
21
21
  } from "./chunk-GS7QWYFT.js";
22
22
  import {
23
23
  TeleprompterView
24
- } from "./chunk-V6Z7GG55.js";
24
+ } from "./chunk-IQFPX2CE.js";
25
25
 
26
26
  // src/fileKind.ts
27
27
  var EXT_TO_LANGUAGE = {
@@ -18602,6 +18602,28 @@ function parseSquisqMediaPayload(raw) {
18602
18602
  }
18603
18603
  return null;
18604
18604
  }
18605
+ function squisqMediaKind(mimeType) {
18606
+ const normalized = mimeType.toLowerCase();
18607
+ if (normalized.startsWith("image/")) return "image";
18608
+ if (normalized.startsWith("video/")) return "video";
18609
+ if (normalized.startsWith("audio/")) return "audio";
18610
+ return "file";
18611
+ }
18612
+ function escapeHtmlAttribute(value) {
18613
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
18614
+ }
18615
+ function buildSquisqMediaReference(payload) {
18616
+ switch (squisqMediaKind(payload.mimeType)) {
18617
+ case "image":
18618
+ return `![${payload.alt}](${payload.name})`;
18619
+ case "video":
18620
+ return `<video src="${escapeHtmlAttribute(payload.name)}" controls width="480"></video>`;
18621
+ case "audio":
18622
+ return `<audio src="${escapeHtmlAttribute(payload.name)}" controls></audio>`;
18623
+ default:
18624
+ return `[${payload.alt}](${payload.name})`;
18625
+ }
18626
+ }
18605
18627
 
18606
18628
  // src/rawEditorIsolation.ts
18607
18629
  var markdownFenceMaskCache = /* @__PURE__ */ new WeakMap();
@@ -19003,13 +19025,13 @@ function RawEditor({
19003
19025
  const raw = dt.getData(SQUISQ_MEDIA_MIME);
19004
19026
  if (!raw) return;
19005
19027
  const payload = parseSquisqMediaPayload(raw);
19006
- if (!payload || !payload.mimeType.startsWith("image/")) return;
19028
+ if (!payload) return;
19007
19029
  e2.preventDefault();
19008
19030
  e2.stopPropagation();
19009
19031
  const target = editor.getTargetAtClientPoint(e2.clientX, e2.clientY);
19010
19032
  const position = target?.position ?? editor.getPosition();
19011
19033
  if (!position) return;
19012
- const markdown = `![${payload.alt}](${payload.name})`;
19034
+ const markdown = buildSquisqMediaReference(payload);
19013
19035
  editor.executeEdits("squisq-media-drop", [
19014
19036
  {
19015
19037
  range: new monaco.Range(
@@ -27681,17 +27703,26 @@ var TiptapVideo = Node3.create({
27681
27703
  startAt: {
27682
27704
  default: null,
27683
27705
  parseHTML: (el) => el.getAttribute("data-squisq-video-start-at"),
27684
- renderHTML: (attrs) => normalizeVideoPlacement(attrs.placement) !== "content" && !normalizeLockToBlock(attrs.lockToBlock) && attrs.startAt != null ? { "data-squisq-video-start-at": String(attrs.startAt) } : {}
27706
+ renderHTML: (attrs) => {
27707
+ const placement = normalizeVideoPlacement(attrs.placement);
27708
+ return (placement === "content" || !normalizeLockToBlock(attrs.lockToBlock)) && attrs.startAt != null ? { "data-squisq-video-start-at": String(attrs.startAt) } : {};
27709
+ }
27685
27710
  },
27686
27711
  clipStart: {
27687
27712
  default: null,
27688
27713
  parseHTML: (el) => el.getAttribute("data-squisq-video-clip-start"),
27689
- renderHTML: (attrs) => normalizeVideoPlacement(attrs.placement) !== "content" && !normalizeLockToBlock(attrs.lockToBlock) && attrs.clipStart != null ? { "data-squisq-video-clip-start": String(attrs.clipStart) } : {}
27714
+ renderHTML: (attrs) => {
27715
+ const placement = normalizeVideoPlacement(attrs.placement);
27716
+ return (placement === "content" || !normalizeLockToBlock(attrs.lockToBlock)) && attrs.clipStart != null ? { "data-squisq-video-clip-start": String(attrs.clipStart) } : {};
27717
+ }
27690
27718
  },
27691
27719
  clipEnd: {
27692
27720
  default: null,
27693
27721
  parseHTML: (el) => el.getAttribute("data-squisq-video-clip-end"),
27694
- renderHTML: (attrs) => normalizeVideoPlacement(attrs.placement) !== "content" && !normalizeLockToBlock(attrs.lockToBlock) && attrs.clipEnd != null ? { "data-squisq-video-clip-end": String(attrs.clipEnd) } : {}
27722
+ renderHTML: (attrs) => {
27723
+ const placement = normalizeVideoPlacement(attrs.placement);
27724
+ return (placement === "content" || !normalizeLockToBlock(attrs.lockToBlock)) && attrs.clipEnd != null ? { "data-squisq-video-clip-end": String(attrs.clipEnd) } : {};
27725
+ }
27695
27726
  },
27696
27727
  // The HTML5 `controls` attribute is boolean-presence; parse its
27697
27728
  // existence (even with an empty string value) as `true`, otherwise
@@ -27748,6 +27779,21 @@ var TiptapAudio = Node4.create({
27748
27779
  default: true,
27749
27780
  parseHTML: (el) => el.hasAttribute("controls"),
27750
27781
  renderHTML: (attrs) => attrs.controls ? { controls: "" } : {}
27782
+ },
27783
+ startAt: {
27784
+ default: null,
27785
+ parseHTML: (el) => el.getAttribute("data-squisq-audio-start-at"),
27786
+ renderHTML: (attrs) => attrs.startAt != null ? { "data-squisq-audio-start-at": String(attrs.startAt) } : {}
27787
+ },
27788
+ clipStart: {
27789
+ default: null,
27790
+ parseHTML: (el) => el.getAttribute("data-squisq-audio-clip-start"),
27791
+ renderHTML: (attrs) => attrs.clipStart != null ? { "data-squisq-audio-clip-start": String(attrs.clipStart) } : {}
27792
+ },
27793
+ clipEnd: {
27794
+ default: null,
27795
+ parseHTML: (el) => el.getAttribute("data-squisq-audio-clip-end"),
27796
+ renderHTML: (attrs) => attrs.clipEnd != null ? { "data-squisq-audio-clip-end": String(attrs.clipEnd) } : {}
27751
27797
  }
27752
27798
  };
27753
27799
  },
@@ -28145,8 +28191,8 @@ var LinkWithTitle = Link2.extend({
28145
28191
  }
28146
28192
  });
28147
28193
  var EMPTY_PROMPTS = [
28148
- "Start typing your content, or drop images on top of me...",
28149
- "Write anything -- paste markdown, drag in images, or just start typing...",
28194
+ "Start typing your content, or drop files on top of me...",
28195
+ "Write anything -- paste markdown, drag in media, or just start typing...",
28150
28196
  "Type away. Markdown syntax works too...",
28151
28197
  "Chapter 1 begins here...",
28152
28198
  "Once upon a time...",
@@ -28378,8 +28424,8 @@ function WysiwygEditor({
28378
28424
  },
28379
28425
  // When image files are dropped onto the editor, upload them via the
28380
28426
  // MediaProvider and insert <img> nodes referencing the relative paths.
28381
- // Also handles drags from the MediaBin, which reference existing
28382
- // entries via a custom MIME type and skip the upload step.
28427
+ // Drags from the MediaBin can also insert existing image, video, audio,
28428
+ // or generic-file references without uploading them again.
28383
28429
  // Falls through to default handling for non-image drops or when no
28384
28430
  // MediaProvider is available.
28385
28431
  handleDrop: (view, event, _slice, moved) => {
@@ -28389,11 +28435,10 @@ function WysiwygEditor({
28389
28435
  const squisqRaw = dt.getData(SQUISQ_MEDIA_MIME);
28390
28436
  if (squisqRaw) {
28391
28437
  const payload = parseSquisqMediaPayload(squisqRaw);
28392
- if (payload && payload.mimeType.startsWith("image/")) {
28438
+ if (payload) {
28393
28439
  event.preventDefault();
28394
28440
  moveSelectionToDropPoint(view, event);
28395
- insertImageNode(view, payload.name, payload.alt);
28396
- return true;
28441
+ return insertExistingMediaReference(view, payload);
28397
28442
  }
28398
28443
  }
28399
28444
  const imageFiles = filesFromDataTransfer(dt);
@@ -28720,6 +28765,30 @@ function insertImageNode(view, src, alt) {
28720
28765
  const tr = view.state.tr.replaceSelectionWith(node2);
28721
28766
  view.dispatch(tr);
28722
28767
  }
28768
+ function insertExistingMediaReference(view, payload) {
28769
+ const kind = squisqMediaKind(payload.mimeType);
28770
+ if (kind === "image") {
28771
+ insertImageNode(view, payload.name, payload.alt);
28772
+ return true;
28773
+ }
28774
+ const { state } = view;
28775
+ if (kind === "video" || kind === "audio") {
28776
+ const nodeType = state.schema.nodes[kind];
28777
+ if (!nodeType) return false;
28778
+ const attrs = kind === "video" ? { src: payload.name, controls: true, width: 480 } : { src: payload.name, controls: true };
28779
+ const node3 = nodeType.create(attrs);
28780
+ const { $from } = state.selection;
28781
+ const pos = $from.depth >= 1 ? $from.after(1) : state.doc.content.size;
28782
+ view.dispatch(state.tr.insert(pos, node3).scrollIntoView());
28783
+ return true;
28784
+ }
28785
+ const linkType = state.schema.marks.link;
28786
+ if (!linkType) return false;
28787
+ const label = payload.alt || payload.name;
28788
+ const node2 = state.schema.text(label, [linkType.create({ href: payload.name })]);
28789
+ view.dispatch(state.tr.replaceSelectionWith(node2).scrollIntoView());
28790
+ return true;
28791
+ }
28723
28792
  function moveSelectionToDropPoint(view, event) {
28724
28793
  const coords = view.posAtCoords({ left: event.clientX, top: event.clientY });
28725
28794
  if (!coords) return;
@@ -30659,53 +30728,56 @@ function setHtmlAttribute(openingTag, name, value) {
30659
30728
  if (attr.test(openingTag)) return openingTag.replace(attr, ` ${name}="${value}"`);
30660
30729
  return openingTag.replace(/\s*\/?>(?=$)/, (end) => ` ${name}="${value}"${end.trimStart()}`);
30661
30730
  }
30662
- function setHtmlVideoClipInLine(original, patch) {
30663
- if (!/^\s*<video\b/i.test(original)) return null;
30664
- const opening = /<video\b[^>]*>/i.exec(original);
30731
+ function setHtmlMediaClipInLine(original, patch) {
30732
+ const tagMatch = /^\s*<(video|audio)\b/i.exec(original);
30733
+ if (!tagMatch) return null;
30734
+ const tag = tagMatch[1].toLowerCase();
30735
+ const opening = new RegExp(`<${tag}\\b[^>]*>`, "i").exec(original);
30665
30736
  if (!opening) return null;
30666
30737
  let next = opening[0];
30738
+ const timingPrefix = `data-squisq-${tag}`;
30667
30739
  if (patch.startAt !== void 0) {
30668
30740
  next = setHtmlAttribute(
30669
30741
  next,
30670
- "data-squisq-video-start-at",
30742
+ `${timingPrefix}-start-at`,
30671
30743
  patch.startAt == null ? null : formatSeconds(patch.startAt)
30672
30744
  );
30673
30745
  }
30674
30746
  if (patch.clipStart !== void 0) {
30675
30747
  next = setHtmlAttribute(
30676
30748
  next,
30677
- "data-squisq-video-clip-start",
30749
+ `${timingPrefix}-clip-start`,
30678
30750
  patch.clipStart == null ? null : formatSeconds(patch.clipStart)
30679
30751
  );
30680
30752
  }
30681
30753
  if (patch.clipEnd !== void 0) {
30682
30754
  next = setHtmlAttribute(
30683
30755
  next,
30684
- "data-squisq-video-clip-end",
30756
+ `${timingPrefix}-clip-end`,
30685
30757
  patch.clipEnd == null ? null : formatSeconds(patch.clipEnd)
30686
30758
  );
30687
30759
  }
30688
- if (patch.placement !== void 0) {
30760
+ if (tag === "video" && patch.placement !== void 0) {
30689
30761
  next = setHtmlAttribute(
30690
30762
  next,
30691
30763
  "data-squisq-video-placement",
30692
30764
  patch.placement == null || patch.placement === "content" ? null : patch.placement
30693
30765
  );
30694
30766
  }
30695
- if (patch.lockToBlock !== void 0) {
30767
+ if (tag === "video" && patch.lockToBlock !== void 0) {
30696
30768
  next = setHtmlAttribute(
30697
30769
  next,
30698
30770
  "data-squisq-video-lock-to-block",
30699
30771
  patch.lockToBlock == null || patch.lockToBlock ? null : "false"
30700
30772
  );
30701
30773
  }
30702
- if (patch.pipSize !== void 0) {
30774
+ if (tag === "video" && patch.pipSize !== void 0) {
30703
30775
  next = setHtmlAttribute(next, "data-squisq-video-pip-size", patch.pipSize);
30704
30776
  }
30705
- if (patch.pipShape !== void 0) {
30777
+ if (tag === "video" && patch.pipShape !== void 0) {
30706
30778
  next = setHtmlAttribute(next, "data-squisq-video-pip-shape", patch.pipShape);
30707
30779
  }
30708
- if (patch.pipPosition !== void 0) {
30780
+ if (tag === "video" && patch.pipPosition !== void 0) {
30709
30781
  next = setHtmlAttribute(next, "data-squisq-video-pip-position", patch.pipPosition);
30710
30782
  }
30711
30783
  return `${original.slice(0, opening.index)}${next}${original.slice(opening.index + opening[0].length)}`;
@@ -30715,9 +30787,9 @@ function setMediaClipInSource(source, line, patch) {
30715
30787
  const idx = line - 1;
30716
30788
  if (idx < 0 || idx >= lines2.length) return null;
30717
30789
  const original = lines2[idx];
30718
- const htmlVideo = setHtmlVideoClipInLine(original, patch);
30719
- if (htmlVideo != null) {
30720
- lines2[idx] = htmlVideo;
30790
+ const htmlMedia = setHtmlMediaClipInLine(original, patch);
30791
+ if (htmlMedia != null) {
30792
+ lines2[idx] = htmlMedia;
30721
30793
  return lines2.join("\n");
30722
30794
  }
30723
30795
  const m = matchTrailingTemplateAnnotation3(original);
@@ -30797,8 +30869,27 @@ function placeClipInBlock(source, fromLine, targetHeadingLine, spec, startAt) {
30797
30869
  if (fromIdx < 0 || fromIdx >= lines2.length) return null;
30798
30870
  const targetIdx0 = targetHeadingLine - 1;
30799
30871
  if (targetIdx0 < 0 || targetIdx0 >= lines2.length) return null;
30800
- const annotation = buildClipAnnotation(spec, Math.max(0, startAt));
30801
- if (headingIndexAbove(lines2, fromIdx) === targetIdx0) {
30872
+ const normalizedStartAt = Math.max(0, startAt);
30873
+ const sameBlock = headingIndexAbove(lines2, fromIdx) === targetIdx0;
30874
+ if (/^\s*<(?:video|audio)\b/i.test(lines2[fromIdx])) {
30875
+ const html = setHtmlMediaClipInLine(lines2[fromIdx], {
30876
+ startAt: normalizedStartAt > 0 ? normalizedStartAt : null,
30877
+ ...spec.clipStart !== void 0 ? { clipStart: spec.clipStart } : {},
30878
+ ...spec.clipEnd !== void 0 ? { clipEnd: spec.clipEnd } : {}
30879
+ });
30880
+ if (html == null) return null;
30881
+ if (sameBlock) {
30882
+ lines2[fromIdx] = html;
30883
+ return lines2.join("\n");
30884
+ }
30885
+ lines2.splice(fromIdx, 1);
30886
+ let targetIdx2 = targetIdx0;
30887
+ if (fromIdx < targetIdx2) targetIdx2 -= 1;
30888
+ lines2.splice(targetIdx2 + 1, 0, "", html);
30889
+ return lines2.join("\n");
30890
+ }
30891
+ const annotation = buildClipAnnotation(spec, normalizedStartAt);
30892
+ if (sameBlock) {
30802
30893
  lines2[fromIdx] = annotation;
30803
30894
  return lines2.join("\n");
30804
30895
  }
@@ -30820,6 +30911,7 @@ import { flattenBlocks as flattenBlocks6, DEFAULT_THEME as DEFAULT_THEME6, getPi
30820
30911
  import { MediaClipLayer, MediaContext as MediaContext5 } from "@bendyline/squisq-react";
30821
30912
 
30822
30913
  // src/embeddedMedia.ts
30914
+ import { parseTimeSeconds as parseTimeSeconds2 } from "@bendyline/squisq/markdown";
30823
30915
  var VIDEO_EXT = /* @__PURE__ */ new Set(["webm", "mp4", "mov", "m4v", "ogv"]);
30824
30916
  var AUDIO_EXT = /* @__PURE__ */ new Set(["mp3", "wav", "ogg", "oga", "m4a", "aac", "flac", "opus"]);
30825
30917
  function mediaKindFromUrl(url) {
@@ -30829,6 +30921,11 @@ function mediaKindFromUrl(url) {
30829
30921
  if (AUDIO_EXT.has(ext)) return "audio";
30830
30922
  return null;
30831
30923
  }
30924
+ function mediaTimeAttribute(attributes, kind, name) {
30925
+ const raw = attributes?.[`data-squisq-${kind}-${name}`];
30926
+ if (raw == null) return void 0;
30927
+ return parseTimeSeconds2(raw) ?? void 0;
30928
+ }
30832
30929
  function collectEmbeddedMedia(block) {
30833
30930
  const out = [];
30834
30931
  const visit = (node2, line) => {
@@ -30851,7 +30948,19 @@ function collectEmbeddedMedia(block) {
30851
30948
  }
30852
30949
  }
30853
30950
  }
30854
- if (src) out.push({ src, kind, sourceLine: here });
30951
+ if (src) {
30952
+ const startAt = mediaTimeAttribute(n.attributes, kind, "start-at");
30953
+ const clipStart = mediaTimeAttribute(n.attributes, kind, "clip-start");
30954
+ const clipEnd = mediaTimeAttribute(n.attributes, kind, "clip-end");
30955
+ out.push({
30956
+ src,
30957
+ kind,
30958
+ sourceLine: here,
30959
+ ...startAt != null ? { startAt } : {},
30960
+ ...clipStart != null ? { clipStart } : {},
30961
+ ...clipEnd != null ? { clipEnd } : {}
30962
+ });
30963
+ }
30855
30964
  }
30856
30965
  if (Array.isArray(n.children)) n.children.forEach((c) => visit(c, here));
30857
30966
  if (Array.isArray(n.htmlChildren)) n.htmlChildren.forEach((c) => visit(c, here));
@@ -30859,18 +30968,30 @@ function collectEmbeddedMedia(block) {
30859
30968
  (block.contents ?? []).forEach((node2) => visit(node2, void 0));
30860
30969
  return out;
30861
30970
  }
30971
+ function resolveEmbeddedMediaTiming(block, media) {
30972
+ const startAt = Math.max(0, media.startAt ?? 0);
30973
+ const sourceIn = Math.max(0, media.clipStart ?? 0);
30974
+ const absoluteStart = block.startTime + startAt;
30975
+ const blockEnd = block.startTime + block.duration;
30976
+ const authoredLength = media.clipEnd == null ? null : Math.max(0, media.clipEnd - sourceIn);
30977
+ const absoluteEnd = authoredLength == null ? blockEnd : Math.min(blockEnd, absoluteStart + authoredLength);
30978
+ return {
30979
+ absoluteStart,
30980
+ absoluteEnd: Math.max(absoluteStart, absoluteEnd),
30981
+ sourceIn
30982
+ };
30983
+ }
30862
30984
  function collectEmbeddedMediaSchedule(doc) {
30863
30985
  const schedule = [];
30864
30986
  const visit = (blocks) => {
30865
30987
  for (const block of blocks) {
30866
30988
  collectEmbeddedMedia(block).forEach((media, index2) => {
30989
+ const timing = resolveEmbeddedMediaTiming(block, media);
30867
30990
  schedule.push({
30868
30991
  id: `embedded:${block.id}:${index2}`,
30869
30992
  kind: media.kind,
30870
30993
  src: media.src,
30871
- absoluteStart: block.startTime,
30872
- absoluteEnd: block.startTime + block.duration,
30873
- sourceIn: 0,
30994
+ ...timing,
30874
30995
  anchor: "block",
30875
30996
  blockId: block.id,
30876
30997
  ...media.sourceLine != null ? { sourceLine: media.sourceLine } : {}
@@ -31990,16 +32111,29 @@ function TimelineTrack({
31990
32111
  blocks.flatMap(
31991
32112
  (b, i) => collectEmbeddedMedia(b).map((m, j) => {
31992
32113
  const id = `embed:${b.id}:${j}`;
31993
- const absStart = b.startTime;
31994
- const length = b.duration;
32114
+ const timing = resolveEmbeddedMediaTiming(b, m);
32115
+ const absStart = timing.absoluteStart;
32116
+ const length = timing.absoluteEnd - timing.absoluteStart;
32117
+ const sourceIn = timing.sourceIn ?? 0;
32118
+ const startAt = Math.max(0, m.startAt ?? 0);
32119
+ const authoredLength = m.clipEnd == null ? null : Math.max(0, m.clipEnd - sourceIn);
32120
+ const previewBlockRemainder = Math.max(0, previewWidth(i) / pxPerSecond - startAt);
32121
+ const previewLength = authoredLength == null ? previewBlockRemainder : Math.min(authoredLength, previewBlockRemainder);
31995
32122
  let left = previewLeft(i);
31996
32123
  let clipWidth = previewWidth(i);
32124
+ left += startAt * pxPerSecond;
32125
+ clipWidth = Math.max(previewLength * pxPerSecond, 4);
31997
32126
  if (drag?.targetId === id && drag.kind === "embed-move") {
31998
32127
  left = drag.preview * pxPerSecond;
31999
32128
  } else if (drag?.targetId === id && drag.kind === "embed-right") {
32000
32129
  clipWidth = Math.max(drag.preview * pxPerSecond, 4);
32001
32130
  }
32002
- const spec = { kind: m.kind, src: m.src };
32131
+ const spec = {
32132
+ kind: m.kind,
32133
+ src: m.src,
32134
+ ...m.clipStart != null ? { clipStart: m.clipStart } : {},
32135
+ ...m.clipEnd != null ? { clipEnd: m.clipEnd } : {}
32136
+ };
32003
32137
  return /* @__PURE__ */ jsx64(
32004
32138
  "div",
32005
32139
  {
@@ -32020,7 +32154,7 @@ function TimelineTrack({
32020
32154
  (newAbsStart) => {
32021
32155
  placeEmbeddedClip(
32022
32156
  m.sourceLine,
32023
- { ...spec, clipEnd: length },
32157
+ { ...spec, clipEnd: sourceIn + length },
32024
32158
  newAbsStart
32025
32159
  );
32026
32160
  },
@@ -32031,7 +32165,7 @@ function TimelineTrack({
32031
32165
  TimelineVideoFilmstrip,
32032
32166
  {
32033
32167
  src: m.src,
32034
- sourceStart: 0,
32168
+ sourceStart: sourceIn,
32035
32169
  sourceLength: length,
32036
32170
  width: clipWidth
32037
32171
  }
@@ -32067,7 +32201,11 @@ function TimelineTrack({
32067
32201
  {
32068
32202
  className: "squisq-timeline-edge squisq-timeline-edge--right",
32069
32203
  onPointerDown: (e2) => beginDrag(e2, "embed-right", id, length, (len) => {
32070
- placeEmbeddedClip(m.sourceLine, { ...spec, clipEnd: len }, absStart);
32204
+ placeEmbeddedClip(
32205
+ m.sourceLine,
32206
+ { ...spec, clipEnd: sourceIn + len },
32207
+ absStart
32208
+ );
32071
32209
  })
32072
32210
  }
32073
32211
  )
@@ -34406,18 +34544,16 @@ function MediaBin({
34406
34544
  entries.map((entry) => {
34407
34545
  const thumb = thumbUrls[entry.name];
34408
34546
  const basename = basenameForPath(entry.name);
34409
- const isImage = isImageMime(entry.mimeType);
34410
34547
  const altText = basename.replace(/\.[^.]+$/, "").replace(/[-_]/g, " ");
34411
34548
  const isUnused = !!usedMediaPaths && !usedMediaPaths.has(entry.name);
34412
34549
  const handleDragStart = (e2) => {
34413
- if (!isImage) return;
34414
- const payload = JSON.stringify({
34550
+ const payload = {
34415
34551
  name: entry.name,
34416
34552
  mimeType: entry.mimeType,
34417
34553
  alt: altText
34418
- });
34419
- e2.dataTransfer.setData(SQUISQ_MEDIA_MIME, payload);
34420
- e2.dataTransfer.setData("text/plain", `![${altText}](${entry.name})`);
34554
+ };
34555
+ e2.dataTransfer.setData(SQUISQ_MEDIA_MIME, JSON.stringify(payload));
34556
+ e2.dataTransfer.setData("text/plain", buildSquisqMediaReference(payload));
34421
34557
  e2.dataTransfer.effectAllowed = "copy";
34422
34558
  };
34423
34559
  return /* @__PURE__ */ jsxs58(
@@ -34427,7 +34563,7 @@ function MediaBin({
34427
34563
  title: `${entry.name}
34428
34564
  ${entry.mimeType}
34429
34565
  ${formatSize(entry.size)}`,
34430
- draggable: isImage,
34566
+ draggable: true,
34431
34567
  tabIndex: 0,
34432
34568
  onContextMenu: (e2) => {
34433
34569
  e2.preventDefault();
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  NarrationStage,
3
3
  useNarrationStage
4
- } from "./chunk-PDCKJCOS.js";
4
+ } from "./chunk-OWOKKZAU.js";
5
5
 
6
6
  // src/teleprompter/TeleprompterView.tsx
7
7
  import { jsx } from "react/jsx-runtime";
@@ -728,9 +728,10 @@ function serializeMediaTag(tag, attrs) {
728
728
  const pipSize = tag === "video" ? /\bdata-squisq-video-pip-size="([^"]*)"/i.exec(attrs)?.[1] : void 0;
729
729
  const pipShape = tag === "video" ? /\bdata-squisq-video-pip-shape="([^"]*)"/i.exec(attrs)?.[1] : void 0;
730
730
  const pipPosition = tag === "video" ? /\bdata-squisq-video-pip-position="([^"]*)"/i.exec(attrs)?.[1] : void 0;
731
- const startAt = tag === "video" ? /\bdata-squisq-video-start-at="([^"]*)"/i.exec(attrs)?.[1] : void 0;
732
- const clipStart = tag === "video" ? /\bdata-squisq-video-clip-start="([^"]*)"/i.exec(attrs)?.[1] : void 0;
733
- const clipEnd = tag === "video" ? /\bdata-squisq-video-clip-end="([^"]*)"/i.exec(attrs)?.[1] : void 0;
731
+ const timingPrefix = `data-squisq-${tag}`;
732
+ const startAt = new RegExp(`\\b${timingPrefix}-start-at="([^"]*)"`, "i").exec(attrs)?.[1];
733
+ const clipStart = new RegExp(`\\b${timingPrefix}-clip-start="([^"]*)"`, "i").exec(attrs)?.[1];
734
+ const clipEnd = new RegExp(`\\b${timingPrefix}-clip-end="([^"]*)"`, "i").exec(attrs)?.[1];
734
735
  const parts = [`<${tag} src="${src}"`];
735
736
  if (controls) parts.push(" controls");
736
737
  if (width) parts.push(` width="${width}"`);
@@ -749,9 +750,9 @@ function serializeMediaTag(tag, attrs) {
749
750
  if (pipPosition === "top-left" || pipPosition === "top-right" || pipPosition === "bottom-left" || pipPosition === "bottom-right") {
750
751
  parts.push(` data-squisq-video-pip-position="${pipPosition}"`);
751
752
  }
752
- if (startAt != null) parts.push(` data-squisq-video-start-at="${startAt}"`);
753
- if (clipStart != null) parts.push(` data-squisq-video-clip-start="${clipStart}"`);
754
- if (clipEnd != null) parts.push(` data-squisq-video-clip-end="${clipEnd}"`);
753
+ if (startAt != null) parts.push(` ${timingPrefix}-start-at="${startAt}"`);
754
+ if (clipStart != null) parts.push(` ${timingPrefix}-clip-start="${clipStart}"`);
755
+ if (clipEnd != null) parts.push(` ${timingPrefix}-clip-end="${clipEnd}"`);
755
756
  parts.push(`></${tag}>`);
756
757
  return parts.join("");
757
758
  }
@@ -175,13 +175,18 @@ async function registerPcmWorklet(ctx) {
175
175
 
176
176
  // src/teleprompter/useMicAnalysis.ts
177
177
  import { useCallback, useEffect as useEffect2, useRef, useState } from "react";
178
- function useMicAnalysis() {
178
+ function useMicAnalysis(constraints) {
179
179
  const [status, setStatus] = useState("idle");
180
180
  const [error, setError] = useState(null);
181
181
  const [devices, setDevices] = useState([]);
182
182
  const [stream, setStream] = useState(null);
183
183
  const [sampleRate, setSampleRate] = useState(null);
184
184
  const graphRef = useRef(null);
185
+ const constraintsRef = useRef(constraints);
186
+ constraintsRef.current = constraints;
187
+ const constraintKey = JSON.stringify(constraints ?? {});
188
+ const previousConstraintKeyRef = useRef(constraintKey);
189
+ const currentDeviceIdRef = useRef(null);
185
190
  const listenersRef = useRef(/* @__PURE__ */ new Set());
186
191
  const generationRef = useRef(0);
187
192
  const refreshDevices = useCallback(async () => {
@@ -226,13 +231,16 @@ function useMicAnalysis() {
226
231
  const start = useCallback(
227
232
  async (deviceId) => {
228
233
  const generation = ++generationRef.current;
234
+ currentDeviceIdRef.current = deviceId;
229
235
  teardown();
230
236
  setStatus("starting");
231
237
  setError(null);
232
238
  try {
233
- const micStream = await requestMicStream(
234
- deviceId ? { deviceId: { exact: deviceId } } : void 0
235
- );
239
+ const baseConstraints = constraintsRef.current;
240
+ const micStream = await requestMicStream({
241
+ ...baseConstraints ?? {},
242
+ ...deviceId ? { deviceId: { exact: deviceId } } : {}
243
+ });
236
244
  if (generation !== generationRef.current) {
237
245
  for (const track of micStream.getTracks()) track.stop();
238
246
  return null;
@@ -311,6 +319,11 @@ function useMicAnalysis() {
311
319
  listenersRef.current.delete(listener);
312
320
  };
313
321
  }, []);
322
+ useEffect2(() => {
323
+ if (previousConstraintKeyRef.current === constraintKey) return;
324
+ previousConstraintKeyRef.current = constraintKey;
325
+ if (graphRef.current) void start(currentDeviceIdRef.current);
326
+ }, [constraintKey, start]);
314
327
  useEffect2(() => {
315
328
  return () => {
316
329
  generationRef.current += 1;
@@ -449,7 +462,7 @@ function useTeleprompter(opts) {
449
462
  const [transport, setTransport] = useState2("stopped");
450
463
  const [countdownRemaining, setCountdownRemaining] = useState2(null);
451
464
  const [view, setView] = useState2({ wordPos: 0, micLevel: 0, voiceActive: false });
452
- const mic = useMicAnalysis();
465
+ const mic = useMicAnalysis(opts.micConstraints);
453
466
  const scriptRef = useRef2(script);
454
467
  const prefsRef = useRef2(prefs);
455
468
  const transportRef = useRef2(transport);
@@ -1429,20 +1442,36 @@ function useNarrationRecorder(options) {
1429
1442
  const micStream = opts.mic.status === "live" && opts.mic.stream ? opts.mic.stream : await opts.mic.start(opts.getMicDeviceId());
1430
1443
  if (superseded()) throw new StartAborted();
1431
1444
  if (!micStream) throw opts.mic.error ?? new Error("Microphone unavailable");
1432
- const audioFormat = resolveFormat("audio");
1445
+ const audioFormat = resolveFormat("audio", opts.audioRecorderOptions?.mimeType);
1446
+ const audioRecorderOptions = {
1447
+ ...opts.audioRecorderOptions
1448
+ };
1449
+ if (audioFormat.mimeType) audioRecorderOptions.mimeType = audioFormat.mimeType;
1450
+ else delete audioRecorderOptions.mimeType;
1433
1451
  audioRecorder = new MediaRecorder(
1434
1452
  micStream,
1435
- audioFormat.mimeType ? { mimeType: audioFormat.mimeType } : void 0
1453
+ Object.keys(audioRecorderOptions).length > 0 ? audioRecorderOptions : void 0
1436
1454
  );
1437
1455
  let cameraMime = null;
1438
1456
  let cameraExt = null;
1439
1457
  if (withCamera) {
1440
- camera = await requestCameraStream({ video: true, audio: false });
1458
+ camera = await requestCameraStream({
1459
+ video: opts.cameraConstraints ?? true,
1460
+ audio: false
1461
+ });
1441
1462
  if (superseded()) throw new StartAborted();
1442
- const videoFormat = resolveFormat("video");
1463
+ const videoFormat = resolveFormat("video", opts.cameraRecorderOptions?.mimeType);
1464
+ const cameraRecorderOptions = {
1465
+ ...opts.cameraRecorderOptions
1466
+ };
1467
+ if (cameraRecorderOptions.videoKeyFrameIntervalDuration !== void 0 && cameraRecorderOptions.videoKeyFrameIntervalCount !== void 0) {
1468
+ delete cameraRecorderOptions.videoKeyFrameIntervalCount;
1469
+ }
1470
+ if (videoFormat.mimeType) cameraRecorderOptions.mimeType = videoFormat.mimeType;
1471
+ else delete cameraRecorderOptions.mimeType;
1443
1472
  cameraRecorder = new MediaRecorder(
1444
1473
  camera,
1445
- videoFormat.mimeType ? { mimeType: videoFormat.mimeType } : void 0
1474
+ Object.keys(cameraRecorderOptions).length > 0 ? cameraRecorderOptions : void 0
1446
1475
  );
1447
1476
  cameraMime = videoFormat.mimeType;
1448
1477
  cameraExt = videoFormat.extension;
@@ -1775,7 +1804,7 @@ import { useCallback as useCallback5, useEffect as useEffect6, useMemo as useMem
1775
1804
  import { wordIndexAtTime } from "@bendyline/squisq/narration";
1776
1805
  function useNarrationStage(opts) {
1777
1806
  const { doc, recording = null, getAudioBasename } = opts;
1778
- const controller = useTeleprompter({ doc });
1807
+ const controller = useTeleprompter({ doc, micConstraints: opts.micConstraints });
1779
1808
  const float = useFloatingWindow(TELEPROMPTER_CSS);
1780
1809
  const controllerRef = useRef5(controller);
1781
1810
  controllerRef.current = controller;
@@ -1785,6 +1814,9 @@ function useNarrationStage(opts) {
1785
1814
  getScript: () => controllerRef.current.script,
1786
1815
  getWordPos: () => controllerRef.current.wordPos,
1787
1816
  getMicDeviceId: () => controllerRef.current.prefs.micDeviceId,
1817
+ cameraConstraints: opts.cameraConstraints,
1818
+ audioRecorderOptions: opts.audioRecorderOptions,
1819
+ cameraRecorderOptions: opts.cameraRecorderOptions,
1788
1820
  onRecordingStart: () => controllerRef.current.play(),
1789
1821
  onRecordingStop: () => controllerRef.current.pause()
1790
1822
  });
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  markdownToTiptap,
3
3
  tiptapToMarkdown
4
- } from "./chunk-WLJ623UZ.js";
4
+ } from "./chunk-K2WJYVS4.js";
5
5
 
6
6
  // src/jsonEditor/JsonEditorContext.tsx
7
7
  import { createContext, useContext, useEffect, useMemo } from "react";