@arbidocs/blocks 0.5.121 → 0.5.123
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ClippingMediaViewer-B6WG7Q64.cjs → ClippingMediaViewer-IEIBT74X.cjs} +8 -8
- package/dist/{ClippingMediaViewer-B6WG7Q64.cjs.map → ClippingMediaViewer-IEIBT74X.cjs.map} +1 -1
- package/dist/{ClippingPdfViewer-6GCVTV3V.cjs → ClippingPdfViewer-OGYLMVCE.cjs} +5 -5
- package/dist/{ClippingPdfViewer-6GCVTV3V.cjs.map → ClippingPdfViewer-OGYLMVCE.cjs.map} +1 -1
- package/dist/{ConnectedCallDock-DC5V5OUJ.cjs → ConnectedCallDock-XJOYVVAH.cjs} +21 -21
- package/dist/{ConnectedCallDock-DC5V5OUJ.cjs.map → ConnectedCallDock-XJOYVVAH.cjs.map} +1 -1
- package/dist/{ConnectedCallRoom-FQLXWTFB.cjs → ConnectedCallRoom-CUDYAPQK.cjs} +14 -14
- package/dist/{ConnectedCallRoom-FQLXWTFB.cjs.map → ConnectedCallRoom-CUDYAPQK.cjs.map} +1 -1
- package/dist/{ConnectedNativeDocxComparison-4Q5S7VLD.cjs → ConnectedNativeDocxComparison-QKHCYD6W.cjs} +15 -15
- package/dist/{ConnectedNativeDocxComparison-4Q5S7VLD.cjs.map → ConnectedNativeDocxComparison-QKHCYD6W.cjs.map} +1 -1
- package/dist/ConnectedPresentation-5DOJ35XL.cjs +321 -0
- package/dist/ConnectedPresentation-5DOJ35XL.cjs.map +1 -0
- package/dist/{ConnectedProductionSetDialog-4EAY6KAZ.cjs → ConnectedProductionSetDialog-LHB4VQWC.cjs} +3 -3
- package/dist/{ConnectedProductionSetDialog-4EAY6KAZ.cjs.map → ConnectedProductionSetDialog-LHB4VQWC.cjs.map} +1 -1
- package/dist/{ConnectedSlidesTab-7QDE2CZD.cjs → ConnectedSlidesTab-7YMEDGUU.cjs} +73 -49
- package/dist/ConnectedSlidesTab-7YMEDGUU.cjs.map +1 -0
- package/dist/{ConnectedStreamedWordArtifact-FFOQJBOI.cjs → ConnectedStreamedWordArtifact-AG62232A.cjs} +15 -15
- package/dist/{ConnectedStreamedWordArtifact-FFOQJBOI.cjs.map → ConnectedStreamedWordArtifact-AG62232A.cjs.map} +1 -1
- package/dist/{DocumentReviewCard-SNWVEZ7S.cjs → DocumentReviewCard-M53XFLI2.cjs} +15 -15
- package/dist/{DocumentReviewCard-SNWVEZ7S.cjs.map → DocumentReviewCard-M53XFLI2.cjs.map} +1 -1
- package/dist/{FilesViewerDialog-IZI63FOV.cjs → FilesViewerDialog-2QCCRWF2.cjs} +50 -49
- package/dist/FilesViewerDialog-2QCCRWF2.cjs.map +1 -0
- package/dist/{WorkspaceTableView-TEIRB6VJ.cjs → WorkspaceTableView-3XYAV4BV.cjs} +8 -8
- package/dist/{WorkspaceTableView-TEIRB6VJ.cjs.map → WorkspaceTableView-3XYAV4BV.cjs.map} +1 -1
- package/dist/agents/index.cjs +296 -296
- package/dist/{ai-KBYNRNZ6.cjs → ai-IEIR7SC2.cjs} +25 -25
- package/dist/ai-IEIR7SC2.cjs.map +1 -0
- package/dist/calls/index.cjs +121 -121
- package/dist/chat/index.cjs +1 -1
- package/dist/{chunk-NLOETJCB.cjs → chunk-2F57IO7S.cjs} +12 -12
- package/dist/{chunk-NLOETJCB.cjs.map → chunk-2F57IO7S.cjs.map} +1 -1
- package/dist/chunk-2RLA6QO2.cjs +174 -0
- package/dist/chunk-2RLA6QO2.cjs.map +1 -0
- package/dist/{chunk-Q6DQGGP4.cjs → chunk-325GVZE6.cjs} +3 -3
- package/dist/{chunk-Q6DQGGP4.cjs.map → chunk-325GVZE6.cjs.map} +1 -1
- package/dist/{chunk-LHWOKOMP.cjs → chunk-46P7IDBX.cjs} +3 -3
- package/dist/chunk-46P7IDBX.cjs.map +1 -0
- package/dist/{chunk-XTR26TNE.cjs → chunk-4TTX6ZNS.cjs} +20 -4
- package/dist/chunk-4TTX6ZNS.cjs.map +1 -0
- package/dist/{chunk-BVAT4EVC.cjs → chunk-5QERK4PK.cjs} +35 -35
- package/dist/{chunk-BVAT4EVC.cjs.map → chunk-5QERK4PK.cjs.map} +1 -1
- package/dist/{chunk-GX2EBMJW.cjs → chunk-6OK7ESTL.cjs} +7 -6
- package/dist/chunk-6OK7ESTL.cjs.map +1 -0
- package/dist/{chunk-C2OICDMI.cjs → chunk-7557LDAN.cjs} +10 -10
- package/dist/{chunk-C2OICDMI.cjs.map → chunk-7557LDAN.cjs.map} +1 -1
- package/dist/{chunk-474GPLCM.cjs → chunk-7L4X6QN2.cjs} +6 -6
- package/dist/{chunk-474GPLCM.cjs.map → chunk-7L4X6QN2.cjs.map} +1 -1
- package/dist/{chunk-52KGMAO5.cjs → chunk-7NLO7ASW.cjs} +15 -15
- package/dist/{chunk-52KGMAO5.cjs.map → chunk-7NLO7ASW.cjs.map} +1 -1
- package/dist/{chunk-H5OLHP2U.cjs → chunk-7OPOPYV6.cjs} +3 -3
- package/dist/{chunk-H5OLHP2U.cjs.map → chunk-7OPOPYV6.cjs.map} +1 -1
- package/dist/{chunk-W6B3RYTX.cjs → chunk-7PN3YENO.cjs} +4 -4
- package/dist/{chunk-W6B3RYTX.cjs.map → chunk-7PN3YENO.cjs.map} +1 -1
- package/dist/{chunk-734NPQNR.cjs → chunk-ADDQKZ5I.cjs} +4 -4
- package/dist/{chunk-734NPQNR.cjs.map → chunk-ADDQKZ5I.cjs.map} +1 -1
- package/dist/{chunk-JQLWKNJO.cjs → chunk-ALK3PUUB.cjs} +4 -4
- package/dist/{chunk-JQLWKNJO.cjs.map → chunk-ALK3PUUB.cjs.map} +1 -1
- package/dist/{chunk-4VZZWYEC.cjs → chunk-BQ5RIWWT.cjs} +3 -3
- package/dist/{chunk-4VZZWYEC.cjs.map → chunk-BQ5RIWWT.cjs.map} +1 -1
- package/dist/{chunk-Q3XJUXID.cjs → chunk-DWMWEALB.cjs} +499 -409
- package/dist/chunk-DWMWEALB.cjs.map +1 -0
- package/dist/{chunk-3SCNXNHI.cjs → chunk-E3L5DB5F.cjs} +5 -5
- package/dist/{chunk-3SCNXNHI.cjs.map → chunk-E3L5DB5F.cjs.map} +1 -1
- package/dist/{chunk-VJ5ZLRBD.cjs → chunk-GFUOCGY6.cjs} +8 -3
- package/dist/chunk-GFUOCGY6.cjs.map +1 -0
- package/dist/{chunk-7VUJGHMY.cjs → chunk-JD2HX2BR.cjs} +272 -156
- package/dist/chunk-JD2HX2BR.cjs.map +1 -0
- package/dist/{chunk-RYVZDFVO.cjs → chunk-JJZOL5MG.cjs} +3 -3
- package/dist/{chunk-RYVZDFVO.cjs.map → chunk-JJZOL5MG.cjs.map} +1 -1
- package/dist/{chunk-QFXILJMJ.cjs → chunk-K4763GD5.cjs} +17 -174
- package/dist/chunk-K4763GD5.cjs.map +1 -0
- package/dist/{chunk-EHP6OH5D.cjs → chunk-KA3F6TIH.cjs} +11 -11
- package/dist/{chunk-EHP6OH5D.cjs.map → chunk-KA3F6TIH.cjs.map} +1 -1
- package/dist/{chunk-UUENH4BJ.cjs → chunk-KXIRVKCJ.cjs} +3 -3
- package/dist/{chunk-UUENH4BJ.cjs.map → chunk-KXIRVKCJ.cjs.map} +1 -1
- package/dist/{chunk-AY6EZSZB.cjs → chunk-L4CIFLCU.cjs} +4 -4
- package/dist/{chunk-AY6EZSZB.cjs.map → chunk-L4CIFLCU.cjs.map} +1 -1
- package/dist/{chunk-4RBQ44RY.cjs → chunk-MLA2BJ2Y.cjs} +28 -28
- package/dist/{chunk-4RBQ44RY.cjs.map → chunk-MLA2BJ2Y.cjs.map} +1 -1
- package/dist/{chunk-DR2AH7JL.cjs → chunk-NG2EE7HR.cjs} +48 -48
- package/dist/{chunk-DR2AH7JL.cjs.map → chunk-NG2EE7HR.cjs.map} +1 -1
- package/dist/{chunk-ENGL7P3I.cjs → chunk-NQML7IUC.cjs} +26 -26
- package/dist/{chunk-ENGL7P3I.cjs.map → chunk-NQML7IUC.cjs.map} +1 -1
- package/dist/{chunk-4NPCDS2X.cjs → chunk-NY3YUCOW.cjs} +3 -3
- package/dist/{chunk-4NPCDS2X.cjs.map → chunk-NY3YUCOW.cjs.map} +1 -1
- package/dist/{chunk-JZEYHDWJ.cjs → chunk-OHHKBURG.cjs} +76 -76
- package/dist/chunk-OHHKBURG.cjs.map +1 -0
- package/dist/{chunk-5534FYXP.cjs → chunk-SDRYSLOX.cjs} +4 -4
- package/dist/{chunk-5534FYXP.cjs.map → chunk-SDRYSLOX.cjs.map} +1 -1
- package/dist/{chunk-2W5E62EY.cjs → chunk-SGYSDABK.cjs} +6 -6
- package/dist/{chunk-2W5E62EY.cjs.map → chunk-SGYSDABK.cjs.map} +1 -1
- package/dist/{chunk-KP5AITQ4.cjs → chunk-SNPK3R3O.cjs} +7 -7
- package/dist/{chunk-KP5AITQ4.cjs.map → chunk-SNPK3R3O.cjs.map} +1 -1
- package/dist/{chunk-EATIFPKP.cjs → chunk-T73BHNQX.cjs} +4 -4
- package/dist/{chunk-EATIFPKP.cjs.map → chunk-T73BHNQX.cjs.map} +1 -1
- package/dist/{chunk-4IOYQLKG.cjs → chunk-TP5PGWIO.cjs} +3 -3
- package/dist/{chunk-4IOYQLKG.cjs.map → chunk-TP5PGWIO.cjs.map} +1 -1
- package/dist/{chunk-GKVE5JGB.cjs → chunk-UFT22LAB.cjs} +3 -3
- package/dist/{chunk-GKVE5JGB.cjs.map → chunk-UFT22LAB.cjs.map} +1 -1
- package/dist/{chunk-FKFQCK3C.cjs → chunk-V6MW7B4B.cjs} +4 -4
- package/dist/{chunk-FKFQCK3C.cjs.map → chunk-V6MW7B4B.cjs.map} +1 -1
- package/dist/{chunk-2PZT3J34.cjs → chunk-VWBYE3XY.cjs} +6 -6
- package/dist/{chunk-2PZT3J34.cjs.map → chunk-VWBYE3XY.cjs.map} +1 -1
- package/dist/{chunk-GQXPNIRE.cjs → chunk-WAV762GP.cjs} +6 -6
- package/dist/{chunk-GQXPNIRE.cjs.map → chunk-WAV762GP.cjs.map} +1 -1
- package/dist/{chunk-5LTIZ44D.cjs → chunk-WFMWKPGY.cjs} +14 -7
- package/dist/chunk-WFMWKPGY.cjs.map +1 -0
- package/dist/{chunk-GPX5VC57.cjs → chunk-WV7BCGCK.cjs} +50 -50
- package/dist/{chunk-GPX5VC57.cjs.map → chunk-WV7BCGCK.cjs.map} +1 -1
- package/dist/{chunk-MZ2KZXNU.cjs → chunk-WXUU3COI.cjs} +3 -3
- package/dist/{chunk-MZ2KZXNU.cjs.map → chunk-WXUU3COI.cjs.map} +1 -1
- package/dist/{chunk-NAJN32MF.cjs → chunk-XF355M7O.cjs} +13 -13
- package/dist/{chunk-NAJN32MF.cjs.map → chunk-XF355M7O.cjs.map} +1 -1
- package/dist/connect/docx.cjs +18 -18
- package/dist/connect/index.cjs +995 -994
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/viewer.cjs +38 -37
- package/dist/connection/conversation/ConnectedMessage.js +1 -1
- package/dist/connection/conversation/ConnectedMessage.js.map +2 -2
- package/dist/connection/presentation/ConnectedSlidesTab.js +39 -15
- package/dist/connection/presentation/ConnectedSlidesTab.js.map +2 -2
- package/dist/connection/presentation/PresentationPill.js +3 -8
- package/dist/connection/presentation/PresentationPill.js.map +2 -2
- package/dist/connection/presentation/requests.d.ts +5 -0
- package/dist/connection/presentation/requests.js +8 -1
- package/dist/connection/presentation/requests.js.map +2 -2
- package/dist/connection/viewer/ConnectedDocumentViewer.js +51 -60
- package/dist/connection/viewer/ConnectedDocumentViewer.js.map +2 -2
- package/dist/connection/viewer/ViewerMoreMenu.d.ts +18 -0
- package/dist/connection/viewer/ViewerMoreMenu.js +45 -0
- package/dist/connection/viewer/ViewerMoreMenu.js.map +7 -0
- package/dist/connection/viewer/ViewerPdfPane.js +4 -1
- package/dist/connection/viewer/ViewerPdfPane.js.map +2 -2
- package/dist/connection/viewer/documentPlace.d.ts +5 -4
- package/dist/connection/viewer/documentPlace.js +2 -2
- package/dist/connection/viewer/documentPlace.js.map +2 -2
- package/dist/connection/viewer/pdf/PdfClipOutlines.d.ts +11 -0
- package/dist/connection/viewer/pdf/PdfClipOutlines.js +42 -0
- package/dist/connection/viewer/pdf/PdfClipOutlines.js.map +7 -0
- package/dist/connection/viewer/pdf/PdfPassageHighlights.js +3 -2
- package/dist/connection/viewer/pdf/PdfPassageHighlights.js.map +2 -2
- package/dist/connection/viewer/pdf/clipOutlines.d.ts +17 -0
- package/dist/connection/viewer/pdf/clipOutlines.js +24 -0
- package/dist/connection/viewer/pdf/clipOutlines.js.map +7 -0
- package/dist/connection/viewer/pdf/passageHighlights.d.ts +2 -0
- package/dist/connection/viewer/pdf/passageHighlights.js +12 -1
- package/dist/connection/viewer/pdf/passageHighlights.js.map +2 -2
- package/dist/connection/viewer/pdf/usePdfClipOutlines.d.ts +7 -0
- package/dist/connection/viewer/pdf/usePdfClipOutlines.js +36 -0
- package/dist/connection/viewer/pdf/usePdfClipOutlines.js.map +7 -0
- package/dist/connection/viewer/pdf/usePdfPassageHighlights.js +11 -2
- package/dist/connection/viewer/pdf/usePdfPassageHighlights.js.map +2 -2
- package/dist/dashboards/index.cjs +1 -1
- package/dist/document-tools/DocumentToolButtons.js +2 -2
- package/dist/document-tools/DocumentToolButtons.js.map +2 -2
- package/dist/document-tools/index.cjs +13 -13
- package/dist/embed/index.cjs +38 -38
- package/dist/files/index.cjs +224 -224
- package/dist/index.cjs +1596 -1595
- package/dist/index.cjs.map +1 -1
- package/dist/presentation/BoxActionBar.d.ts +2 -2
- package/dist/presentation/BoxActionBar.js +62 -30
- package/dist/presentation/BoxActionBar.js.map +2 -2
- package/dist/presentation/ClipActionBar.js +60 -7
- package/dist/presentation/ClipActionBar.js.map +2 -2
- package/dist/presentation/ClipViewControls.d.ts +3 -3
- package/dist/presentation/ClipViewControls.js +59 -64
- package/dist/presentation/ClipViewControls.js.map +2 -2
- package/dist/presentation/PresentControls.d.ts +11 -0
- package/dist/presentation/PresentControls.js +117 -0
- package/dist/presentation/PresentControls.js.map +7 -0
- package/dist/presentation/PresenterView.js +36 -5
- package/dist/presentation/PresenterView.js.map +2 -2
- package/dist/presentation/SlidesEditor.js +73 -43
- package/dist/presentation/SlidesEditor.js.map +2 -2
- package/dist/presentation/StageItem.js +0 -5
- package/dist/presentation/StageItem.js.map +2 -2
- package/dist/presentation/panel-id.d.ts +12 -2
- package/dist/presentation/panel-id.js +6 -1
- package/dist/presentation/panel-id.js.map +2 -2
- package/dist/presentation/slide-layout.d.ts +5 -1
- package/dist/presentation/slide-layout.js +16 -0
- package/dist/presentation/slide-layout.js.map +2 -2
- package/dist/settings/index.cjs +146 -146
- package/dist/shell/PanelPortal.js +13 -4
- package/dist/shell/PanelPortal.js.map +2 -2
- package/dist/shell/index.cjs +92 -91
- package/dist/shell/panel-windows.js +1 -0
- package/dist/shell/panel-windows.js.map +2 -2
- package/dist/studio/index.cjs +28 -28
- package/dist/styles.css +1 -1
- package/dist/workflow-canvas/index.cjs +52 -52
- package/node_modules/@arbidocs/coedit/package.json +1 -1
- package/package.json +5 -5
- package/dist/ConnectedPresentation-4HLDZDWV.cjs +0 -183
- package/dist/ConnectedPresentation-4HLDZDWV.cjs.map +0 -1
- package/dist/ConnectedSlidesTab-7QDE2CZD.cjs.map +0 -1
- package/dist/FilesViewerDialog-IZI63FOV.cjs.map +0 -1
- package/dist/ai-KBYNRNZ6.cjs.map +0 -1
- package/dist/chunk-5LTIZ44D.cjs.map +0 -1
- package/dist/chunk-7VUJGHMY.cjs.map +0 -1
- package/dist/chunk-GX2EBMJW.cjs.map +0 -1
- package/dist/chunk-JZEYHDWJ.cjs.map +0 -1
- package/dist/chunk-LHWOKOMP.cjs.map +0 -1
- package/dist/chunk-Q3XJUXID.cjs.map +0 -1
- package/dist/chunk-QFXILJMJ.cjs.map +0 -1
- package/dist/chunk-VJ5ZLRBD.cjs.map +0 -1
- package/dist/chunk-XTR26TNE.cjs.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/presentation/ClipViewControls.tsx"],
|
|
4
|
-
"sourcesContent": ["/**\n *
|
|
5
|
-
"mappings": ";AA4DI,
|
|
4
|
+
"sourcesContent": ["/**\n * A clip's view controls (in its action bar's View popover, so nothing covers what it shows):\n * arrows scroll what the clip shows over its source (a press steps, holding keeps scrolling; an\n * arrow at the source's edge is off), and a zoom slider zooms about the clip's centre.\n * The clip's frame on the slide does not change; only the part of the source it shows. Presses\n * here never start moving the clip.\n */\n\nimport { useEffect, useRef, type PointerEvent as ReactPointerEvent, type ReactNode } from 'react'\nimport { ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from 'lucide-react'\nimport { cn } from '@arbidocs/react/ui'\nimport { ZoomSlider } from '../components/ZoomSlider'\nimport type { Clip } from './model'\nimport { usePresentation } from './store'\n\n/** One step scrolls this much of what the clip shows. */\nconst PAN_STEP = 0.1\n/** The closest a clip zooms, in percent of how close it was cut. */\nconst MAX_ZOOM = 800\n/** Holding a button repeats it after this long, this often. */\nconst HOLD_DELAY = 350\nconst HOLD_EVERY = 90\n\nconst BUTTON =\n 'flex size-7 items-center justify-center rounded-full border border-border bg-background text-foreground hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-30 pointer-coarse:size-11'\n\n/** A button that acts on press and keeps acting while held. */\nfunction HoldButton({\n label,\n disabled,\n onStep,\n className,\n children,\n}: {\n label: string\n disabled?: boolean\n onStep: () => void\n className?: string\n children: ReactNode\n}) {\n const timer = useRef<ReturnType<typeof setTimeout> | null>(null)\n const stop = () => {\n if (timer.current) clearTimeout(timer.current)\n timer.current = null\n }\n useEffect(() => stop, [])\n useEffect(() => {\n if (disabled) stop()\n }, [disabled])\n const start = (event: ReactPointerEvent) => {\n event.stopPropagation()\n if (disabled || event.button !== 0) return\n onStep()\n const repeat = () => {\n onStep()\n timer.current = setTimeout(repeat, HOLD_EVERY)\n }\n timer.current = setTimeout(repeat, HOLD_DELAY)\n }\n return (\n <button\n type=\"button\"\n className={cn(BUTTON, className)}\n title={label}\n aria-label={label}\n disabled={disabled}\n onPointerDown={start}\n onPointerUp={stop}\n onPointerLeave={stop}\n onPointerCancel={stop}\n // Keyboard: Enter / Space step once.\n onClick={(event) => event.detail === 0 && onStep()}\n >\n {children}\n </button>\n )\n}\n\nexport function ClipViewControls({ clip }: { clip: Clip }) {\n const { panClip, setClipZoom } = usePresentation.getState()\n const { rect } = clip\n const zoomBase = clip.zoomBase ?? rect.w\n // As far out as it goes: the widest area of this shape the source holds.\n const minZoom = Math.min(100, Math.floor((zoomBase / Math.min(1, rect.w / rect.h)) * 100))\n const pan = (dx: number, dy: number) => () => panClip(clip.id, dx * rect.w, dy * rect.h)\n const zoom = Math.round((zoomBase / rect.w) * 100)\n const edge = 1e-6\n return (\n <div\n className=\"flex flex-col items-center gap-2 p-1\"\n // A press here is not a press on the clip (which would start moving it).\n onPointerDown={(event) => event.stopPropagation()}\n data-testid=\"clip-view-controls\"\n >\n {/* Move the view: what the clip shows, over its source (the wheel does the same). */}\n <div className=\"grid grid-cols-3 grid-rows-3 place-items-center gap-0.5\">\n <HoldButton\n label=\"Show more above\"\n disabled={rect.y <= edge}\n onStep={pan(0, -PAN_STEP)}\n className=\"col-start-2 row-start-1\"\n >\n <ChevronUp className=\"size-4\" />\n </HoldButton>\n <HoldButton\n label=\"Show more on the left\"\n disabled={rect.x <= edge}\n onStep={pan(-PAN_STEP, 0)}\n className=\"col-start-1 row-start-2\"\n >\n <ChevronLeft className=\"size-4\" />\n </HoldButton>\n <HoldButton\n label=\"Show more on the right\"\n disabled={rect.x + rect.w >= 1 - edge}\n onStep={pan(PAN_STEP, 0)}\n className=\"col-start-3 row-start-2\"\n >\n <ChevronRight className=\"size-4\" />\n </HoldButton>\n <HoldButton\n label=\"Show more below\"\n disabled={rect.y + rect.h >= 1 - edge}\n onStep={pan(0, PAN_STEP)}\n className=\"col-start-2 row-start-3\"\n >\n <ChevronDown className=\"size-4\" />\n </HoldButton>\n </div>\n {/* Zoom: a slider, from as far out as the source allows to MAX_ZOOM. */}\n <div data-testid=\"clip-zoom-control\">\n <ZoomSlider\n value={zoom}\n min={minZoom}\n max={Math.max(MAX_ZOOM, zoom)}\n zoomOutStep={25}\n onChange={(value) => setClipZoom(clip.id, value)}\n label=\"Zoom\"\n valueLabel={<span data-testid=\"clip-zoom\">{zoom}%</span>}\n className=\"pointer-coarse:h-11\"\n sliderClassName=\"w-28\"\n />\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The presented slides' controls, bottom left: previous and next slide (with where you are),
|
|
3
|
+
* blank, full screen and stop. Like a slideshow's, they show while the pointer moves (or a key is
|
|
4
|
+
* pressed) and fade after a moment, with the pointer, so a shared screen rarely shows them.
|
|
5
|
+
*/
|
|
6
|
+
export interface PresentControlsProps {
|
|
7
|
+
/** The window presented in (full screen is that window's). */
|
|
8
|
+
view: Window | null;
|
|
9
|
+
shown: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function PresentControls({ view, shown }: PresentControlsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronLeft, ChevronRight, EyeOff, Maximize, Square } from "lucide-react";
|
|
4
|
+
import { cn } from "@arbidocs/react/ui";
|
|
5
|
+
import { usePresentation } from "./store.js";
|
|
6
|
+
import { toggleFullscreen } from "./usePresenterKeys.js";
|
|
7
|
+
const CONTROL = "inline-flex size-8 items-center justify-center rounded-full text-white/85 hover:bg-white/15 hover:text-white disabled:opacity-30 disabled:hover:bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/60 pointer-coarse:size-11";
|
|
8
|
+
function Control({
|
|
9
|
+
label,
|
|
10
|
+
onClick,
|
|
11
|
+
disabled,
|
|
12
|
+
children,
|
|
13
|
+
testId
|
|
14
|
+
}) {
|
|
15
|
+
return /* @__PURE__ */ jsx(
|
|
16
|
+
"button",
|
|
17
|
+
{
|
|
18
|
+
type: "button",
|
|
19
|
+
className: CONTROL,
|
|
20
|
+
title: label,
|
|
21
|
+
"aria-label": label,
|
|
22
|
+
disabled,
|
|
23
|
+
onClick: (event) => {
|
|
24
|
+
event.stopPropagation();
|
|
25
|
+
onClick();
|
|
26
|
+
},
|
|
27
|
+
"data-testid": testId,
|
|
28
|
+
children
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
function PresentControls({ view, shown }) {
|
|
33
|
+
const current = usePresentation((s) => s.current);
|
|
34
|
+
const count = usePresentation((s) => s.slides.length);
|
|
35
|
+
const blank = usePresentation((s) => s.blank);
|
|
36
|
+
const actions = usePresentation.getState();
|
|
37
|
+
return /* @__PURE__ */ jsxs(
|
|
38
|
+
"div",
|
|
39
|
+
{
|
|
40
|
+
className: cn(
|
|
41
|
+
"absolute bottom-4 left-4 z-30 flex items-center gap-0.5 rounded-full bg-black/60 p-1 text-white shadow-lg backdrop-blur transition-opacity duration-300",
|
|
42
|
+
shown ? "opacity-100" : "pointer-events-none opacity-0"
|
|
43
|
+
),
|
|
44
|
+
onPointerDown: (event) => event.stopPropagation(),
|
|
45
|
+
onDoubleClick: (event) => event.stopPropagation(),
|
|
46
|
+
role: "toolbar",
|
|
47
|
+
"aria-label": "Slideshow",
|
|
48
|
+
"data-testid": "present-controls",
|
|
49
|
+
children: [
|
|
50
|
+
/* @__PURE__ */ jsx(
|
|
51
|
+
Control,
|
|
52
|
+
{
|
|
53
|
+
label: "Previous slide (\u2190)",
|
|
54
|
+
onClick: () => actions.goTo(current - 1),
|
|
55
|
+
disabled: current === 0,
|
|
56
|
+
testId: "present-previous",
|
|
57
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4" })
|
|
58
|
+
}
|
|
59
|
+
),
|
|
60
|
+
/* @__PURE__ */ jsxs(
|
|
61
|
+
"span",
|
|
62
|
+
{
|
|
63
|
+
className: "min-w-12 text-center text-xs tabular-nums text-white/85",
|
|
64
|
+
"data-testid": "present-position",
|
|
65
|
+
children: [
|
|
66
|
+
current + 1,
|
|
67
|
+
" / ",
|
|
68
|
+
count
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
),
|
|
72
|
+
/* @__PURE__ */ jsx(
|
|
73
|
+
Control,
|
|
74
|
+
{
|
|
75
|
+
label: "Next slide (\u2192, or click the slide)",
|
|
76
|
+
onClick: () => actions.goTo(current + 1),
|
|
77
|
+
disabled: current >= count - 1,
|
|
78
|
+
testId: "present-next",
|
|
79
|
+
children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4" })
|
|
80
|
+
}
|
|
81
|
+
),
|
|
82
|
+
/* @__PURE__ */ jsx("div", { className: "mx-1 h-4 w-px bg-white/25" }),
|
|
83
|
+
/* @__PURE__ */ jsx(
|
|
84
|
+
Control,
|
|
85
|
+
{
|
|
86
|
+
label: blank ? "Show the slide (B)" : "Blank the screen (B)",
|
|
87
|
+
onClick: () => actions.setBlank(!blank),
|
|
88
|
+
testId: "present-blank",
|
|
89
|
+
children: /* @__PURE__ */ jsx(EyeOff, { className: cn("size-4", blank && "text-amber-300") })
|
|
90
|
+
}
|
|
91
|
+
),
|
|
92
|
+
/* @__PURE__ */ jsx(
|
|
93
|
+
Control,
|
|
94
|
+
{
|
|
95
|
+
label: "Full screen (F)",
|
|
96
|
+
onClick: () => view && toggleFullscreen(view),
|
|
97
|
+
testId: "present-fullscreen",
|
|
98
|
+
children: /* @__PURE__ */ jsx(Maximize, { className: "size-4" })
|
|
99
|
+
}
|
|
100
|
+
),
|
|
101
|
+
/* @__PURE__ */ jsx(
|
|
102
|
+
Control,
|
|
103
|
+
{
|
|
104
|
+
label: "Stop presenting (Esc)",
|
|
105
|
+
onClick: () => actions.setPresenting(false),
|
|
106
|
+
testId: "present-stop",
|
|
107
|
+
children: /* @__PURE__ */ jsx(Square, { className: "size-3.5 fill-current" })
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
export {
|
|
115
|
+
PresentControls
|
|
116
|
+
};
|
|
117
|
+
//# sourceMappingURL=PresentControls.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/presentation/PresentControls.tsx"],
|
|
4
|
+
"sourcesContent": ["/**\n * The presented slides' controls, bottom left: previous and next slide (with where you are),\n * blank, full screen and stop. Like a slideshow's, they show while the pointer moves (or a key is\n * pressed) and fade after a moment, with the pointer, so a shared screen rarely shows them.\n */\n\nimport type { ReactNode } from 'react'\nimport { ChevronLeft, ChevronRight, EyeOff, Maximize, Square } from 'lucide-react'\nimport { cn } from '@arbidocs/react/ui'\nimport { usePresentation } from './store'\nimport { toggleFullscreen } from './usePresenterKeys'\n\nconst CONTROL =\n 'inline-flex size-8 items-center justify-center rounded-full text-white/85 hover:bg-white/15 hover:text-white disabled:opacity-30 disabled:hover:bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/60 pointer-coarse:size-11'\n\nfunction Control({\n label,\n onClick,\n disabled,\n children,\n testId,\n}: {\n label: string\n onClick: () => void\n disabled?: boolean\n children: ReactNode\n testId: string\n}) {\n return (\n <button\n type=\"button\"\n className={CONTROL}\n title={label}\n aria-label={label}\n disabled={disabled}\n onClick={(event) => {\n // A click on the controls is not a click on the slide (which goes to the next one).\n event.stopPropagation()\n onClick()\n }}\n data-testid={testId}\n >\n {children}\n </button>\n )\n}\n\nexport interface PresentControlsProps {\n /** The window presented in (full screen is that window's). */\n view: Window | null\n shown: boolean\n}\n\nexport function PresentControls({ view, shown }: PresentControlsProps) {\n const current = usePresentation((s) => s.current)\n const count = usePresentation((s) => s.slides.length)\n const blank = usePresentation((s) => s.blank)\n const actions = usePresentation.getState()\n return (\n <div\n className={cn(\n 'absolute bottom-4 left-4 z-30 flex items-center gap-0.5 rounded-full bg-black/60 p-1 text-white shadow-lg backdrop-blur transition-opacity duration-300',\n shown ? 'opacity-100' : 'pointer-events-none opacity-0'\n )}\n onPointerDown={(event) => event.stopPropagation()}\n onDoubleClick={(event) => event.stopPropagation()}\n role=\"toolbar\"\n aria-label=\"Slideshow\"\n data-testid=\"present-controls\"\n >\n <Control\n label=\"Previous slide (\u2190)\"\n onClick={() => actions.goTo(current - 1)}\n disabled={current === 0}\n testId=\"present-previous\"\n >\n <ChevronLeft className=\"size-4\" />\n </Control>\n <span\n className=\"min-w-12 text-center text-xs tabular-nums text-white/85\"\n data-testid=\"present-position\"\n >\n {current + 1} / {count}\n </span>\n <Control\n label=\"Next slide (\u2192, or click the slide)\"\n onClick={() => actions.goTo(current + 1)}\n disabled={current >= count - 1}\n testId=\"present-next\"\n >\n <ChevronRight className=\"size-4\" />\n </Control>\n <div className=\"mx-1 h-4 w-px bg-white/25\" />\n <Control\n label={blank ? 'Show the slide (B)' : 'Blank the screen (B)'}\n onClick={() => actions.setBlank(!blank)}\n testId=\"present-blank\"\n >\n <EyeOff className={cn('size-4', blank && 'text-amber-300')} />\n </Control>\n <Control\n label=\"Full screen (F)\"\n onClick={() => view && toggleFullscreen(view)}\n testId=\"present-fullscreen\"\n >\n <Maximize className=\"size-4\" />\n </Control>\n <Control\n label=\"Stop presenting (Esc)\"\n onClick={() => actions.setPresenting(false)}\n testId=\"present-stop\"\n >\n <Square className=\"size-3.5 fill-current\" />\n </Control>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AA6BI,cAiDE,YAjDF;AAtBJ,SAAS,aAAa,cAAc,QAAQ,UAAU,cAAc;AACpE,SAAS,UAAU;AACnB,SAAS,uBAAuB;AAChC,SAAS,wBAAwB;AAEjC,MAAM,UACJ;AAEF,SAAS,QAAQ;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,WAAW;AAAA,MACX,OAAO;AAAA,MACP,cAAY;AAAA,MACZ;AAAA,MACA,SAAS,CAAC,UAAU;AAElB,cAAM,gBAAgB;AACtB,gBAAQ;AAAA,MACV;AAAA,MACA,eAAa;AAAA,MAEZ;AAAA;AAAA,EACH;AAEJ;AAQO,SAAS,gBAAgB,EAAE,MAAM,MAAM,GAAyB;AACrE,QAAM,UAAU,gBAAgB,CAAC,MAAM,EAAE,OAAO;AAChD,QAAM,QAAQ,gBAAgB,CAAC,MAAM,EAAE,OAAO,MAAM;AACpD,QAAM,QAAQ,gBAAgB,CAAC,MAAM,EAAE,KAAK;AAC5C,QAAM,UAAU,gBAAgB,SAAS;AACzC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,QAAQ,gBAAgB;AAAA,MAC1B;AAAA,MACA,eAAe,CAAC,UAAU,MAAM,gBAAgB;AAAA,MAChD,eAAe,CAAC,UAAU,MAAM,gBAAgB;AAAA,MAChD,MAAK;AAAA,MACL,cAAW;AAAA,MACX,eAAY;AAAA,MAEZ;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,OAAM;AAAA,YACN,SAAS,MAAM,QAAQ,KAAK,UAAU,CAAC;AAAA,YACvC,UAAU,YAAY;AAAA,YACtB,QAAO;AAAA,YAEP,8BAAC,eAAY,WAAU,UAAS;AAAA;AAAA,QAClC;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,eAAY;AAAA,YAEX;AAAA,wBAAU;AAAA,cAAE;AAAA,cAAI;AAAA;AAAA;AAAA,QACnB;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAM;AAAA,YACN,SAAS,MAAM,QAAQ,KAAK,UAAU,CAAC;AAAA,YACvC,UAAU,WAAW,QAAQ;AAAA,YAC7B,QAAO;AAAA,YAEP,8BAAC,gBAAa,WAAU,UAAS;AAAA;AAAA,QACnC;AAAA,QACA,oBAAC,SAAI,WAAU,6BAA4B;AAAA,QAC3C;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,QAAQ,uBAAuB;AAAA,YACtC,SAAS,MAAM,QAAQ,SAAS,CAAC,KAAK;AAAA,YACtC,QAAO;AAAA,YAEP,8BAAC,UAAO,WAAW,GAAG,UAAU,SAAS,gBAAgB,GAAG;AAAA;AAAA,QAC9D;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAM;AAAA,YACN,SAAS,MAAM,QAAQ,iBAAiB,IAAI;AAAA,YAC5C,QAAO;AAAA,YAEP,8BAAC,YAAS,WAAU,UAAS;AAAA;AAAA,QAC/B;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAM;AAAA,YACN,SAAS,MAAM,QAAQ,cAAc,KAAK;AAAA,YAC1C,QAAO;AAAA,YAEP,8BAAC,UAAO,WAAU,yBAAwB;AAAA;AAAA,QAC5C;AAAA;AAAA;AAAA,EACF;AAEJ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useState } from "react";
|
|
4
4
|
import { PresentationStage } from "./PresentationStage.js";
|
|
5
5
|
import { SlidesEditor } from "./SlidesEditor.js";
|
|
6
6
|
import { STAGE_ASPECT } from "./model.js";
|
|
7
7
|
import { usePresentation } from "./store.js";
|
|
8
8
|
import { toggleFullscreen, usePresenterKeys } from "./usePresenterKeys.js";
|
|
9
|
+
import { PresentControls } from "./PresentControls.js";
|
|
9
10
|
const PRESENT_WIDTH = 1280;
|
|
10
11
|
function useStageShapedWindow(view, presenting) {
|
|
11
12
|
useEffect(() => {
|
|
@@ -34,6 +35,28 @@ function useStageShapedWindow(view, presenting) {
|
|
|
34
35
|
};
|
|
35
36
|
}, [view, presenting]);
|
|
36
37
|
}
|
|
38
|
+
const IDLE_MS = 2200;
|
|
39
|
+
function usePointerActive(view) {
|
|
40
|
+
const [active, setActive] = useState(true);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!view) return;
|
|
43
|
+
setActive(true);
|
|
44
|
+
let timer = view.setTimeout(() => setActive(false), IDLE_MS);
|
|
45
|
+
const wake = () => {
|
|
46
|
+
setActive(true);
|
|
47
|
+
view.clearTimeout(timer);
|
|
48
|
+
timer = view.setTimeout(() => setActive(false), IDLE_MS);
|
|
49
|
+
};
|
|
50
|
+
view.addEventListener("pointermove", wake);
|
|
51
|
+
view.addEventListener("keydown", wake);
|
|
52
|
+
return () => {
|
|
53
|
+
view.clearTimeout(timer);
|
|
54
|
+
view.removeEventListener("pointermove", wake);
|
|
55
|
+
view.removeEventListener("keydown", wake);
|
|
56
|
+
};
|
|
57
|
+
}, [view]);
|
|
58
|
+
return active;
|
|
59
|
+
}
|
|
37
60
|
function PresenterView({ onPresent, ...editor }) {
|
|
38
61
|
const presenting = usePresentation((s) => s.presenting);
|
|
39
62
|
const [view, setView] = useState(null);
|
|
@@ -42,16 +65,24 @@ function PresenterView({ onPresent, ...editor }) {
|
|
|
42
65
|
}, []);
|
|
43
66
|
usePresenterKeys(presenting ? view : null, true);
|
|
44
67
|
useStageShapedWindow(view, presenting);
|
|
68
|
+
const active = usePointerActive(presenting ? view : null);
|
|
45
69
|
if (presenting) {
|
|
46
|
-
return /* @__PURE__ */
|
|
70
|
+
return /* @__PURE__ */ jsxs(
|
|
47
71
|
"div",
|
|
48
72
|
{
|
|
49
73
|
ref: rootRef,
|
|
50
|
-
className: "h-full w-full select-none bg-black",
|
|
51
|
-
style: { cursor: "none" },
|
|
74
|
+
className: "relative h-full w-full select-none bg-black",
|
|
75
|
+
style: { cursor: active ? "default" : "none" },
|
|
76
|
+
onClick: () => {
|
|
77
|
+
const state = usePresentation.getState();
|
|
78
|
+
state.goTo(state.current + 1);
|
|
79
|
+
},
|
|
52
80
|
onDoubleClick: () => view && toggleFullscreen(view),
|
|
53
81
|
"data-testid": "presentation-present",
|
|
54
|
-
children:
|
|
82
|
+
children: [
|
|
83
|
+
/* @__PURE__ */ jsx(PresentationStage, {}),
|
|
84
|
+
/* @__PURE__ */ jsx(PresentControls, { view, shown: active })
|
|
85
|
+
]
|
|
55
86
|
}
|
|
56
87
|
);
|
|
57
88
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/presentation/PresenterView.tsx"],
|
|
4
|
-
"sourcesContent": ["/**\n * The presentation in a window of its own, in one of two modes.\n *\n * Present: nothing but the stage on black, no controls and no pointer: what is shared in a call or\n * shown on a projector. Keys drive it (arrows / Page Up / Page Down change slide, B blanks, F or\n * double-click toggles full screen, Space plays videos, Escape stops presenting).\n *\n * Edit: the slides editor (`SlidesEditor`), the same as in the viewer's tab, when the presentation\n * is edited in its own window (beside the document its clips come from).\n */\n\nimport { useCallback, useEffect, useState } from 'react'\nimport { PresentationStage } from './PresentationStage'\nimport { SlidesEditor, type SlidesEditorProps } from './SlidesEditor'\nimport { STAGE_ASPECT } from './model'\nimport { usePresentation } from './store'\nimport { toggleFullscreen, usePresenterKeys } from './usePresenterKeys'\n\n/** The presenting window's page width, at most (smaller on a small screen). */\nconst PRESENT_WIDTH = 1280\n\n/**\n * While presenting in a pop-out, its page is made 16:9 and wide enough to share sharply, so a\n * shared window is all stage with no bars; editing again gives the window back its size. Only a\n * pop-out (a window this app opened) can be resized; the app's own window is left alone.\n */\nfunction useStageShapedWindow(view: Window | null, presenting: boolean) {\n useEffect(() => {\n if (!view || !presenting || !view.opener) return\n const before = { width: view.outerWidth, height: view.outerHeight }\n const chromeWidth = view.outerWidth - view.innerWidth\n const chromeHeight = view.outerHeight - view.innerHeight\n // Wide enough for a sharp share (a call receives about what the window shows), and 16:9.\n const width = Math.max(\n view.innerWidth,\n Math.min(PRESENT_WIDTH, Math.round((view.screen.availWidth - chromeWidth) * 0.6))\n )\n const height = Math.min(\n Math.round(width / STAGE_ASPECT),\n view.screen.availHeight - chromeHeight\n )\n try {\n view.resizeTo(Math.round(height * STAGE_ASPECT) + chromeWidth, height + chromeHeight)\n } catch {\n return // not resizable: the stage letterboxes in black\n }\n return () => {\n try {\n if (!view.closed) view.resizeTo(before.width, before.height)\n } catch {\n // gone or not resizable\n }\n }\n }, [view, presenting])\n}\n\nexport type PresenterViewProps = Omit<SlidesEditorProps, 'onPresent'> & {\n /** Present (default: here, in this window). */\n onPresent?: () => void\n}\n\nexport function PresenterView({ onPresent, ...editor }: PresenterViewProps) {\n const presenting = usePresentation((s) => s.presenting)\n const [view, setView] = useState<Window | null>(null)\n // The keys work in whichever window the presenter is in.\n const rootRef = useCallback((node: HTMLDivElement | null) => {\n if (node) setView(node.ownerDocument.defaultView)\n }, [])\n usePresenterKeys(presenting ? view : null, true)\n useStageShapedWindow(view, presenting)\n\n if (presenting) {\n return (\n <div\n ref={rootRef}\n className=\"h-full w-full select-none bg-black\"\n style={{ cursor: 'none' }}\n onDoubleClick={() => view && toggleFullscreen(view)}\n data-testid=\"presentation-present\"\n >\n <PresentationStage />\n </div>\n )\n }\n return (\n <div ref={rootRef} className=\"h-full min-h-0\" data-testid=\"presenter-view\">\n <SlidesEditor\n {...editor}\n onPresent={onPresent ?? (() => usePresentation.getState().setPresenting(true))}\n />\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["/**\n * The presentation in a window of its own, in one of two modes.\n *\n * Present: nothing but the stage on black, no controls and no pointer: what is shared in a call or\n * shown on a projector. Keys drive it (arrows / Page Up / Page Down change slide, B blanks, F or\n * double-click toggles full screen, Space plays videos, Escape stops presenting).\n *\n * Edit: the slides editor (`SlidesEditor`), the same as in the viewer's tab, when the presentation\n * is edited in its own window (beside the document its clips come from).\n */\n\nimport { useCallback, useEffect, useState } from 'react'\nimport { PresentationStage } from './PresentationStage'\nimport { SlidesEditor, type SlidesEditorProps } from './SlidesEditor'\nimport { STAGE_ASPECT } from './model'\nimport { usePresentation } from './store'\nimport { toggleFullscreen, usePresenterKeys } from './usePresenterKeys'\nimport { PresentControls } from './PresentControls'\n\n/** The presenting window's page width, at most (smaller on a small screen). */\nconst PRESENT_WIDTH = 1280\n\n/**\n * While presenting in a pop-out, its page is made 16:9 and wide enough to share sharply, so a\n * shared window is all stage with no bars; editing again gives the window back its size. Only a\n * pop-out (a window this app opened) can be resized; the app's own window is left alone.\n */\nfunction useStageShapedWindow(view: Window | null, presenting: boolean) {\n useEffect(() => {\n if (!view || !presenting || !view.opener) return\n const before = { width: view.outerWidth, height: view.outerHeight }\n const chromeWidth = view.outerWidth - view.innerWidth\n const chromeHeight = view.outerHeight - view.innerHeight\n // Wide enough for a sharp share (a call receives about what the window shows), and 16:9.\n const width = Math.max(\n view.innerWidth,\n Math.min(PRESENT_WIDTH, Math.round((view.screen.availWidth - chromeWidth) * 0.6))\n )\n const height = Math.min(\n Math.round(width / STAGE_ASPECT),\n view.screen.availHeight - chromeHeight\n )\n try {\n view.resizeTo(Math.round(height * STAGE_ASPECT) + chromeWidth, height + chromeHeight)\n } catch {\n return // not resizable: the stage letterboxes in black\n }\n return () => {\n try {\n if (!view.closed) view.resizeTo(before.width, before.height)\n } catch {\n // gone or not resizable\n }\n }\n }, [view, presenting])\n}\n\n/** How long the pointer (or a key) has to rest before the controls fade. */\nconst IDLE_MS = 2200\n\n/** The pointer has moved (or a key was pressed) in `view` within the last moment. */\nfunction usePointerActive(view: Window | null): boolean {\n const [active, setActive] = useState(true)\n useEffect(() => {\n if (!view) return\n setActive(true)\n let timer = view.setTimeout(() => setActive(false), IDLE_MS)\n const wake = () => {\n setActive(true)\n view.clearTimeout(timer)\n timer = view.setTimeout(() => setActive(false), IDLE_MS)\n }\n view.addEventListener('pointermove', wake)\n view.addEventListener('keydown', wake)\n return () => {\n view.clearTimeout(timer)\n view.removeEventListener('pointermove', wake)\n view.removeEventListener('keydown', wake)\n }\n }, [view])\n return active\n}\n\nexport type PresenterViewProps = Omit<SlidesEditorProps, 'onPresent'> & {\n /** Present (default: here, in this window). */\n onPresent?: () => void\n}\n\nexport function PresenterView({ onPresent, ...editor }: PresenterViewProps) {\n const presenting = usePresentation((s) => s.presenting)\n const [view, setView] = useState<Window | null>(null)\n // The keys work in whichever window the presenter is in.\n const rootRef = useCallback((node: HTMLDivElement | null) => {\n if (node) setView(node.ownerDocument.defaultView)\n }, [])\n usePresenterKeys(presenting ? view : null, true)\n useStageShapedWindow(view, presenting)\n\n const active = usePointerActive(presenting ? view : null)\n\n if (presenting) {\n return (\n <div\n ref={rootRef}\n className=\"relative h-full w-full select-none bg-black\"\n // The pointer shows while it moves, with the controls; idle, nothing but the slide.\n style={{ cursor: active ? 'default' : 'none' }}\n // A click on the slide goes to the next one, as in a slideshow.\n onClick={() => {\n const state = usePresentation.getState()\n state.goTo(state.current + 1)\n }}\n onDoubleClick={() => view && toggleFullscreen(view)}\n data-testid=\"presentation-present\"\n >\n <PresentationStage />\n <PresentControls view={view} shown={active} />\n </div>\n )\n }\n return (\n <div ref={rootRef} className=\"h-full min-h-0\" data-testid=\"presenter-view\">\n <SlidesEditor\n {...editor}\n onPresent={onPresent ?? (() => usePresentation.getState().setPresenting(true))}\n />\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -35,27 +35,6 @@ import { usePresentation } from "./store.js";
|
|
|
35
35
|
import { newBox } from "./boxes.js";
|
|
36
36
|
import { accentColor, isLightBackground } from "./theme.js";
|
|
37
37
|
import { usePresenterKeys } from "./usePresenterKeys.js";
|
|
38
|
-
function SlideAction({
|
|
39
|
-
label,
|
|
40
|
-
onClick,
|
|
41
|
-
disabled,
|
|
42
|
-
children
|
|
43
|
-
}) {
|
|
44
|
-
return /* @__PURE__ */ jsx(
|
|
45
|
-
Button,
|
|
46
|
-
{
|
|
47
|
-
type: "button",
|
|
48
|
-
size: "icon",
|
|
49
|
-
variant: "ghost",
|
|
50
|
-
className: "size-7 text-muted-foreground hover:text-foreground pointer-coarse:size-10",
|
|
51
|
-
title: label,
|
|
52
|
-
"aria-label": label,
|
|
53
|
-
disabled,
|
|
54
|
-
onClick,
|
|
55
|
-
children
|
|
56
|
-
}
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
38
|
const SlideCard = memo(function SlideCard2({
|
|
60
39
|
index,
|
|
61
40
|
count,
|
|
@@ -67,7 +46,7 @@ const SlideCard = memo(function SlideCard2({
|
|
|
67
46
|
}) {
|
|
68
47
|
const empty = usePresentation((s) => {
|
|
69
48
|
const slide = s.slides[index];
|
|
70
|
-
return !!slide && !slide.items.length && !slide.boxes.length && slide.layout !== "title" && slide.layout !== "section";
|
|
49
|
+
return !!slide && !slide.items.length && !slide.boxes.length && !slide.points.trim() && slide.layout !== "title" && slide.layout !== "section";
|
|
71
50
|
});
|
|
72
51
|
const notes = usePresentation((s) => s.slides[index]?.notes ?? "");
|
|
73
52
|
const textEditing = useMemo(
|
|
@@ -102,26 +81,61 @@ const SlideCard = memo(function SlideCard2({
|
|
|
102
81
|
"data-testid": `slide-actions-${index}`,
|
|
103
82
|
children: [
|
|
104
83
|
/* @__PURE__ */ jsx(LayoutMenu, { index, compact: true }),
|
|
105
|
-
/* @__PURE__ */
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
84
|
+
/* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
85
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
86
|
+
Button,
|
|
87
|
+
{
|
|
88
|
+
type: "button",
|
|
89
|
+
size: "icon",
|
|
90
|
+
variant: "ghost",
|
|
91
|
+
className: "size-7 text-muted-foreground hover:text-foreground pointer-coarse:size-11",
|
|
92
|
+
title: "Slide actions",
|
|
93
|
+
"aria-label": `Slide ${index + 1} actions`,
|
|
94
|
+
"data-testid": `slide-menu-${index}`,
|
|
95
|
+
children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "size-3.5" })
|
|
96
|
+
}
|
|
97
|
+
) }),
|
|
98
|
+
/* @__PURE__ */ jsxs(DropdownMenuContent, { align: "start", children: [
|
|
99
|
+
/* @__PURE__ */ jsxs(
|
|
100
|
+
DropdownMenuItem,
|
|
101
|
+
{
|
|
102
|
+
disabled: index === 0,
|
|
103
|
+
onSelect: () => actions.moveSlide(index, index - 1),
|
|
104
|
+
children: [
|
|
105
|
+
/* @__PURE__ */ jsx(ArrowUp, { className: "size-4" }),
|
|
106
|
+
/* @__PURE__ */ jsx("span", { className: "ml-2", children: "Move up" })
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
),
|
|
110
|
+
/* @__PURE__ */ jsxs(
|
|
111
|
+
DropdownMenuItem,
|
|
112
|
+
{
|
|
113
|
+
disabled: index === count - 1,
|
|
114
|
+
onSelect: () => actions.moveSlide(index, index + 1),
|
|
115
|
+
children: [
|
|
116
|
+
/* @__PURE__ */ jsx(ArrowDown, { className: "size-4" }),
|
|
117
|
+
/* @__PURE__ */ jsx("span", { className: "ml-2", children: "Move down" })
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
),
|
|
121
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, { onSelect: () => actions.duplicateSlide(index), children: [
|
|
122
|
+
/* @__PURE__ */ jsx(Copy, { className: "size-4" }),
|
|
123
|
+
/* @__PURE__ */ jsx("span", { className: "ml-2", children: "Duplicate" })
|
|
124
|
+
] }),
|
|
125
|
+
/* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
126
|
+
/* @__PURE__ */ jsxs(
|
|
127
|
+
DropdownMenuItem,
|
|
128
|
+
{
|
|
129
|
+
className: "text-destructive",
|
|
130
|
+
onSelect: () => actions.removeSlide(index),
|
|
131
|
+
children: [
|
|
132
|
+
/* @__PURE__ */ jsx(Trash2, { className: "size-4" }),
|
|
133
|
+
/* @__PURE__ */ jsx("span", { className: "ml-2", children: "Delete slide" })
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
] })
|
|
138
|
+
] })
|
|
125
139
|
]
|
|
126
140
|
}
|
|
127
141
|
)
|
|
@@ -251,6 +265,7 @@ function SlidesEditor({
|
|
|
251
265
|
const slideIds = useMemo(() => ids.split(" "), [ids]);
|
|
252
266
|
const current = usePresentation((s) => s.current);
|
|
253
267
|
const captions = usePresentation((s) => s.captions);
|
|
268
|
+
const presenting = usePresentation((s) => s.presenting);
|
|
254
269
|
const [selection, setSelection] = useState(null);
|
|
255
270
|
const [view, setView] = useState(null);
|
|
256
271
|
const list = useRef(null);
|
|
@@ -326,7 +341,22 @@ function SlidesEditor({
|
|
|
326
341
|
)
|
|
327
342
|
] })
|
|
328
343
|
] }),
|
|
329
|
-
/* @__PURE__ */ jsxs(
|
|
344
|
+
presenting ? /* @__PURE__ */ jsxs(
|
|
345
|
+
Button,
|
|
346
|
+
{
|
|
347
|
+
type: "button",
|
|
348
|
+
size: "sm",
|
|
349
|
+
variant: "destructive",
|
|
350
|
+
className: "ml-0.5 h-8 rounded-full",
|
|
351
|
+
onClick: () => usePresentation.getState().setPresenting(false),
|
|
352
|
+
title: "Stop presenting (the presentation window closes)",
|
|
353
|
+
"data-testid": "presentation-stop-button",
|
|
354
|
+
children: [
|
|
355
|
+
/* @__PURE__ */ jsx(Square, { className: "size-3 fill-current" }),
|
|
356
|
+
"Stop presenting"
|
|
357
|
+
]
|
|
358
|
+
}
|
|
359
|
+
) : /* @__PURE__ */ jsxs(
|
|
330
360
|
Button,
|
|
331
361
|
{
|
|
332
362
|
type: "button",
|
|
@@ -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\n/** A small icon button of a slide's hover bar. */\nfunction SlideAction({\n label,\n onClick,\n disabled,\n children,\n}: {\n label: string\n onClick: () => void\n disabled?: boolean\n children: ReactNode\n}) {\n return (\n <Button\n type=\"button\"\n size=\"icon\"\n variant=\"ghost\"\n className=\"size-7 text-muted-foreground hover:text-foreground pointer-coarse:size-10\"\n title={label}\n aria-label={label}\n disabled={disabled}\n onClick={onClick}\n >\n {children}\n </Button>\n )\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.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 <SlideAction\n label=\"Move up\"\n disabled={index === 0}\n onClick={() => actions.moveSlide(index, index - 1)}\n >\n <ArrowUp className=\"size-3.5\" />\n </SlideAction>\n <SlideAction\n label=\"Move down\"\n disabled={index === count - 1}\n onClick={() => actions.moveSlide(index, index + 1)}\n >\n <ArrowDown className=\"size-3.5\" />\n </SlideAction>\n <SlideAction label=\"Duplicate\" onClick={() => actions.duplicateSlide(index)}>\n <Copy className=\"size-3.5\" />\n </SlideAction>\n <SlideAction label=\"Delete slide\" onClick={() => actions.removeSlide(index)}>\n <Trash2 className=\"size-3.5\" />\n </SlideAction>\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 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 <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 </div>\n\n <ClipTray start={trayStart} />\n <Notice />\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
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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",
|
|
6
6
|
"names": ["SlideCard"]
|
|
7
7
|
}
|
|
@@ -3,7 +3,6 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { memo, useRef, useState } from "react";
|
|
4
4
|
import { cn } from "@arbidocs/react/ui";
|
|
5
5
|
import { ClipImage } from "./ClipImage.js";
|
|
6
|
-
import { ClipViewControls } from "./ClipViewControls.js";
|
|
7
6
|
import {
|
|
8
7
|
frameHeight,
|
|
9
8
|
moveFrame,
|
|
@@ -36,7 +35,6 @@ const StageItem = memo(function StageItem2({
|
|
|
36
35
|
}) {
|
|
37
36
|
const element = useRef(null);
|
|
38
37
|
const gesture = useRef(null);
|
|
39
|
-
const [dragging, setDragging] = useState(false);
|
|
40
38
|
const [reshape, setReshape] = useState(null);
|
|
41
39
|
const shown = reshape ? { ...clip, ...reshape.shape } : clip;
|
|
42
40
|
const box = reshape?.frame ?? frame;
|
|
@@ -86,7 +84,6 @@ const StageItem = memo(function StageItem2({
|
|
|
86
84
|
const g = gesture.current;
|
|
87
85
|
if (!g) return;
|
|
88
86
|
if (!g.next) {
|
|
89
|
-
setDragging(true);
|
|
90
87
|
host.dragging(true);
|
|
91
88
|
if (element.current) element.current.style.willChange = "transform";
|
|
92
89
|
}
|
|
@@ -117,7 +114,6 @@ const StageItem = memo(function StageItem2({
|
|
|
117
114
|
}
|
|
118
115
|
if (!g.next) return;
|
|
119
116
|
setReshape(null);
|
|
120
|
-
setDragging(false);
|
|
121
117
|
host.dragging(false);
|
|
122
118
|
host.guides([]);
|
|
123
119
|
host.commit(clip.id, g.next.frame, g.next.shape);
|
|
@@ -167,7 +163,6 @@ const StageItem = memo(function StageItem2({
|
|
|
167
163
|
children: label
|
|
168
164
|
}
|
|
169
165
|
),
|
|
170
|
-
selected && !dragging && /* @__PURE__ */ jsx(ClipViewControls, { clip }),
|
|
171
166
|
selected && CORNERS.map(({ corner, left, top, cursor }) => /* @__PURE__ */ jsx(
|
|
172
167
|
"div",
|
|
173
168
|
{
|