@jupytergis/base 0.13.3 → 0.14.0

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 (115) hide show
  1. package/lib/commands/BaseCommandIDs.d.ts +14 -13
  2. package/lib/commands/BaseCommandIDs.js +14 -14
  3. package/lib/commands/index.js +523 -117
  4. package/lib/commands/operationCommands.d.ts +22 -0
  5. package/lib/commands/operationCommands.js +305 -0
  6. package/lib/constants.js +9 -9
  7. package/lib/dialogs/ProcessingFormDialog.d.ts +1 -1
  8. package/lib/dialogs/ProcessingFormDialog.js +2 -2
  9. package/lib/dialogs/layerBrowserDialog.d.ts +2 -0
  10. package/lib/dialogs/layerBrowserDialog.js +12 -5
  11. package/lib/dialogs/layerCreationFormDialog.d.ts +7 -0
  12. package/lib/dialogs/layerCreationFormDialog.js +10 -2
  13. package/lib/dialogs/symbology/components/color_ramp/ColorRampControls.d.ts +2 -1
  14. package/lib/dialogs/symbology/components/color_ramp/ColorRampControls.js +21 -19
  15. package/lib/dialogs/symbology/components/color_ramp/ColorRampSelector.d.ts +3 -1
  16. package/lib/dialogs/symbology/components/color_ramp/ColorRampSelector.js +13 -5
  17. package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.js +6 -4
  18. package/lib/dialogs/symbology/vector_layer/types/Categorized.js +7 -11
  19. package/lib/dialogs/symbology/vector_layer/types/Graduated.js +7 -10
  20. package/lib/dialogs/symbology/vector_layer/types/Heatmap.js +7 -8
  21. package/lib/formbuilder/creationform.d.ts +8 -8
  22. package/lib/formbuilder/creationform.js +130 -85
  23. package/lib/formbuilder/editform.d.ts +1 -7
  24. package/lib/formbuilder/editform.js +64 -52
  25. package/lib/formbuilder/formselectors.d.ts +5 -4
  26. package/lib/formbuilder/index.d.ts +1 -1
  27. package/lib/formbuilder/index.js +1 -1
  28. package/lib/formbuilder/objectform/SchemaForm.d.ts +36 -0
  29. package/lib/formbuilder/objectform/SchemaForm.js +77 -0
  30. package/lib/formbuilder/objectform/StoryEditorForm.d.ts +3 -9
  31. package/lib/formbuilder/objectform/StoryEditorForm.js +20 -14
  32. package/lib/formbuilder/objectform/components/LayerSelect.js +3 -8
  33. package/lib/formbuilder/objectform/components/SegmentFormSymbology.js +23 -10
  34. package/lib/formbuilder/objectform/components/SourcePropertiesField.d.ts +7 -0
  35. package/lib/formbuilder/objectform/components/SourcePropertiesField.js +29 -0
  36. package/lib/formbuilder/objectform/components/StorySegmentReset.js +1 -1
  37. package/lib/formbuilder/objectform/fileselectorwidget.js +1 -1
  38. package/lib/formbuilder/objectform/layer/heatmapLayerForm.d.ts +3 -12
  39. package/lib/formbuilder/objectform/layer/heatmapLayerForm.js +87 -55
  40. package/lib/formbuilder/objectform/layer/hillshadeLayerForm.d.ts +3 -8
  41. package/lib/formbuilder/objectform/layer/hillshadeLayerForm.js +36 -10
  42. package/lib/formbuilder/objectform/layer/layerform.d.ts +7 -9
  43. package/lib/formbuilder/objectform/layer/layerform.js +33 -20
  44. package/lib/formbuilder/objectform/layer/storySegmentLayerForm.d.ts +3 -5
  45. package/lib/formbuilder/objectform/layer/storySegmentLayerForm.js +73 -29
  46. package/lib/formbuilder/objectform/layer/vectorlayerform.d.ts +3 -14
  47. package/lib/formbuilder/objectform/layer/vectorlayerform.js +36 -29
  48. package/lib/formbuilder/objectform/layer/webGlLayerForm.d.ts +3 -10
  49. package/lib/formbuilder/objectform/layer/webGlLayerForm.js +37 -13
  50. package/lib/formbuilder/objectform/process/dissolveProcessForm.d.ts +8 -18
  51. package/lib/formbuilder/objectform/process/dissolveProcessForm.js +68 -56
  52. package/lib/formbuilder/objectform/processingForm.d.ts +12 -0
  53. package/lib/formbuilder/objectform/processingForm.js +33 -0
  54. package/lib/formbuilder/objectform/schemaUtils.d.ts +16 -0
  55. package/lib/formbuilder/objectform/schemaUtils.js +59 -0
  56. package/lib/formbuilder/objectform/source/geojsonsource.d.ts +3 -19
  57. package/lib/formbuilder/objectform/source/geojsonsource.js +94 -53
  58. package/lib/formbuilder/objectform/source/geotiffsource.d.ts +3 -20
  59. package/lib/formbuilder/objectform/source/geotiffsource.js +73 -74
  60. package/lib/formbuilder/objectform/source/pathbasedsource.d.ts +3 -19
  61. package/lib/formbuilder/objectform/source/pathbasedsource.js +76 -75
  62. package/lib/formbuilder/objectform/source/sourceform.d.ts +7 -8
  63. package/lib/formbuilder/objectform/source/sourceform.js +28 -11
  64. package/lib/formbuilder/objectform/source/tilesourceform.d.ts +3 -7
  65. package/lib/formbuilder/objectform/source/tilesourceform.js +63 -53
  66. package/lib/formbuilder/objectform/useSchemaFormState.d.ts +48 -0
  67. package/lib/formbuilder/objectform/useSchemaFormState.js +35 -0
  68. package/lib/index.d.ts +0 -1
  69. package/lib/index.js +0 -1
  70. package/lib/keybindings.json +10 -10
  71. package/lib/mainview/mainView.d.ts +11 -7
  72. package/lib/mainview/mainView.js +62 -35
  73. package/lib/mainview/mainviewmodel.js +2 -2
  74. package/lib/menus.js +8 -8
  75. package/lib/panelview/components/layers.js +5 -2
  76. package/lib/panelview/objectproperties.js +2 -2
  77. package/lib/panelview/rightpanel.js +17 -2
  78. package/lib/panelview/story-maps/SpectaPanel.d.ts +15 -0
  79. package/lib/panelview/story-maps/SpectaPanel.js +35 -0
  80. package/lib/panelview/story-maps/StoryViewerPanel.d.ts +24 -9
  81. package/lib/panelview/story-maps/StoryViewerPanel.js +22 -268
  82. package/lib/panelview/story-maps/{PreviewModeSwitch.js → components/PreviewModeSwitch.js} +1 -1
  83. package/lib/panelview/story-maps/components/SpectaDesktopView.d.ts +21 -0
  84. package/lib/panelview/story-maps/components/SpectaDesktopView.js +49 -0
  85. package/lib/panelview/story-maps/components/SpectaMobileView.d.ts +17 -0
  86. package/lib/panelview/story-maps/{MobileSpectaPanel.js → components/SpectaMobileView.js} +8 -22
  87. package/lib/panelview/story-maps/{StoryNavBar.d.ts → components/StoryNavBar.d.ts} +1 -1
  88. package/lib/panelview/story-maps/{StoryNavBar.js → components/StoryNavBar.js} +2 -4
  89. package/lib/panelview/story-maps/hooks/useStoryMap.d.ts +39 -0
  90. package/lib/panelview/story-maps/hooks/useStoryMap.js +252 -0
  91. package/lib/processing/index.d.ts +2 -2
  92. package/lib/processing/index.js +62 -35
  93. package/lib/processing/processingCommands.d.ts +1 -1
  94. package/lib/processing/processingCommands.js +26 -6
  95. package/lib/shared/components/Collapsible.d.ts +6 -0
  96. package/lib/shared/components/Collapsible.js +26 -0
  97. package/lib/statusbar/SpectaPresentationProgressBar.d.ts +7 -0
  98. package/lib/statusbar/SpectaPresentationProgressBar.js +40 -0
  99. package/lib/toolbar/widget.js +4 -2
  100. package/lib/tools.d.ts +6 -0
  101. package/lib/tools.js +9 -0
  102. package/lib/types.d.ts +29 -2
  103. package/package.json +2 -4
  104. package/style/base.css +23 -1
  105. package/style/dialog.css +5 -0
  106. package/style/leftPanel.css +14 -37
  107. package/style/shared/button.css +0 -10
  108. package/style/shared/switch.css +8 -7
  109. package/style/spectaProgressBar.css +144 -0
  110. package/style/storyPanel.css +33 -0
  111. package/style/symbologyDialog.css +2 -2
  112. package/lib/formbuilder/objectform/baseform.d.ts +0 -91
  113. package/lib/formbuilder/objectform/baseform.js +0 -231
  114. package/lib/panelview/story-maps/MobileSpectaPanel.d.ts +0 -7
  115. /package/lib/panelview/story-maps/{PreviewModeSwitch.d.ts → components/PreviewModeSwitch.d.ts} +0 -0
@@ -1,9 +1,7 @@
1
- import { UUID } from '@lumino/coreutils';
2
- import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
3
- import { cn } from "../../shared/components/utils";
4
- import StoryNavBar from './StoryNavBar';
1
+ import React, { useEffect, useState } from 'react';
5
2
  import StoryContentSection from './components/StoryContentSection';
6
3
  import StoryImageSection from './components/StoryImageSection';
4
+ import StoryNavBar from './components/StoryNavBar';
7
5
  import StorySubtitleSection from './components/StorySubtitleSection';
8
6
  import StoryTitleSection from './components/StoryTitleSection';
9
7
  /**
@@ -11,122 +9,20 @@ import StoryTitleSection from './components/StoryTitleSection';
11
9
  */
12
10
  function getStoryNavPlacement(isSpecta, hasImage, storyType, isMobile) {
13
11
  if (isSpecta) {
14
- return isMobile ? 'subtitle-specta-mobile' : 'subtitle-specta';
12
+ return isMobile ? null : 'subtitle-specta';
15
13
  }
16
14
  if (storyType !== 'guided') {
17
15
  return null;
18
16
  }
19
17
  return hasImage ? 'over-image' : 'below-title';
20
18
  }
21
- const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, className, addLayer, removeLayer, onSegmentTransitionEnd, }, ref) => {
22
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
23
- const [currentIndexDisplayed, setCurrentIndexDisplayed] = useState(() => model.getCurrentSegmentIndex());
24
- const [storyData, setStoryData] = useState((_a = model.getSelectedStory().story) !== null && _a !== void 0 ? _a : null);
19
+ /**
20
+ * Story viewer (presentational). Receives story state and callbacks from parent.
21
+ * Desktop scroll/sentinel/imperative handle live in SpectaDesktopView.
22
+ */
23
+ function StoryViewerPanel({ model, isSpecta, isMobile = false, className, segmentContainerRef, storyData, currentIndex, activeSlide, layerName, handlePrev, handleNext, hasPrev, hasNext, setIndex, }) {
24
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
25
25
  const [imageLoaded, setImageLoaded] = useState(false);
26
- const panelRef = useRef(null);
27
- const segmentContainerRef = useRef(null);
28
- const topSentinelRef = useRef(null);
29
- const bottomSentinelRef = useRef(null);
30
- const atTopRef = useRef(false);
31
- const atBottomRef = useRef(false);
32
- const setIndex = useCallback((index) => {
33
- model.setCurrentSegmentIndex(index);
34
- setCurrentIndexDisplayed(index);
35
- }, [model]);
36
- /** Layers affected by layer override
37
- * We want to remove added layers (ie Heatmap)
38
- * and Restore the original symbology for modified layers
39
- */
40
- const overrideLayerEntriesRef = useRef([]);
41
- const clearOverrideLayers = useCallback(() => {
42
- overrideLayerEntriesRef.current.forEach(({ layerId, action }) => {
43
- if (action === 'remove') {
44
- removeLayer === null || removeLayer === void 0 ? void 0 : removeLayer(layerId);
45
- }
46
- else {
47
- const layer = model.getLayer(layerId);
48
- if (layer) {
49
- model.triggerLayerUpdate(layerId, layer);
50
- }
51
- }
52
- });
53
- overrideLayerEntriesRef.current = [];
54
- }, [model]);
55
- // Derive story segments from story data
56
- const storySegments = useMemo(() => {
57
- if (!(storyData === null || storyData === void 0 ? void 0 : storyData.storySegments)) {
58
- return [];
59
- }
60
- return storyData.storySegments
61
- .map(storySegmentId => model.getLayer(storySegmentId))
62
- .filter((layer) => layer !== undefined);
63
- }, [storyData, model]);
64
- // Derive current story segment from story segments and currentIndexDisplayed
65
- const currentStorySegment = useMemo(() => {
66
- return storySegments[currentIndexDisplayed];
67
- }, [storySegments, currentIndexDisplayed]);
68
- // Derive active slide and layer name from current story segment
69
- const activeSlide = useMemo(() => {
70
- return currentStorySegment === null || currentStorySegment === void 0 ? void 0 : currentStorySegment.parameters;
71
- }, [currentStorySegment]);
72
- const layerName = useMemo(() => { var _a; return (_a = currentStorySegment === null || currentStorySegment === void 0 ? void 0 : currentStorySegment.name) !== null && _a !== void 0 ? _a : ''; }, [currentStorySegment]);
73
- // Derive story segment ID for zooming
74
- const currentStorySegmentId = useMemo(() => {
75
- var _a;
76
- return (_a = storyData === null || storyData === void 0 ? void 0 : storyData.storySegments) === null || _a === void 0 ? void 0 : _a[currentIndexDisplayed];
77
- }, [storyData, currentIndexDisplayed]);
78
- const hasPrev = currentIndexDisplayed > 0;
79
- const hasNext = currentIndexDisplayed < storySegments.length - 1;
80
- const zoomToCurrentLayer = () => {
81
- if (currentStorySegmentId) {
82
- model.centerOnPosition(currentStorySegmentId);
83
- }
84
- };
85
- const setSelectedLayerByIndex = useCallback((index) => {
86
- var _a;
87
- const storySegmentId = (_a = storyData === null || storyData === void 0 ? void 0 : storyData.storySegments) === null || _a === void 0 ? void 0 : _a[index];
88
- if (storySegmentId) {
89
- model.selected = {
90
- [storySegmentId]: {
91
- type: 'layer',
92
- },
93
- };
94
- }
95
- }, [storyData, model]);
96
- // On unmount: remove override layers and restore layer symbology
97
- useEffect(() => {
98
- return () => {
99
- var _a;
100
- clearOverrideLayers();
101
- (_a = storyData === null || storyData === void 0 ? void 0 : storyData.storySegments) === null || _a === void 0 ? void 0 : _a.forEach(segmentId => {
102
- var _a;
103
- const segment = model.getLayer(segmentId);
104
- const overrides = (_a = segment === null || segment === void 0 ? void 0 : segment.parameters) === null || _a === void 0 ? void 0 : _a.layerOverride;
105
- if (Array.isArray(overrides)) {
106
- overrides.forEach((override) => {
107
- const targetLayerId = override.targetLayer;
108
- const targetLayer = model.getLayer(targetLayerId);
109
- targetLayer &&
110
- model.triggerLayerUpdate(targetLayerId, targetLayer);
111
- });
112
- }
113
- });
114
- };
115
- }, [storyData, model, clearOverrideLayers]);
116
- useEffect(() => {
117
- const updateStory = () => {
118
- var _a;
119
- clearOverrideLayers();
120
- const { story } = model.getSelectedStory();
121
- setStoryData(story !== null && story !== void 0 ? story : null);
122
- setIndex((_a = model.getCurrentSegmentIndex()) !== null && _a !== void 0 ? _a : 0);
123
- };
124
- updateStory();
125
- model.sharedModel.storyMapsChanged.connect(updateStory);
126
- return () => {
127
- model.sharedModel.storyMapsChanged.disconnect(updateStory);
128
- };
129
- }, [model, setIndex, clearOverrideLayers]);
130
26
  // Prefetch image when slide changes
131
27
  useEffect(() => {
132
28
  var _a;
@@ -151,33 +47,8 @@ const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, classN
151
47
  img.onload = null;
152
48
  img.onerror = null;
153
49
  };
154
- }, [(_b = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.content) === null || _b === void 0 ? void 0 : _b.image]);
155
- // Auto-zoom when slide changes
156
- useEffect(() => {
157
- if (currentStorySegmentId) {
158
- zoomToCurrentLayer();
159
- }
160
- }, [currentStorySegmentId, model]);
161
- // Set selected layer and apply symbology when segment changes; remove previous segment's override layers first.
162
- useEffect(() => {
163
- if (!(storyData === null || storyData === void 0 ? void 0 : storyData.storySegments) || currentIndexDisplayed < 0) {
164
- return;
165
- }
166
- clearOverrideLayers();
167
- setSelectedLayerByIndex(currentIndexDisplayed);
168
- overrideSymbology(currentIndexDisplayed);
169
- }, [
170
- storyData,
171
- currentIndexDisplayed,
172
- setSelectedLayerByIndex,
173
- clearOverrideLayers,
174
- ]);
175
- // Set selected layer on initial render and when story data changes
176
- useEffect(() => {
177
- if ((storyData === null || storyData === void 0 ? void 0 : storyData.storySegments) && currentIndexDisplayed >= 0) {
178
- setSelectedLayerByIndex(currentIndexDisplayed);
179
- }
180
- }, [storyData, currentIndexDisplayed, setSelectedLayerByIndex]);
50
+ }, [(_a = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.content) === null || _a === void 0 ? void 0 : _a.image]);
51
+ // ! TODO come back for this
181
52
  // Listen for layer selection changes in unguided mode
182
53
  useEffect(() => {
183
54
  // ! TODO this logic (getting a single selected layer) is also in the processing index.ts, move to tools
@@ -214,71 +85,7 @@ const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, classN
214
85
  model.sharedModel.awareness.off('change', handleSelectedStorySegmentChange);
215
86
  };
216
87
  }, [model, storyData, setIndex]);
217
- // Apply layer overrides for the segment at the given index
218
- const overrideSymbology = (index) => {
219
- var _a;
220
- if (index < 0 || !storySegments[index]) {
221
- return;
222
- }
223
- const segment = storySegments[index];
224
- const layerOverrides = (_a = segment.parameters) === null || _a === void 0 ? void 0 : _a.layerOverride;
225
- if (!Array.isArray(layerOverrides)) {
226
- return;
227
- }
228
- // Apply all layer overrides for this segment
229
- layerOverrides.forEach(override => {
230
- const { color, opacity, symbologyState, targetLayer: targetLayerId, visible, } = override;
231
- if (!targetLayerId) {
232
- return;
233
- }
234
- overrideLayerEntriesRef.current.push({
235
- layerId: targetLayerId,
236
- action: 'restore',
237
- });
238
- const targetLayer = model.getLayer(targetLayerId);
239
- if (targetLayer === null || targetLayer === void 0 ? void 0 : targetLayer.parameters) {
240
- if (symbologyState !== undefined) {
241
- targetLayer.parameters.symbologyState = symbologyState;
242
- }
243
- if (color !== undefined) {
244
- targetLayer.parameters.color = color;
245
- }
246
- if (opacity !== undefined) {
247
- targetLayer.parameters.opacity = opacity;
248
- }
249
- if (visible !== undefined) {
250
- targetLayer.visible = visible;
251
- }
252
- // Heatmaps are actually a different layer, not just symbology
253
- // so they need special handling
254
- if ((symbologyState === null || symbologyState === void 0 ? void 0 : symbologyState.renderType) === 'Heatmap') {
255
- targetLayer.type = 'HeatmapLayer';
256
- if (addLayer) {
257
- const newId = UUID.uuid4();
258
- addLayer(newId, targetLayer, 100);
259
- overrideLayerEntriesRef.current.push({
260
- layerId: newId,
261
- action: 'remove',
262
- });
263
- }
264
- }
265
- else {
266
- model.triggerLayerUpdate(targetLayerId, targetLayer);
267
- }
268
- }
269
- });
270
- };
271
- const handlePrev = useCallback(() => {
272
- if (hasPrev) {
273
- setIndex(currentIndexDisplayed - 1);
274
- }
275
- }, [currentIndexDisplayed, setIndex]);
276
- const handleNext = useCallback(() => {
277
- if (hasNext) {
278
- setIndex(currentIndexDisplayed + 1);
279
- }
280
- }, [currentIndexDisplayed, storySegments.length, setIndex]);
281
- if (!storyData || ((_c = storyData === null || storyData === void 0 ? void 0 : storyData.storySegments) === null || _c === void 0 ? void 0 : _c.length) === 0) {
88
+ if (!storyData || ((_b = storyData === null || storyData === void 0 ? void 0 : storyData.storySegments) === null || _b === void 0 ? void 0 : _b.length) === 0) {
282
89
  return (React.createElement("div", { style: { padding: '1rem' } },
283
90
  React.createElement("p", null, "No Segments available. Add one using the Add Layer menu.")));
284
91
  }
@@ -288,75 +95,22 @@ const StoryViewerPanel = forwardRef(({ model, isSpecta, isMobile = false, classN
288
95
  hasPrev,
289
96
  hasNext,
290
97
  };
291
- // IntersectionObserver for at-top/at-bottom (avoids layout reads in scroll path)
292
- useEffect(() => {
293
- const root = panelRef.current;
294
- const topEl = topSentinelRef.current;
295
- const bottomEl = bottomSentinelRef.current;
296
- if (!root || !topEl || !bottomEl) {
297
- return;
298
- }
299
- const observer = new IntersectionObserver((entries) => {
300
- for (const entry of entries) {
301
- if (entry.target === topEl) {
302
- atTopRef.current = entry.isIntersecting;
303
- }
304
- else if (entry.target === bottomEl) {
305
- atBottomRef.current = entry.isIntersecting;
306
- }
307
- }
308
- }, { root, threshold: 0, rootMargin: '0px' });
309
- observer.observe(topEl);
310
- observer.observe(bottomEl);
311
- return () => observer.disconnect();
312
- }, [currentIndexDisplayed]);
313
- // Expose methods via ref for parent component to use
314
- useImperativeHandle(ref, () => ({
315
- handlePrev,
316
- handleNext,
317
- spectaMode: isSpecta,
318
- hasPrev,
319
- hasNext,
320
- getAtTop: () => atTopRef.current,
321
- getAtBottom: () => atBottomRef.current,
322
- getScrollContainer: () => panelRef.current,
323
- }), [handlePrev, handleNext, storyData, isSpecta, hasPrev, hasNext]);
324
- const hasImage = !!(((_d = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.content) === null || _d === void 0 ? void 0 : _d.image) && imageLoaded);
325
- const storyType = (_e = storyData.storyType) !== null && _e !== void 0 ? _e : 'guided';
98
+ const hasImage = !!(((_c = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.content) === null || _c === void 0 ? void 0 : _c.image) && imageLoaded);
99
+ const storyType = (_d = storyData.storyType) !== null && _d !== void 0 ? _d : 'guided';
326
100
  const navPlacement = getStoryNavPlacement(isSpecta, hasImage, storyType, isMobile);
327
101
  const navSlot = navPlacement !== null ? (React.createElement(StoryNavBar, Object.assign({ placement: navPlacement }, storyNavBarProps))) : null;
328
102
  // Get transition time from current segment, default to 0.3s
329
- const transitionTime = (_g = (_f = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.transition) === null || _f === void 0 ? void 0 : _f.time) !== null && _g !== void 0 ? _g : 0.3;
330
- // Notify parent when segment transition animation ends (e.g. for scroll-guard cleanup)
331
- useEffect(() => {
332
- const el = segmentContainerRef.current;
333
- if (!el || !onSegmentTransitionEnd) {
334
- return;
335
- }
336
- const handleAnimationEnd = (e) => {
337
- if (e.animationName === 'fadeIn') {
338
- el.removeEventListener('animationend', handleAnimationEnd);
339
- onSegmentTransitionEnd();
340
- }
341
- };
342
- el.addEventListener('animationend', handleAnimationEnd);
343
- return () => el.removeEventListener('animationend', handleAnimationEnd);
344
- }, [currentIndexDisplayed, onSegmentTransitionEnd]);
345
- return (React.createElement("div", { ref: panelRef, className: cn('jgis-story-viewer-panel', className), id: "jgis-story-segment-panel" },
346
- React.createElement("div", { ref: topSentinelRef, "aria-hidden": true, "data-story-scroll-sentinel": "top", style: { height: 1, minHeight: 1, pointerEvents: 'none' } }),
347
- React.createElement("div", { ref: segmentContainerRef, key: currentIndexDisplayed, className: "jgis-story-segment-container", style: {
103
+ const transitionTime = (_f = (_e = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.transition) === null || _e === void 0 ? void 0 : _e.time) !== null && _f !== void 0 ? _f : 0.3;
104
+ return (React.createElement("div", { className: "jgis-story-viewer-panel" },
105
+ React.createElement("div", { ref: segmentContainerRef, key: currentIndex, className: "jgis-story-segment-container", style: {
348
106
  animationDuration: `${transitionTime}s`,
349
107
  } },
350
108
  React.createElement("div", { id: "jgis-story-segment-header" },
351
- React.createElement("h1", { className: "jgis-story-viewer-title" }, layerName !== null && layerName !== void 0 ? layerName : `Slide ${currentIndexDisplayed + 1}`),
352
- ((_h = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.content) === null || _h === void 0 ? void 0 : _h.image) && imageLoaded ? (React.createElement(StoryImageSection, { imageUrl: activeSlide.content.image, imageLoaded: imageLoaded, layerName: layerName !== null && layerName !== void 0 ? layerName : '', slideNumber: currentIndexDisplayed, navSlot: navPlacement === 'over-image' ? navSlot : null })) : (React.createElement(StoryTitleSection, { title: (_j = storyData.title) !== null && _j !== void 0 ? _j : '', navSlot: navPlacement === 'below-title' ? navSlot : null })),
353
- React.createElement(StorySubtitleSection, { title: (_l = (_k = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.content) === null || _k === void 0 ? void 0 : _k.title) !== null && _l !== void 0 ? _l : '', navSlot: navPlacement === 'subtitle-specta' ||
354
- navPlacement === 'subtitle-specta-mobile'
355
- ? navSlot
356
- : null })),
109
+ React.createElement("h1", { className: "jgis-story-viewer-title" }, layerName !== null && layerName !== void 0 ? layerName : `Slide ${currentIndex + 1}`),
110
+ ((_g = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.content) === null || _g === void 0 ? void 0 : _g.image) && imageLoaded ? (React.createElement(StoryImageSection, { imageUrl: activeSlide.content.image, imageLoaded: imageLoaded, layerName: layerName !== null && layerName !== void 0 ? layerName : '', slideNumber: currentIndex, navSlot: navPlacement === 'over-image' ? navSlot : null })) : (React.createElement(StoryTitleSection, { title: (_h = storyData.title) !== null && _h !== void 0 ? _h : '', navSlot: navPlacement === 'below-title' ? navSlot : null })),
111
+ React.createElement(StorySubtitleSection, { title: (_k = (_j = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.content) === null || _j === void 0 ? void 0 : _j.title) !== null && _k !== void 0 ? _k : '', navSlot: navPlacement === 'subtitle-specta' ? navSlot : null })),
357
112
  React.createElement("div", { id: "jgis-story-segment-content" },
358
- React.createElement(StoryContentSection, { markdown: (_o = (_m = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.content) === null || _m === void 0 ? void 0 : _m.markdown) !== null && _o !== void 0 ? _o : '' }))),
359
- React.createElement("div", { ref: bottomSentinelRef, "aria-hidden": true, "data-story-scroll-sentinel": "bottom", style: { height: 1, minHeight: 1, pointerEvents: 'none' } })));
360
- });
113
+ React.createElement(StoryContentSection, { markdown: (_m = (_l = activeSlide === null || activeSlide === void 0 ? void 0 : activeSlide.content) === null || _l === void 0 ? void 0 : _l.markdown) !== null && _m !== void 0 ? _m : '' })))));
114
+ }
361
115
  StoryViewerPanel.displayName = 'StoryViewerPanel';
362
116
  export default StoryViewerPanel;
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { Switch } from "../../shared/components/Switch";
2
+ import { Switch } from "../../../shared/components/Switch";
3
3
  export function PreviewModeSwitch({ checked, onCheckedChange, }) {
4
4
  return (React.createElement("div", { style: {
5
5
  display: 'flex',
@@ -0,0 +1,21 @@
1
+ import { IJGISStoryMap, IJupyterGISModel, IStorySegmentLayer } from '@jupytergis/schema';
2
+ import { RefObject } from 'react';
3
+ import { type IStoryViewerPanelHandle } from "../StoryViewerPanel";
4
+ interface ISpectaDesktopViewProps {
5
+ model: IJupyterGISModel;
6
+ isSpecta: boolean;
7
+ containerRef: RefObject<HTMLDivElement>;
8
+ storyViewerPanelRef: RefObject<IStoryViewerPanelHandle>;
9
+ segmentContainerRef: RefObject<HTMLDivElement>;
10
+ storyData: IJGISStoryMap | null;
11
+ currentIndex: number;
12
+ activeSlide: IStorySegmentLayer['parameters'] | undefined;
13
+ layerName: string;
14
+ handlePrev: () => void;
15
+ handleNext: () => void;
16
+ hasPrev: boolean;
17
+ hasNext: boolean;
18
+ setIndex: (index: number) => void;
19
+ }
20
+ export declare function SpectaDesktopView({ model, isSpecta, containerRef, storyViewerPanelRef, segmentContainerRef, storyData, currentIndex, activeSlide, layerName, handlePrev, handleNext, hasPrev, hasNext, setIndex, }: ISpectaDesktopViewProps): JSX.Element;
21
+ export {};
@@ -0,0 +1,49 @@
1
+ import React, { useEffect, useImperativeHandle, useRef, } from 'react';
2
+ import StoryViewerPanel from "../StoryViewerPanel";
3
+ import SpectaPresentationProgressBar from "../../../statusbar/SpectaPresentationProgressBar";
4
+ export function SpectaDesktopView({ model, isSpecta, containerRef, storyViewerPanelRef, segmentContainerRef, storyData, currentIndex, activeSlide, layerName, handlePrev, handleNext, hasPrev, hasNext, setIndex, }) {
5
+ const scrollContainerRef = useRef(null);
6
+ const topSentinelRef = useRef(null);
7
+ const bottomSentinelRef = useRef(null);
8
+ const atTopRef = useRef(false);
9
+ const atBottomRef = useRef(false);
10
+ useEffect(() => {
11
+ const root = scrollContainerRef.current;
12
+ const topEl = topSentinelRef.current;
13
+ const bottomEl = bottomSentinelRef.current;
14
+ if (!root || !topEl || !bottomEl) {
15
+ return;
16
+ }
17
+ const observer = new IntersectionObserver((entries) => {
18
+ for (const entry of entries) {
19
+ if (entry.target === topEl) {
20
+ atTopRef.current = entry.isIntersecting;
21
+ }
22
+ else if (entry.target === bottomEl) {
23
+ atBottomRef.current = entry.isIntersecting;
24
+ }
25
+ }
26
+ }, { root, threshold: 0, rootMargin: '0px' });
27
+ observer.observe(topEl);
28
+ observer.observe(bottomEl);
29
+ return () => observer.disconnect();
30
+ }, [currentIndex]);
31
+ useImperativeHandle(storyViewerPanelRef, () => ({
32
+ handlePrev,
33
+ handleNext,
34
+ spectaMode: isSpecta,
35
+ hasPrev,
36
+ hasNext,
37
+ getAtTop: () => atTopRef.current,
38
+ getAtBottom: () => atBottomRef.current,
39
+ getScrollContainer: () => scrollContainerRef.current,
40
+ }), [handlePrev, handleNext, isSpecta, hasPrev, hasNext]);
41
+ return (React.createElement(React.Fragment, null,
42
+ React.createElement("div", { className: "jgis-specta-right-panel-container-mod jgis-right-panel-container" },
43
+ React.createElement("div", { ref: containerRef, className: "jgis-specta-story-panel-container" },
44
+ React.createElement("div", { ref: scrollContainerRef, className: "jgis-story-viewer-panel-specta-mod", id: "jgis-story-segment-panel" },
45
+ React.createElement("div", { ref: topSentinelRef, "aria-hidden": true, "data-story-scroll-sentinel": "top", style: { height: 1, minHeight: 1, pointerEvents: 'none' } }),
46
+ React.createElement(StoryViewerPanel, { model: model, isSpecta: isSpecta, className: "jgis-story-viewer-panel-specta-mod", segmentContainerRef: segmentContainerRef, storyData: storyData, currentIndex: currentIndex, activeSlide: activeSlide, layerName: layerName, handlePrev: handlePrev, handleNext: handleNext, hasPrev: hasPrev, hasNext: hasNext, setIndex: setIndex }),
47
+ React.createElement("div", { ref: bottomSentinelRef, "aria-hidden": true, "data-story-scroll-sentinel": "bottom", style: { height: 1, minHeight: 1, pointerEvents: 'none' } })))),
48
+ React.createElement(SpectaPresentationProgressBar, { model: model })));
49
+ }
@@ -0,0 +1,17 @@
1
+ import { IJGISStoryMap, IJupyterGISModel, IStorySegmentLayer } from '@jupytergis/schema';
2
+ import React, { RefObject } from 'react';
3
+ interface ISpectaMobileViewProps {
4
+ model: IJupyterGISModel;
5
+ segmentContainerRef: RefObject<HTMLDivElement>;
6
+ storyData: IJGISStoryMap | null;
7
+ currentIndex: number;
8
+ activeSlide: IStorySegmentLayer['parameters'] | undefined;
9
+ layerName: string;
10
+ handlePrev: () => void;
11
+ handleNext: () => void;
12
+ hasPrev: boolean;
13
+ hasNext: boolean;
14
+ setIndex: (index: number) => void;
15
+ }
16
+ export declare function SpectaMobileView({ model, segmentContainerRef, storyData, currentIndex, activeSlide, layerName, handlePrev, handleNext, hasPrev, hasNext, setIndex, }: ISpectaMobileViewProps): React.JSX.Element;
17
+ export {};
@@ -1,7 +1,8 @@
1
1
  import React, { useEffect, useState } from 'react';
2
- import { Button } from "../../shared/components/Button";
3
- import { Drawer, DrawerContent, DrawerTrigger, } from "../../shared/components/Drawer";
4
- import StoryViewerPanel from './StoryViewerPanel';
2
+ import { Button } from "../../../shared/components/Button";
3
+ import { Drawer, DrawerContent, DrawerTrigger, } from "../../../shared/components/Drawer";
4
+ import StoryViewerPanel from '../StoryViewerPanel';
5
+ import { getSpectaPresentationStyle } from '../hooks/useStoryMap';
5
6
  const MAIN_ID = 'jp-main-content-panel';
6
7
  const SEGMENT_PANEL_ID = 'jgis-story-segment-panel';
7
8
  const SEGMENT_HEADER_ID = 'jgis-story-segment-header';
@@ -28,30 +29,14 @@ function getFirstSnapFromSegmentHeader(mainEl, segmentPanelEl, segmentHeaderEl)
28
29
  const clamped = Math.max(SNAP_FIRST_MIN, Math.min(SNAP_FIRST_MAX, fraction));
29
30
  return clamped;
30
31
  }
31
- /** Build inline styles for specta presentation (bg and text color from story). */
32
- function getSpectaPresentationStyle(model) {
33
- const story = model.getSelectedStory().story;
34
- const bgColor = story === null || story === void 0 ? void 0 : story.presentationBgColor;
35
- const textColor = story === null || story === void 0 ? void 0 : story.presentationTextColor;
36
- const style = {};
37
- if (bgColor) {
38
- style['--jgis-specta-bg-color'] = bgColor;
39
- style.backgroundColor = bgColor;
40
- }
41
- if (textColor) {
42
- style['--jgis-specta-text-color'] = textColor;
43
- style.color = textColor;
44
- }
45
- return style;
46
- }
47
- export function MobileSpectaPanel({ model }) {
32
+ export function SpectaMobileView({ model, segmentContainerRef, storyData, currentIndex, activeSlide, layerName, handlePrev, handleNext, hasPrev, hasNext, setIndex, }) {
48
33
  const [container, setContainer] = useState(null);
49
34
  const [snapPoints, setSnapPoints] = useState([
50
35
  SNAP_FIRST_DEFAULT,
51
36
  1,
52
37
  ]);
53
38
  const [snap, setSnap] = useState(snapPoints[0]);
54
- const presentationStyle = getSpectaPresentationStyle(model);
39
+ const presentationStyle = getSpectaPresentationStyle(storyData);
55
40
  // Keep active snap in sync with snapPoints so Vaul's --snap-point-height stays defined.
56
41
  useEffect(() => {
57
42
  const isInSnapPoints = snapPoints.some(p => p === snap ||
@@ -110,5 +95,6 @@ export function MobileSpectaPanel({ model }) {
110
95
  React.createElement(DrawerTrigger, { asChild: true },
111
96
  React.createElement(Button, null, "Open Story Panel")),
112
97
  React.createElement(DrawerContent, { style: presentationStyle },
113
- React.createElement(StoryViewerPanel, { isSpecta: true, isMobile: true, model: model })))));
98
+ React.createElement("div", { id: SEGMENT_PANEL_ID, className: "jgis-story-viewer-panel" },
99
+ React.createElement(StoryViewerPanel, { model: model, isSpecta: true, isMobile: true, segmentContainerRef: segmentContainerRef, storyData: storyData, currentIndex: currentIndex, activeSlide: activeSlide, layerName: layerName, handlePrev: handlePrev, handleNext: handleNext, hasPrev: hasPrev, hasNext: hasNext, setIndex: setIndex }))))));
114
100
  }
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import type { StoryNavPlacement } from './StoryViewerPanel';
2
+ import type { StoryNavPlacement } from '../StoryViewerPanel';
3
3
  interface IStoryNavBarProps {
4
4
  placement: StoryNavPlacement;
5
5
  onPrev: () => void;
@@ -1,14 +1,12 @@
1
1
  import { ChevronLeft, ChevronRight } from 'lucide-react';
2
2
  import React from 'react';
3
- import { Button } from "../../shared/components/Button";
3
+ import { Button } from "../../../shared/components/Button";
4
4
  function StoryNavBar({ placement, onPrev, onNext, hasPrev, hasNext, }) {
5
5
  const containerClassName = placement === 'over-image'
6
6
  ? 'jgis-story-viewer-nav-container'
7
7
  : placement === 'subtitle-specta'
8
8
  ? 'jgis-story-viewer-nav-container-specta-mod'
9
- : placement === 'subtitle-specta-mobile'
10
- ? 'jgis-story-viewer-nav-container-specta-mobile'
11
- : undefined;
9
+ : undefined;
12
10
  const navbarClassName = placement === 'subtitle-specta'
13
11
  ? 'jgis-story-navbar jgis-story-navbar-specta-mod'
14
12
  : 'jgis-story-navbar';
@@ -0,0 +1,39 @@
1
+ import type { IJGISLayer, IJGISStoryMap, IJupyterGISModel } from '@jupytergis/schema';
2
+ import { CSSProperties, RefObject } from 'react';
3
+ /** Entry for a layer affected by layer override
4
+ * remove if we added a layer or restore if we modified an existing layer.
5
+ **/
6
+ export interface IOverrideLayerEntry {
7
+ layerId: string;
8
+ action: 'remove' | 'restore';
9
+ }
10
+ export interface IUseStoryMapParams {
11
+ model: IJupyterGISModel;
12
+ overrideLayerEntriesRef: RefObject<IOverrideLayerEntry[]>;
13
+ removeLayer?: (id: string) => void;
14
+ addLayer?: (id: string, layer: IJGISLayer, index: number) => Promise<void>;
15
+ isSpecta: boolean;
16
+ /** Panel root element for applying specta presentation CSS variables. */
17
+ panelRef?: RefObject<HTMLDivElement | null>;
18
+ }
19
+ /** Inline style for specta presentation (bg and text color from story). */
20
+ export declare function getSpectaPresentationStyle(story: IJGISStoryMap | null): CSSProperties;
21
+ export declare function useStoryMap({ model, overrideLayerEntriesRef, removeLayer, addLayer, panelRef, isSpecta, }: IUseStoryMapParams): {
22
+ storyData: IJGISStoryMap | null;
23
+ storySegments: IJGISLayer[];
24
+ currentIndex: number;
25
+ clearOverrideLayers: () => void;
26
+ setIndex: (index: number) => void;
27
+ handlePrev: () => void;
28
+ handleNext: () => void;
29
+ hasPrev: boolean;
30
+ hasNext: boolean;
31
+ setSelectedLayerByIndex: (index: number) => void;
32
+ currentStorySegment: IJGISLayer;
33
+ activeSlide: {
34
+ [k: string]: any;
35
+ } | undefined;
36
+ layerName: string;
37
+ currentStorySegmentId: string | undefined;
38
+ zoomToCurrentLayer: () => void;
39
+ };