@arbidocs/blocks 0.5.124 → 0.5.126

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.
Files changed (152) hide show
  1. package/dist/{ClippingMediaViewer-ZFEX5A3U.cjs → ClippingMediaViewer-47BAXNLO.cjs} +10 -11
  2. package/dist/ClippingMediaViewer-47BAXNLO.cjs.map +1 -0
  3. package/dist/{ClippingPdfViewer-3LRTEPHV.cjs → ClippingPdfViewer-D5BYHEAH.cjs} +16 -14
  4. package/dist/ClippingPdfViewer-D5BYHEAH.cjs.map +1 -0
  5. package/dist/{ConnectedCallDock-XJOYVVAH.cjs → ConnectedCallDock-733O5M36.cjs} +5 -5
  6. package/dist/{ConnectedCallDock-XJOYVVAH.cjs.map → ConnectedCallDock-733O5M36.cjs.map} +1 -1
  7. package/dist/{ConnectedCallRoom-CUDYAPQK.cjs → ConnectedCallRoom-TAUGSRGV.cjs} +5 -5
  8. package/dist/{ConnectedCallRoom-CUDYAPQK.cjs.map → ConnectedCallRoom-TAUGSRGV.cjs.map} +1 -1
  9. package/dist/{ConnectedNativeDocxComparison-QKHCYD6W.cjs → ConnectedNativeDocxComparison-HDE522YC.cjs} +9 -9
  10. package/dist/{ConnectedNativeDocxComparison-QKHCYD6W.cjs.map → ConnectedNativeDocxComparison-HDE522YC.cjs.map} +1 -1
  11. package/dist/{ConnectedPresentation-OV2KBOLK.cjs → ConnectedPresentation-SI3L77J4.cjs} +33 -34
  12. package/dist/ConnectedPresentation-SI3L77J4.cjs.map +1 -0
  13. package/dist/{ConnectedSlidesTab-LUAGZ6FD.cjs → ConnectedSlidesTab-JSPU6EY5.cjs} +37 -43
  14. package/dist/ConnectedSlidesTab-JSPU6EY5.cjs.map +1 -0
  15. package/dist/{ConnectedStreamedWordArtifact-AG62232A.cjs → ConnectedStreamedWordArtifact-4FHPOAFZ.cjs} +9 -9
  16. package/dist/{ConnectedStreamedWordArtifact-AG62232A.cjs.map → ConnectedStreamedWordArtifact-4FHPOAFZ.cjs.map} +1 -1
  17. package/dist/{DocumentReviewCard-M53XFLI2.cjs → DocumentReviewCard-BDNXP3D4.cjs} +4 -4
  18. package/dist/{DocumentReviewCard-M53XFLI2.cjs.map → DocumentReviewCard-BDNXP3D4.cjs.map} +1 -1
  19. package/dist/{FilesViewerDialog-JWKNJLWG.cjs → FilesViewerDialog-WQRNSKWM.cjs} +10 -11
  20. package/dist/FilesViewerDialog-WQRNSKWM.cjs.map +1 -0
  21. package/dist/{NativeDocxEditor-2LQY2CTV.cjs → NativeDocxEditor-SUDULUN3.cjs} +6 -6
  22. package/dist/{NativeDocxEditor-2LQY2CTV.cjs.map → NativeDocxEditor-SUDULUN3.cjs.map} +1 -1
  23. package/dist/{SpotlightStrip-ED22UYTM.cjs → SpotlightStrip-QQ2WUI32.cjs} +28 -29
  24. package/dist/SpotlightStrip-QQ2WUI32.cjs.map +1 -0
  25. package/dist/WorkspaceTableView-3XYAV4BV.cjs +1 -1
  26. package/dist/agents/index.cjs +1 -1
  27. package/dist/ai-IEIR7SC2.cjs +1 -1
  28. package/dist/calls/index.cjs +2 -2
  29. package/dist/{chunk-ZZZITHGK.cjs → chunk-26SFI2BH.cjs} +16 -3
  30. package/dist/chunk-26SFI2BH.cjs.map +1 -0
  31. package/dist/{chunk-NG2EE7HR.cjs → chunk-463QM764.cjs} +3 -3
  32. package/dist/{chunk-NG2EE7HR.cjs.map → chunk-463QM764.cjs.map} +1 -1
  33. package/dist/{chunk-XGM4VVNV.cjs → chunk-4KEQ62SL.cjs} +6 -6
  34. package/dist/{chunk-XGM4VVNV.cjs.map → chunk-4KEQ62SL.cjs.map} +1 -1
  35. package/dist/{chunk-YGBHY67W.cjs → chunk-6B4CCDDB.cjs} +5 -5
  36. package/dist/{chunk-YGBHY67W.cjs.map → chunk-6B4CCDDB.cjs.map} +1 -1
  37. package/dist/{chunk-JY7UUZQG.cjs → chunk-7Q46YAX4.cjs} +74 -71
  38. package/dist/chunk-7Q46YAX4.cjs.map +1 -0
  39. package/dist/{chunk-3ZZZO4F7.cjs → chunk-BY3QA3S5.cjs} +9 -9
  40. package/dist/{chunk-3ZZZO4F7.cjs.map → chunk-BY3QA3S5.cjs.map} +1 -1
  41. package/dist/{chunk-VKH3BEZE.cjs → chunk-CMPHJRVV.cjs} +3 -3
  42. package/dist/{chunk-VKH3BEZE.cjs.map → chunk-CMPHJRVV.cjs.map} +1 -1
  43. package/dist/{chunk-BX6G2ZCI.cjs → chunk-FB3JXSQT.cjs} +71 -43
  44. package/dist/chunk-FB3JXSQT.cjs.map +1 -0
  45. package/dist/{chunk-EMRY62HS.cjs → chunk-FGOJBOZX.cjs} +4 -4
  46. package/dist/{chunk-EMRY62HS.cjs.map → chunk-FGOJBOZX.cjs.map} +1 -1
  47. package/dist/{chunk-V6MW7B4B.cjs → chunk-I4X72WUT.cjs} +6 -6
  48. package/dist/{chunk-V6MW7B4B.cjs.map → chunk-I4X72WUT.cjs.map} +1 -1
  49. package/dist/{chunk-ORNLCBNE.cjs → chunk-JVEDKKNO.cjs} +43 -4
  50. package/dist/chunk-JVEDKKNO.cjs.map +1 -0
  51. package/dist/{chunk-7557LDAN.cjs → chunk-LOB2ATS5.cjs} +23 -10
  52. package/dist/chunk-LOB2ATS5.cjs.map +1 -0
  53. package/dist/{chunk-MXBXZXXQ.cjs → chunk-LXM5QXII.cjs} +3 -3
  54. package/dist/{chunk-MXBXZXXQ.cjs.map → chunk-LXM5QXII.cjs.map} +1 -1
  55. package/dist/chunk-OALQGO5C.cjs +18 -0
  56. package/dist/{chunk-SDRYSLOX.cjs.map → chunk-OALQGO5C.cjs.map} +1 -1
  57. package/dist/{chunk-2CSLFAGF.cjs → chunk-Q6N5D3R5.cjs} +34 -8
  58. package/dist/chunk-Q6N5D3R5.cjs.map +1 -0
  59. package/dist/{chunk-MVXHTAJ4.cjs → chunk-QWVUBTW3.cjs} +9 -9
  60. package/dist/{chunk-MVXHTAJ4.cjs.map → chunk-QWVUBTW3.cjs.map} +1 -1
  61. package/dist/{chunk-ADPRIBSH.cjs → chunk-X7ALF4MP.cjs} +7 -7
  62. package/dist/{chunk-ADPRIBSH.cjs.map → chunk-X7ALF4MP.cjs.map} +1 -1
  63. package/dist/{chunk-COD5ENHK.cjs → chunk-XNB27POJ.cjs} +4 -4
  64. package/dist/{chunk-COD5ENHK.cjs.map → chunk-XNB27POJ.cjs.map} +1 -1
  65. package/dist/{chunk-H236SNEV.cjs → chunk-ZCU72Q76.cjs} +191 -165
  66. package/dist/chunk-ZCU72Q76.cjs.map +1 -0
  67. package/dist/connect/docx.cjs +13 -13
  68. package/dist/connect/index.cjs +198 -163
  69. package/dist/connect/index.cjs.map +1 -1
  70. package/dist/connect/viewer.cjs +8 -9
  71. package/dist/connection/ai.d.ts +1 -0
  72. package/dist/connection/docx/ConnectedNativeDocxEditor.js +4 -5
  73. package/dist/connection/docx/ConnectedNativeDocxEditor.js.map +2 -2
  74. package/dist/connection/docx/save-native-version.d.ts +3 -0
  75. package/dist/connection/docx/save-native-version.js +19 -0
  76. package/dist/connection/docx/save-native-version.js.map +7 -0
  77. package/dist/connection/presentation/ClippingPdfViewer.js +8 -5
  78. package/dist/connection/presentation/ClippingPdfViewer.js.map +2 -2
  79. package/dist/connection/presentation/ConnectedSlidesTab.js +8 -13
  80. package/dist/connection/presentation/ConnectedSlidesTab.js.map +2 -2
  81. package/dist/connection/presentation/usePresentationController.js +24 -15
  82. package/dist/connection/presentation/usePresentationController.js.map +2 -2
  83. package/dist/connection/presentation/usePresentationFiles.d.ts +2 -1
  84. package/dist/connection/presentation/usePresentationFiles.js +15 -6
  85. package/dist/connection/presentation/usePresentationFiles.js.map +2 -2
  86. package/dist/connection/search/ConnectedSearchPage.d.ts +0 -11
  87. package/dist/connection/search/ConnectedSearchPage.js +3 -0
  88. package/dist/connection/search/ConnectedSearchPage.js.map +2 -2
  89. package/dist/connection/search/SearchResultRows.js +75 -43
  90. package/dist/connection/search/SearchResultRows.js.map +2 -2
  91. package/dist/connection/viewer/CleanViewControl.d.ts +3 -1
  92. package/dist/connection/viewer/CleanViewControl.js +5 -1
  93. package/dist/connection/viewer/CleanViewControl.js.map +2 -2
  94. package/dist/connection/viewer/ConnectedDocumentViewer.js +1 -1
  95. package/dist/connection/viewer/ConnectedDocumentViewer.js.map +2 -2
  96. package/dist/connection/viewer/VersionDropdown.js +1 -1
  97. package/dist/connection/viewer/VersionDropdown.js.map +1 -1
  98. package/dist/connection/viewer/ViewerPdfPane.js +2 -0
  99. package/dist/connection/viewer/ViewerPdfPane.js.map +2 -2
  100. package/dist/connection/viewer/pdf/usePassageTextFallback.d.ts +12 -0
  101. package/dist/connection/viewer/pdf/usePassageTextFallback.js +30 -0
  102. package/dist/connection/viewer/pdf/usePassageTextFallback.js.map +7 -0
  103. package/dist/connection/viewer/pdf/usePdfPassageHighlights.js +2 -11
  104. package/dist/connection/viewer/pdf/usePdfPassageHighlights.js.map +2 -2
  105. package/dist/connection/viewer/useCleanView.d.ts +1 -1
  106. package/dist/connection/viewer/useCleanView.js +2 -2
  107. package/dist/connection/viewer/useCleanView.js.map +2 -2
  108. package/dist/connection/viewer/useViewedDocument.d.ts +1 -0
  109. package/dist/connection/viewer/useViewerTabs.js +13 -1
  110. package/dist/connection/viewer/useViewerTabs.js.map +2 -2
  111. package/dist/docx/DocxCommandMenu.js +2 -1
  112. package/dist/docx/DocxCommandMenu.js.map +2 -2
  113. package/dist/docx/NativeDocxEditor.d.ts +2 -1
  114. package/dist/docx/NativeDocxEditor.js +30 -5
  115. package/dist/docx/NativeDocxEditor.js.map +2 -2
  116. package/dist/docx/index.cjs +38 -38
  117. package/dist/embed/index.cjs +2 -2
  118. package/dist/facts/index.cjs +43 -43
  119. package/dist/index.cjs +73 -73
  120. package/dist/presentation/ClipCaptureOverlay.js +14 -1
  121. package/dist/presentation/ClipCaptureOverlay.js.map +2 -2
  122. package/dist/presentation/ClipImage.js +7 -4
  123. package/dist/presentation/ClipImage.js.map +2 -2
  124. package/dist/presentation/ClipTray.js +11 -7
  125. package/dist/presentation/ClipTray.js.map +2 -2
  126. package/dist/presentation/SlidesEditor.js +6 -2
  127. package/dist/presentation/SlidesEditor.js.map +2 -2
  128. package/dist/presentation/store.d.ts +2 -0
  129. package/dist/presentation/store.js +15 -0
  130. package/dist/presentation/store.js.map +3 -3
  131. package/dist/settings/index.cjs +1 -1
  132. package/dist/studio/index.cjs +1 -1
  133. package/dist/styles.css +1 -1
  134. package/dist/workflow-canvas/index.cjs +12 -12
  135. package/node_modules/@arbidocs/coedit/package.json +1 -1
  136. package/package.json +5 -5
  137. package/dist/ClippingMediaViewer-ZFEX5A3U.cjs.map +0 -1
  138. package/dist/ClippingPdfViewer-3LRTEPHV.cjs.map +0 -1
  139. package/dist/ConnectedPresentation-OV2KBOLK.cjs.map +0 -1
  140. package/dist/ConnectedSlidesTab-LUAGZ6FD.cjs.map +0 -1
  141. package/dist/FilesViewerDialog-JWKNJLWG.cjs.map +0 -1
  142. package/dist/SpotlightStrip-ED22UYTM.cjs.map +0 -1
  143. package/dist/chunk-2CSLFAGF.cjs.map +0 -1
  144. package/dist/chunk-7557LDAN.cjs.map +0 -1
  145. package/dist/chunk-BX6G2ZCI.cjs.map +0 -1
  146. package/dist/chunk-GFUOCGY6.cjs +0 -32
  147. package/dist/chunk-GFUOCGY6.cjs.map +0 -1
  148. package/dist/chunk-H236SNEV.cjs.map +0 -1
  149. package/dist/chunk-JY7UUZQG.cjs.map +0 -1
  150. package/dist/chunk-ORNLCBNE.cjs.map +0 -1
  151. package/dist/chunk-SDRYSLOX.cjs +0 -18
  152. package/dist/chunk-ZZZITHGK.cjs.map +0 -1
@@ -24,17 +24,20 @@ function PdfClip({ clip, className }) {
24
24
  const view = canvas?.ownerDocument.defaultView;
25
25
  if (!canvas || !view) return;
26
26
  const measure = () => {
27
- const box = canvas.getBoundingClientRect();
28
27
  const dpr = view.devicePixelRatio || 1;
29
- const width = Math.max(1, Math.round(box.width * dpr));
30
- const height = Math.max(1, Math.round(box.height * dpr));
28
+ const width = Math.max(1, Math.round(canvas.clientWidth * dpr));
29
+ const height = Math.max(1, Math.round(canvas.clientHeight * dpr));
31
30
  setSize((s) => s.width === width && s.height === height ? s : { width, height });
32
31
  };
33
32
  measure();
34
33
  const Observer = view.ResizeObserver ?? ResizeObserver;
35
34
  const observer = new Observer(measure);
36
35
  observer.observe(canvas);
37
- return () => observer.disconnect();
36
+ view.addEventListener("resize", measure);
37
+ return () => {
38
+ observer.disconnect();
39
+ view.removeEventListener("resize", measure);
40
+ };
38
41
  }, []);
39
42
  useLayoutEffect(() => {
40
43
  const canvas = canvasRef.current;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/presentation/ClipImage.tsx"],
4
- "sourcesContent": ["/**\n * A clip as shown on a slide or in the tray.\n *\n * A PDF clip is drawn into a canvas the size of its box in device pixels (measured in whichever\n * window it is in), from a bitmap of its region rendered for that size: as sharp as the screen\n * allows. The canvas remembers what area its bitmap covers, so when the clip is scrolled or\n * resized it is redrawn at once from the bitmap it has (shifted or scaled: cheap), and the sharp\n * rendering follows once the change settles. An older render still running is cancelled; renders\n * across clips are queued a few at a time (`renderPdfRegion`), so the page stays smooth.\n *\n * An image or video clip is its own file, cropped (`MediaClip`).\n */\n\nimport { memo, useEffect, useLayoutEffect, useRef, useState } from 'react'\nimport { closePdfRegionImage, renderPdfRegion, type PdfRegionImage } from '@arbidocs/react/viewers'\nimport { cn } from '@arbidocs/react/ui'\nimport { MediaClip } from './MediaClip'\nimport type { Clip, Rect } from './model'\n\n/** Rendered widths are rounded up to this step, so small size changes reuse the rendering. */\nconst WIDTH_STEP = 160\n/** A new rendering is asked for once the clip has stopped changing for this long. */\nconst SETTLE_MS = 150\n\nexport interface ClipImageProps {\n clip: Clip\n className?: string\n /** On the presented stage: a video clip plays with its sound (the editor's copy is muted). */\n live?: boolean\n}\n\n/** An image or video clip is its own file, cropped; a PDF clip is drawn for its size. */\nexport const ClipImage = memo(function ClipImage({\n clip,\n className,\n live = false,\n}: ClipImageProps) {\n return clip.kind === 'pdf' ? (\n <PdfClip clip={clip} className={className} />\n ) : (\n <MediaClip clip={clip} className={className} live={live} />\n )\n})\n\ninterface Rendered {\n image: PdfRegionImage\n /** The page area the image shows. */\n rect: Rect\n /** Its width in device pixels. */\n width: number\n}\n\nconst sameRect = (a: Rect, b: Rect) => a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h\n\nfunction PdfClip({ clip, className }: Omit<ClipImageProps, 'live'>) {\n const canvasRef = useRef<HTMLCanvasElement>(null)\n // The canvas's size in device pixels.\n const [size, setSize] = useState({ width: 0, height: 0 })\n const [rendered, setRendered] = useState<Rendered | null>(null)\n const [failed, setFailed] = useState(false)\n\n // Size the canvas to its box in its own window's device pixels.\n useEffect(() => {\n const canvas = canvasRef.current\n const view = canvas?.ownerDocument.defaultView\n if (!canvas || !view) return\n const measure = () => {\n const box = canvas.getBoundingClientRect()\n const dpr = view.devicePixelRatio || 1\n const width = Math.max(1, Math.round(box.width * dpr))\n const height = Math.max(1, Math.round(box.height * dpr))\n setSize((s) => (s.width === width && s.height === height ? s : { width, height }))\n }\n measure()\n const Observer = (view as typeof window).ResizeObserver ?? ResizeObserver\n const observer = new Observer(measure)\n observer.observe(canvas)\n return () => observer.disconnect()\n }, [])\n\n // Draw what there is now: the last rendering placed where its area falls in the clip's area.\n useLayoutEffect(() => {\n const canvas = canvasRef.current\n const context = canvas?.getContext('2d')\n if (!canvas || !context || !size.width) return\n if (canvas.width !== size.width) canvas.width = size.width\n if (canvas.height !== size.height) canvas.height = size.height\n context.fillStyle = '#fff'\n context.fillRect(0, 0, size.width, size.height)\n if (!rendered) return\n const { rect } = clip\n const sx = size.width / rect.w\n const sy = size.height / rect.h\n try {\n context.drawImage(\n rendered.image,\n (rendered.rect.x - rect.x) * sx,\n (rendered.rect.y - rect.y) * sy,\n rendered.rect.w * sx,\n rendered.rect.h * sy\n )\n } catch {\n // A rendering already freed: draw it again rather than fail.\n setRendered(null)\n }\n }, [size, rendered, clip])\n\n // Render the region again once the clip has settled, if what is drawn is not sharp or not it.\n const wanted = Math.ceil(size.width / WIDTH_STEP) * WIDTH_STEP\n const stale =\n wanted > 0 &&\n (!rendered ||\n !sameRect(rendered.rect, clip.rect) ||\n wanted > rendered.width ||\n wanted < rendered.width / 2)\n useEffect(() => {\n if (!stale) return\n const controller = new AbortController()\n const rect = clip.rect\n const timer = setTimeout(() => {\n renderPdfRegion({\n file: clip.source,\n page: clip.page,\n rect,\n masks: clip.masks,\n width: wanted,\n signal: controller.signal,\n })\n .then((image) => {\n if (controller.signal.aborted) return closePdfRegionImage(image)\n setFailed(false)\n setRendered({ image, rect, width: wanted })\n })\n .catch(() => !controller.signal.aborted && setFailed(true))\n }, SETTLE_MS)\n return () => {\n clearTimeout(timer)\n controller.abort()\n }\n }, [stale, wanted, clip.source, clip.page, clip.rect, clip.masks])\n\n // A rendering's memory is freed once the next has replaced it on screen. Never in a cleanup: a\n // remount (React's StrictMode replays mount effects) would free the one still in use. One left\n // when the clip goes is reclaimed by the browser with it.\n const previous = useRef<Rendered | null>(null)\n useEffect(() => {\n if (previous.current && previous.current !== rendered) {\n closePdfRegionImage(previous.current.image)\n }\n previous.current = rendered\n }, [rendered])\n\n return (\n <div\n className={cn('relative h-full w-full overflow-hidden bg-white', className)}\n data-testid={`clip-image-${clip.id}`}\n >\n <canvas\n ref={canvasRef}\n className=\"pointer-events-none block h-full w-full\"\n role=\"img\"\n aria-label={clip.caption}\n aria-busy={!rendered}\n />\n {failed && !rendered && (\n <div className=\"absolute inset-0 flex items-center justify-center p-2 text-center text-xs text-neutral-500\">\n {\"Couldn't draw this clip\"}\n </div>\n )}\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * A clip as shown on a slide or in the tray.\n *\n * A PDF clip is drawn into a canvas the size of its box in device pixels (measured in whichever\n * window it is in), from a bitmap of its region rendered for that size: as sharp as the screen\n * allows. The canvas remembers what area its bitmap covers, so when the clip is scrolled or\n * resized it is redrawn at once from the bitmap it has (shifted or scaled: cheap), and the sharp\n * rendering follows once the change settles. An older render still running is cancelled; renders\n * across clips are queued a few at a time (`renderPdfRegion`), so the page stays smooth.\n *\n * An image or video clip is its own file, cropped (`MediaClip`).\n */\n\nimport { memo, useEffect, useLayoutEffect, useRef, useState } from 'react'\nimport { closePdfRegionImage, renderPdfRegion, type PdfRegionImage } from '@arbidocs/react/viewers'\nimport { cn } from '@arbidocs/react/ui'\nimport { MediaClip } from './MediaClip'\nimport type { Clip, Rect } from './model'\n\n/** Rendered widths are rounded up to this step, so small size changes reuse the rendering. */\nconst WIDTH_STEP = 160\n/** A new rendering is asked for once the clip has stopped changing for this long. */\nconst SETTLE_MS = 150\n\nexport interface ClipImageProps {\n clip: Clip\n className?: string\n /** On the presented stage: a video clip plays with its sound (the editor's copy is muted). */\n live?: boolean\n}\n\n/** An image or video clip is its own file, cropped; a PDF clip is drawn for its size. */\nexport const ClipImage = memo(function ClipImage({\n clip,\n className,\n live = false,\n}: ClipImageProps) {\n return clip.kind === 'pdf' ? (\n <PdfClip clip={clip} className={className} />\n ) : (\n <MediaClip clip={clip} className={className} live={live} />\n )\n})\n\ninterface Rendered {\n image: PdfRegionImage\n /** The page area the image shows. */\n rect: Rect\n /** Its width in device pixels. */\n width: number\n}\n\nconst sameRect = (a: Rect, b: Rect) => a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h\n\nfunction PdfClip({ clip, className }: Omit<ClipImageProps, 'live'>) {\n const canvasRef = useRef<HTMLCanvasElement>(null)\n // The canvas's size in device pixels.\n const [size, setSize] = useState({ width: 0, height: 0 })\n const [rendered, setRendered] = useState<Rendered | null>(null)\n const [failed, setFailed] = useState(false)\n\n // Layout dimensions exclude the spotlight's temporary fly-in transform. A transformed\n // bounding box can be thumbnail-sized, and transforms never trigger ResizeObserver again.\n // Render from the PDF at the final layout size in this window's device pixels.\n useEffect(() => {\n const canvas = canvasRef.current\n const view = canvas?.ownerDocument.defaultView\n if (!canvas || !view) return\n const measure = () => {\n const dpr = view.devicePixelRatio || 1\n const width = Math.max(1, Math.round(canvas.clientWidth * dpr))\n const height = Math.max(1, Math.round(canvas.clientHeight * dpr))\n setSize((s) => (s.width === width && s.height === height ? s : { width, height }))\n }\n measure()\n const Observer = (view as typeof window).ResizeObserver ?? ResizeObserver\n const observer = new Observer(measure)\n observer.observe(canvas)\n view.addEventListener('resize', measure)\n return () => {\n observer.disconnect()\n view.removeEventListener('resize', measure)\n }\n }, [])\n\n // Draw what there is now: the last rendering placed where its area falls in the clip's area.\n useLayoutEffect(() => {\n const canvas = canvasRef.current\n const context = canvas?.getContext('2d')\n if (!canvas || !context || !size.width) return\n if (canvas.width !== size.width) canvas.width = size.width\n if (canvas.height !== size.height) canvas.height = size.height\n context.fillStyle = '#fff'\n context.fillRect(0, 0, size.width, size.height)\n if (!rendered) return\n const { rect } = clip\n const sx = size.width / rect.w\n const sy = size.height / rect.h\n try {\n context.drawImage(\n rendered.image,\n (rendered.rect.x - rect.x) * sx,\n (rendered.rect.y - rect.y) * sy,\n rendered.rect.w * sx,\n rendered.rect.h * sy\n )\n } catch {\n // A rendering already freed: draw it again rather than fail.\n setRendered(null)\n }\n }, [size, rendered, clip])\n\n // Render the region again once the clip has settled, if what is drawn is not sharp or not it.\n const wanted = Math.ceil(size.width / WIDTH_STEP) * WIDTH_STEP\n const stale =\n wanted > 0 &&\n (!rendered ||\n !sameRect(rendered.rect, clip.rect) ||\n wanted > rendered.width ||\n wanted < rendered.width / 2)\n useEffect(() => {\n if (!stale) return\n const controller = new AbortController()\n const rect = clip.rect\n const timer = setTimeout(() => {\n renderPdfRegion({\n file: clip.source,\n page: clip.page,\n rect,\n masks: clip.masks,\n width: wanted,\n signal: controller.signal,\n })\n .then((image) => {\n if (controller.signal.aborted) return closePdfRegionImage(image)\n setFailed(false)\n setRendered({ image, rect, width: wanted })\n })\n .catch(() => !controller.signal.aborted && setFailed(true))\n }, SETTLE_MS)\n return () => {\n clearTimeout(timer)\n controller.abort()\n }\n }, [stale, wanted, clip.source, clip.page, clip.rect, clip.masks])\n\n // A rendering's memory is freed once the next has replaced it on screen. Never in a cleanup: a\n // remount (React's StrictMode replays mount effects) would free the one still in use. One left\n // when the clip goes is reclaimed by the browser with it.\n const previous = useRef<Rendered | null>(null)\n useEffect(() => {\n if (previous.current && previous.current !== rendered) {\n closePdfRegionImage(previous.current.image)\n }\n previous.current = rendered\n }, [rendered])\n\n return (\n <div\n className={cn('relative h-full w-full overflow-hidden bg-white', className)}\n data-testid={`clip-image-${clip.id}`}\n >\n <canvas\n ref={canvasRef}\n className=\"pointer-events-none block h-full w-full\"\n role=\"img\"\n aria-label={clip.caption}\n aria-busy={!rendered}\n />\n {failed && !rendered && (\n <div className=\"absolute inset-0 flex items-center justify-center p-2 text-center text-xs text-neutral-500\">\n {\"Couldn't draw this clip\"}\n </div>\n )}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["ClipImage"]
7
7
  }
@@ -18,7 +18,7 @@ function Fitted({ clip, box }) {
18
18
  const size = wide ? { width: box.width, height: box.width / clip.aspect } : { width: box.height * clip.aspect, height: box.height };
19
19
  return /* @__PURE__ */ jsx("div", { className: "pointer-events-none", style: size, children: /* @__PURE__ */ jsx(ClipImage, { clip }) });
20
20
  }
21
- function Label({ clip }) {
21
+ function Label({ clip, number }) {
22
22
  const [editing, setEditing] = useState(false);
23
23
  if (editing) {
24
24
  return /* @__PURE__ */ jsx(
@@ -39,18 +39,22 @@ function Label({ clip }) {
39
39
  }
40
40
  );
41
41
  }
42
- return /* @__PURE__ */ jsx(
42
+ return /* @__PURE__ */ jsxs(
43
43
  "button",
44
44
  {
45
45
  type: "button",
46
46
  onDoubleClick: () => setEditing(true),
47
47
  title: "Double-click to rename",
48
48
  className: "block w-full truncate rounded px-0.5 text-left text-[11px] text-muted-foreground hover:text-foreground",
49
- children: clip.caption || "Untitled"
49
+ children: [
50
+ number,
51
+ ". ",
52
+ clip.caption || "Untitled"
53
+ ]
50
54
  }
51
55
  );
52
56
  }
53
- function TrayTile({ clip, onSlide }) {
57
+ function TrayTile({ clip, onSlide, number }) {
54
58
  const { place, removeClip } = usePresentation.getState();
55
59
  const drag = useClipDrag(clip.id, () => !onSlide && place(clip.id));
56
60
  const itemRef = useRef(null);
@@ -99,7 +103,7 @@ function TrayTile({ clip, onSlide }) {
99
103
  children: /* @__PURE__ */ jsx(X, { className: "size-3" })
100
104
  }
101
105
  ),
102
- /* @__PURE__ */ jsx(Label, { clip }),
106
+ /* @__PURE__ */ jsx(Label, { clip, number }),
103
107
  drag.ghost && createPortal(
104
108
  /* @__PURE__ */ jsx(
105
109
  "div",
@@ -125,12 +129,12 @@ function ClipTray({ start }) {
125
129
  return /* @__PURE__ */ jsx("div", { className: "shrink-0 border-t border-border bg-background", "data-testid": "clip-tray-bar", children: clips.length || start ? /* @__PURE__ */ jsxs(
126
130
  "ul",
127
131
  {
128
- className: "flex items-start gap-2 overflow-x-auto overscroll-x-contain px-3 py-2 [scrollbar-width:none]",
132
+ className: "flex max-h-48 flex-wrap items-start gap-2 overflow-y-auto px-3 py-2 [scrollbar-width:none]",
129
133
  "aria-label": "Clips",
130
134
  "data-testid": "clip-tray",
131
135
  children: [
132
136
  start && /* @__PURE__ */ jsx("li", { className: "shrink-0", children: start }),
133
- clips.map((clip) => /* @__PURE__ */ jsx(TrayTile, { clip, onSlide: onSlide.has(clip.id) }, clip.id))
137
+ clips.map((clip, index) => /* @__PURE__ */ jsx(TrayTile, { clip, number: index + 1, onSlide: onSlide.has(clip.id) }, clip.id))
134
138
  ]
135
139
  }
136
140
  ) : /* @__PURE__ */ jsxs(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/presentation/ClipTray.tsx"],
4
- "sourcesContent": ["/**\n * The clips taken so far, as a strip of tiles along the bottom (the files explorer's tile look).\n * Drag one up onto the slide or onto a slide card, with a mouse, pen or finger, or tap it to add\n * it to this slide; a check marks those already on it; \u00D7 deletes a clip; double-click its label\n * to rename it. The strip scrolls sideways; on a touch screen a sideways swipe scrolls and a drag\n * upwards moves the clip.\n */\n\nimport { useMemo, useRef, useState, type ReactNode } from 'react'\nimport { createPortal } from 'react-dom'\nimport { Check, X } from 'lucide-react'\nimport { cn } from '@arbidocs/react/ui'\nimport { ClipImage } from './ClipImage'\nimport type { Clip } from './model'\nimport { currentSlide, usePresentation } from './store'\nimport { useClipDrag } from './useClipDrag'\n\n/** Tile height (px); its width follows the clip's shape, within these bounds. */\nconst TILE_HEIGHT = 72\nconst TILE_WIDTH = { min: 56, max: 168 }\n\nfunction tileSize(aspect: number, height = TILE_HEIGHT) {\n const width = Math.min(Math.max(height * aspect, TILE_WIDTH.min), TILE_WIDTH.max)\n return { width, height }\n}\n\n/** The clip, fitted in a box, aspect kept. */\nfunction Fitted({ clip, box }: { clip: Clip; box: { width: number; height: number } }) {\n const wide = clip.aspect > box.width / box.height\n const size = wide\n ? { width: box.width, height: box.width / clip.aspect }\n : { width: box.height * clip.aspect, height: box.height }\n return (\n <div className=\"pointer-events-none\" style={size}>\n <ClipImage clip={clip} />\n </div>\n )\n}\n\nfunction Label({ clip }: { clip: Clip }) {\n const [editing, setEditing] = useState(false)\n if (editing) {\n return (\n <input\n autoFocus\n defaultValue={clip.caption}\n aria-label=\"Clip label\"\n className=\"w-full rounded border border-input bg-background px-1 text-[11px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n onBlur={(event) => {\n usePresentation.getState().setCaption(clip.id, event.target.value)\n setEditing(false)\n }}\n onKeyDown={(event) => {\n if (event.key === 'Enter') event.currentTarget.blur()\n if (event.key === 'Escape') setEditing(false)\n }}\n />\n )\n }\n return (\n <button\n type=\"button\"\n onDoubleClick={() => setEditing(true)}\n title=\"Double-click to rename\"\n className=\"block w-full truncate rounded px-0.5 text-left text-[11px] text-muted-foreground hover:text-foreground\"\n >\n {clip.caption || 'Untitled'}\n </button>\n )\n}\n\nfunction TrayTile({ clip, onSlide }: { clip: Clip; onSlide: boolean }) {\n const { place, removeClip } = usePresentation.getState()\n const drag = useClipDrag(clip.id, () => !onSlide && place(clip.id))\n const itemRef = useRef<HTMLLIElement>(null)\n const size = tileSize(clip.aspect)\n return (\n <li\n ref={itemRef}\n className=\"group relative flex shrink-0 flex-col gap-1\"\n style={{ width: size.width }}\n data-testid={`tray-clip-${clip.id}`}\n >\n <div\n className={cn(\n 'relative flex cursor-grab touch-pan-x select-none items-center justify-center overflow-hidden rounded-lg border bg-muted active:cursor-grabbing',\n onSlide ? 'border-primary/40' : 'border-border hover:bg-muted/70',\n drag.ghost && 'opacity-40'\n )}\n style={{ height: size.height }}\n {...drag.handlers}\n title={\n onSlide ? 'On this slide: drag to place it again' : 'Drag onto the slide, or tap to add'\n }\n data-testid=\"tray-clip-handle\"\n >\n <Fitted clip={clip} box={{ width: size.width - 8, height: size.height - 8 }} />\n {onSlide && (\n <span\n className=\"absolute left-1 top-1 flex size-4 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm\"\n aria-label=\"On this slide\"\n >\n <Check className=\"size-3\" />\n </span>\n )}\n </div>\n <button\n type=\"button\"\n className=\"absolute right-1 top-1 flex size-5 items-center justify-center rounded-md bg-background/90 text-muted-foreground opacity-0 shadow-sm backdrop-blur-sm transition-opacity hover:text-destructive focus-visible:opacity-100 group-hover:opacity-100 pointer-coarse:size-11 pointer-coarse:opacity-100\"\n title=\"Delete clip\"\n aria-label={`Delete ${clip.caption || 'clip'}`}\n onClick={() => removeClip(clip.id)}\n >\n <X className=\"size-3\" />\n </button>\n <Label clip={clip} />\n {/* The tile following the pointer while dragging, above everything in its window. */}\n {drag.ghost &&\n createPortal(\n <div\n className=\"pointer-events-none fixed z-[100] flex -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-lg border border-border bg-card p-1 shadow-xl\"\n style={{ left: drag.ghost.x, top: drag.ghost.y }}\n data-testid=\"tray-drag-ghost\"\n >\n <Fitted clip={clip} box={tileSize(clip.aspect, 96)} />\n </div>,\n // The tray's own window (the app's, or the presentation pop-out's).\n itemRef.current?.ownerDocument.body ?? document.body\n )}\n </li>\n )\n}\n\nexport function ClipTray({ start }: { start?: ReactNode }) {\n const all = usePresentation((s) => s.clips)\n // A picture that is part of a slide's design (an imported deck's) is no clip to arrange.\n const clips = useMemo(() => all.filter((clip) => !clip.plain), [all])\n const slide = usePresentation(currentSlide)\n const onSlide = new Set(slide?.items.map((item) => item.clipId))\n return (\n <div className=\"shrink-0 border-t border-border bg-background\" data-testid=\"clip-tray-bar\">\n {clips.length || start ? (\n <ul\n className=\"flex items-start gap-2 overflow-x-auto overscroll-x-contain px-3 py-2 [scrollbar-width:none]\"\n aria-label=\"Clips\"\n data-testid=\"clip-tray\"\n >\n {start && <li className=\"shrink-0\">{start}</li>}\n {clips.map((clip) => (\n <TrayTile key={clip.id} clip={clip} onSlide={onSlide.has(clip.id)} />\n ))}\n </ul>\n ) : (\n <p\n className=\"px-4 py-5 text-center text-xs text-muted-foreground\"\n data-testid=\"clip-tray-empty\"\n >\n Your clips appear here. In a document, choose <strong>Clip</strong> and drag over a page,\n image or video.\n </p>\n )}\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * The clips taken so far, as a strip of tiles along the bottom (the files explorer's tile look).\n * Drag one up onto the slide or onto a slide card, with a mouse, pen or finger, or tap it to add\n * it to this slide; a check marks those already on it; \u00D7 deletes a clip; double-click its label\n * to rename it. The strip scrolls sideways; on a touch screen a sideways swipe scrolls and a drag\n * upwards moves the clip.\n */\n\nimport { useMemo, useRef, useState, type ReactNode } from 'react'\nimport { createPortal } from 'react-dom'\nimport { Check, X } from 'lucide-react'\nimport { cn } from '@arbidocs/react/ui'\nimport { ClipImage } from './ClipImage'\nimport type { Clip } from './model'\nimport { currentSlide, usePresentation } from './store'\nimport { useClipDrag } from './useClipDrag'\n\n/** Tile height (px); its width follows the clip's shape, within these bounds. */\nconst TILE_HEIGHT = 72\nconst TILE_WIDTH = { min: 56, max: 168 }\n\nfunction tileSize(aspect: number, height = TILE_HEIGHT) {\n const width = Math.min(Math.max(height * aspect, TILE_WIDTH.min), TILE_WIDTH.max)\n return { width, height }\n}\n\n/** The clip, fitted in a box, aspect kept. */\nfunction Fitted({ clip, box }: { clip: Clip; box: { width: number; height: number } }) {\n const wide = clip.aspect > box.width / box.height\n const size = wide\n ? { width: box.width, height: box.width / clip.aspect }\n : { width: box.height * clip.aspect, height: box.height }\n return (\n <div className=\"pointer-events-none\" style={size}>\n <ClipImage clip={clip} />\n </div>\n )\n}\n\nfunction Label({ clip, number }: { clip: Clip; number: number }) {\n const [editing, setEditing] = useState(false)\n if (editing) {\n return (\n <input\n autoFocus\n defaultValue={clip.caption}\n aria-label=\"Clip label\"\n className=\"w-full rounded border border-input bg-background px-1 text-[11px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n onBlur={(event) => {\n usePresentation.getState().setCaption(clip.id, event.target.value)\n setEditing(false)\n }}\n onKeyDown={(event) => {\n if (event.key === 'Enter') event.currentTarget.blur()\n if (event.key === 'Escape') setEditing(false)\n }}\n />\n )\n }\n return (\n <button\n type=\"button\"\n onDoubleClick={() => setEditing(true)}\n title=\"Double-click to rename\"\n className=\"block w-full truncate rounded px-0.5 text-left text-[11px] text-muted-foreground hover:text-foreground\"\n >\n {number}. {clip.caption || 'Untitled'}\n </button>\n )\n}\n\nfunction TrayTile({ clip, onSlide, number }: { clip: Clip; onSlide: boolean; number: number }) {\n const { place, removeClip } = usePresentation.getState()\n const drag = useClipDrag(clip.id, () => !onSlide && place(clip.id))\n const itemRef = useRef<HTMLLIElement>(null)\n const size = tileSize(clip.aspect)\n return (\n <li\n ref={itemRef}\n className=\"group relative flex shrink-0 flex-col gap-1\"\n style={{ width: size.width }}\n data-testid={`tray-clip-${clip.id}`}\n >\n <div\n className={cn(\n 'relative flex cursor-grab touch-pan-x select-none items-center justify-center overflow-hidden rounded-lg border bg-muted active:cursor-grabbing',\n onSlide ? 'border-primary/40' : 'border-border hover:bg-muted/70',\n drag.ghost && 'opacity-40'\n )}\n style={{ height: size.height }}\n {...drag.handlers}\n title={\n onSlide ? 'On this slide: drag to place it again' : 'Drag onto the slide, or tap to add'\n }\n data-testid=\"tray-clip-handle\"\n >\n <Fitted clip={clip} box={{ width: size.width - 8, height: size.height - 8 }} />\n {onSlide && (\n <span\n className=\"absolute left-1 top-1 flex size-4 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm\"\n aria-label=\"On this slide\"\n >\n <Check className=\"size-3\" />\n </span>\n )}\n </div>\n <button\n type=\"button\"\n className=\"absolute right-1 top-1 flex size-5 items-center justify-center rounded-md bg-background/90 text-muted-foreground opacity-0 shadow-sm backdrop-blur-sm transition-opacity hover:text-destructive focus-visible:opacity-100 group-hover:opacity-100 pointer-coarse:size-11 pointer-coarse:opacity-100\"\n title=\"Delete clip\"\n aria-label={`Delete ${clip.caption || 'clip'}`}\n onClick={() => removeClip(clip.id)}\n >\n <X className=\"size-3\" />\n </button>\n <Label clip={clip} number={number} />\n {/* The tile following the pointer while dragging, above everything in its window. */}\n {drag.ghost &&\n createPortal(\n <div\n className=\"pointer-events-none fixed z-[100] flex -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-lg border border-border bg-card p-1 shadow-xl\"\n style={{ left: drag.ghost.x, top: drag.ghost.y }}\n data-testid=\"tray-drag-ghost\"\n >\n <Fitted clip={clip} box={tileSize(clip.aspect, 96)} />\n </div>,\n // The tray's own window (the app's, or the presentation pop-out's).\n itemRef.current?.ownerDocument.body ?? document.body\n )}\n </li>\n )\n}\n\nexport function ClipTray({ start }: { start?: ReactNode }) {\n const all = usePresentation((s) => s.clips)\n // A picture that is part of a slide's design (an imported deck's) is no clip to arrange.\n const clips = useMemo(() => all.filter((clip) => !clip.plain), [all])\n const slide = usePresentation(currentSlide)\n const onSlide = new Set(slide?.items.map((item) => item.clipId))\n return (\n <div className=\"shrink-0 border-t border-border bg-background\" data-testid=\"clip-tray-bar\">\n {clips.length || start ? (\n <ul\n className=\"flex max-h-48 flex-wrap items-start gap-2 overflow-y-auto px-3 py-2 [scrollbar-width:none]\"\n aria-label=\"Clips\"\n data-testid=\"clip-tray\"\n >\n {start && <li className=\"shrink-0\">{start}</li>}\n {clips.map((clip, index) => (\n <TrayTile key={clip.id} clip={clip} number={index + 1} onSlide={onSlide.has(clip.id)} />\n ))}\n </ul>\n ) : (\n <p\n className=\"px-4 py-5 text-center text-xs text-muted-foreground\"\n data-testid=\"clip-tray-empty\"\n >\n Your clips appear here. In a document, choose <strong>Clip</strong> and drag over a page,\n image or video.\n </p>\n )}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -61,8 +61,11 @@ const SlideCard = memo(function SlideCard2({
61
61
  return /* @__PURE__ */ jsxs("div", { className: "group relative flex gap-2", "data-testid": `slide-${index}`, children: [
62
62
  /* @__PURE__ */ jsxs("div", { className: "flex w-9 shrink-0 flex-col items-center gap-0.5", children: [
63
63
  /* @__PURE__ */ jsx(
64
- "span",
64
+ "button",
65
65
  {
66
+ type: "button",
67
+ "aria-label": `Show slide ${index + 1}`,
68
+ onClick: () => usePresentation.getState().goTo(index),
66
69
  className: cn(
67
70
  "py-1 text-xs tabular-nums",
68
71
  current ? "font-semibold text-foreground" : "text-muted-foreground"
@@ -273,13 +276,14 @@ function SlidesEditor({
273
276
  if (node) setView(node.ownerDocument.defaultView);
274
277
  }, []);
275
278
  const onSelect = useCallback((index, clipId) => {
279
+ usePresentation.getState().goTo(index);
276
280
  setSelection(clipId ? { index, clipId } : null);
277
281
  }, []);
278
282
  const keySelection = useMemo(
279
283
  () => ({ clipId: selection?.clipId ?? null, clear: () => setSelection(null) }),
280
284
  [selection]
281
285
  );
282
- usePresenterKeys(view, false, keySelection);
286
+ usePresenterKeys(view, presenting, keySelection);
283
287
  useEffect(() => {
284
288
  const card = list.current?.querySelector(`[data-testid="slide-${current}"]`);
285
289
  card?.scrollIntoView?.({ block: "nearest", behavior: "smooth" });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/presentation/SlidesEditor.tsx"],
4
- "sourcesContent": ["/**\n * The presentation editor: its slides one under the other, scrolled like a PDF's pages, each one\n * edited where it is \u2014 clips dragged, resized and reshaped on it, its title and points typed on\n * it. Hovering a slide shows its actions (layout, move up or down, duplicate, delete); between two\n * slides, + inserts one. One small floating bar holds the rest (Design, the host's Save and other\n * actions, Present, \"\u2026\"), so no header takes room from the slides. The clips collected for the\n * presentation wait in the tray along the bottom, to be dragged onto any slide.\n *\n * The same editor shows wherever the host mounts it (the viewer's tab, or its own window).\n */\n\nimport { memo, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'\nimport {\n ArrowDown,\n ArrowUp,\n Circle,\n Copy,\n Minus,\n MoreHorizontal,\n Play,\n Plus,\n RectangleHorizontal,\n Shapes,\n Square,\n Trash2,\n Type,\n} from 'lucide-react'\nimport {\n Button,\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n cn,\n} from '@arbidocs/react/ui'\nimport { ClipTray } from './ClipTray'\nimport { DesignMenu } from './DesignMenu'\nimport { LayoutMenu } from './LayoutMenu'\nimport { Notice } from './Notice'\nimport { PresentationStage } from './PresentationStage'\nimport type { SlideTextEditing } from './SlideDecor'\nimport { usePresentation } from './store'\nimport { newBox, type BoxShape } from './boxes'\nimport { accentColor, isLightBackground } from './theme'\nimport { usePresenterKeys } from './usePresenterKeys'\n\nexport interface SlidesEditorProps {\n /** Present: the host opens the shareable window. */\n onPresent: () => void\n /** In the floating bar, before Present (e.g. Save, pop out). */\n actions?: ReactNode\n /** At the top of the \"\u2026\" menu (e.g. New from template, Save as, Export). */\n menuItems?: ReactNode\n /** First in the clip tray (e.g. Generate image). */\n trayStart?: ReactNode\n onChooseLogo?: () => void\n onEditImage?: (clipId: string) => void\n onCropImage?: (clipId: string) => void\n}\n\ninterface Selection {\n index: number\n clipId: string\n}\n\nconst SlideCard = memo(function SlideCard({\n index,\n count,\n current,\n selected,\n onSelect,\n onEditImage,\n onCropImage,\n}: {\n index: number\n count: number\n current: boolean\n selected: string | null\n onSelect: (index: number, clipId: string | null) => void\n onEditImage?: (clipId: string) => void\n onCropImage?: (clipId: string) => void\n}) {\n const empty = usePresentation((s) => {\n const slide = s.slides[index]\n return (\n !!slide &&\n !slide.items.length &&\n !slide.boxes.length &&\n !slide.points.trim() &&\n slide.layout !== 'title' &&\n slide.layout !== 'section'\n )\n })\n const notes = usePresentation((s) => s.slides[index]?.notes ?? '')\n const textEditing = useMemo<SlideTextEditing>(\n () => ({\n onTitle: (title) => usePresentation.getState().setSlideCaption(title, index),\n onPoints: (points) => usePresentation.getState().setSlidePoints(points, index),\n }),\n [index]\n )\n const select = useCallback((clipId: string | null) => onSelect(index, clipId), [onSelect, index])\n const actions = usePresentation.getState()\n return (\n <div className=\"group relative flex gap-2\" data-testid={`slide-${index}`}>\n {/* The gutter: the slide's number, and its actions on hover (always on a touch screen's\n current slide), clear of the slide itself. */}\n <div className=\"flex w-9 shrink-0 flex-col items-center gap-0.5\">\n <span\n className={cn(\n 'py-1 text-xs tabular-nums',\n current ? 'font-semibold text-foreground' : 'text-muted-foreground'\n )}\n >\n {index + 1}\n </span>\n <div\n className={cn(\n 'flex flex-col items-center gap-0.5 transition-opacity',\n 'opacity-0 focus-within:opacity-100 group-hover:opacity-100',\n current && 'pointer-coarse:opacity-100'\n )}\n data-testid={`slide-actions-${index}`}\n >\n <LayoutMenu index={index} compact />\n {/* The slide's other actions, behind \"\u2026\": moving it, copying it, deleting it. */}\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n size=\"icon\"\n variant=\"ghost\"\n className=\"size-7 text-muted-foreground hover:text-foreground pointer-coarse:size-11\"\n title=\"Slide actions\"\n aria-label={`Slide ${index + 1} actions`}\n data-testid={`slide-menu-${index}`}\n >\n <MoreHorizontal className=\"size-3.5\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\">\n <DropdownMenuItem\n disabled={index === 0}\n onSelect={() => actions.moveSlide(index, index - 1)}\n >\n <ArrowUp className=\"size-4\" />\n <span className=\"ml-2\">Move up</span>\n </DropdownMenuItem>\n <DropdownMenuItem\n disabled={index === count - 1}\n onSelect={() => actions.moveSlide(index, index + 1)}\n >\n <ArrowDown className=\"size-4\" />\n <span className=\"ml-2\">Move down</span>\n </DropdownMenuItem>\n <DropdownMenuItem onSelect={() => actions.duplicateSlide(index)}>\n <Copy className=\"size-4\" />\n <span className=\"ml-2\">Duplicate</span>\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n className=\"text-destructive\"\n onSelect={() => actions.removeSlide(index)}\n >\n <Trash2 className=\"size-4\" />\n <span className=\"ml-2\">Delete slide</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </div>\n <div\n className={cn(\n 'relative aspect-video min-w-0 flex-1 rounded-sm',\n current && 'ring-1 ring-primary/40 ring-offset-4 ring-offset-transparent'\n )}\n >\n <PresentationStage\n editable\n slideIndex={index}\n selected={selected}\n onSelect={select}\n onEditImage={onEditImage}\n onCropImage={onCropImage}\n textEditing={textEditing}\n overlay={\n empty && (\n <div className=\"pointer-events-none absolute inset-x-0 bottom-[8%] flex justify-center\">\n <span className=\"rounded-full bg-black/40 px-3 py-1 text-[1.4cqw] text-white/70\">\n Drag a clip here from the tray\n </span>\n </div>\n )\n }\n />\n {(notes || current) && (\n <textarea\n value={notes}\n rows={notes.split('\\n').length}\n placeholder=\"Speaker notes\"\n onChange={(event) =>\n usePresentation.getState().setSlideNotes(event.target.value, index)\n }\n className=\"mt-2 block w-full resize-none rounded-sm border-0 bg-transparent px-1 py-0.5 text-xs text-muted-foreground placeholder:text-muted-foreground/50 focus:bg-background focus:text-foreground focus:outline-none focus:ring-1 focus:ring-ring\"\n aria-label={`Speaker notes for slide ${index + 1}`}\n data-testid={`slide-notes-${index}`}\n />\n )}\n </div>\n </div>\n )\n})\n\nconst INSERTS: { shape: BoxShape; label: string; icon: ReactNode }[] = [\n { shape: 'text', label: 'Text box', icon: <Type className=\"size-4\" /> },\n { shape: 'rect', label: 'Rectangle', icon: <Square className=\"size-4\" /> },\n {\n shape: 'roundRect',\n label: 'Rounded rectangle',\n icon: <RectangleHorizontal className=\"size-4\" />,\n },\n { shape: 'ellipse', label: 'Ellipse', icon: <Circle className=\"size-4\" /> },\n { shape: 'line', label: 'Line', icon: <Minus className=\"size-4\" /> },\n]\n\n/** Insert a text box or a shape on the current slide; it is selected, ready to type in. */\nfunction InsertMenu({ onInserted }: { onInserted: (index: number, boxId: string) => void }) {\n const insert = (shape: BoxShape) => {\n const state = usePresentation.getState()\n const slide = state.slides[state.current]\n const box = newBox(shape, accentColor(state.theme.accent))\n // On a light slide (an imported deck's), text is dark.\n const dark = isLightBackground(slide?.background)\n const { id, ...rest } = dark\n ? {\n ...box,\n paragraphs: box.paragraphs.map((p) => ({\n ...p,\n runs: p.runs.map((r) => ({ color: '#1f2328', ...r })),\n })),\n }\n : box\n void id\n onInserted(state.current, state.addBox(rest))\n }\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n size=\"icon\"\n variant=\"ghost\"\n className=\"size-8 text-muted-foreground hover:text-foreground pointer-coarse:size-11\"\n title=\"Insert a text box or shape\"\n aria-label=\"Insert\"\n data-testid=\"slides-insert\"\n >\n <Shapes className=\"size-4\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {INSERTS.map(({ shape, label, icon }) => (\n <DropdownMenuItem\n key={shape}\n onSelect={() => insert(shape)}\n data-testid={`slides-insert-${shape}`}\n >\n {icon}\n <span className=\"ml-2\">{label}</span>\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/** Between two slides (and after the last): + inserts a slide there, shown on hover. */\nfunction InsertSlide({ after, always }: { after: number; always?: boolean }) {\n return (\n <div className=\"group/insert relative flex h-8 items-center justify-center pl-11\">\n <button\n type=\"button\"\n onClick={() => usePresentation.getState().addSlide(undefined, after)}\n className={cn(\n 'flex items-center gap-1 rounded-full border border-dashed border-border bg-background px-2.5 py-0.5 text-xs text-muted-foreground transition-opacity hover:border-solid hover:text-foreground focus-visible:opacity-100',\n !always && 'opacity-0 group-hover/insert:opacity-100 pointer-coarse:opacity-100'\n )}\n aria-label={`New slide after slide ${after + 1}`}\n data-testid={`slide-insert-${after}`}\n >\n <Plus className=\"size-3\" />\n Slide\n </button>\n </div>\n )\n}\n\nexport function SlidesEditor({\n onPresent,\n actions,\n menuItems,\n trayStart,\n onChooseLogo,\n onEditImage,\n onCropImage,\n}: SlidesEditorProps) {\n const ids = usePresentation((s) => s.slides.map((slide) => slide.id).join(' '))\n const slideIds = useMemo(() => ids.split(' '), [ids])\n const current = usePresentation((s) => s.current)\n const captions = usePresentation((s) => s.captions)\n // Presenting in the presentation's own window: Present becomes Stop presenting.\n const presenting = usePresentation((s) => s.presenting)\n const [selection, setSelection] = useState<Selection | null>(null)\n const [view, setView] = useState<Window | null>(null)\n const list = useRef<HTMLDivElement>(null)\n const rootRef = useCallback((node: HTMLDivElement | null) => {\n if (node) setView(node.ownerDocument.defaultView)\n }, [])\n\n const onSelect = useCallback((index: number, clipId: string | null) => {\n setSelection(clipId ? { index, clipId } : null)\n }, [])\n const keySelection = useMemo(\n () => ({ clipId: selection?.clipId ?? null, clear: () => setSelection(null) }),\n [selection]\n )\n usePresenterKeys(view, false, keySelection)\n\n // A slide added, duplicated or moved becomes the current one: brought into view.\n useEffect(() => {\n const card = list.current?.querySelector(`[data-testid=\"slide-${current}\"]`)\n card?.scrollIntoView?.({ block: 'nearest', behavior: 'smooth' })\n }, [current, slideIds.length])\n\n return (\n <div\n ref={rootRef}\n className=\"relative flex h-full min-h-0 flex-col bg-muted/30\"\n data-testid=\"slides-editor\"\n >\n <div ref={list} className=\"min-h-0 flex-1 overflow-y-auto\">\n <div className=\"mx-auto flex max-w-[1100px] flex-col pb-6 pl-2 pr-6 pt-14\">\n {slideIds.map((id, index) => (\n <div key={id}>\n <SlideCard\n index={index}\n count={slideIds.length}\n current={index === current}\n selected={selection?.index === index ? selection.clipId : null}\n onSelect={onSelect}\n onEditImage={onEditImage}\n onCropImage={onCropImage}\n />\n <InsertSlide after={index} always={index === slideIds.length - 1} />\n </div>\n ))}\n </div>\n </div>\n\n {/* The rest, in one small floating bar: no header row. */}\n <div\n className=\"absolute right-4 top-2.5 z-40 flex items-center gap-0.5 rounded-full border border-border bg-background p-1 shadow-md\"\n data-testid=\"slides-toolbar\"\n >\n <DesignMenu onChooseLogo={onChooseLogo} />\n <InsertMenu onInserted={(index, boxId) => setSelection({ index, clipId: boxId })} />\n {actions}\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n size=\"icon\"\n variant=\"ghost\"\n className=\"size-8 text-muted-foreground hover:text-foreground pointer-coarse:size-11\"\n title=\"More\"\n aria-label=\"More\"\n >\n <MoreHorizontal className=\"size-4\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {menuItems}\n {menuItems && <DropdownMenuSeparator />}\n <DropdownMenuCheckboxItem\n checked={captions}\n onCheckedChange={(on) => usePresentation.getState().setCaptions(!!on)}\n >\n Show source labels\n </DropdownMenuCheckboxItem>\n </DropdownMenuContent>\n </DropdownMenu>\n {presenting ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"destructive\"\n className=\"ml-0.5 h-8 rounded-full\"\n onClick={() => usePresentation.getState().setPresenting(false)}\n title=\"Stop presenting (the presentation window closes)\"\n data-testid=\"presentation-stop-button\"\n >\n <Square className=\"size-3 fill-current\" />\n Stop presenting\n </Button>\n ) : (\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"ml-0.5 h-8 rounded-full\"\n onClick={() => {\n setSelection(null)\n onPresent()\n }}\n title=\"Present: the slides alone, in a window to share or put full screen\"\n data-testid=\"presentation-present-button\"\n >\n <Play className=\"size-3.5\" />\n Present\n </Button>\n )}\n </div>\n\n <ClipTray start={trayStart} />\n <Notice />\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * The presentation editor: its slides one under the other, scrolled like a PDF's pages, each one\n * edited where it is \u2014 clips dragged, resized and reshaped on it, its title and points typed on\n * it. Hovering a slide shows its actions (layout, move up or down, duplicate, delete); between two\n * slides, + inserts one. One small floating bar holds the rest (Design, the host's Save and other\n * actions, Present, \"\u2026\"), so no header takes room from the slides. The clips collected for the\n * presentation wait in the tray along the bottom, to be dragged onto any slide.\n *\n * The same editor shows wherever the host mounts it (the viewer's tab, or its own window).\n */\n\nimport { memo, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'\nimport {\n ArrowDown,\n ArrowUp,\n Circle,\n Copy,\n Minus,\n MoreHorizontal,\n Play,\n Plus,\n RectangleHorizontal,\n Shapes,\n Square,\n Trash2,\n Type,\n} from 'lucide-react'\nimport {\n Button,\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n cn,\n} from '@arbidocs/react/ui'\nimport { ClipTray } from './ClipTray'\nimport { DesignMenu } from './DesignMenu'\nimport { LayoutMenu } from './LayoutMenu'\nimport { Notice } from './Notice'\nimport { PresentationStage } from './PresentationStage'\nimport type { SlideTextEditing } from './SlideDecor'\nimport { usePresentation } from './store'\nimport { newBox, type BoxShape } from './boxes'\nimport { accentColor, isLightBackground } from './theme'\nimport { usePresenterKeys } from './usePresenterKeys'\n\nexport interface SlidesEditorProps {\n /** Present: the host opens the shareable window. */\n onPresent: () => void\n /** In the floating bar, before Present (e.g. Save, pop out). */\n actions?: ReactNode\n /** At the top of the \"\u2026\" menu (e.g. New from template, Save as, Export). */\n menuItems?: ReactNode\n /** First in the clip tray (e.g. Generate image). */\n trayStart?: ReactNode\n onChooseLogo?: () => void\n onEditImage?: (clipId: string) => void\n onCropImage?: (clipId: string) => void\n}\n\ninterface Selection {\n index: number\n clipId: string\n}\n\nconst SlideCard = memo(function SlideCard({\n index,\n count,\n current,\n selected,\n onSelect,\n onEditImage,\n onCropImage,\n}: {\n index: number\n count: number\n current: boolean\n selected: string | null\n onSelect: (index: number, clipId: string | null) => void\n onEditImage?: (clipId: string) => void\n onCropImage?: (clipId: string) => void\n}) {\n const empty = usePresentation((s) => {\n const slide = s.slides[index]\n return (\n !!slide &&\n !slide.items.length &&\n !slide.boxes.length &&\n !slide.points.trim() &&\n slide.layout !== 'title' &&\n slide.layout !== 'section'\n )\n })\n const notes = usePresentation((s) => s.slides[index]?.notes ?? '')\n const textEditing = useMemo<SlideTextEditing>(\n () => ({\n onTitle: (title) => usePresentation.getState().setSlideCaption(title, index),\n onPoints: (points) => usePresentation.getState().setSlidePoints(points, index),\n }),\n [index]\n )\n const select = useCallback((clipId: string | null) => onSelect(index, clipId), [onSelect, index])\n const actions = usePresentation.getState()\n return (\n <div className=\"group relative flex gap-2\" data-testid={`slide-${index}`}>\n {/* The gutter: the slide's number, and its actions on hover (always on a touch screen's\n current slide), clear of the slide itself. */}\n <div className=\"flex w-9 shrink-0 flex-col items-center gap-0.5\">\n <button\n type=\"button\"\n aria-label={`Show slide ${index + 1}`}\n onClick={() => usePresentation.getState().goTo(index)}\n className={cn(\n 'py-1 text-xs tabular-nums',\n current ? 'font-semibold text-foreground' : 'text-muted-foreground'\n )}\n >\n {index + 1}\n </button>\n <div\n className={cn(\n 'flex flex-col items-center gap-0.5 transition-opacity',\n 'opacity-0 focus-within:opacity-100 group-hover:opacity-100',\n current && 'pointer-coarse:opacity-100'\n )}\n data-testid={`slide-actions-${index}`}\n >\n <LayoutMenu index={index} compact />\n {/* The slide's other actions, behind \"\u2026\": moving it, copying it, deleting it. */}\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n size=\"icon\"\n variant=\"ghost\"\n className=\"size-7 text-muted-foreground hover:text-foreground pointer-coarse:size-11\"\n title=\"Slide actions\"\n aria-label={`Slide ${index + 1} actions`}\n data-testid={`slide-menu-${index}`}\n >\n <MoreHorizontal className=\"size-3.5\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\">\n <DropdownMenuItem\n disabled={index === 0}\n onSelect={() => actions.moveSlide(index, index - 1)}\n >\n <ArrowUp className=\"size-4\" />\n <span className=\"ml-2\">Move up</span>\n </DropdownMenuItem>\n <DropdownMenuItem\n disabled={index === count - 1}\n onSelect={() => actions.moveSlide(index, index + 1)}\n >\n <ArrowDown className=\"size-4\" />\n <span className=\"ml-2\">Move down</span>\n </DropdownMenuItem>\n <DropdownMenuItem onSelect={() => actions.duplicateSlide(index)}>\n <Copy className=\"size-4\" />\n <span className=\"ml-2\">Duplicate</span>\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n className=\"text-destructive\"\n onSelect={() => actions.removeSlide(index)}\n >\n <Trash2 className=\"size-4\" />\n <span className=\"ml-2\">Delete slide</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </div>\n <div\n className={cn(\n 'relative aspect-video min-w-0 flex-1 rounded-sm',\n current && 'ring-1 ring-primary/40 ring-offset-4 ring-offset-transparent'\n )}\n >\n <PresentationStage\n editable\n slideIndex={index}\n selected={selected}\n onSelect={select}\n onEditImage={onEditImage}\n onCropImage={onCropImage}\n textEditing={textEditing}\n overlay={\n empty && (\n <div className=\"pointer-events-none absolute inset-x-0 bottom-[8%] flex justify-center\">\n <span className=\"rounded-full bg-black/40 px-3 py-1 text-[1.4cqw] text-white/70\">\n Drag a clip here from the tray\n </span>\n </div>\n )\n }\n />\n {(notes || current) && (\n <textarea\n value={notes}\n rows={notes.split('\\n').length}\n placeholder=\"Speaker notes\"\n onChange={(event) =>\n usePresentation.getState().setSlideNotes(event.target.value, index)\n }\n className=\"mt-2 block w-full resize-none rounded-sm border-0 bg-transparent px-1 py-0.5 text-xs text-muted-foreground placeholder:text-muted-foreground/50 focus:bg-background focus:text-foreground focus:outline-none focus:ring-1 focus:ring-ring\"\n aria-label={`Speaker notes for slide ${index + 1}`}\n data-testid={`slide-notes-${index}`}\n />\n )}\n </div>\n </div>\n )\n})\n\nconst INSERTS: { shape: BoxShape; label: string; icon: ReactNode }[] = [\n { shape: 'text', label: 'Text box', icon: <Type className=\"size-4\" /> },\n { shape: 'rect', label: 'Rectangle', icon: <Square className=\"size-4\" /> },\n {\n shape: 'roundRect',\n label: 'Rounded rectangle',\n icon: <RectangleHorizontal className=\"size-4\" />,\n },\n { shape: 'ellipse', label: 'Ellipse', icon: <Circle className=\"size-4\" /> },\n { shape: 'line', label: 'Line', icon: <Minus className=\"size-4\" /> },\n]\n\n/** Insert a text box or a shape on the current slide; it is selected, ready to type in. */\nfunction InsertMenu({ onInserted }: { onInserted: (index: number, boxId: string) => void }) {\n const insert = (shape: BoxShape) => {\n const state = usePresentation.getState()\n const slide = state.slides[state.current]\n const box = newBox(shape, accentColor(state.theme.accent))\n // On a light slide (an imported deck's), text is dark.\n const dark = isLightBackground(slide?.background)\n const { id, ...rest } = dark\n ? {\n ...box,\n paragraphs: box.paragraphs.map((p) => ({\n ...p,\n runs: p.runs.map((r) => ({ color: '#1f2328', ...r })),\n })),\n }\n : box\n void id\n onInserted(state.current, state.addBox(rest))\n }\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n size=\"icon\"\n variant=\"ghost\"\n className=\"size-8 text-muted-foreground hover:text-foreground pointer-coarse:size-11\"\n title=\"Insert a text box or shape\"\n aria-label=\"Insert\"\n data-testid=\"slides-insert\"\n >\n <Shapes className=\"size-4\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {INSERTS.map(({ shape, label, icon }) => (\n <DropdownMenuItem\n key={shape}\n onSelect={() => insert(shape)}\n data-testid={`slides-insert-${shape}`}\n >\n {icon}\n <span className=\"ml-2\">{label}</span>\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/** Between two slides (and after the last): + inserts a slide there, shown on hover. */\nfunction InsertSlide({ after, always }: { after: number; always?: boolean }) {\n return (\n <div className=\"group/insert relative flex h-8 items-center justify-center pl-11\">\n <button\n type=\"button\"\n onClick={() => usePresentation.getState().addSlide(undefined, after)}\n className={cn(\n 'flex items-center gap-1 rounded-full border border-dashed border-border bg-background px-2.5 py-0.5 text-xs text-muted-foreground transition-opacity hover:border-solid hover:text-foreground focus-visible:opacity-100',\n !always && 'opacity-0 group-hover/insert:opacity-100 pointer-coarse:opacity-100'\n )}\n aria-label={`New slide after slide ${after + 1}`}\n data-testid={`slide-insert-${after}`}\n >\n <Plus className=\"size-3\" />\n Slide\n </button>\n </div>\n )\n}\n\nexport function SlidesEditor({\n onPresent,\n actions,\n menuItems,\n trayStart,\n onChooseLogo,\n onEditImage,\n onCropImage,\n}: SlidesEditorProps) {\n const ids = usePresentation((s) => s.slides.map((slide) => slide.id).join(' '))\n const slideIds = useMemo(() => ids.split(' '), [ids])\n const current = usePresentation((s) => s.current)\n const captions = usePresentation((s) => s.captions)\n // Presenting in the presentation's own window: Present becomes Stop presenting.\n const presenting = usePresentation((s) => s.presenting)\n const [selection, setSelection] = useState<Selection | null>(null)\n const [view, setView] = useState<Window | null>(null)\n const list = useRef<HTMLDivElement>(null)\n const rootRef = useCallback((node: HTMLDivElement | null) => {\n if (node) setView(node.ownerDocument.defaultView)\n }, [])\n\n const onSelect = useCallback((index: number, clipId: string | null) => {\n usePresentation.getState().goTo(index)\n setSelection(clipId ? { index, clipId } : null)\n }, [])\n const keySelection = useMemo(\n () => ({ clipId: selection?.clipId ?? null, clear: () => setSelection(null) }),\n [selection]\n )\n usePresenterKeys(view, presenting, keySelection)\n\n // A slide added, duplicated or moved becomes the current one: brought into view.\n useEffect(() => {\n const card = list.current?.querySelector(`[data-testid=\"slide-${current}\"]`)\n card?.scrollIntoView?.({ block: 'nearest', behavior: 'smooth' })\n }, [current, slideIds.length])\n\n return (\n <div\n ref={rootRef}\n className=\"relative flex h-full min-h-0 flex-col bg-muted/30\"\n data-testid=\"slides-editor\"\n >\n <div ref={list} className=\"min-h-0 flex-1 overflow-y-auto\">\n <div className=\"mx-auto flex max-w-[1100px] flex-col pb-6 pl-2 pr-6 pt-14\">\n {slideIds.map((id, index) => (\n <div key={id}>\n <SlideCard\n index={index}\n count={slideIds.length}\n current={index === current}\n selected={selection?.index === index ? selection.clipId : null}\n onSelect={onSelect}\n onEditImage={onEditImage}\n onCropImage={onCropImage}\n />\n <InsertSlide after={index} always={index === slideIds.length - 1} />\n </div>\n ))}\n </div>\n </div>\n\n {/* The rest, in one small floating bar: no header row. */}\n <div\n className=\"absolute right-4 top-2.5 z-40 flex items-center gap-0.5 rounded-full border border-border bg-background p-1 shadow-md\"\n data-testid=\"slides-toolbar\"\n >\n <DesignMenu onChooseLogo={onChooseLogo} />\n <InsertMenu onInserted={(index, boxId) => setSelection({ index, clipId: boxId })} />\n {actions}\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n size=\"icon\"\n variant=\"ghost\"\n className=\"size-8 text-muted-foreground hover:text-foreground pointer-coarse:size-11\"\n title=\"More\"\n aria-label=\"More\"\n >\n <MoreHorizontal className=\"size-4\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {menuItems}\n {menuItems && <DropdownMenuSeparator />}\n <DropdownMenuCheckboxItem\n checked={captions}\n onCheckedChange={(on) => usePresentation.getState().setCaptions(!!on)}\n >\n Show source labels\n </DropdownMenuCheckboxItem>\n </DropdownMenuContent>\n </DropdownMenu>\n {presenting ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"destructive\"\n className=\"ml-0.5 h-8 rounded-full\"\n onClick={() => usePresentation.getState().setPresenting(false)}\n title=\"Stop presenting (the presentation window closes)\"\n data-testid=\"presentation-stop-button\"\n >\n <Square className=\"size-3 fill-current\" />\n Stop presenting\n </Button>\n ) : (\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"ml-0.5 h-8 rounded-full\"\n onClick={() => {\n setSelection(null)\n onPresent()\n }}\n title=\"Present: the slides alone, in a window to share or put full screen\"\n data-testid=\"presentation-present-button\"\n >\n <Play className=\"size-3.5\" />\n Present\n </Button>\n )}\n </div>\n\n <ClipTray start={trayStart} />\n <Notice />\n </div>\n )\n}\n"],
5
+ "mappings": ";AA8GQ,cAoCM,YApCN;AAnGR,SAAS,MAAM,aAAa,WAAW,SAAS,QAAQ,gBAAgC;AACxF;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB;AAC3B,SAAS,cAAc;AACvB,SAAS,yBAAyB;AAElC,SAAS,uBAAuB;AAChC,SAAS,cAA6B;AACtC,SAAS,aAAa,yBAAyB;AAC/C,SAAS,wBAAwB;AAqBjC,MAAM,YAAY,KAAK,SAASA,WAAU;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,QAAM,QAAQ,gBAAgB,CAAC,MAAM;AACnC,UAAM,QAAQ,EAAE,OAAO,KAAK;AAC5B,WACE,CAAC,CAAC,SACF,CAAC,MAAM,MAAM,UACb,CAAC,MAAM,MAAM,UACb,CAAC,MAAM,OAAO,KAAK,KACnB,MAAM,WAAW,WACjB,MAAM,WAAW;AAAA,EAErB,CAAC;AACD,QAAM,QAAQ,gBAAgB,CAAC,MAAM,EAAE,OAAO,KAAK,GAAG,SAAS,EAAE;AACjE,QAAM,cAAc;AAAA,IAClB,OAAO;AAAA,MACL,SAAS,CAAC,UAAU,gBAAgB,SAAS,EAAE,gBAAgB,OAAO,KAAK;AAAA,MAC3E,UAAU,CAAC,WAAW,gBAAgB,SAAS,EAAE,eAAe,QAAQ,KAAK;AAAA,IAC/E;AAAA,IACA,CAAC,KAAK;AAAA,EACR;AACA,QAAM,SAAS,YAAY,CAAC,WAA0B,SAAS,OAAO,MAAM,GAAG,CAAC,UAAU,KAAK,CAAC;AAChG,QAAM,UAAU,gBAAgB,SAAS;AACzC,SACE,qBAAC,SAAI,WAAU,6BAA4B,eAAa,SAAS,KAAK,IAGpE;AAAA,yBAAC,SAAI,WAAU,mDACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,cAAY,cAAc,QAAQ,CAAC;AAAA,UACnC,SAAS,MAAM,gBAAgB,SAAS,EAAE,KAAK,KAAK;AAAA,UACpD,WAAW;AAAA,YACT;AAAA,YACA,UAAU,kCAAkC;AAAA,UAC9C;AAAA,UAEC,kBAAQ;AAAA;AAAA,MACX;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA,WAAW;AAAA,UACb;AAAA,UACA,eAAa,iBAAiB,KAAK;AAAA,UAEnC;AAAA,gCAAC,cAAW,OAAc,SAAO,MAAC;AAAA,YAElC,qBAAC,gBACC;AAAA,kCAAC,uBAAoB,SAAO,MAC1B;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,MAAK;AAAA,kBACL,SAAQ;AAAA,kBACR,WAAU;AAAA,kBACV,OAAM;AAAA,kBACN,cAAY,SAAS,QAAQ,CAAC;AAAA,kBAC9B,eAAa,cAAc,KAAK;AAAA,kBAEhC,8BAAC,kBAAe,WAAU,YAAW;AAAA;AAAA,cACvC,GACF;AAAA,cACA,qBAAC,uBAAoB,OAAM,SACzB;AAAA;AAAA,kBAAC;AAAA;AAAA,oBACC,UAAU,UAAU;AAAA,oBACpB,UAAU,MAAM,QAAQ,UAAU,OAAO,QAAQ,CAAC;AAAA,oBAElD;AAAA,0CAAC,WAAQ,WAAU,UAAS;AAAA,sBAC5B,oBAAC,UAAK,WAAU,QAAO,qBAAO;AAAA;AAAA;AAAA,gBAChC;AAAA,gBACA;AAAA,kBAAC;AAAA;AAAA,oBACC,UAAU,UAAU,QAAQ;AAAA,oBAC5B,UAAU,MAAM,QAAQ,UAAU,OAAO,QAAQ,CAAC;AAAA,oBAElD;AAAA,0CAAC,aAAU,WAAU,UAAS;AAAA,sBAC9B,oBAAC,UAAK,WAAU,QAAO,uBAAS;AAAA;AAAA;AAAA,gBAClC;AAAA,gBACA,qBAAC,oBAAiB,UAAU,MAAM,QAAQ,eAAe,KAAK,GAC5D;AAAA,sCAAC,QAAK,WAAU,UAAS;AAAA,kBACzB,oBAAC,UAAK,WAAU,QAAO,uBAAS;AAAA,mBAClC;AAAA,gBACA,oBAAC,yBAAsB;AAAA,gBACvB;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAU;AAAA,oBACV,UAAU,MAAM,QAAQ,YAAY,KAAK;AAAA,oBAEzC;AAAA,0CAAC,UAAO,WAAU,UAAS;AAAA,sBAC3B,oBAAC,UAAK,WAAU,QAAO,0BAAY;AAAA;AAAA;AAAA,gBACrC;AAAA,iBACF;AAAA,eACF;AAAA;AAAA;AAAA,MACF;AAAA,OACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA,WAAW;AAAA,QACb;AAAA,QAEA;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,UAAQ;AAAA,cACR,YAAY;AAAA,cACZ;AAAA,cACA,UAAU;AAAA,cACV;AAAA,cACA;AAAA,cACA;AAAA,cACA,SACE,SACE,oBAAC,SAAI,WAAU,0EACb,8BAAC,UAAK,WAAU,kEAAiE,4CAEjF,GACF;AAAA;AAAA,UAGN;AAAA,WACE,SAAS,YACT;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,MAAM,MAAM,MAAM,IAAI,EAAE;AAAA,cACxB,aAAY;AAAA,cACZ,UAAU,CAAC,UACT,gBAAgB,SAAS,EAAE,cAAc,MAAM,OAAO,OAAO,KAAK;AAAA,cAEpE,WAAU;AAAA,cACV,cAAY,2BAA2B,QAAQ,CAAC;AAAA,cAChD,eAAa,eAAe,KAAK;AAAA;AAAA,UACnC;AAAA;AAAA;AAAA,IAEJ;AAAA,KACF;AAEJ,CAAC;AAED,MAAM,UAAiE;AAAA,EACrE,EAAE,OAAO,QAAQ,OAAO,YAAY,MAAM,oBAAC,QAAK,WAAU,UAAS,EAAG;AAAA,EACtE,EAAE,OAAO,QAAQ,OAAO,aAAa,MAAM,oBAAC,UAAO,WAAU,UAAS,EAAG;AAAA,EACzE;AAAA,IACE,OAAO;AAAA,IACP,OAAO;AAAA,IACP,MAAM,oBAAC,uBAAoB,WAAU,UAAS;AAAA,EAChD;AAAA,EACA,EAAE,OAAO,WAAW,OAAO,WAAW,MAAM,oBAAC,UAAO,WAAU,UAAS,EAAG;AAAA,EAC1E,EAAE,OAAO,QAAQ,OAAO,QAAQ,MAAM,oBAAC,SAAM,WAAU,UAAS,EAAG;AACrE;AAGA,SAAS,WAAW,EAAE,WAAW,GAA2D;AAC1F,QAAM,SAAS,CAAC,UAAoB;AAClC,UAAM,QAAQ,gBAAgB,SAAS;AACvC,UAAM,QAAQ,MAAM,OAAO,MAAM,OAAO;AACxC,UAAM,MAAM,OAAO,OAAO,YAAY,MAAM,MAAM,MAAM,CAAC;AAEzD,UAAM,OAAO,kBAAkB,OAAO,UAAU;AAChD,UAAM,EAAE,IAAI,GAAG,KAAK,IAAI,OACpB;AAAA,MACE,GAAG;AAAA,MACH,YAAY,IAAI,WAAW,IAAI,CAAC,OAAO;AAAA,QACrC,GAAG;AAAA,QACH,MAAM,EAAE,KAAK,IAAI,CAAC,OAAO,EAAE,OAAO,WAAW,GAAG,EAAE,EAAE;AAAA,MACtD,EAAE;AAAA,IACJ,IACA;AACJ,SAAK;AACL,eAAW,MAAM,SAAS,MAAM,OAAO,IAAI,CAAC;AAAA,EAC9C;AACA,SACE,qBAAC,gBACC;AAAA,wBAAC,uBAAoB,SAAO,MAC1B;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,WAAU;AAAA,QACV,OAAM;AAAA,QACN,cAAW;AAAA,QACX,eAAY;AAAA,QAEZ,8BAAC,UAAO,WAAU,UAAS;AAAA;AAAA,IAC7B,GACF;AAAA,IACA,oBAAC,uBAAoB,OAAM,OACxB,kBAAQ,IAAI,CAAC,EAAE,OAAO,OAAO,KAAK,MACjC;AAAA,MAAC;AAAA;AAAA,QAEC,UAAU,MAAM,OAAO,KAAK;AAAA,QAC5B,eAAa,iBAAiB,KAAK;AAAA,QAElC;AAAA;AAAA,UACD,oBAAC,UAAK,WAAU,QAAQ,iBAAM;AAAA;AAAA;AAAA,MALzB;AAAA,IAMP,CACD,GACH;AAAA,KACF;AAEJ;AAGA,SAAS,YAAY,EAAE,OAAO,OAAO,GAAwC;AAC3E,SACE,oBAAC,SAAI,WAAU,oEACb;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS,MAAM,gBAAgB,SAAS,EAAE,SAAS,QAAW,KAAK;AAAA,MACnE,WAAW;AAAA,QACT;AAAA,QACA,CAAC,UAAU;AAAA,MACb;AAAA,MACA,cAAY,yBAAyB,QAAQ,CAAC;AAAA,MAC9C,eAAa,gBAAgB,KAAK;AAAA,MAElC;AAAA,4BAAC,QAAK,WAAU,UAAS;AAAA,QAAE;AAAA;AAAA;AAAA,EAE7B,GACF;AAEJ;AAEO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAsB;AACpB,QAAM,MAAM,gBAAgB,CAAC,MAAM,EAAE,OAAO,IAAI,CAAC,UAAU,MAAM,EAAE,EAAE,KAAK,GAAG,CAAC;AAC9E,QAAM,WAAW,QAAQ,MAAM,IAAI,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC;AACpD,QAAM,UAAU,gBAAgB,CAAC,MAAM,EAAE,OAAO;AAChD,QAAM,WAAW,gBAAgB,CAAC,MAAM,EAAE,QAAQ;AAElD,QAAM,aAAa,gBAAgB,CAAC,MAAM,EAAE,UAAU;AACtD,QAAM,CAAC,WAAW,YAAY,IAAI,SAA2B,IAAI;AACjE,QAAM,CAAC,MAAM,OAAO,IAAI,SAAwB,IAAI;AACpD,QAAM,OAAO,OAAuB,IAAI;AACxC,QAAM,UAAU,YAAY,CAAC,SAAgC;AAC3D,QAAI,KAAM,SAAQ,KAAK,cAAc,WAAW;AAAA,EAClD,GAAG,CAAC,CAAC;AAEL,QAAM,WAAW,YAAY,CAAC,OAAe,WAA0B;AACrE,oBAAgB,SAAS,EAAE,KAAK,KAAK;AACrC,iBAAa,SAAS,EAAE,OAAO,OAAO,IAAI,IAAI;AAAA,EAChD,GAAG,CAAC,CAAC;AACL,QAAM,eAAe;AAAA,IACnB,OAAO,EAAE,QAAQ,WAAW,UAAU,MAAM,OAAO,MAAM,aAAa,IAAI,EAAE;AAAA,IAC5E,CAAC,SAAS;AAAA,EACZ;AACA,mBAAiB,MAAM,YAAY,YAAY;AAG/C,YAAU,MAAM;AACd,UAAM,OAAO,KAAK,SAAS,cAAc,uBAAuB,OAAO,IAAI;AAC3E,UAAM,iBAAiB,EAAE,OAAO,WAAW,UAAU,SAAS,CAAC;AAAA,EACjE,GAAG,CAAC,SAAS,SAAS,MAAM,CAAC;AAE7B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,WAAU;AAAA,MACV,eAAY;AAAA,MAEZ;AAAA,4BAAC,SAAI,KAAK,MAAM,WAAU,kCACxB,8BAAC,SAAI,WAAU,6DACZ,mBAAS,IAAI,CAAC,IAAI,UACjB,qBAAC,SACC;AAAA;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA,OAAO,SAAS;AAAA,cAChB,SAAS,UAAU;AAAA,cACnB,UAAU,WAAW,UAAU,QAAQ,UAAU,SAAS;AAAA,cAC1D;AAAA,cACA;AAAA,cACA;AAAA;AAAA,UACF;AAAA,UACA,oBAAC,eAAY,OAAO,OAAO,QAAQ,UAAU,SAAS,SAAS,GAAG;AAAA,aAV1D,EAWV,CACD,GACH,GACF;AAAA,QAGA;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,eAAY;AAAA,YAEZ;AAAA,kCAAC,cAAW,cAA4B;AAAA,cACxC,oBAAC,cAAW,YAAY,CAAC,OAAO,UAAU,aAAa,EAAE,OAAO,QAAQ,MAAM,CAAC,GAAG;AAAA,cACjF;AAAA,cACD,qBAAC,gBACC;AAAA,oCAAC,uBAAoB,SAAO,MAC1B;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,MAAK;AAAA,oBACL,SAAQ;AAAA,oBACR,WAAU;AAAA,oBACV,OAAM;AAAA,oBACN,cAAW;AAAA,oBAEX,8BAAC,kBAAe,WAAU,UAAS;AAAA;AAAA,gBACrC,GACF;AAAA,gBACA,qBAAC,uBAAoB,OAAM,OACxB;AAAA;AAAA,kBACA,aAAa,oBAAC,yBAAsB;AAAA,kBACrC;AAAA,oBAAC;AAAA;AAAA,sBACC,SAAS;AAAA,sBACT,iBAAiB,CAAC,OAAO,gBAAgB,SAAS,EAAE,YAAY,CAAC,CAAC,EAAE;AAAA,sBACrE;AAAA;AAAA,kBAED;AAAA,mBACF;AAAA,iBACF;AAAA,cACC,aACC;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,MAAK;AAAA,kBACL,SAAQ;AAAA,kBACR,WAAU;AAAA,kBACV,SAAS,MAAM,gBAAgB,SAAS,EAAE,cAAc,KAAK;AAAA,kBAC7D,OAAM;AAAA,kBACN,eAAY;AAAA,kBAEZ;AAAA,wCAAC,UAAO,WAAU,uBAAsB;AAAA,oBAAE;AAAA;AAAA;AAAA,cAE5C,IAEA;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,MAAK;AAAA,kBACL,WAAU;AAAA,kBACV,SAAS,MAAM;AACb,iCAAa,IAAI;AACjB,8BAAU;AAAA,kBACZ;AAAA,kBACA,OAAM;AAAA,kBACN,eAAY;AAAA,kBAEZ;AAAA,wCAAC,QAAK,WAAU,YAAW;AAAA,oBAAE;AAAA;AAAA;AAAA,cAE/B;AAAA;AAAA;AAAA,QAEJ;AAAA,QAEA,oBAAC,YAAS,OAAO,WAAW;AAAA,QAC5B,oBAAC,UAAO;AAAA;AAAA;AAAA,EACV;AAEJ;",
6
6
  "names": ["SlideCard"]
7
7
  }
@@ -42,6 +42,7 @@ interface PresentationState {
42
42
  /** The document it is saved as, once saved: saving again adds a version to it. */
43
43
  savedId: string | null;
44
44
  /** Which version of that document this is (as opened or saved), when known. */
45
+ autosave: boolean;
45
46
  version: number | null;
46
47
  /**
47
48
  * The document (or draft) this presentation is edited as, in the viewer: its slides tab shows
@@ -166,6 +167,7 @@ interface PresentationActions {
166
167
  savedId: string | null;
167
168
  /** Its version, when known. */
168
169
  version?: number | null;
170
+ autosave?: boolean;
169
171
  /** Edited as (default `savedId`): an older file opened unsaved is still its own. */
170
172
  boundTo?: string | null;
171
173
  captions: boolean;
@@ -44,6 +44,7 @@ const initialState = () => ({
44
44
  notice: null,
45
45
  title: "Untitled presentation",
46
46
  savedId: null,
47
+ autosave: false,
47
48
  version: null,
48
49
  boundTo: null,
49
50
  clean: null,
@@ -52,6 +53,19 @@ const initialState = () => ({
52
53
  function arranged(slide, clips, look) {
53
54
  const aspects = slide.items.map((item) => aspectOf(clips, item.clipId));
54
55
  const frames = autoLayout(aspects, reserveOf(slide, look));
56
+ const pdfScales = slide.items.flatMap((item, i) => {
57
+ const clip = clips.find((clip2) => clip2.id === item.clipId);
58
+ return clip?.kind === "pdf" ? [frames[i].w / clip.rect.w] : [];
59
+ });
60
+ const pageScale = pdfScales.length ? Math.min(...pdfScales) : null;
61
+ if (pageScale !== null)
62
+ slide.items.forEach((item, i) => {
63
+ const clip = clips.find((clip2) => clip2.id === item.clipId);
64
+ if (clip?.kind !== "pdf") return;
65
+ const old = frames[i];
66
+ const w = clip.rect.w * pageScale;
67
+ frames[i] = { ...old, x: old.x + (old.w - w) / 2, w };
68
+ });
55
69
  return {
56
70
  ...slide,
57
71
  auto: true,
@@ -409,6 +423,7 @@ const usePresentation = create()((set, get) => {
409
423
  title: opened.title,
410
424
  savedId: opened.savedId,
411
425
  version: opened.version ?? null,
426
+ autosave: opened.autosave ?? false,
412
427
  boundTo: opened.boundTo ?? opened.savedId,
413
428
  captions: opened.captions,
414
429
  labelColumn: opened.labelColumn ?? null,