@arbidocs/blocks 0.5.122 → 0.5.124

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 (124) hide show
  1. package/dist/{ClippingMediaViewer-IEIBT74X.cjs → ClippingMediaViewer-ZFEX5A3U.cjs} +11 -10
  2. package/dist/ClippingMediaViewer-ZFEX5A3U.cjs.map +1 -0
  3. package/dist/{ClippingPdfViewer-OGYLMVCE.cjs → ClippingPdfViewer-3LRTEPHV.cjs} +45 -11
  4. package/dist/ClippingPdfViewer-3LRTEPHV.cjs.map +1 -0
  5. package/dist/{ConnectedPresentation-AU25OW25.cjs → ConnectedPresentation-OV2KBOLK.cjs} +27 -153
  6. package/dist/ConnectedPresentation-OV2KBOLK.cjs.map +1 -0
  7. package/dist/{ConnectedSlidesTab-I75LKKN7.cjs → ConnectedSlidesTab-LUAGZ6FD.cjs} +60 -36
  8. package/dist/ConnectedSlidesTab-LUAGZ6FD.cjs.map +1 -0
  9. package/dist/{FilesViewerDialog-4352KIOG.cjs → FilesViewerDialog-JWKNJLWG.cjs} +10 -7
  10. package/dist/FilesViewerDialog-JWKNJLWG.cjs.map +1 -0
  11. package/dist/SpotlightStrip-ED22UYTM.cjs +398 -0
  12. package/dist/SpotlightStrip-ED22UYTM.cjs.map +1 -0
  13. package/dist/{chunk-LHWOKOMP.cjs → chunk-3IH55CDE.cjs} +4 -4
  14. package/dist/chunk-3IH55CDE.cjs.map +1 -0
  15. package/dist/chunk-3ZZZO4F7.cjs +145 -0
  16. package/dist/chunk-3ZZZO4F7.cjs.map +1 -0
  17. package/dist/{chunk-WFMWKPGY.cjs → chunk-ADPRIBSH.cjs} +9 -33
  18. package/dist/chunk-ADPRIBSH.cjs.map +1 -0
  19. package/dist/{chunk-UTJ223TI.cjs → chunk-BX6G2ZCI.cjs} +138 -146
  20. package/dist/chunk-BX6G2ZCI.cjs.map +1 -0
  21. package/dist/{chunk-WRLGLZIA.cjs → chunk-EMRY62HS.cjs} +2 -2
  22. package/dist/{chunk-WRLGLZIA.cjs.map → chunk-EMRY62HS.cjs.map} +1 -1
  23. package/dist/{chunk-USNTRL22.cjs → chunk-H236SNEV.cjs} +199 -1616
  24. package/dist/chunk-H236SNEV.cjs.map +1 -0
  25. package/dist/chunk-JITYC4OS.cjs +66 -0
  26. package/dist/chunk-JITYC4OS.cjs.map +1 -0
  27. package/dist/chunk-JY7UUZQG.cjs +1513 -0
  28. package/dist/chunk-JY7UUZQG.cjs.map +1 -0
  29. package/dist/chunk-MVXHTAJ4.cjs +111 -0
  30. package/dist/chunk-MVXHTAJ4.cjs.map +1 -0
  31. package/dist/{chunk-4TTX6ZNS.cjs → chunk-ORNLCBNE.cjs} +24 -2
  32. package/dist/chunk-ORNLCBNE.cjs.map +1 -0
  33. package/dist/chunk-WVZLAAIZ.cjs +31 -0
  34. package/dist/chunk-WVZLAAIZ.cjs.map +1 -0
  35. package/dist/chunk-ZZZITHGK.cjs +119 -0
  36. package/dist/chunk-ZZZITHGK.cjs.map +1 -0
  37. package/dist/connect/index.cjs +52 -52
  38. package/dist/connect/viewer.cjs +8 -5
  39. package/dist/connection/presentation/ClippingMediaViewer.js +3 -3
  40. package/dist/connection/presentation/ClippingMediaViewer.js.map +2 -2
  41. package/dist/connection/presentation/ClippingPdfViewer.d.ts +4 -3
  42. package/dist/connection/presentation/ClippingPdfViewer.js +48 -9
  43. package/dist/connection/presentation/ClippingPdfViewer.js.map +2 -2
  44. package/dist/connection/presentation/ConnectedSlidesTab.js +35 -13
  45. package/dist/connection/presentation/ConnectedSlidesTab.js.map +2 -2
  46. package/dist/connection/presentation/SpotlightLayer.d.ts +5 -0
  47. package/dist/connection/presentation/SpotlightLayer.js +19 -0
  48. package/dist/connection/presentation/SpotlightLayer.js.map +7 -0
  49. package/dist/connection/presentation/SpotlightStrip.d.ts +10 -0
  50. package/dist/connection/presentation/SpotlightStrip.js +237 -0
  51. package/dist/connection/presentation/SpotlightStrip.js.map +7 -0
  52. package/dist/connection/presentation/useViewerClipping.d.ts +4 -3
  53. package/dist/connection/presentation/useViewerClipping.js +8 -4
  54. package/dist/connection/presentation/useViewerClipping.js.map +2 -2
  55. package/dist/connection/viewer/ConnectedDocumentViewer.js +52 -61
  56. package/dist/connection/viewer/ConnectedDocumentViewer.js.map +2 -2
  57. package/dist/connection/viewer/ViewerMoreMenu.d.ts +18 -0
  58. package/dist/connection/viewer/ViewerMoreMenu.js +45 -0
  59. package/dist/connection/viewer/ViewerMoreMenu.js.map +7 -0
  60. package/dist/connection/viewer/pdf/PdfPassageHighlights.js +3 -2
  61. package/dist/connection/viewer/pdf/PdfPassageHighlights.js.map +2 -2
  62. package/dist/connection/viewer/pdf/passageHighlights.d.ts +5 -0
  63. package/dist/connection/viewer/pdf/passageHighlights.js +14 -1
  64. package/dist/connection/viewer/pdf/passageHighlights.js.map +2 -2
  65. package/dist/connection/viewer/useCleanView.d.ts +3 -1
  66. package/dist/connection/viewer/useCleanView.js +6 -3
  67. package/dist/connection/viewer/useCleanView.js.map +2 -2
  68. package/dist/document-tools/DocumentToolButtons.js +3 -3
  69. package/dist/document-tools/DocumentToolButtons.js.map +2 -2
  70. package/dist/document-tools/index.cjs +13 -13
  71. package/dist/index.cjs +53 -53
  72. package/dist/presentation/BoxActionBar.d.ts +2 -2
  73. package/dist/presentation/BoxActionBar.js +62 -30
  74. package/dist/presentation/BoxActionBar.js.map +2 -2
  75. package/dist/presentation/ClipActionBar.js +60 -7
  76. package/dist/presentation/ClipActionBar.js.map +2 -2
  77. package/dist/presentation/ClipCaptureOverlay.d.ts +7 -3
  78. package/dist/presentation/ClipCaptureOverlay.js +43 -14
  79. package/dist/presentation/ClipCaptureOverlay.js.map +2 -2
  80. package/dist/presentation/ClipViewControls.d.ts +3 -3
  81. package/dist/presentation/ClipViewControls.js +59 -64
  82. package/dist/presentation/ClipViewControls.js.map +2 -2
  83. package/dist/presentation/PresentControls.d.ts +8 -0
  84. package/dist/presentation/PresentControls.js +1 -0
  85. package/dist/presentation/PresentControls.js.map +2 -2
  86. package/dist/presentation/PresentationStage.d.ts +10 -2
  87. package/dist/presentation/PresentationStage.js +6 -3
  88. package/dist/presentation/PresentationStage.js.map +2 -2
  89. package/dist/presentation/PresenterView.js +1 -22
  90. package/dist/presentation/PresenterView.js.map +2 -2
  91. package/dist/presentation/SlidesEditor.js +56 -42
  92. package/dist/presentation/SlidesEditor.js.map +2 -2
  93. package/dist/presentation/SpotlightView.d.ts +19 -0
  94. package/dist/presentation/SpotlightView.js +180 -0
  95. package/dist/presentation/SpotlightView.js.map +7 -0
  96. package/dist/presentation/StageItem.js +0 -5
  97. package/dist/presentation/StageItem.js.map +2 -2
  98. package/dist/presentation/passage-clip.d.ts +17 -0
  99. package/dist/presentation/passage-clip.js +25 -0
  100. package/dist/presentation/passage-clip.js.map +7 -0
  101. package/dist/presentation/spotlight.d.ts +58 -0
  102. package/dist/presentation/spotlight.js +110 -0
  103. package/dist/presentation/spotlight.js.map +7 -0
  104. package/dist/presentation/store.d.ts +5 -0
  105. package/dist/presentation/store.js +19 -0
  106. package/dist/presentation/store.js.map +2 -2
  107. package/dist/presentation/usePointerActive.d.ts +2 -0
  108. package/dist/presentation/usePointerActive.js +28 -0
  109. package/dist/presentation/usePointerActive.js.map +7 -0
  110. package/dist/styles.css +1 -1
  111. package/node_modules/@arbidocs/coedit/package.json +1 -1
  112. package/package.json +5 -5
  113. package/dist/ClippingMediaViewer-IEIBT74X.cjs.map +0 -1
  114. package/dist/ClippingPdfViewer-OGYLMVCE.cjs.map +0 -1
  115. package/dist/ConnectedPresentation-AU25OW25.cjs.map +0 -1
  116. package/dist/ConnectedSlidesTab-I75LKKN7.cjs.map +0 -1
  117. package/dist/FilesViewerDialog-4352KIOG.cjs.map +0 -1
  118. package/dist/chunk-4TTX6ZNS.cjs.map +0 -1
  119. package/dist/chunk-LHWOKOMP.cjs.map +0 -1
  120. package/dist/chunk-USNTRL22.cjs.map +0 -1
  121. package/dist/chunk-UTJ223TI.cjs.map +0 -1
  122. package/dist/chunk-WFMWKPGY.cjs.map +0 -1
  123. package/dist/chunk-XK4TVJKD.cjs +0 -73
  124. package/dist/chunk-XK4TVJKD.cjs.map +0 -1
@@ -9,7 +9,7 @@ const PAN_STEP = 0.1;
9
9
  const MAX_ZOOM = 800;
10
10
  const HOLD_DELAY = 350;
11
11
  const HOLD_EVERY = 90;
12
- const BUTTON = "pointer-events-auto flex size-7 items-center justify-center rounded-full border border-border bg-background/90 text-foreground shadow-sm backdrop-blur-sm hover:bg-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-30 pointer-coarse:size-11";
12
+ const BUTTON = "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";
13
13
  function HoldButton({
14
14
  label,
15
15
  disabled,
@@ -64,74 +64,69 @@ function ClipViewControls({ clip }) {
64
64
  return /* @__PURE__ */ jsxs(
65
65
  "div",
66
66
  {
67
- className: "pointer-events-none absolute inset-0 z-10",
67
+ className: "flex flex-col items-center gap-2 p-1",
68
68
  onPointerDown: (event) => event.stopPropagation(),
69
69
  "data-testid": "clip-view-controls",
70
70
  children: [
71
- /* @__PURE__ */ jsx(
72
- HoldButton,
71
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 grid-rows-3 place-items-center gap-0.5", children: [
72
+ /* @__PURE__ */ jsx(
73
+ HoldButton,
74
+ {
75
+ label: "Show more above",
76
+ disabled: rect.y <= edge,
77
+ onStep: pan(0, -PAN_STEP),
78
+ className: "col-start-2 row-start-1",
79
+ children: /* @__PURE__ */ jsx(ChevronUp, { className: "size-4" })
80
+ }
81
+ ),
82
+ /* @__PURE__ */ jsx(
83
+ HoldButton,
84
+ {
85
+ label: "Show more on the left",
86
+ disabled: rect.x <= edge,
87
+ onStep: pan(-PAN_STEP, 0),
88
+ className: "col-start-1 row-start-2",
89
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4" })
90
+ }
91
+ ),
92
+ /* @__PURE__ */ jsx(
93
+ HoldButton,
94
+ {
95
+ label: "Show more on the right",
96
+ disabled: rect.x + rect.w >= 1 - edge,
97
+ onStep: pan(PAN_STEP, 0),
98
+ className: "col-start-3 row-start-2",
99
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4" })
100
+ }
101
+ ),
102
+ /* @__PURE__ */ jsx(
103
+ HoldButton,
104
+ {
105
+ label: "Show more below",
106
+ disabled: rect.y + rect.h >= 1 - edge,
107
+ onStep: pan(0, PAN_STEP),
108
+ className: "col-start-2 row-start-3",
109
+ children: /* @__PURE__ */ jsx(ChevronDown, { className: "size-4" })
110
+ }
111
+ )
112
+ ] }),
113
+ /* @__PURE__ */ jsx("div", { "data-testid": "clip-zoom-control", children: /* @__PURE__ */ jsx(
114
+ ZoomSlider,
73
115
  {
74
- label: "Show more above",
75
- disabled: rect.y <= edge,
76
- onStep: pan(0, -PAN_STEP),
77
- className: "absolute left-1/2 top-4 -translate-x-1/2 pointer-coarse:top-7",
78
- children: /* @__PURE__ */ jsx(ChevronUp, { className: "size-4" })
116
+ value: zoom,
117
+ min: minZoom,
118
+ max: Math.max(MAX_ZOOM, zoom),
119
+ zoomOutStep: 25,
120
+ onChange: (value) => setClipZoom(clip.id, value),
121
+ label: "Zoom",
122
+ valueLabel: /* @__PURE__ */ jsxs("span", { "data-testid": "clip-zoom", children: [
123
+ zoom,
124
+ "%"
125
+ ] }),
126
+ className: "pointer-coarse:h-11",
127
+ sliderClassName: "w-28"
79
128
  }
80
- ),
81
- /* @__PURE__ */ jsx(
82
- HoldButton,
83
- {
84
- label: "Show more below",
85
- disabled: rect.y + rect.h >= 1 - edge,
86
- onStep: pan(0, PAN_STEP),
87
- className: "absolute bottom-10 left-1/2 -translate-x-1/2 pointer-coarse:bottom-14",
88
- children: /* @__PURE__ */ jsx(ChevronDown, { className: "size-4" })
89
- }
90
- ),
91
- /* @__PURE__ */ jsx(
92
- HoldButton,
93
- {
94
- label: "Show more on the left",
95
- disabled: rect.x <= edge,
96
- onStep: pan(-PAN_STEP, 0),
97
- className: "absolute left-4 top-1/2 -translate-y-1/2 pointer-coarse:left-7",
98
- children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4" })
99
- }
100
- ),
101
- /* @__PURE__ */ jsx(
102
- HoldButton,
103
- {
104
- label: "Show more on the right",
105
- disabled: rect.x + rect.w >= 1 - edge,
106
- onStep: pan(PAN_STEP, 0),
107
- className: "absolute right-4 top-1/2 -translate-y-1/2 pointer-coarse:right-7",
108
- children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4" })
109
- }
110
- ),
111
- /* @__PURE__ */ jsx(
112
- "div",
113
- {
114
- className: "pointer-events-auto absolute bottom-1.5 left-1/2 -translate-x-1/2",
115
- "data-testid": "clip-zoom-control",
116
- children: /* @__PURE__ */ jsx(
117
- ZoomSlider,
118
- {
119
- value: zoom,
120
- min: minZoom,
121
- max: Math.max(MAX_ZOOM, zoom),
122
- zoomOutStep: 25,
123
- onChange: (value) => setClipZoom(clip.id, value),
124
- label: "Zoom",
125
- valueLabel: /* @__PURE__ */ jsxs("span", { "data-testid": "clip-zoom", children: [
126
- zoom,
127
- "%"
128
- ] }),
129
- className: "bg-background/95 shadow-sm backdrop-blur-sm pointer-coarse:h-11",
130
- sliderClassName: "w-24"
131
- }
132
- )
133
- }
134
- )
129
+ ) })
135
130
  ]
136
131
  }
137
132
  );
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/presentation/ClipViewControls.tsx"],
4
- "sourcesContent": ["/**\n * The selected clip's view controls, on a layer over it (editor only): arrows at the middle of each\n * edge scroll what the clip shows over its source (a press steps, holding keeps scrolling; an arrow\n * 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 'pointer-events-auto flex size-7 items-center justify-center rounded-full border border-border bg-background/90 text-foreground shadow-sm backdrop-blur-sm hover:bg-background 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=\"pointer-events-none absolute inset-0 z-10\"\n // A press on the layer 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 <HoldButton\n label=\"Show more above\"\n disabled={rect.y <= edge}\n onStep={pan(0, -PAN_STEP)}\n className=\"absolute left-1/2 top-4 -translate-x-1/2 pointer-coarse:top-7\"\n >\n <ChevronUp 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=\"absolute bottom-10 left-1/2 -translate-x-1/2 pointer-coarse:bottom-14\"\n >\n <ChevronDown 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=\"absolute left-4 top-1/2 -translate-y-1/2 pointer-coarse:left-7\"\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=\"absolute right-4 top-1/2 -translate-y-1/2 pointer-coarse:right-7\"\n >\n <ChevronRight className=\"size-4\" />\n </HoldButton>\n {/* Zoom: a slider, from as far out as the source allows to MAX_ZOOM. */}\n <div\n className=\"pointer-events-auto absolute bottom-1.5 left-1/2 -translate-x-1/2\"\n data-testid=\"clip-zoom-control\"\n >\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 // Over the clip: on the surface colour, so it reads over any content.\n className=\"bg-background/95 shadow-sm backdrop-blur-sm pointer-coarse:h-11\"\n sliderClassName=\"w-24\"\n />\n </div>\n </div>\n )\n}\n"],
5
- "mappings": "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",
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
  }
@@ -3,6 +3,14 @@
3
3
  * blank, full screen and stop. Like a slideshow's, they show while the pointer moves (or a key is
4
4
  * pressed) and fade after a moment, with the pointer, so a shared screen rarely shows them.
5
5
  */
6
+ import type { ReactNode } from 'react';
7
+ export declare function Control({ label, onClick, disabled, children, testId, }: {
8
+ label: string;
9
+ onClick: () => void;
10
+ disabled?: boolean;
11
+ children: ReactNode;
12
+ testId: string;
13
+ }): import("react").JSX.Element;
6
14
  export interface PresentControlsProps {
7
15
  /** The window presented in (full screen is that window's). */
8
16
  view: Window | null;
@@ -112,6 +112,7 @@ function PresentControls({ view, shown }) {
112
112
  );
113
113
  }
114
114
  export {
115
+ Control,
115
116
  PresentControls
116
117
  };
117
118
  //# sourceMappingURL=PresentControls.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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;",
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\nexport function 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;AAEK,SAAS,QAAQ;AAAA,EACtB;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
6
  "names": []
7
7
  }
@@ -9,11 +9,19 @@
9
9
  */
10
10
  import { type ReactNode } from 'react';
11
11
  import { type SlideTextEditing } from './SlideDecor';
12
- import { type Guide } from './model';
12
+ import { type Clip, type Guide, type Slide } from './model';
13
13
  export interface PresentationStageProps {
14
14
  editable?: boolean;
15
15
  /** The slide shown (default: the current one). */
16
16
  slideIndex?: number;
17
+ /**
18
+ * A slide that is not one of the presentation's, shown with its own clips (the viewer's
19
+ * spotlight); the presentation's look still applies.
20
+ */
21
+ show?: {
22
+ slide: Slide;
23
+ clips: readonly Clip[];
24
+ };
17
25
  selected?: string | null;
18
26
  onSelect?: (clipId: string | null) => void;
19
27
  /** Offered on a selected image clip: make a new version of it. */
@@ -29,4 +37,4 @@ export interface PresentationStageProps {
29
37
  export interface GuideLayerHandle {
30
38
  set(guides: Guide[]): void;
31
39
  }
32
- export declare function PresentationStage({ editable, slideIndex, selected, onSelect, onEditImage, onCropImage, textEditing, overlay, className, }: PresentationStageProps): import("react").JSX.Element;
40
+ export declare function PresentationStage({ editable, slideIndex, show, selected, onSelect, onEditImage, onCropImage, textEditing, overlay, className, }: PresentationStageProps): import("react").JSX.Element;
@@ -37,6 +37,7 @@ const GuideLayer = memo(
37
37
  function PresentationStage({
38
38
  editable = false,
39
39
  slideIndex,
40
+ show,
40
41
  selected = null,
41
42
  onSelect,
42
43
  onEditImage,
@@ -46,9 +47,11 @@ function PresentationStage({
46
47
  className
47
48
  }) {
48
49
  const index = usePresentation((s) => slideIndex ?? s.current);
49
- const slide = usePresentation((s) => s.slides[index]);
50
- const clips = usePresentation((s) => s.clips);
51
- const blank = usePresentation((s) => s.blank && !editable);
50
+ const deckSlide = usePresentation((s) => s.slides[index]);
51
+ const deckClips = usePresentation((s) => s.clips);
52
+ const slide = show?.slide ?? deckSlide;
53
+ const clips = show?.clips ?? deckClips;
54
+ const blank = usePresentation((s) => s.blank && !editable && !show);
52
55
  const captions = usePresentation((s) => s.captions);
53
56
  const columnValues = usePresentation((s) => s.columnValues);
54
57
  const theme = usePresentation((s) => s.theme);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/presentation/PresentationStage.tsx"],
4
- "sourcesContent": ["/**\n * The 16:9 stage: a slide's clips in their frames, its title, points and logo, as large as the\n * container allows. Presenting shows it bare; the editor's copy is `editable`: each clip\n * (`StageItem`) is moved, resized and reshaped by dragging, snapping with guide lines; clips are\n * selected for their floating actions, and clips dragged from the tray are dropped onto it.\n *\n * A slide's stage re-renders only when its slide (or the clips, theme or labels) change; a clip's\n * drag re-renders nothing here but the guides and, at its start and end, the floating actions.\n */\n\nimport {\n forwardRef,\n memo,\n useEffect,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react'\nimport { cn } from '@arbidocs/react/ui'\nimport { ClipActionBar } from './ClipActionBar'\nimport { SlideDecor, type SlideTextEditing } from './SlideDecor'\nimport { StageItem, type StageItemHost } from './StageItem'\nimport { StageBox, type StageBoxHost } from './StageBox'\nimport { BoxActionBar } from './BoxActionBar'\nimport { FONTS, backgroundCss, slideBackgroundCss } from './theme'\nimport { clipLabel, type Clip, type Frame, type Guide } from './model'\nimport { usePresentation } from './store'\nimport { DROP_ATTRIBUTE } from './useClipDrag'\n\nexport interface PresentationStageProps {\n editable?: boolean\n /** The slide shown (default: the current one). */\n slideIndex?: number\n selected?: string | null\n onSelect?: (clipId: string | null) => void\n /** Offered on a selected image clip: make a new version of it. */\n onEditImage?: (clipId: string) => void\n /** Offered on a selected image clip: choose the part of the image it shows. */\n onCropImage?: (clipId: string) => void\n /** Typing the slide's title and points on the slide itself (the editor's stages). */\n textEditing?: SlideTextEditing\n /** Over the slide (e.g. the editor's hover actions). */\n overlay?: ReactNode\n className?: string\n}\n\nexport interface GuideLayerHandle {\n set(guides: Guide[]): void\n}\n\n/** The snap lines of a dragged clip: its own state, so drawing them re-renders nothing else. */\nconst GuideLayer = memo(\n forwardRef<GuideLayerHandle>(function GuideLayer(_, ref) {\n const [guides, setGuides] = useState<Guide[]>([])\n useImperativeHandle(ref, () => ({ set: setGuides }), [])\n return (\n <>\n {guides.map((guide) => (\n <div\n key={`${guide.axis}-${guide.at}`}\n className=\"pointer-events-none absolute z-10 bg-primary\"\n style={\n guide.axis === 'x'\n ? { left: `${guide.at * 100}%`, top: 0, bottom: 0, width: 1 }\n : { top: `${guide.at * 100}%`, left: 0, right: 0, height: 1 }\n }\n data-testid={`stage-guide-${guide.axis}`}\n />\n ))}\n </>\n )\n })\n)\n\nexport function PresentationStage({\n editable = false,\n slideIndex,\n selected = null,\n onSelect,\n onEditImage,\n onCropImage,\n textEditing,\n overlay,\n className,\n}: PresentationStageProps) {\n const index = usePresentation((s) => slideIndex ?? s.current)\n const slide = usePresentation((s) => s.slides[index])\n const clips = usePresentation((s) => s.clips)\n const blank = usePresentation((s) => s.blank && !editable)\n const captions = usePresentation((s) => s.captions)\n const columnValues = usePresentation((s) => s.columnValues)\n const theme = usePresentation((s) => s.theme)\n const logoImage = usePresentation((s) => s.logoImage)\n const media = usePresentation((s) => s.media)\n const stageRef = useRef<HTMLDivElement>(null)\n const guideLayer = useRef<GuideLayerHandle>(null)\n // A clip dragged from the tray: the stage shows it can take it.\n const dropping = usePresentation((s) => editable && s.dragging !== null)\n // A clip being dragged on the stage: its floating actions hide meanwhile.\n const [dragging, setDragging] = useState(false)\n\n // The latest values the clips' gestures read as they begin (the host itself never changes).\n const latest = useRef({ slide, clips, index, onSelect })\n latest.current = { slide, clips, index, onSelect }\n const host = useMemo<StageItemHost>(\n () => ({\n stageBox: () => stageRef.current?.getBoundingClientRect() ?? null,\n others: (clipId) => {\n const { slide: current, clips: all } = latest.current\n return (current?.items ?? [])\n .filter((item) => item.clipId !== clipId)\n .map((item) => ({\n frame: item.frame,\n aspect: all.find((clip) => clip.id === item.clipId)?.aspect ?? 1,\n }))\n },\n select: (clipId) => {\n usePresentation.getState().goTo(latest.current.index)\n latest.current.onSelect?.(clipId)\n },\n guides: (guides) => guideLayer.current?.set(guides),\n dragging: setDragging,\n commit: (clipId: string, frame: Frame, shape?: Pick<Clip, 'rect' | 'aspect'>) => {\n const state = usePresentation.getState()\n state.goTo(latest.current.index)\n if (shape) state.reshapeClip(clipId, frame, shape.rect, shape.aspect)\n else state.setFrame(clipId, frame)\n },\n }),\n []\n )\n\n const boxHost = useMemo<StageBoxHost>(\n () => ({\n stageBox: () => stageRef.current?.getBoundingClientRect() ?? null,\n select: (boxId) => {\n usePresentation.getState().goTo(latest.current.index)\n latest.current.onSelect?.(boxId)\n },\n commit: (boxId, frame) =>\n usePresentation.getState().updateBox(boxId, (box) => ({ ...box, frame })),\n setText: (boxId, text) => usePresentation.getState().setBoxText(boxId, text),\n }),\n []\n )\n const font = FONTS[theme.font].css\n const boxes = slide?.boxes ?? []\n const selectedBox = selected ? boxes.find((box) => box.id === selected) : undefined\n const renderBoxes = (front: boolean) =>\n boxes\n .filter((box) => !!box.front === front)\n .map((box) => (\n <StageBox\n key={box.id}\n box={box}\n font={font}\n editable={editable}\n selected={editable && selected === box.id}\n host={boxHost}\n />\n ))\n\n const clipOf = (id: string): Clip | undefined => clips.find((clip) => clip.id === id)\n const selectedFrame = selected\n ? slide?.items.find((item) => item.clipId === selected)?.frame\n : undefined\n\n // Scrolling over the selected clip (wheel or trackpad; Shift for sideways) scrolls what it shows\n // over its source: the lines above or below, say. A native listener, so the editor itself does\n // not scroll; steps are gathered and applied once per animation frame.\n useEffect(() => {\n const stage = stageRef.current\n const view = stage?.ownerDocument.defaultView\n if (!editable || !selected || !stage || !view) return\n let sum = { x: 0, y: 0 }\n let request: number | null = null\n const onWheel = (event: WheelEvent) => {\n const item = (event.target as Element | null)?.closest?.(\n `[data-testid=\"stage-item-${selected}\"]`\n )\n const clip = usePresentation.getState().clips.find((entry) => entry.id === selected)\n if (!item || !clip) return\n event.preventDefault()\n const box = item.getBoundingClientRect()\n const horizontal = event.shiftKey && !event.deltaX\n const deltaX = horizontal ? event.deltaY : event.deltaX\n const deltaY = horizontal ? 0 : event.deltaY\n // Screen pixels to fractions of the source: the clip's area spans the clip's box.\n sum = {\n x: sum.x + (deltaX / box.width) * clip.rect.w,\n y: sum.y + (deltaY / box.height) * clip.rect.h,\n }\n request ??= view.requestAnimationFrame(() => {\n request = null\n usePresentation.getState().panClip(selected, sum.x, sum.y)\n sum = { x: 0, y: 0 }\n })\n }\n stage.addEventListener('wheel', onWheel, { passive: false })\n return () => {\n stage.removeEventListener('wheel', onWheel)\n if (request !== null) view.cancelAnimationFrame(request)\n }\n }, [editable, selected])\n\n return (\n <div\n className={cn('relative h-full w-full', !editable && 'overflow-hidden bg-black', className)}\n // The stage's size comes from this box's (container query units): 16:9, as large as fits.\n style={{ containerType: 'size' }}\n data-testid={editable ? 'presentation-stage-editor' : 'presentation-stage'}\n >\n <div\n ref={stageRef}\n className={cn(\n 'absolute inset-0 m-auto',\n editable ? 'shadow-md' : 'overflow-hidden',\n dropping && 'outline outline-2 outline-offset-2 outline-primary'\n )}\n style={{\n width: 'min(100cqw, calc(100cqh * 16 / 9))',\n height: 'min(100cqh, calc(100cqw * 9 / 16))',\n containerType: 'inline-size',\n background: slide?.background\n ? slideBackgroundCss(slide.background, media)\n : backgroundCss(theme.background),\n fontFamily: FONTS[theme.font].css,\n }}\n onPointerDown={() => {\n if (!editable) return\n usePresentation.getState().goTo(index)\n onSelect?.(null)\n }}\n {...(editable ? { [DROP_ATTRIBUTE]: 'stage', 'data-slide-index': index } : {})}\n aria-label={`Slide ${index + 1}`}\n >\n {/* Blanked: the shared stage is black. */}\n {blank && <div className=\"absolute inset-0 z-20 bg-black\" />}\n {/* The title, points and logo, by the slide's layout. */}\n {slide && (\n <SlideDecor\n slide={slide}\n theme={theme}\n labels={captions}\n logoImage={logoImage}\n editable={editable}\n editing={textEditing}\n />\n )}\n {renderBoxes(false)}\n {slide?.items.map((item) => {\n const clip = clipOf(item.clipId)\n if (!clip) return null\n return (\n <StageItem\n key={clip.id}\n clip={clip}\n frame={item.frame}\n editable={editable}\n selected={editable && selected === clip.id}\n dim={false}\n label={captions ? clipLabel(clip, columnValues) : ''}\n host={host}\n />\n )\n })}\n {renderBoxes(true)}\n {/* Smart guides: the lines the dragged clip snapped to (editor only). */}\n {editable && <GuideLayer ref={guideLayer} />}\n {/* The selected clip's floating actions, above it (the stage leaves room). */}\n {editable && selectedFrame && selected && !dragging && (\n <ClipActionBar\n clipId={selected}\n at={selectedFrame}\n onRemoved={() => onSelect?.(null)}\n onEditImage={\n onEditImage && clipOf(selected)?.kind === 'image'\n ? () => onEditImage(selected)\n : undefined\n }\n onCropImage={\n onCropImage && clipOf(selected)?.kind === 'image'\n ? () => onCropImage(selected)\n : undefined\n }\n />\n )}\n {editable && selectedBox && (\n <BoxActionBar box={selectedBox} onRemoved={() => onSelect?.(null)} />\n )}\n {overlay}\n </div>\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * The 16:9 stage: a slide's clips in their frames, its title, points and logo, as large as the\n * container allows. Presenting shows it bare; the editor's copy is `editable`: each clip\n * (`StageItem`) is moved, resized and reshaped by dragging, snapping with guide lines; clips are\n * selected for their floating actions, and clips dragged from the tray are dropped onto it.\n *\n * A slide's stage re-renders only when its slide (or the clips, theme or labels) change; a clip's\n * drag re-renders nothing here but the guides and, at its start and end, the floating actions.\n */\n\nimport {\n forwardRef,\n memo,\n useEffect,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react'\nimport { cn } from '@arbidocs/react/ui'\nimport { ClipActionBar } from './ClipActionBar'\nimport { SlideDecor, type SlideTextEditing } from './SlideDecor'\nimport { StageItem, type StageItemHost } from './StageItem'\nimport { StageBox, type StageBoxHost } from './StageBox'\nimport { BoxActionBar } from './BoxActionBar'\nimport { FONTS, backgroundCss, slideBackgroundCss } from './theme'\nimport { clipLabel, type Clip, type Frame, type Guide, type Slide } from './model'\nimport { usePresentation } from './store'\nimport { DROP_ATTRIBUTE } from './useClipDrag'\n\nexport interface PresentationStageProps {\n editable?: boolean\n /** The slide shown (default: the current one). */\n slideIndex?: number\n /**\n * A slide that is not one of the presentation's, shown with its own clips (the viewer's\n * spotlight); the presentation's look still applies.\n */\n show?: { slide: Slide; clips: readonly Clip[] }\n selected?: string | null\n onSelect?: (clipId: string | null) => void\n /** Offered on a selected image clip: make a new version of it. */\n onEditImage?: (clipId: string) => void\n /** Offered on a selected image clip: choose the part of the image it shows. */\n onCropImage?: (clipId: string) => void\n /** Typing the slide's title and points on the slide itself (the editor's stages). */\n textEditing?: SlideTextEditing\n /** Over the slide (e.g. the editor's hover actions). */\n overlay?: ReactNode\n className?: string\n}\n\nexport interface GuideLayerHandle {\n set(guides: Guide[]): void\n}\n\n/** The snap lines of a dragged clip: its own state, so drawing them re-renders nothing else. */\nconst GuideLayer = memo(\n forwardRef<GuideLayerHandle>(function GuideLayer(_, ref) {\n const [guides, setGuides] = useState<Guide[]>([])\n useImperativeHandle(ref, () => ({ set: setGuides }), [])\n return (\n <>\n {guides.map((guide) => (\n <div\n key={`${guide.axis}-${guide.at}`}\n className=\"pointer-events-none absolute z-10 bg-primary\"\n style={\n guide.axis === 'x'\n ? { left: `${guide.at * 100}%`, top: 0, bottom: 0, width: 1 }\n : { top: `${guide.at * 100}%`, left: 0, right: 0, height: 1 }\n }\n data-testid={`stage-guide-${guide.axis}`}\n />\n ))}\n </>\n )\n })\n)\n\nexport function PresentationStage({\n editable = false,\n slideIndex,\n show,\n selected = null,\n onSelect,\n onEditImage,\n onCropImage,\n textEditing,\n overlay,\n className,\n}: PresentationStageProps) {\n const index = usePresentation((s) => slideIndex ?? s.current)\n const deckSlide = usePresentation((s) => s.slides[index])\n const deckClips = usePresentation((s) => s.clips)\n const slide = show?.slide ?? deckSlide\n const clips = show?.clips ?? deckClips\n const blank = usePresentation((s) => s.blank && !editable && !show)\n const captions = usePresentation((s) => s.captions)\n const columnValues = usePresentation((s) => s.columnValues)\n const theme = usePresentation((s) => s.theme)\n const logoImage = usePresentation((s) => s.logoImage)\n const media = usePresentation((s) => s.media)\n const stageRef = useRef<HTMLDivElement>(null)\n const guideLayer = useRef<GuideLayerHandle>(null)\n // A clip dragged from the tray: the stage shows it can take it.\n const dropping = usePresentation((s) => editable && s.dragging !== null)\n // A clip being dragged on the stage: its floating actions hide meanwhile.\n const [dragging, setDragging] = useState(false)\n\n // The latest values the clips' gestures read as they begin (the host itself never changes).\n const latest = useRef({ slide, clips, index, onSelect })\n latest.current = { slide, clips, index, onSelect }\n const host = useMemo<StageItemHost>(\n () => ({\n stageBox: () => stageRef.current?.getBoundingClientRect() ?? null,\n others: (clipId) => {\n const { slide: current, clips: all } = latest.current\n return (current?.items ?? [])\n .filter((item) => item.clipId !== clipId)\n .map((item) => ({\n frame: item.frame,\n aspect: all.find((clip) => clip.id === item.clipId)?.aspect ?? 1,\n }))\n },\n select: (clipId) => {\n usePresentation.getState().goTo(latest.current.index)\n latest.current.onSelect?.(clipId)\n },\n guides: (guides) => guideLayer.current?.set(guides),\n dragging: setDragging,\n commit: (clipId: string, frame: Frame, shape?: Pick<Clip, 'rect' | 'aspect'>) => {\n const state = usePresentation.getState()\n state.goTo(latest.current.index)\n if (shape) state.reshapeClip(clipId, frame, shape.rect, shape.aspect)\n else state.setFrame(clipId, frame)\n },\n }),\n []\n )\n\n const boxHost = useMemo<StageBoxHost>(\n () => ({\n stageBox: () => stageRef.current?.getBoundingClientRect() ?? null,\n select: (boxId) => {\n usePresentation.getState().goTo(latest.current.index)\n latest.current.onSelect?.(boxId)\n },\n commit: (boxId, frame) =>\n usePresentation.getState().updateBox(boxId, (box) => ({ ...box, frame })),\n setText: (boxId, text) => usePresentation.getState().setBoxText(boxId, text),\n }),\n []\n )\n const font = FONTS[theme.font].css\n const boxes = slide?.boxes ?? []\n const selectedBox = selected ? boxes.find((box) => box.id === selected) : undefined\n const renderBoxes = (front: boolean) =>\n boxes\n .filter((box) => !!box.front === front)\n .map((box) => (\n <StageBox\n key={box.id}\n box={box}\n font={font}\n editable={editable}\n selected={editable && selected === box.id}\n host={boxHost}\n />\n ))\n\n const clipOf = (id: string): Clip | undefined => clips.find((clip) => clip.id === id)\n const selectedFrame = selected\n ? slide?.items.find((item) => item.clipId === selected)?.frame\n : undefined\n\n // Scrolling over the selected clip (wheel or trackpad; Shift for sideways) scrolls what it shows\n // over its source: the lines above or below, say. A native listener, so the editor itself does\n // not scroll; steps are gathered and applied once per animation frame.\n useEffect(() => {\n const stage = stageRef.current\n const view = stage?.ownerDocument.defaultView\n if (!editable || !selected || !stage || !view) return\n let sum = { x: 0, y: 0 }\n let request: number | null = null\n const onWheel = (event: WheelEvent) => {\n const item = (event.target as Element | null)?.closest?.(\n `[data-testid=\"stage-item-${selected}\"]`\n )\n const clip = usePresentation.getState().clips.find((entry) => entry.id === selected)\n if (!item || !clip) return\n event.preventDefault()\n const box = item.getBoundingClientRect()\n const horizontal = event.shiftKey && !event.deltaX\n const deltaX = horizontal ? event.deltaY : event.deltaX\n const deltaY = horizontal ? 0 : event.deltaY\n // Screen pixels to fractions of the source: the clip's area spans the clip's box.\n sum = {\n x: sum.x + (deltaX / box.width) * clip.rect.w,\n y: sum.y + (deltaY / box.height) * clip.rect.h,\n }\n request ??= view.requestAnimationFrame(() => {\n request = null\n usePresentation.getState().panClip(selected, sum.x, sum.y)\n sum = { x: 0, y: 0 }\n })\n }\n stage.addEventListener('wheel', onWheel, { passive: false })\n return () => {\n stage.removeEventListener('wheel', onWheel)\n if (request !== null) view.cancelAnimationFrame(request)\n }\n }, [editable, selected])\n\n return (\n <div\n className={cn('relative h-full w-full', !editable && 'overflow-hidden bg-black', className)}\n // The stage's size comes from this box's (container query units): 16:9, as large as fits.\n style={{ containerType: 'size' }}\n data-testid={editable ? 'presentation-stage-editor' : 'presentation-stage'}\n >\n <div\n ref={stageRef}\n className={cn(\n 'absolute inset-0 m-auto',\n editable ? 'shadow-md' : 'overflow-hidden',\n dropping && 'outline outline-2 outline-offset-2 outline-primary'\n )}\n style={{\n width: 'min(100cqw, calc(100cqh * 16 / 9))',\n height: 'min(100cqh, calc(100cqw * 9 / 16))',\n containerType: 'inline-size',\n background: slide?.background\n ? slideBackgroundCss(slide.background, media)\n : backgroundCss(theme.background),\n fontFamily: FONTS[theme.font].css,\n }}\n onPointerDown={() => {\n if (!editable) return\n usePresentation.getState().goTo(index)\n onSelect?.(null)\n }}\n {...(editable ? { [DROP_ATTRIBUTE]: 'stage', 'data-slide-index': index } : {})}\n aria-label={`Slide ${index + 1}`}\n >\n {/* Blanked: the shared stage is black. */}\n {blank && <div className=\"absolute inset-0 z-20 bg-black\" />}\n {/* The title, points and logo, by the slide's layout. */}\n {slide && (\n <SlideDecor\n slide={slide}\n theme={theme}\n labels={captions}\n logoImage={logoImage}\n editable={editable}\n editing={textEditing}\n />\n )}\n {renderBoxes(false)}\n {slide?.items.map((item) => {\n const clip = clipOf(item.clipId)\n if (!clip) return null\n return (\n <StageItem\n key={clip.id}\n clip={clip}\n frame={item.frame}\n editable={editable}\n selected={editable && selected === clip.id}\n dim={false}\n label={captions ? clipLabel(clip, columnValues) : ''}\n host={host}\n />\n )\n })}\n {renderBoxes(true)}\n {/* Smart guides: the lines the dragged clip snapped to (editor only). */}\n {editable && <GuideLayer ref={guideLayer} />}\n {/* The selected clip's floating actions, above it (the stage leaves room). */}\n {editable && selectedFrame && selected && !dragging && (\n <ClipActionBar\n clipId={selected}\n at={selectedFrame}\n onRemoved={() => onSelect?.(null)}\n onEditImage={\n onEditImage && clipOf(selected)?.kind === 'image'\n ? () => onEditImage(selected)\n : undefined\n }\n onCropImage={\n onCropImage && clipOf(selected)?.kind === 'image'\n ? () => onCropImage(selected)\n : undefined\n }\n />\n )}\n {editable && selectedBox && (\n <BoxActionBar box={selectedBox} onRemoved={() => onSelect?.(null)} />\n )}\n {overlay}\n </div>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["GuideLayer"]
7
7
  }
@@ -7,6 +7,7 @@ import { STAGE_ASPECT } from "./model.js";
7
7
  import { usePresentation } from "./store.js";
8
8
  import { toggleFullscreen, usePresenterKeys } from "./usePresenterKeys.js";
9
9
  import { PresentControls } from "./PresentControls.js";
10
+ import { usePointerActive } from "./usePointerActive.js";
10
11
  const PRESENT_WIDTH = 1280;
11
12
  function useStageShapedWindow(view, presenting) {
12
13
  useEffect(() => {
@@ -35,28 +36,6 @@ function useStageShapedWindow(view, presenting) {
35
36
  };
36
37
  }, [view, presenting]);
37
38
  }
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
- }
60
39
  function PresenterView({ onPresent, ...editor }) {
61
40
  const presenting = usePresentation((s) => s.presenting);
62
41
  const [view, setView] = useState(null);
@@ -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'\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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",
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'\nimport { usePointerActive } from './usePointerActive'\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 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": ";AA6EM,SAaE,KAbF;AAlEN,SAAS,aAAa,WAAW,gBAAgB;AACjD,SAAS,yBAAyB;AAClC,SAAS,oBAA4C;AACrD,SAAS,oBAAoB;AAC7B,SAAS,uBAAuB;AAChC,SAAS,kBAAkB,wBAAwB;AACnD,SAAS,uBAAuB;AAChC,SAAS,wBAAwB;AAGjC,MAAM,gBAAgB;AAOtB,SAAS,qBAAqB,MAAqB,YAAqB;AACtE,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,KAAK,OAAQ;AAC1C,UAAM,SAAS,EAAE,OAAO,KAAK,YAAY,QAAQ,KAAK,YAAY;AAClE,UAAM,cAAc,KAAK,aAAa,KAAK;AAC3C,UAAM,eAAe,KAAK,cAAc,KAAK;AAE7C,UAAM,QAAQ,KAAK;AAAA,MACjB,KAAK;AAAA,MACL,KAAK,IAAI,eAAe,KAAK,OAAO,KAAK,OAAO,aAAa,eAAe,GAAG,CAAC;AAAA,IAClF;AACA,UAAM,SAAS,KAAK;AAAA,MAClB,KAAK,MAAM,QAAQ,YAAY;AAAA,MAC/B,KAAK,OAAO,cAAc;AAAA,IAC5B;AACA,QAAI;AACF,WAAK,SAAS,KAAK,MAAM,SAAS,YAAY,IAAI,aAAa,SAAS,YAAY;AAAA,IACtF,QAAQ;AACN;AAAA,IACF;AACA,WAAO,MAAM;AACX,UAAI;AACF,YAAI,CAAC,KAAK,OAAQ,MAAK,SAAS,OAAO,OAAO,OAAO,MAAM;AAAA,MAC7D,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,GAAG,CAAC,MAAM,UAAU,CAAC;AACvB;AAOO,SAAS,cAAc,EAAE,WAAW,GAAG,OAAO,GAAuB;AAC1E,QAAM,aAAa,gBAAgB,CAAC,MAAM,EAAE,UAAU;AACtD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAwB,IAAI;AAEpD,QAAM,UAAU,YAAY,CAAC,SAAgC;AAC3D,QAAI,KAAM,SAAQ,KAAK,cAAc,WAAW;AAAA,EAClD,GAAG,CAAC,CAAC;AACL,mBAAiB,aAAa,OAAO,MAAM,IAAI;AAC/C,uBAAqB,MAAM,UAAU;AAErC,QAAM,SAAS,iBAAiB,aAAa,OAAO,IAAI;AAExD,MAAI,YAAY;AACd,WACE;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,WAAU;AAAA,QAEV,OAAO,EAAE,QAAQ,SAAS,YAAY,OAAO;AAAA,QAE7C,SAAS,MAAM;AACb,gBAAM,QAAQ,gBAAgB,SAAS;AACvC,gBAAM,KAAK,MAAM,UAAU,CAAC;AAAA,QAC9B;AAAA,QACA,eAAe,MAAM,QAAQ,iBAAiB,IAAI;AAAA,QAClD,eAAY;AAAA,QAEZ;AAAA,8BAAC,qBAAkB;AAAA,UACnB,oBAAC,mBAAgB,MAAY,OAAO,QAAQ;AAAA;AAAA;AAAA,IAC9C;AAAA,EAEJ;AACA,SACE,oBAAC,SAAI,KAAK,SAAS,WAAU,kBAAiB,eAAY,kBACxD;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,WAAW,cAAc,MAAM,gBAAgB,SAAS,EAAE,cAAc,IAAI;AAAA;AAAA,EAC9E,GACF;AAEJ;",
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__ */ jsx(
106
- SlideAction,
107
- {
108
- label: "Move up",
109
- disabled: index === 0,
110
- onClick: () => actions.moveSlide(index, index - 1),
111
- children: /* @__PURE__ */ jsx(ArrowUp, { className: "size-3.5" })
112
- }
113
- ),
114
- /* @__PURE__ */ jsx(
115
- SlideAction,
116
- {
117
- label: "Move down",
118
- disabled: index === count - 1,
119
- onClick: () => actions.moveSlide(index, index + 1),
120
- children: /* @__PURE__ */ jsx(ArrowDown, { className: "size-3.5" })
121
- }
122
- ),
123
- /* @__PURE__ */ jsx(SlideAction, { label: "Duplicate", onClick: () => actions.duplicateSlide(index), children: /* @__PURE__ */ jsx(Copy, { className: "size-3.5" }) }),
124
- /* @__PURE__ */ jsx(SlideAction, { label: "Delete slide", onClick: () => actions.removeSlide(index), children: /* @__PURE__ */ jsx(Trash2, { className: "size-3.5" }) })
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
  )