@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
@@ -8,7 +8,7 @@ import {
8
8
  useArbiSessionState,
9
9
  useArbiSessionStore
10
10
  } from "@arbidocs/react";
11
- import { Button } from "@arbidocs/react/ui";
11
+ import { Button, DropdownMenuItem } from "@arbidocs/react/ui";
12
12
  import { useBlocksEnvironment } from "../../provider/environment.js";
13
13
  import { usePanelHostSupport } from "../../shell/panel-host.js";
14
14
  import {
@@ -23,6 +23,8 @@ import { editPresentation } from "./requests.js";
23
23
  import { usePresentationController } from "./usePresentationController.js";
24
24
  import { usePresentationFiles } from "./usePresentationFiles.js";
25
25
  const listOf = (numbers) => numbers.length < 2 ? String(numbers[0] ?? "") : `${numbers.slice(0, -1).join(", ")} and ${numbers[numbers.length - 1]}`;
26
+ const AUTO_SAVE_AFTER_MS = 2500;
27
+ const DEFAULT_NAMES = /^(new|untitled) presentation( \d+)?$/i;
26
28
  const STALE = "This presentation was changed outside the editor: Save to draw its slides again.";
27
29
  const hasContent = () => hasUnsavedChanges(usePresentation.getState());
28
30
  function ConnectedSlidesTab({
@@ -138,6 +140,29 @@ function ConnectedSlidesTab({
138
140
  )
139
141
  ).finally(() => setLoading(false));
140
142
  }, [currentVersion, heldVersion, isDraft, version, loading, open, documentId, target, notify]);
143
+ const draftHasContent = usePresentation(
144
+ (state) => isDraft && state.boundTo === target && state.savedId === null && hasUnsavedChanges(state)
145
+ );
146
+ const autoSaved = useRef(null);
147
+ useEffect(() => {
148
+ if (!draftHasContent || autoSaved.current === target) return;
149
+ const timer = setTimeout(() => {
150
+ autoSaved.current = target;
151
+ const state = usePresentation.getState();
152
+ const firstTitle = state.slides.find((slide) => slide.caption.trim())?.caption.trim();
153
+ const named = state.title && !DEFAULT_NAMES.test(state.title) ? state.title : null;
154
+ const title = named ?? firstTitle ?? (state.title || "Untitled presentation");
155
+ if (title !== state.title) state.setTitle(title);
156
+ void save(title).catch((error) => {
157
+ autoSaved.current = null;
158
+ notify(
159
+ error instanceof Error ? error.message : "The presentation could not be saved",
160
+ "error"
161
+ );
162
+ });
163
+ }, AUTO_SAVE_AFTER_MS);
164
+ return () => clearTimeout(timer);
165
+ }, [draftHasContent, target, save, draftTitle, notify]);
141
166
  useEffect(() => {
142
167
  if (isDraft || version || savedId !== documentId || boundTo !== target) return;
143
168
  return registerDocumentTurnFlush(documentId, async () => {
@@ -194,20 +219,17 @@ function ConnectedSlidesTab({
194
219
  SlidesEditor,
195
220
  {
196
221
  ...controller.editor,
197
- actions: /* @__PURE__ */ jsxs(Fragment, { children: [
198
- controller.editor.actions,
199
- /* @__PURE__ */ jsx(
200
- Button,
222
+ menuItems: /* @__PURE__ */ jsxs(Fragment, { children: [
223
+ controller.editor.menuItems,
224
+ /* @__PURE__ */ jsxs(
225
+ DropdownMenuItem,
201
226
  {
202
- type: "button",
203
- size: "icon",
204
- variant: "ghost",
205
- className: "size-8 text-muted-foreground hover:text-foreground pointer-coarse:size-11",
206
- title: "Edit in its own window, beside the documents you clip from",
207
- "aria-label": "Pop out",
208
- onClick: () => openPresentation(session, { windows, title: popoutTitle("Presentation") }),
227
+ onSelect: () => openPresentation(session, { windows, title: popoutTitle("Presentation") }),
209
228
  "data-testid": "presentation-pop-out",
210
- children: /* @__PURE__ */ jsx(ExternalLink, { className: "size-4" })
229
+ children: [
230
+ /* @__PURE__ */ jsx(ExternalLink, { className: "size-4" }),
231
+ /* @__PURE__ */ jsx("span", { className: "ml-2", children: "Edit in its own window" })
232
+ ]
211
233
  }
212
234
  )
213
235
  ] }),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/connection/presentation/ConnectedSlidesTab.tsx"],
4
- "sourcesContent": ["/**\n * A presentation's slides tab in the document viewer: the slides editor, on the presentation the\n * viewer shows. Opening a saved presentation loads it (unless it is the one being edited); a new\n * one (a presentation draft) takes the clips gathered so far. Clips gathered with no presentation\n * open join the one opened next. Saving a draft makes it a document, and the tab moves to it.\n *\n * The editor can also move to the presentation's own window (Pop out), beside the documents its\n * clips come from; the tab then says so. Present opens that window with the slides alone.\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { ExternalLink, Loader2, PanelLeftClose, Presentation } from 'lucide-react'\nimport {\n isPanelOpen,\n registerDocumentTurnFlush,\n useArbiSessionState,\n useArbiSessionStore,\n} from '@arbidocs/react'\nimport { Button } from '@arbidocs/react/ui'\nimport { useBlocksEnvironment } from '../../provider/environment'\nimport { usePanelHostSupport } from '../../shell/panel-host'\nimport {\n PRESENTATION_PANEL,\n openPresentation,\n presentationWindowOnlyPresents,\n} from '../../presentation/panel-id'\nimport { SlidesEditor } from '../../presentation/SlidesEditor'\nimport { hasUnsavedChanges, usePresentation } from '../../presentation/store'\nimport { EmptyState } from '../../components/EmptyState'\nimport { editPresentation } from './requests'\nimport { usePresentationController } from './usePresentationController'\nimport { usePresentationFiles } from './usePresentationFiles'\n\nexport interface ConnectedSlidesTabProps {\n /** The viewed document's id, or a presentation draft's (`draft-\u2026`). */\n documentId: string\n /** The version on screen, when an older one. */\n version?: number\n /** The document's current version: a newer one than the one being edited is opened. */\n currentVersion?: number\n /** A new presentation, not saved yet: its draft title. */\n draftTitle?: string\n}\n\n/** \"2, 3 and 5\". */\nconst listOf = (numbers: number[]) =>\n numbers.length < 2\n ? String(numbers[0] ?? '')\n : `${numbers.slice(0, -1).join(', ')} and ${numbers[numbers.length - 1]}`\n\nconst STALE = 'This presentation was changed outside the editor: Save to draw its slides again.'\n\n/** The presentation has anything in it worth asking about before it is replaced. */\nconst hasContent = () => hasUnsavedChanges(usePresentation.getState())\n\nexport function ConnectedSlidesTab({\n documentId,\n version,\n currentVersion,\n draftTitle,\n}: ConnectedSlidesTabProps) {\n const session = useArbiSessionStore()\n const { windows, popoutTitle } = usePanelHostSupport()\n const { confirm } = useBlocksEnvironment()\n const { open, save } = usePresentationFiles()\n const notify = usePresentation((s) => s.notify)\n const boundTo = usePresentation((s) => s.boundTo)\n const savedId = usePresentation((s) => s.savedId)\n const [loading, setLoading] = useState(false)\n const isDraft = draftTitle !== undefined\n const target = version ? `${documentId}@v${version}` : documentId\n const poppedOut = useArbiSessionState(\n (state) => isPanelOpen(state, PRESENTATION_PANEL) && !presentationWindowOnlyPresents()\n )\n const controller = usePresentationController(!poppedOut)\n const asked = useRef<string | null>(null)\n // Another presentation, with changes, is being edited and the user kept it.\n const [declined, setDeclined] = useState(false)\n const [force, setForce] = useState(0)\n\n // Show the presentation this tab is for (once per presentation: a remount does not ask again).\n useEffect(() => {\n const state = usePresentation.getState()\n if (state.boundTo === target || asked.current === `${target}#${force}`) return\n asked.current = `${target}#${force}`\n setDeclined(false)\n void (async () => {\n // Clips gathered with no presentation open join this one.\n const gathered = state.boundTo === null ? state.clips : []\n if (state.boundTo !== null && hasContent() && !force) {\n const ok = await confirm({\n title: 'Open presentation',\n message: 'Replace the presentation being edited? Save it first to keep its changes.',\n kind: 'confirm',\n })\n if (!ok) return setDeclined(true)\n }\n if (isDraft) {\n if (state.boundTo !== null) state.reset()\n usePresentation.getState().bind(target)\n usePresentation.getState().setTitle(draftTitle || 'Untitled presentation')\n return\n }\n setLoading(true)\n try {\n const { missing, stale, imported, changedOutside } = await open(documentId, version)\n usePresentation.getState().bind(target)\n if (gathered.length) usePresentation.getState().adoptClips(gathered)\n if (stale) notify(STALE, 'error')\n if (imported) {\n notify(\n ['Opened from PowerPoint, ready to edit.', ...imported.notes].join(' \u00B7 '),\n 'success'\n )\n }\n if (changedOutside) {\n notify(\n `${changedOutside.length === 1 ? `Slide ${changedOutside[0]} was` : `Slides ${listOf(changedOutside)} were`} changed in PowerPoint: brought in as changed there.`,\n 'success'\n )\n }\n if (missing) {\n notify(\n `${missing} clip${missing === 1 ? '' : 's'} could not be opened (moved, deleted or not shared with you)`,\n 'error'\n )\n }\n } catch (error) {\n notify(\n error instanceof Error ? error.message : 'The presentation could not be opened',\n 'error'\n )\n } finally {\n setLoading(false)\n }\n })()\n // eslint-disable-next-line react-hooks/exhaustive-deps -- a draft's title changes as it is typed\n }, [target, documentId, version, isDraft, force, open, confirm, notify])\n\n // A draft's title is the presentation's.\n useEffect(() => {\n if (isDraft && boundTo === target && draftTitle) usePresentation.getState().setTitle(draftTitle)\n }, [isDraft, boundTo, target, draftTitle])\n\n // Saved as another document (a draft's first save, Save as, an older file saved as a\n // PowerPoint): the viewer follows it. Only once this tab's presentation was the one edited\n // (another, still bound as the tab opens, is not \"its\" save).\n const wasBound = useRef(false)\n useEffect(() => {\n if (boundTo === target) wasBound.current = true\n if (!wasBound.current || !savedId || boundTo !== savedId || savedId === target) return\n if (isDraft) session.clearDraftDocument(documentId)\n editPresentation(session, { kind: 'open', documentId: savedId })\n }, [isDraft, savedId, boundTo, target, documentId, session])\n\n // A newer version saved meanwhile (by ARBI, asked in the chat beside the deck, or by someone\n // else): shown at once, unless this one has changes of its own, which are never thrown away.\n const heldVersion = usePresentation((s) => (s.boundTo === target ? s.version : null))\n const toldNewer = useRef<number | null>(null)\n useEffect(() => {\n if (isDraft || version || !currentVersion || heldVersion === null) return\n if (currentVersion <= heldVersion || loading) return\n if (hasContent()) {\n if (toldNewer.current === currentVersion) return\n toldNewer.current = currentVersion\n notify(\n `A newer version (${currentVersion}) was saved while you edited. Save as\u2026 keeps yours; reopen the presentation to see it.`,\n 'error'\n )\n return\n }\n setLoading(true)\n void open(documentId)\n .then(() => {\n usePresentation.getState().bind(target)\n notify(`Updated to version ${currentVersion}.`, 'success')\n })\n .catch((error: unknown) =>\n notify(\n error instanceof Error ? error.message : 'The new version could not be opened',\n 'error'\n )\n )\n .finally(() => setLoading(false))\n }, [currentVersion, heldVersion, isDraft, version, loading, open, documentId, target, notify])\n\n // A chat turn about this presentation (the chat beside it) saves its unsaved changes first, so\n // ARBI works on the slides as they are on screen.\n useEffect(() => {\n if (isDraft || version || savedId !== documentId || boundTo !== target) return\n return registerDocumentTurnFlush(documentId, async () => {\n const state = usePresentation.getState()\n if (state.savedId === documentId && hasUnsavedChanges(state)) await save(state.title)\n })\n }, [isDraft, version, savedId, boundTo, documentId, target, save])\n\n const present = useCallback(() => {\n openPresentation(session, { present: true, windows, title: popoutTitle('Presentation') })\n usePresentation.getState().setPresenting(true)\n }, [session, windows, popoutTitle])\n\n if (poppedOut) {\n return (\n <div className=\"flex h-full items-center justify-center p-6\" data-testid=\"slides-popped-out\">\n <EmptyState\n icon={ExternalLink}\n title=\"Editing in its own window\"\n description=\"The presentation is open in its own window, beside the documents you clip from.\"\n action={\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => session.closePanel(PRESENTATION_PANEL)}\n >\n <PanelLeftClose className=\"size-4\" />\n Bring it back here\n </Button>\n }\n />\n </div>\n )\n }\n if (declined && boundTo !== target) {\n return (\n <div className=\"flex h-full items-center justify-center p-6\" data-testid=\"slides-declined\">\n <EmptyState\n icon={Presentation}\n title=\"Another presentation is being edited\"\n description=\"It has changes. Save it first, or open this one in its place.\"\n action={\n <Button type=\"button\" variant=\"outline\" onClick={() => setForce((n) => n + 1)}>\n Open this one instead\n </Button>\n }\n />\n </div>\n )\n }\n if (loading || boundTo !== target) {\n return (\n <div className=\"flex h-full items-center justify-center gap-2 text-sm text-muted-foreground\">\n <Loader2 className=\"size-4 animate-spin\" />\n Opening the presentation\u2026\n </div>\n )\n }\n return (\n <>\n {controller.dialogs}\n <SlidesEditor\n {...controller.editor}\n actions={\n <>\n {controller.editor.actions}\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=\"Edit in its own window, beside the documents you clip from\"\n aria-label=\"Pop out\"\n onClick={() =>\n openPresentation(session, { windows, title: popoutTitle('Presentation') })\n }\n data-testid=\"presentation-pop-out\"\n >\n <ExternalLink className=\"size-4\" />\n </Button>\n </>\n }\n onPresent={present}\n />\n </>\n )\n}\n"],
5
- "mappings": ";AAiNY,SA2CF,UAtCI,KALF;AAvMZ,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AACzD,SAAS,cAAc,SAAS,gBAAgB,oBAAoB;AACpE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc;AACvB,SAAS,4BAA4B;AACrC,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB,uBAAuB;AACnD,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB;AACjC,SAAS,iCAAiC;AAC1C,SAAS,4BAA4B;AAcrC,MAAM,SAAS,CAAC,YACd,QAAQ,SAAS,IACb,OAAO,QAAQ,CAAC,KAAK,EAAE,IACvB,GAAG,QAAQ,MAAM,GAAG,EAAE,EAAE,KAAK,IAAI,CAAC,QAAQ,QAAQ,QAAQ,SAAS,CAAC,CAAC;AAE3E,MAAM,QAAQ;AAGd,MAAM,aAAa,MAAM,kBAAkB,gBAAgB,SAAS,CAAC;AAE9D,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA4B;AAC1B,QAAM,UAAU,oBAAoB;AACpC,QAAM,EAAE,SAAS,YAAY,IAAI,oBAAoB;AACrD,QAAM,EAAE,QAAQ,IAAI,qBAAqB;AACzC,QAAM,EAAE,MAAM,KAAK,IAAI,qBAAqB;AAC5C,QAAM,SAAS,gBAAgB,CAAC,MAAM,EAAE,MAAM;AAC9C,QAAM,UAAU,gBAAgB,CAAC,MAAM,EAAE,OAAO;AAChD,QAAM,UAAU,gBAAgB,CAAC,MAAM,EAAE,OAAO;AAChD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,UAAU,eAAe;AAC/B,QAAM,SAAS,UAAU,GAAG,UAAU,KAAK,OAAO,KAAK;AACvD,QAAM,YAAY;AAAA,IAChB,CAAC,UAAU,YAAY,OAAO,kBAAkB,KAAK,CAAC,+BAA+B;AAAA,EACvF;AACA,QAAM,aAAa,0BAA0B,CAAC,SAAS;AACvD,QAAM,QAAQ,OAAsB,IAAI;AAExC,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,CAAC;AAGpC,YAAU,MAAM;AACd,UAAM,QAAQ,gBAAgB,SAAS;AACvC,QAAI,MAAM,YAAY,UAAU,MAAM,YAAY,GAAG,MAAM,IAAI,KAAK,GAAI;AACxE,UAAM,UAAU,GAAG,MAAM,IAAI,KAAK;AAClC,gBAAY,KAAK;AACjB,UAAM,YAAY;AAEhB,YAAM,WAAW,MAAM,YAAY,OAAO,MAAM,QAAQ,CAAC;AACzD,UAAI,MAAM,YAAY,QAAQ,WAAW,KAAK,CAAC,OAAO;AACpD,cAAM,KAAK,MAAM,QAAQ;AAAA,UACvB,OAAO;AAAA,UACP,SAAS;AAAA,UACT,MAAM;AAAA,QACR,CAAC;AACD,YAAI,CAAC,GAAI,QAAO,YAAY,IAAI;AAAA,MAClC;AACA,UAAI,SAAS;AACX,YAAI,MAAM,YAAY,KAAM,OAAM,MAAM;AACxC,wBAAgB,SAAS,EAAE,KAAK,MAAM;AACtC,wBAAgB,SAAS,EAAE,SAAS,cAAc,uBAAuB;AACzE;AAAA,MACF;AACA,iBAAW,IAAI;AACf,UAAI;AACF,cAAM,EAAE,SAAS,OAAO,UAAU,eAAe,IAAI,MAAM,KAAK,YAAY,OAAO;AACnF,wBAAgB,SAAS,EAAE,KAAK,MAAM;AACtC,YAAI,SAAS,OAAQ,iBAAgB,SAAS,EAAE,WAAW,QAAQ;AACnE,YAAI,MAAO,QAAO,OAAO,OAAO;AAChC,YAAI,UAAU;AACZ;AAAA,YACE,CAAC,0CAA0C,GAAG,SAAS,KAAK,EAAE,KAAK,QAAK;AAAA,YACxE;AAAA,UACF;AAAA,QACF;AACA,YAAI,gBAAgB;AAClB;AAAA,YACE,GAAG,eAAe,WAAW,IAAI,SAAS,eAAe,CAAC,CAAC,SAAS,UAAU,OAAO,cAAc,CAAC,OAAO;AAAA,YAC3G;AAAA,UACF;AAAA,QACF;AACA,YAAI,SAAS;AACX;AAAA,YACE,GAAG,OAAO,QAAQ,YAAY,IAAI,KAAK,GAAG;AAAA,YAC1C;AAAA,UACF;AAAA,QACF;AAAA,MACF,SAAS,OAAO;AACd;AAAA,UACE,iBAAiB,QAAQ,MAAM,UAAU;AAAA,UACzC;AAAA,QACF;AAAA,MACF,UAAE;AACA,mBAAW,KAAK;AAAA,MAClB;AAAA,IACF,GAAG;AAAA,EAEL,GAAG,CAAC,QAAQ,YAAY,SAAS,SAAS,OAAO,MAAM,SAAS,MAAM,CAAC;AAGvE,YAAU,MAAM;AACd,QAAI,WAAW,YAAY,UAAU,WAAY,iBAAgB,SAAS,EAAE,SAAS,UAAU;AAAA,EACjG,GAAG,CAAC,SAAS,SAAS,QAAQ,UAAU,CAAC;AAKzC,QAAM,WAAW,OAAO,KAAK;AAC7B,YAAU,MAAM;AACd,QAAI,YAAY,OAAQ,UAAS,UAAU;AAC3C,QAAI,CAAC,SAAS,WAAW,CAAC,WAAW,YAAY,WAAW,YAAY,OAAQ;AAChF,QAAI,QAAS,SAAQ,mBAAmB,UAAU;AAClD,qBAAiB,SAAS,EAAE,MAAM,QAAQ,YAAY,QAAQ,CAAC;AAAA,EACjE,GAAG,CAAC,SAAS,SAAS,SAAS,QAAQ,YAAY,OAAO,CAAC;AAI3D,QAAM,cAAc,gBAAgB,CAAC,MAAO,EAAE,YAAY,SAAS,EAAE,UAAU,IAAK;AACpF,QAAM,YAAY,OAAsB,IAAI;AAC5C,YAAU,MAAM;AACd,QAAI,WAAW,WAAW,CAAC,kBAAkB,gBAAgB,KAAM;AACnE,QAAI,kBAAkB,eAAe,QAAS;AAC9C,QAAI,WAAW,GAAG;AAChB,UAAI,UAAU,YAAY,eAAgB;AAC1C,gBAAU,UAAU;AACpB;AAAA,QACE,oBAAoB,cAAc;AAAA,QAClC;AAAA,MACF;AACA;AAAA,IACF;AACA,eAAW,IAAI;AACf,SAAK,KAAK,UAAU,EACjB,KAAK,MAAM;AACV,sBAAgB,SAAS,EAAE,KAAK,MAAM;AACtC,aAAO,sBAAsB,cAAc,KAAK,SAAS;AAAA,IAC3D,CAAC,EACA;AAAA,MAAM,CAAC,UACN;AAAA,QACE,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QACzC;AAAA,MACF;AAAA,IACF,EACC,QAAQ,MAAM,WAAW,KAAK,CAAC;AAAA,EACpC,GAAG,CAAC,gBAAgB,aAAa,SAAS,SAAS,SAAS,MAAM,YAAY,QAAQ,MAAM,CAAC;AAI7F,YAAU,MAAM;AACd,QAAI,WAAW,WAAW,YAAY,cAAc,YAAY,OAAQ;AACxE,WAAO,0BAA0B,YAAY,YAAY;AACvD,YAAM,QAAQ,gBAAgB,SAAS;AACvC,UAAI,MAAM,YAAY,cAAc,kBAAkB,KAAK,EAAG,OAAM,KAAK,MAAM,KAAK;AAAA,IACtF,CAAC;AAAA,EACH,GAAG,CAAC,SAAS,SAAS,SAAS,SAAS,YAAY,QAAQ,IAAI,CAAC;AAEjE,QAAM,UAAU,YAAY,MAAM;AAChC,qBAAiB,SAAS,EAAE,SAAS,MAAM,SAAS,OAAO,YAAY,cAAc,EAAE,CAAC;AACxF,oBAAgB,SAAS,EAAE,cAAc,IAAI;AAAA,EAC/C,GAAG,CAAC,SAAS,SAAS,WAAW,CAAC;AAElC,MAAI,WAAW;AACb,WACE,oBAAC,SAAI,WAAU,+CAA8C,eAAY,qBACvE;AAAA,MAAC;AAAA;AAAA,QACC,MAAM;AAAA,QACN,OAAM;AAAA,QACN,aAAY;AAAA,QACZ,QACE;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,SAAS,MAAM,QAAQ,WAAW,kBAAkB;AAAA,YAEpD;AAAA,kCAAC,kBAAe,WAAU,UAAS;AAAA,cAAE;AAAA;AAAA;AAAA,QAEvC;AAAA;AAAA,IAEJ,GACF;AAAA,EAEJ;AACA,MAAI,YAAY,YAAY,QAAQ;AAClC,WACE,oBAAC,SAAI,WAAU,+CAA8C,eAAY,mBACvE;AAAA,MAAC;AAAA;AAAA,QACC,MAAM;AAAA,QACN,OAAM;AAAA,QACN,aAAY;AAAA,QACZ,QACE,oBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,SAAS,MAAM,SAAS,CAAC,MAAM,IAAI,CAAC,GAAG,mCAE/E;AAAA;AAAA,IAEJ,GACF;AAAA,EAEJ;AACA,MAAI,WAAW,YAAY,QAAQ;AACjC,WACE,qBAAC,SAAI,WAAU,+EACb;AAAA,0BAAC,WAAQ,WAAU,uBAAsB;AAAA,MAAE;AAAA,OAE7C;AAAA,EAEJ;AACA,SACE,iCACG;AAAA,eAAW;AAAA,IACZ;AAAA,MAAC;AAAA;AAAA,QACE,GAAG,WAAW;AAAA,QACf,SACE,iCACG;AAAA,qBAAW,OAAO;AAAA,UACnB;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,WAAU;AAAA,cACV,OAAM;AAAA,cACN,cAAW;AAAA,cACX,SAAS,MACP,iBAAiB,SAAS,EAAE,SAAS,OAAO,YAAY,cAAc,EAAE,CAAC;AAAA,cAE3E,eAAY;AAAA,cAEZ,8BAAC,gBAAa,WAAU,UAAS;AAAA;AAAA,UACnC;AAAA,WACF;AAAA,QAEF,WAAW;AAAA;AAAA,IACb;AAAA,KACF;AAEJ;",
4
+ "sourcesContent": ["/**\n * A presentation's slides tab in the document viewer: the slides editor, on the presentation the\n * viewer shows. Opening a saved presentation loads it (unless it is the one being edited); a new\n * one (a presentation draft) takes the clips gathered so far. Clips gathered with no presentation\n * open join the one opened next. Saving a draft makes it a document, and the tab moves to it.\n *\n * The editor can also move to the presentation's own window (Pop out), beside the documents its\n * clips come from; the tab then says so. Present opens that window with the slides alone.\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { ExternalLink, Loader2, PanelLeftClose, Presentation } from 'lucide-react'\nimport {\n isPanelOpen,\n registerDocumentTurnFlush,\n useArbiSessionState,\n useArbiSessionStore,\n} from '@arbidocs/react'\nimport { Button, DropdownMenuItem } from '@arbidocs/react/ui'\nimport { useBlocksEnvironment } from '../../provider/environment'\nimport { usePanelHostSupport } from '../../shell/panel-host'\nimport {\n PRESENTATION_PANEL,\n openPresentation,\n presentationWindowOnlyPresents,\n} from '../../presentation/panel-id'\nimport { SlidesEditor } from '../../presentation/SlidesEditor'\nimport { hasUnsavedChanges, usePresentation } from '../../presentation/store'\nimport { EmptyState } from '../../components/EmptyState'\nimport { editPresentation } from './requests'\nimport { usePresentationController } from './usePresentationController'\nimport { usePresentationFiles } from './usePresentationFiles'\n\nexport interface ConnectedSlidesTabProps {\n /** The viewed document's id, or a presentation draft's (`draft-\u2026`). */\n documentId: string\n /** The version on screen, when an older one. */\n version?: number\n /** The document's current version: a newer one than the one being edited is opened. */\n currentVersion?: number\n /** A new presentation, not saved yet: its draft title. */\n draftTitle?: string\n}\n\n/** \"2, 3 and 5\". */\nconst listOf = (numbers: number[]) =>\n numbers.length < 2\n ? String(numbers[0] ?? '')\n : `${numbers.slice(0, -1).join(', ')} and ${numbers[numbers.length - 1]}`\n\n/** A new presentation saves itself this long after its first content (typing settles first). */\nconst AUTO_SAVE_AFTER_MS = 2500\n/** The names a new presentation starts with (not chosen by anyone). */\nconst DEFAULT_NAMES = /^(new|untitled) presentation( \\d+)?$/i\n\nconst STALE = 'This presentation was changed outside the editor: Save to draw its slides again.'\n\n/** The presentation has anything in it worth asking about before it is replaced. */\nconst hasContent = () => hasUnsavedChanges(usePresentation.getState())\n\nexport function ConnectedSlidesTab({\n documentId,\n version,\n currentVersion,\n draftTitle,\n}: ConnectedSlidesTabProps) {\n const session = useArbiSessionStore()\n const { windows, popoutTitle } = usePanelHostSupport()\n const { confirm } = useBlocksEnvironment()\n const { open, save } = usePresentationFiles()\n const notify = usePresentation((s) => s.notify)\n const boundTo = usePresentation((s) => s.boundTo)\n const savedId = usePresentation((s) => s.savedId)\n const [loading, setLoading] = useState(false)\n const isDraft = draftTitle !== undefined\n const target = version ? `${documentId}@v${version}` : documentId\n const poppedOut = useArbiSessionState(\n (state) => isPanelOpen(state, PRESENTATION_PANEL) && !presentationWindowOnlyPresents()\n )\n const controller = usePresentationController(!poppedOut)\n const asked = useRef<string | null>(null)\n // Another presentation, with changes, is being edited and the user kept it.\n const [declined, setDeclined] = useState(false)\n const [force, setForce] = useState(0)\n\n // Show the presentation this tab is for (once per presentation: a remount does not ask again).\n useEffect(() => {\n const state = usePresentation.getState()\n if (state.boundTo === target || asked.current === `${target}#${force}`) return\n asked.current = `${target}#${force}`\n setDeclined(false)\n void (async () => {\n // Clips gathered with no presentation open join this one.\n const gathered = state.boundTo === null ? state.clips : []\n if (state.boundTo !== null && hasContent() && !force) {\n const ok = await confirm({\n title: 'Open presentation',\n message: 'Replace the presentation being edited? Save it first to keep its changes.',\n kind: 'confirm',\n })\n if (!ok) return setDeclined(true)\n }\n if (isDraft) {\n if (state.boundTo !== null) state.reset()\n usePresentation.getState().bind(target)\n usePresentation.getState().setTitle(draftTitle || 'Untitled presentation')\n return\n }\n setLoading(true)\n try {\n const { missing, stale, imported, changedOutside } = await open(documentId, version)\n usePresentation.getState().bind(target)\n if (gathered.length) usePresentation.getState().adoptClips(gathered)\n if (stale) notify(STALE, 'error')\n if (imported) {\n notify(\n ['Opened from PowerPoint, ready to edit.', ...imported.notes].join(' \u00B7 '),\n 'success'\n )\n }\n if (changedOutside) {\n notify(\n `${changedOutside.length === 1 ? `Slide ${changedOutside[0]} was` : `Slides ${listOf(changedOutside)} were`} changed in PowerPoint: brought in as changed there.`,\n 'success'\n )\n }\n if (missing) {\n notify(\n `${missing} clip${missing === 1 ? '' : 's'} could not be opened (moved, deleted or not shared with you)`,\n 'error'\n )\n }\n } catch (error) {\n notify(\n error instanceof Error ? error.message : 'The presentation could not be opened',\n 'error'\n )\n } finally {\n setLoading(false)\n }\n })()\n // eslint-disable-next-line react-hooks/exhaustive-deps -- a draft's title changes as it is typed\n }, [target, documentId, version, isDraft, force, open, confirm, notify])\n\n // A draft's title is the presentation's.\n useEffect(() => {\n if (isDraft && boundTo === target && draftTitle) usePresentation.getState().setTitle(draftTitle)\n }, [isDraft, boundTo, target, draftTitle])\n\n // Saved as another document (a draft's first save, Save as, an older file saved as a\n // PowerPoint): the viewer follows it. Only once this tab's presentation was the one edited\n // (another, still bound as the tab opens, is not \"its\" save).\n const wasBound = useRef(false)\n useEffect(() => {\n if (boundTo === target) wasBound.current = true\n if (!wasBound.current || !savedId || boundTo !== savedId || savedId === target) return\n if (isDraft) session.clearDraftDocument(documentId)\n editPresentation(session, { kind: 'open', documentId: savedId })\n }, [isDraft, savedId, boundTo, target, documentId, session])\n\n // A newer version saved meanwhile (by ARBI, asked in the chat beside the deck, or by someone\n // else): shown at once, unless this one has changes of its own, which are never thrown away.\n const heldVersion = usePresentation((s) => (s.boundTo === target ? s.version : null))\n const toldNewer = useRef<number | null>(null)\n useEffect(() => {\n if (isDraft || version || !currentVersion || heldVersion === null) return\n if (currentVersion <= heldVersion || loading) return\n if (hasContent()) {\n if (toldNewer.current === currentVersion) return\n toldNewer.current = currentVersion\n notify(\n `A newer version (${currentVersion}) was saved while you edited. Save as\u2026 keeps yours; reopen the presentation to see it.`,\n 'error'\n )\n return\n }\n setLoading(true)\n void open(documentId)\n .then(() => {\n usePresentation.getState().bind(target)\n notify(`Updated to version ${currentVersion}.`, 'success')\n })\n .catch((error: unknown) =>\n notify(\n error instanceof Error ? error.message : 'The new version could not be opened',\n 'error'\n )\n )\n .finally(() => setLoading(false))\n }, [currentVersion, heldVersion, isDraft, version, loading, open, documentId, target, notify])\n\n // A new presentation saves itself once it has something in it (a clip, a title, a point): from\n // then on it is a document of Presentations, which a chat can work in and look at (its PDF), and\n // which is not lost with the tab. Later saves stay the person's (each a version they chose).\n const draftHasContent = usePresentation(\n (state) =>\n isDraft && state.boundTo === target && state.savedId === null && hasUnsavedChanges(state)\n )\n const autoSaved = useRef<string | null>(null)\n useEffect(() => {\n if (!draftHasContent || autoSaved.current === target) return\n const timer = setTimeout(() => {\n autoSaved.current = target\n const state = usePresentation.getState()\n // Still called what every new one is called: named after its first slide's title instead.\n const firstTitle = state.slides.find((slide) => slide.caption.trim())?.caption.trim()\n const named = state.title && !DEFAULT_NAMES.test(state.title) ? state.title : null\n const title = named ?? firstTitle ?? (state.title || 'Untitled presentation')\n if (title !== state.title) state.setTitle(title)\n void save(title).catch((error: unknown) => {\n autoSaved.current = null\n notify(\n error instanceof Error ? error.message : 'The presentation could not be saved',\n 'error'\n )\n })\n }, AUTO_SAVE_AFTER_MS)\n return () => clearTimeout(timer)\n }, [draftHasContent, target, save, draftTitle, notify])\n\n // A chat turn about this presentation (the chat beside it) saves its unsaved changes first, so\n // ARBI works on the slides as they are on screen.\n useEffect(() => {\n if (isDraft || version || savedId !== documentId || boundTo !== target) return\n return registerDocumentTurnFlush(documentId, async () => {\n const state = usePresentation.getState()\n if (state.savedId === documentId && hasUnsavedChanges(state)) await save(state.title)\n })\n }, [isDraft, version, savedId, boundTo, documentId, target, save])\n\n const present = useCallback(() => {\n openPresentation(session, { present: true, windows, title: popoutTitle('Presentation') })\n usePresentation.getState().setPresenting(true)\n }, [session, windows, popoutTitle])\n\n if (poppedOut) {\n return (\n <div className=\"flex h-full items-center justify-center p-6\" data-testid=\"slides-popped-out\">\n <EmptyState\n icon={ExternalLink}\n title=\"Editing in its own window\"\n description=\"The presentation is open in its own window, beside the documents you clip from.\"\n action={\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => session.closePanel(PRESENTATION_PANEL)}\n >\n <PanelLeftClose className=\"size-4\" />\n Bring it back here\n </Button>\n }\n />\n </div>\n )\n }\n if (declined && boundTo !== target) {\n return (\n <div className=\"flex h-full items-center justify-center p-6\" data-testid=\"slides-declined\">\n <EmptyState\n icon={Presentation}\n title=\"Another presentation is being edited\"\n description=\"It has changes. Save it first, or open this one in its place.\"\n action={\n <Button type=\"button\" variant=\"outline\" onClick={() => setForce((n) => n + 1)}>\n Open this one instead\n </Button>\n }\n />\n </div>\n )\n }\n if (loading || boundTo !== target) {\n return (\n <div className=\"flex h-full items-center justify-center gap-2 text-sm text-muted-foreground\">\n <Loader2 className=\"size-4 animate-spin\" />\n Opening the presentation\u2026\n </div>\n )\n }\n return (\n <>\n {controller.dialogs}\n <SlidesEditor\n {...controller.editor}\n menuItems={\n <>\n {controller.editor.menuItems}\n <DropdownMenuItem\n onSelect={() =>\n openPresentation(session, { windows, title: popoutTitle('Presentation') })\n }\n data-testid=\"presentation-pop-out\"\n >\n <ExternalLink className=\"size-4\" />\n <span className=\"ml-2\">Edit in its own window</span>\n </DropdownMenuItem>\n </>\n }\n onPresent={present}\n />\n </>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The viewer's spotlight, over its body: nothing until a clip is gathered, then the strip of
3
+ * gathered clips and, when shown, the spotlight itself (their own chunk, loaded then).
4
+ */
5
+ export declare function SpotlightLayer(): import("react").JSX.Element | null;
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { lazy, Suspense, useEffect } from "react";
4
+ import { useSpotlight } from "../../presentation/spotlight.js";
5
+ import { useConnectedWorkspace } from "../ConnectionProvider.js";
6
+ const SpotlightStrip = lazy(
7
+ () => import("./SpotlightStrip.js").then((m) => ({ default: m.SpotlightStrip }))
8
+ );
9
+ function SpotlightLayer() {
10
+ const { userId } = useConnectedWorkspace();
11
+ const gathered = useSpotlight((s) => s.clips.length > 0);
12
+ useEffect(() => useSpotlight.getState().own(userId ?? null), [userId]);
13
+ if (!gathered) return null;
14
+ return /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(SpotlightStrip, {}) });
15
+ }
16
+ export {
17
+ SpotlightLayer
18
+ };
19
+ //# sourceMappingURL=SpotlightLayer.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/connection/presentation/SpotlightLayer.tsx"],
4
+ "sourcesContent": ["/**\n * The viewer's spotlight, over its body: nothing until a clip is gathered, then the strip of\n * gathered clips and, when shown, the spotlight itself (their own chunk, loaded then).\n */\n\nimport { lazy, Suspense, useEffect } from 'react'\nimport { useSpotlight } from '../../presentation/spotlight'\nimport { useConnectedWorkspace } from '../ConnectionProvider'\n\nconst SpotlightStrip = lazy(() =>\n import('./SpotlightStrip').then((m) => ({ default: m.SpotlightStrip }))\n)\n\nexport function SpotlightLayer() {\n // Clips gathered by another user are dropped.\n const { userId } = useConnectedWorkspace()\n const gathered = useSpotlight((s) => s.clips.length > 0)\n useEffect(() => useSpotlight.getState().own(userId ?? null), [userId])\n if (!gathered) return null\n return (\n <Suspense fallback={null}>\n <SpotlightStrip />\n </Suspense>\n )\n}\n"],
5
+ "mappings": ";AAqBM;AAhBN,SAAS,MAAM,UAAU,iBAAiB;AAC1C,SAAS,oBAAoB;AAC7B,SAAS,6BAA6B;AAEtC,MAAM,iBAAiB;AAAA,EAAK,MAC1B,OAAO,kBAAkB,EAAE,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE;AACxE;AAEO,SAAS,iBAAiB;AAE/B,QAAM,EAAE,OAAO,IAAI,sBAAsB;AACzC,QAAM,WAAW,aAAa,CAAC,MAAM,EAAE,MAAM,SAAS,CAAC;AACvD,YAAU,MAAM,aAAa,SAAS,EAAE,IAAI,UAAU,IAAI,GAAG,CAAC,MAAM,CAAC;AACrE,MAAI,CAAC,SAAU,QAAO;AACtB,SACE,oBAAC,YAAS,UAAU,MAClB,8BAAC,kBAAe,GAClB;AAEJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The clips gathered in the viewer, in a small strip over the document: each clip (× takes it
3
+ * out), a note to show as the spotlight's title, another document to clip from, and Show (or S),
4
+ * which puts them in the spotlight over this same viewer. Rarer actions — keep them as a slide,
5
+ * add them to the presentation's tray, clear them — are in its "…" menu.
6
+ *
7
+ * The strip and the spotlight stay over whichever document the viewer shows, so clips are gathered
8
+ * from several documents in a row. Esc brings the document back where it was scrolled to.
9
+ */
10
+ export declare function SpotlightStrip(): import("react").JSX.Element;
@@ -0,0 +1,237 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { Eraser, MoreHorizontal, Inbox, Presentation, StickyNote, X } from "lucide-react";
5
+ import { useArbiSessionStore, useDocuments } from "@arbidocs/react";
6
+ import {
7
+ Button,
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuTrigger,
12
+ Input,
13
+ Popover,
14
+ PopoverContent,
15
+ PopoverTrigger,
16
+ cn,
17
+ getDocumentTitle
18
+ } from "@arbidocs/react/ui";
19
+ import { ClipImage } from "../../presentation/ClipImage.js";
20
+ import { SpotlightView } from "../../presentation/SpotlightView.js";
21
+ import { useSpotlight } from "../../presentation/spotlight.js";
22
+ import { DocumentPicker } from "../../settings/DocumentPicker.js";
23
+ const frameOf = (node) => node?.closest('[data-slot="viewer-frame"]') ?? null;
24
+ const isTyping = (target) => !!target?.closest?.('input, textarea, select, [contenteditable="true"]');
25
+ function clipOnPage(frame, clip) {
26
+ const clipped = Array.from(frame.querySelectorAll("[data-clip-document]")).find(
27
+ (element) => element.getAttribute("data-clip-document") === clip.documentId
28
+ );
29
+ const page = clipped?.querySelector(`[data-page-number="${clip.page}"]`);
30
+ if (!page) return null;
31
+ const box = page.getBoundingClientRect();
32
+ const view = frame.ownerDocument.defaultView;
33
+ const rect = {
34
+ left: box.left + clip.rect.x * box.width,
35
+ top: box.top + clip.rect.y * box.height,
36
+ width: clip.rect.w * box.width,
37
+ height: clip.rect.h * box.height
38
+ };
39
+ const visible = rect.width > 0 && rect.top + rect.height > 0 && rect.left + rect.width > 0 && (!view || rect.top < view.innerHeight && rect.left < view.innerWidth);
40
+ return visible ? rect : null;
41
+ }
42
+ function useDocumentSwitcher(clips) {
43
+ const session = useArbiSessionStore();
44
+ const { data: documents } = useDocuments();
45
+ const groups = useMemo(() => {
46
+ const all = (documents ?? []).map((doc) => ({
47
+ id: doc.external_id,
48
+ title: getDocumentTitle(doc, "Untitled"),
49
+ fileName: doc.file_name
50
+ })).sort((a, b) => a.title.localeCompare(b.title));
51
+ const clipped = new Set(clips.map((clip) => clip.documentId));
52
+ const from = all.filter((doc) => clipped.has(doc.id));
53
+ return [
54
+ ...from.length ? [{ id: "clipped", heading: "Clipped from", docs: from }] : [],
55
+ { id: "all", heading: "Documents", docs: all }
56
+ ];
57
+ }, [documents, clips]);
58
+ const open = (id) => session.setViewingDocument({ id, page: null });
59
+ return { groups, open };
60
+ }
61
+ function SpotlightStrip() {
62
+ const clips = useSpotlight((s) => s.clips);
63
+ const note = useSpotlight((s) => s.note);
64
+ const showing = useSpotlight((s) => s.showing);
65
+ const actions = useSpotlight.getState();
66
+ const switcher = useDocumentSwitcher(clips);
67
+ const [noteOpen, setNoteOpen] = useState(false);
68
+ const strip = useRef(null);
69
+ const [view, setView] = useState(null);
70
+ const stripRef = useCallback((node) => {
71
+ strip.current = node;
72
+ if (node) setView(node.ownerDocument.defaultView);
73
+ }, []);
74
+ useEffect(() => {
75
+ if (!view || showing) return;
76
+ const onKeyDown = (event) => {
77
+ if (event.metaKey || event.ctrlKey || event.altKey || isTyping(event.target)) return;
78
+ if (event.key !== "s" && event.key !== "S") return;
79
+ event.preventDefault();
80
+ useSpotlight.getState().show();
81
+ };
82
+ view.addEventListener("keydown", onKeyDown);
83
+ return () => view.removeEventListener("keydown", onKeyDown);
84
+ }, [view, showing]);
85
+ const scrolled = useRef(null);
86
+ useEffect(() => {
87
+ const scroller = frameOf(strip.current)?.querySelector(
88
+ '[data-slot="pdf-scroller"]'
89
+ );
90
+ if (showing) {
91
+ scrolled.current = scroller ? { element: scroller, top: scroller.scrollTop, left: scroller.scrollLeft } : null;
92
+ return;
93
+ }
94
+ const before = scrolled.current;
95
+ scrolled.current = null;
96
+ if (before?.element.isConnected) {
97
+ before.element.scrollTop = before.top;
98
+ before.element.scrollLeft = before.left;
99
+ }
100
+ }, [showing]);
101
+ const fromRect = useCallback((clip) => {
102
+ const frame = frameOf(strip.current);
103
+ const onPage = frame ? clipOnPage(frame, clip) : null;
104
+ return onPage ?? strip.current?.querySelector(`[data-spotlight-clip="${clip.id}"]`)?.getBoundingClientRect() ?? null;
105
+ }, []);
106
+ const count = `${clips.length} clip${clips.length === 1 ? "" : "s"}`;
107
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
108
+ /* @__PURE__ */ jsxs(
109
+ "div",
110
+ {
111
+ ref: stripRef,
112
+ className: cn(
113
+ "absolute bottom-[4.5rem] left-1/2 z-20 flex max-w-[calc(100%-1.5rem)] -translate-x-1/2 items-center gap-1.5 rounded-full border border-border bg-background/95 p-1 pl-1.5 shadow-lg backdrop-blur",
114
+ showing && "invisible"
115
+ ),
116
+ role: "toolbar",
117
+ "aria-label": "Gathered clips",
118
+ "data-testid": "spotlight-strip",
119
+ children: [
120
+ /* @__PURE__ */ jsx("ul", { className: "flex min-w-0 items-center gap-1 overflow-x-auto [scrollbar-width:none]", children: clips.map((clip, index) => /* @__PURE__ */ jsxs(
121
+ "li",
122
+ {
123
+ className: "group relative h-8 shrink-0 overflow-hidden rounded-md border border-border bg-white",
124
+ style: { width: `${Math.min(4.5, Math.max(1.5, 2 * clip.aspect))}rem` },
125
+ title: clip.caption,
126
+ "data-spotlight-clip": clip.id,
127
+ "data-testid": `spotlight-chip-${index}`,
128
+ children: [
129
+ /* @__PURE__ */ jsx(ClipImage, { clip, className: "h-full w-full" }),
130
+ /* @__PURE__ */ jsx(
131
+ "button",
132
+ {
133
+ type: "button",
134
+ className: "absolute inset-0 flex items-center justify-center bg-black/50 text-white opacity-0 transition-opacity focus-visible:opacity-100 group-hover:opacity-100",
135
+ onClick: () => actions.remove(clip.id),
136
+ "aria-label": `Remove ${clip.caption}`,
137
+ "data-testid": `spotlight-remove-${index}`,
138
+ children: /* @__PURE__ */ jsx(X, { className: "size-3.5" })
139
+ }
140
+ )
141
+ ]
142
+ },
143
+ clip.id
144
+ )) }),
145
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", "data-testid": "spotlight-count", children: count }),
146
+ /* @__PURE__ */ jsxs(Popover, { open: noteOpen, onOpenChange: setNoteOpen, children: [
147
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
148
+ Button,
149
+ {
150
+ type: "button",
151
+ variant: note ? "secondary" : "ghost",
152
+ size: "sm",
153
+ className: "h-7 max-w-40 shrink-0 gap-1 rounded-full px-2 text-xs",
154
+ title: note ? `Note: ${note}` : "Type a note to show as the title",
155
+ "data-testid": "spotlight-note",
156
+ children: [
157
+ /* @__PURE__ */ jsx(StickyNote, { className: "size-3.5 shrink-0" }),
158
+ note ? /* @__PURE__ */ jsx("span", { className: "truncate", children: note }) : /* @__PURE__ */ jsx("span", { children: "Note" })
159
+ ]
160
+ }
161
+ ) }),
162
+ /* @__PURE__ */ jsx(PopoverContent, { side: "top", className: "w-72 p-2", children: /* @__PURE__ */ jsx(
163
+ Input,
164
+ {
165
+ autoFocus: true,
166
+ value: note,
167
+ placeholder: "Type a note",
168
+ onChange: (event) => actions.setNote(event.target.value),
169
+ onKeyDown: (event) => {
170
+ if (event.key === "Enter") setNoteOpen(false);
171
+ },
172
+ "data-testid": "spotlight-note-input"
173
+ }
174
+ ) })
175
+ ] }),
176
+ /* @__PURE__ */ jsx("div", { className: "w-44 shrink-0 [&_[role=combobox]]:h-7 [&_[role=combobox]]:rounded-full [&_[role=combobox]]:text-xs", children: /* @__PURE__ */ jsx(
177
+ DocumentPicker,
178
+ {
179
+ groups: switcher.groups,
180
+ value: "",
181
+ onValueChange: switcher.open,
182
+ labels: { placeholder: "Clip another document" }
183
+ }
184
+ ) }),
185
+ /* @__PURE__ */ jsxs(
186
+ Button,
187
+ {
188
+ type: "button",
189
+ size: "sm",
190
+ className: "h-7 shrink-0 gap-1 rounded-full px-3 text-xs",
191
+ onClick: actions.show,
192
+ title: "Show the clips over the document (S)",
193
+ "data-testid": "spotlight-show",
194
+ children: [
195
+ /* @__PURE__ */ jsx(Presentation, { className: "size-3.5" }),
196
+ "Show"
197
+ ]
198
+ }
199
+ ),
200
+ /* @__PURE__ */ jsxs(DropdownMenu, { children: [
201
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
202
+ Button,
203
+ {
204
+ type: "button",
205
+ variant: "ghost",
206
+ size: "icon",
207
+ className: "size-7 shrink-0 rounded-full",
208
+ "aria-label": "More",
209
+ "data-testid": "spotlight-more",
210
+ children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "size-4" })
211
+ }
212
+ ) }),
213
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", side: "top", children: [
214
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { onSelect: actions.keep, "data-testid": "spotlight-menu-keep", children: [
215
+ /* @__PURE__ */ jsx(Presentation, { className: "mr-2 size-4" }),
216
+ " Keep as slide"
217
+ ] }),
218
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { onSelect: actions.addToTray, "data-testid": "spotlight-menu-tray", children: [
219
+ /* @__PURE__ */ jsx(Inbox, { className: "mr-2 size-4" }),
220
+ " Add to the presentation tray"
221
+ ] }),
222
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { onSelect: actions.clear, "data-testid": "spotlight-menu-clear", children: [
223
+ /* @__PURE__ */ jsx(Eraser, { className: "mr-2 size-4" }),
224
+ " Clear the clips"
225
+ ] })
226
+ ] })
227
+ ] })
228
+ ]
229
+ }
230
+ ),
231
+ showing && /* @__PURE__ */ jsx(SpotlightView, { fromRect })
232
+ ] });
233
+ }
234
+ export {
235
+ SpotlightStrip
236
+ };
237
+ //# sourceMappingURL=SpotlightStrip.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/connection/presentation/SpotlightStrip.tsx"],
4
+ "sourcesContent": ["/**\n * The clips gathered in the viewer, in a small strip over the document: each clip (\u00D7 takes it\n * out), a note to show as the spotlight's title, another document to clip from, and Show (or S),\n * which puts them in the spotlight over this same viewer. Rarer actions \u2014 keep them as a slide,\n * add them to the presentation's tray, clear them \u2014 are in its \"\u2026\" menu.\n *\n * The strip and the spotlight stay over whichever document the viewer shows, so clips are gathered\n * from several documents in a row. Esc brings the document back where it was scrolled to.\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { Eraser, MoreHorizontal, Inbox, Presentation, StickyNote, X } from 'lucide-react'\nimport { useArbiSessionStore, useDocuments } from '@arbidocs/react'\nimport {\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n Input,\n Popover,\n PopoverContent,\n PopoverTrigger,\n cn,\n getDocumentTitle,\n} from '@arbidocs/react/ui'\nimport { ClipImage } from '../../presentation/ClipImage'\nimport type { Clip } from '../../presentation/model'\nimport { SpotlightView, type ScreenRect } from '../../presentation/SpotlightView'\nimport { useSpotlight } from '../../presentation/spotlight'\nimport { DocumentPicker, type PickerGroup } from '../../settings/DocumentPicker'\n\n/** The viewer this strip is over (its frame), found from the strip's own element. */\nconst frameOf = (node: HTMLElement | null) =>\n node?.closest<HTMLElement>('[data-slot=\"viewer-frame\"]') ?? null\n\nconst isTyping = (target: EventTarget | null) =>\n !!(target as HTMLElement | null)?.closest?.('input, textarea, select, [contenteditable=\"true\"]')\n\n/**\n * Where a clip is on screen now: its area of its page, while its document is clipped in the viewer\n * (`data-clip-document`) and that page is in view.\n */\nfunction clipOnPage(frame: HTMLElement, clip: Clip): ScreenRect | null {\n const clipped = Array.from(frame.querySelectorAll('[data-clip-document]')).find(\n (element) => element.getAttribute('data-clip-document') === clip.documentId\n )\n const page = clipped?.querySelector<HTMLElement>(`[data-page-number=\"${clip.page}\"]`)\n if (!page) return null\n const box = page.getBoundingClientRect()\n const view = frame.ownerDocument.defaultView\n const rect = {\n left: box.left + clip.rect.x * box.width,\n top: box.top + clip.rect.y * box.height,\n width: clip.rect.w * box.width,\n height: clip.rect.h * box.height,\n }\n const visible =\n rect.width > 0 &&\n rect.top + rect.height > 0 &&\n rect.left + rect.width > 0 &&\n (!view || (rect.top < view.innerHeight && rect.left < view.innerWidth))\n return visible ? rect : null\n}\n\nfunction useDocumentSwitcher(clips: readonly Clip[]) {\n const session = useArbiSessionStore()\n const { data: documents } = useDocuments()\n const groups = useMemo<PickerGroup[]>(() => {\n const all = (documents ?? [])\n .map((doc) => ({\n id: doc.external_id,\n title: getDocumentTitle(doc, 'Untitled'),\n fileName: doc.file_name,\n }))\n .sort((a, b) => a.title.localeCompare(b.title))\n const clipped = new Set(clips.map((clip) => clip.documentId))\n const from = all.filter((doc) => clipped.has(doc.id))\n return [\n ...(from.length ? [{ id: 'clipped', heading: 'Clipped from', docs: from }] : []),\n { id: 'all', heading: 'Documents', docs: all },\n ]\n }, [documents, clips])\n const open = (id: string) => session.setViewingDocument({ id, page: null })\n return { groups, open }\n}\n\nexport function SpotlightStrip() {\n const clips = useSpotlight((s) => s.clips)\n const note = useSpotlight((s) => s.note)\n const showing = useSpotlight((s) => s.showing)\n const actions = useSpotlight.getState()\n const switcher = useDocumentSwitcher(clips)\n const [noteOpen, setNoteOpen] = useState(false)\n\n const strip = useRef<HTMLDivElement | null>(null)\n const [view, setView] = useState<Window | null>(null)\n const stripRef = useCallback((node: HTMLDivElement | null) => {\n strip.current = node\n if (node) setView(node.ownerDocument.defaultView)\n }, [])\n\n // S shows the spotlight (not while typing).\n useEffect(() => {\n if (!view || showing) return\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.metaKey || event.ctrlKey || event.altKey || isTyping(event.target)) return\n if (event.key !== 's' && event.key !== 'S') return\n event.preventDefault()\n useSpotlight.getState().show()\n }\n view.addEventListener('keydown', onKeyDown)\n return () => view.removeEventListener('keydown', onKeyDown)\n }, [view, showing])\n\n // Back from the spotlight, the document is where it was scrolled to.\n const scrolled = useRef<{ element: HTMLElement; top: number; left: number } | null>(null)\n useEffect(() => {\n const scroller = frameOf(strip.current)?.querySelector<HTMLElement>(\n '[data-slot=\"pdf-scroller\"]'\n )\n if (showing) {\n scrolled.current = scroller\n ? { element: scroller, top: scroller.scrollTop, left: scroller.scrollLeft }\n : null\n return\n }\n const before = scrolled.current\n scrolled.current = null\n if (before?.element.isConnected) {\n before.element.scrollTop = before.top\n before.element.scrollLeft = before.left\n }\n }, [showing])\n\n const fromRect = useCallback((clip: Clip) => {\n const frame = frameOf(strip.current)\n const onPage = frame ? clipOnPage(frame, clip) : null\n // Not on screen (another document's clip): from its chip in the strip.\n return (\n onPage ??\n strip.current?.querySelector(`[data-spotlight-clip=\"${clip.id}\"]`)?.getBoundingClientRect() ??\n null\n )\n }, [])\n\n const count = `${clips.length} clip${clips.length === 1 ? '' : 's'}`\n return (\n <>\n <div\n ref={stripRef}\n className={cn(\n 'absolute bottom-[4.5rem] left-1/2 z-20 flex max-w-[calc(100%-1.5rem)] -translate-x-1/2 items-center gap-1.5 rounded-full border border-border bg-background/95 p-1 pl-1.5 shadow-lg backdrop-blur',\n showing && 'invisible'\n )}\n role=\"toolbar\"\n aria-label=\"Gathered clips\"\n data-testid=\"spotlight-strip\"\n >\n <ul className=\"flex min-w-0 items-center gap-1 overflow-x-auto [scrollbar-width:none]\">\n {clips.map((clip, index) => (\n <li\n key={clip.id}\n className=\"group relative h-8 shrink-0 overflow-hidden rounded-md border border-border bg-white\"\n style={{ width: `${Math.min(4.5, Math.max(1.5, 2 * clip.aspect))}rem` }}\n title={clip.caption}\n data-spotlight-clip={clip.id}\n data-testid={`spotlight-chip-${index}`}\n >\n <ClipImage clip={clip} className=\"h-full w-full\" />\n <button\n type=\"button\"\n className=\"absolute inset-0 flex items-center justify-center bg-black/50 text-white opacity-0 transition-opacity focus-visible:opacity-100 group-hover:opacity-100\"\n onClick={() => actions.remove(clip.id)}\n aria-label={`Remove ${clip.caption}`}\n data-testid={`spotlight-remove-${index}`}\n >\n <X className=\"size-3.5\" />\n </button>\n </li>\n ))}\n </ul>\n <span className=\"shrink-0 text-xs text-muted-foreground\" data-testid=\"spotlight-count\">\n {count}\n </span>\n <Popover open={noteOpen} onOpenChange={setNoteOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant={note ? 'secondary' : 'ghost'}\n size=\"sm\"\n className=\"h-7 max-w-40 shrink-0 gap-1 rounded-full px-2 text-xs\"\n title={note ? `Note: ${note}` : 'Type a note to show as the title'}\n data-testid=\"spotlight-note\"\n >\n <StickyNote className=\"size-3.5 shrink-0\" />\n {note ? <span className=\"truncate\">{note}</span> : <span>Note</span>}\n </Button>\n </PopoverTrigger>\n <PopoverContent side=\"top\" className=\"w-72 p-2\">\n <Input\n autoFocus\n value={note}\n placeholder=\"Type a note\"\n onChange={(event) => actions.setNote(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter') setNoteOpen(false)\n }}\n data-testid=\"spotlight-note-input\"\n />\n </PopoverContent>\n </Popover>\n <div className=\"w-44 shrink-0 [&_[role=combobox]]:h-7 [&_[role=combobox]]:rounded-full [&_[role=combobox]]:text-xs\">\n <DocumentPicker\n groups={switcher.groups}\n value=\"\"\n onValueChange={switcher.open}\n labels={{ placeholder: 'Clip another document' }}\n />\n </div>\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-7 shrink-0 gap-1 rounded-full px-3 text-xs\"\n onClick={actions.show}\n title=\"Show the clips over the document (S)\"\n data-testid=\"spotlight-show\"\n >\n <Presentation className=\"size-3.5\" />\n Show\n </Button>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"size-7 shrink-0 rounded-full\"\n aria-label=\"More\"\n data-testid=\"spotlight-more\"\n >\n <MoreHorizontal className=\"size-4\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" side=\"top\">\n <DropdownMenuItem onSelect={actions.keep} data-testid=\"spotlight-menu-keep\">\n <Presentation className=\"mr-2 size-4\" /> Keep as slide\n </DropdownMenuItem>\n <DropdownMenuItem onSelect={actions.addToTray} data-testid=\"spotlight-menu-tray\">\n <Inbox className=\"mr-2 size-4\" /> Add to the presentation tray\n </DropdownMenuItem>\n <DropdownMenuItem onSelect={actions.clear} data-testid=\"spotlight-menu-clear\">\n <Eraser className=\"mr-2 size-4\" /> Clear the clips\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n {showing && <SpotlightView fromRect={fromRect} />}\n </>\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The document viewer's Clip mode: whether it is on, and its header controls. Clips made in it
3
- * join the presentation's tray (a presentation being made, or one started from them); it stays on
4
- * from one document to the next, so clips are gathered from several documents in a row. Beside it,
5
- * the Presentation pill goes back to arrange them.
3
+ * are gathered in the viewer's spotlight strip (`SpotlightStrip`: Show them over the document, or
4
+ * keep them as a slide); it stays on from one document to the next, so clips are gathered from
5
+ * several documents in a row. Beside it, the Presentation pill goes back to the presentation.
6
6
  */
7
7
  import { type ReactNode } from 'react';
8
8
  export declare function useViewerClipping(
@@ -11,4 +11,5 @@ clippable: boolean, compact: boolean): {
11
11
  clipping: boolean;
12
12
  stop: () => void;
13
13
  button: ReactNode;
14
+ layer: ReactNode;
14
15
  };
@@ -1,16 +1,20 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useCallback, useState } from "react";
3
+ import { useCallback } from "react";
4
+ import { useSpotlight } from "../../presentation/spotlight.js";
4
5
  import { ClipButton } from "../../document-tools/DocumentToolButtons.js";
5
6
  import { PresentationPill } from "./PresentationPill.js";
7
+ import { SpotlightLayer } from "./SpotlightLayer.js";
6
8
  function useViewerClipping(clippable, compact) {
7
- const [on, setOn] = useState(false);
8
- const stop = useCallback(() => setOn(false), []);
9
+ const on = useSpotlight((s) => s.clipping);
10
+ const setOn = useSpotlight((s) => s.setClipping);
11
+ const stop = useCallback(() => setOn(false), [setOn]);
9
12
  const button = /* @__PURE__ */ jsxs(Fragment, { children: [
10
13
  /* @__PURE__ */ jsx(PresentationPill, { compact }),
11
14
  clippable && /* @__PURE__ */ jsx(ClipButton, { active: on, compact, onClick: () => setOn(!on) })
12
15
  ] });
13
- return { clipping: on && clippable, stop, button };
16
+ const layer = /* @__PURE__ */ jsx(SpotlightLayer, {});
17
+ return { clipping: on && clippable, stop, button, layer };
14
18
  }
15
19
  export {
16
20
  useViewerClipping
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/connection/presentation/useViewerClipping.tsx"],
4
- "sourcesContent": ["/**\n * The document viewer's Clip mode: whether it is on, and its header controls. Clips made in it\n * join the presentation's tray (a presentation being made, or one started from them); it stays on\n * from one document to the next, so clips are gathered from several documents in a row. Beside it,\n * the Presentation pill goes back to arrange them.\n */\n\nimport { useCallback, useState, type ReactNode } from 'react'\nimport { ClipButton } from '../../document-tools/DocumentToolButtons'\nimport { PresentationPill } from './PresentationPill'\n\nexport function useViewerClipping(\n /** A PDF, an image or a video is on screen. Clip mode shows over the viewer's other modes. */\n clippable: boolean,\n compact: boolean\n): { clipping: boolean; stop: () => void; button: ReactNode } {\n const [on, setOn] = useState(false)\n const stop = useCallback(() => setOn(false), [])\n const button = (\n <>\n <PresentationPill compact={compact} />\n {clippable && <ClipButton active={on} compact={compact} onClick={() => setOn(!on)} />}\n </>\n )\n return { clipping: on && clippable, stop, button }\n}\n"],
5
- "mappings": ";AAmBI,mBACE,KADF;AAZJ,SAAS,aAAa,gBAAgC;AACtD,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB;AAE1B,SAAS,kBAEd,WACA,SAC4D;AAC5D,QAAM,CAAC,IAAI,KAAK,IAAI,SAAS,KAAK;AAClC,QAAM,OAAO,YAAY,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC;AAC/C,QAAM,SACJ,iCACE;AAAA,wBAAC,oBAAiB,SAAkB;AAAA,IACnC,aAAa,oBAAC,cAAW,QAAQ,IAAI,SAAkB,SAAS,MAAM,MAAM,CAAC,EAAE,GAAG;AAAA,KACrF;AAEF,SAAO,EAAE,UAAU,MAAM,WAAW,MAAM,OAAO;AACnD;",
4
+ "sourcesContent": ["/**\n * The document viewer's Clip mode: whether it is on, and its header controls. Clips made in it\n * are gathered in the viewer's spotlight strip (`SpotlightStrip`: Show them over the document, or\n * keep them as a slide); it stays on from one document to the next, so clips are gathered from\n * several documents in a row. Beside it, the Presentation pill goes back to the presentation.\n */\n\nimport { useCallback, type ReactNode } from 'react'\nimport { useSpotlight } from '../../presentation/spotlight'\nimport { ClipButton } from '../../document-tools/DocumentToolButtons'\nimport { PresentationPill } from './PresentationPill'\nimport { SpotlightLayer } from './SpotlightLayer'\n\nexport function useViewerClipping(\n /** A PDF, an image or a video is on screen. Clip mode shows over the viewer's other modes. */\n clippable: boolean,\n compact: boolean\n): { clipping: boolean; stop: () => void; button: ReactNode; layer: ReactNode } {\n // In the spotlight's store: it stays on while the viewer opens another document.\n const on = useSpotlight((s) => s.clipping)\n const setOn = useSpotlight((s) => s.setClipping)\n const stop = useCallback(() => setOn(false), [setOn])\n const button = (\n <>\n <PresentationPill compact={compact} />\n {clippable && <ClipButton active={on} compact={compact} onClick={() => setOn(!on)} />}\n </>\n )\n // Over the viewer's body: the gathered clips' strip, and the spotlight.\n const layer = <SpotlightLayer />\n return { clipping: on && clippable, stop, button, layer }\n}\n"],
5
+ "mappings": ";AAuBI,mBACE,KADF;AAhBJ,SAAS,mBAAmC;AAC5C,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB;AACjC,SAAS,sBAAsB;AAExB,SAAS,kBAEd,WACA,SAC8E;AAE9E,QAAM,KAAK,aAAa,CAAC,MAAM,EAAE,QAAQ;AACzC,QAAM,QAAQ,aAAa,CAAC,MAAM,EAAE,WAAW;AAC/C,QAAM,OAAO,YAAY,MAAM,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC;AACpD,QAAM,SACJ,iCACE;AAAA,wBAAC,oBAAiB,SAAkB;AAAA,IACnC,aAAa,oBAAC,cAAW,QAAQ,IAAI,SAAkB,SAAS,MAAM,MAAM,CAAC,EAAE,GAAG;AAAA,KACrF;AAGF,QAAM,QAAQ,oBAAC,kBAAe;AAC9B,SAAO,EAAE,UAAU,MAAM,WAAW,MAAM,QAAQ,MAAM;AAC1D;",
6
6
  "names": []
7
7
  }