@jupytergis/base 0.16.0-alpha.0 → 0.16.0-alpha.1

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 (60) hide show
  1. package/lib/commands/index.js +1 -2
  2. package/lib/constants.js +4 -0
  3. package/lib/features/layers/forms/layer/index.d.ts +0 -1
  4. package/lib/features/layers/forms/layer/index.js +0 -1
  5. package/lib/features/layers/symbology/Grammar.js +101 -20
  6. package/lib/features/layers/symbology/components/MappingRow.js +32 -28
  7. package/lib/features/layers/symbology/components/ScaleEditor.js +3 -3
  8. package/lib/features/layers/symbology/components/color_ramp/ColorRampControls.js +1 -1
  9. package/lib/features/layers/symbology/components/color_stops/StopContainer.js +1 -1
  10. package/lib/features/layers/symbology/components/color_stops/StopRow.js +13 -1
  11. package/lib/features/layers/symbology/grammarToOLStyle.js +22 -2
  12. package/lib/features/layers/symbology/styleBuilder.d.ts +3 -0
  13. package/lib/features/layers/symbology/styleBuilder.js +15 -2
  14. package/lib/features/layers/symbology/symbologyDialog.js +3 -5
  15. package/lib/features/story/SpectaPanel.js +1 -1
  16. package/lib/features/story/components/ListStoryStageOverlay.d.ts +0 -1
  17. package/lib/features/story/components/ListStoryStageOverlay.js +52 -34
  18. package/lib/features/story/components/ListStoryTitleBar.d.ts +7 -0
  19. package/lib/features/story/components/ListStoryTitleBar.js +20 -0
  20. package/lib/features/story/components/ListStoryTitleBarDesktop.d.ts +2 -0
  21. package/lib/features/story/components/ListStoryTitleBarDesktop.js +55 -0
  22. package/lib/features/story/components/ListStoryTitleBarMobile.d.ts +2 -0
  23. package/lib/features/story/components/ListStoryTitleBarMobile.js +41 -0
  24. package/lib/features/story/components/SpectaMobileListModeContent.d.ts +13 -0
  25. package/lib/features/story/components/SpectaMobileListModeContent.js +36 -0
  26. package/lib/features/story/components/SpectaMobileSingleModeContent.d.ts +14 -0
  27. package/lib/features/story/components/SpectaMobileSingleModeContent.js +98 -0
  28. package/lib/features/story/components/SpectaMobileView.d.ts +7 -3
  29. package/lib/features/story/components/SpectaMobileView.js +11 -99
  30. package/lib/features/story/context/ListStoryScrollTrackContext.d.ts +4 -0
  31. package/lib/features/story/context/ListStoryScrollTrackContext.js +50 -6
  32. package/lib/features/story/hooks/useStoryMap.js +1 -16
  33. package/lib/features/story/types/types.d.ts +5 -0
  34. package/lib/features/story/utils/computeListStoryScrollState.d.ts +2 -0
  35. package/lib/features/story/utils/computeListStoryScrollState.js +34 -25
  36. package/lib/mainview/components/MainViewMapSurface.d.ts +15 -0
  37. package/lib/mainview/components/MainViewMapSurface.js +13 -0
  38. package/lib/mainview/components/MainViewOverlayLayer.d.ts +9 -0
  39. package/lib/mainview/components/MainViewOverlayLayer.js +11 -0
  40. package/lib/mainview/components/MainViewSidePanels.d.ts +17 -0
  41. package/lib/mainview/components/MainViewSidePanels.js +10 -0
  42. package/lib/mainview/components/MainViewSpectaPanel.d.ts +17 -0
  43. package/lib/mainview/components/MainViewSpectaPanel.js +8 -0
  44. package/lib/mainview/components/MainViewStoryStage.d.ts +13 -0
  45. package/lib/mainview/components/MainViewStoryStage.js +17 -0
  46. package/lib/mainview/components/PositionedFloater.d.ts +10 -0
  47. package/lib/mainview/components/PositionedFloater.js +7 -0
  48. package/lib/mainview/mainView.d.ts +3 -7
  49. package/lib/mainview/mainView.js +84 -164
  50. package/lib/shared/formbuilder/formselectors.js +1 -4
  51. package/lib/types.js +0 -1
  52. package/package.json +2 -2
  53. package/style/base.css +18 -4
  54. package/style/layerBrowser.css +3 -3
  55. package/style/storyPanel.css +192 -2
  56. package/style/symbologyDialog.css +269 -32
  57. package/lib/features/layers/forms/layer/heatmapLayerForm.d.ts +0 -3
  58. package/lib/features/layers/forms/layer/heatmapLayerForm.js +0 -96
  59. package/lib/features/layers/symbology/Heatmap.d.ts +0 -4
  60. package/lib/features/layers/symbology/Heatmap.js +0 -109
@@ -2,6 +2,7 @@ import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
2
2
  import { ListStoryMapOverlayPanel } from "./ListStoryMapOverlayPanel";
3
3
  import { ListStoryOverlayMarkdown } from "./ListStoryOverlayMarkdown";
4
4
  import { useCurrentSegmentIndex } from "../hooks/useCurrentSegmentIndex";
5
+ import { isIntraSegmentScroll } from "../utils/computeListStoryScrollState";
5
6
  import { getSpectaPresentationCssVars } from "../utils/spectaPresentation";
6
7
  import { buildStorySegmentViewItems, getStoryMarkdownFromSlide, } from "../utils/storySegmentViewItems";
7
8
  import { getSegmentDisplayMode } from '../utils/listStoryScrollTrack';
@@ -25,12 +26,21 @@ function SegmentOverlayPane({ pane, segmentIndex, config, storyData, items, }) {
25
26
  const isMap = config.type === 'map';
26
27
  return (React.createElement("div", { "data-pane": pane, className: `jgis-story-segment-overlay-pane jgis-story-${isMap ? 'map' : 'markdown'}-scroll-pane` }, isMap ? (React.createElement(ListStoryMapOverlayPanel, { storyData: storyData, segmentIndex: config.segmentIndex, items: items })) : config.markdown ? (React.createElement(ListStoryOverlayMarkdown, { key: `pane-${pane}-seg-${segmentIndex}`, source: config.markdown })) : null));
27
28
  }
29
+ function buildFallbackTransition(activeItem) {
30
+ const mode = getSegmentDisplayMode(activeItem.activeSlide);
31
+ return {
32
+ progress: 0,
33
+ fromIndex: activeItem.index,
34
+ toIndex: activeItem.index,
35
+ fromMode: mode,
36
+ toMode: mode,
37
+ };
38
+ }
28
39
  /**
29
40
  * List-story stage overlay: map + markdown segments on the map stage.
30
- * The story column scrolls only the virtual track; this is the visible UI.
31
41
  */
32
42
  export function ListStoryStageOverlay({ model, segmentTransition, }) {
33
- var _a;
43
+ var _a, _b;
34
44
  const overlayRef = useRef(null);
35
45
  const stackRef = useRef(null);
36
46
  const [stageHeight, setStageHeight] = useState(0);
@@ -43,12 +53,19 @@ export function ListStoryStageOverlay({ model, segmentTransition, }) {
43
53
  story === null || story === void 0 ? void 0 : story.presentationBgColor,
44
54
  story === null || story === void 0 ? void 0 : story.presentationTextColor,
45
55
  ]);
46
- const isTransitioning = segmentTransition !== null;
47
56
  const activeItem = items.find(item => item.index === currentIndex);
48
- const activeMode = getSegmentDisplayMode(activeItem === null || activeItem === void 0 ? void 0 : activeItem.activeSlide);
49
- const transitionProgress = isTransitioning ? segmentTransition.progress : 1;
57
+ const transition = useMemo(() => {
58
+ if (segmentTransition) {
59
+ return segmentTransition;
60
+ }
61
+ if (activeItem) {
62
+ return buildFallbackTransition(activeItem);
63
+ }
64
+ return null;
65
+ }, [segmentTransition, activeItem]);
66
+ const intraSegmentScroll = isIntraSegmentScroll(transition);
50
67
  const { fromIndex, toIndex, fromPaneConfig, toPaneConfig } = useMemo(() => {
51
- if (!model || !activeItem) {
68
+ if (!model || !transition) {
52
69
  return {
53
70
  fromIndex: currentIndex,
54
71
  toIndex: currentIndex,
@@ -56,22 +73,22 @@ export function ListStoryStageOverlay({ model, segmentTransition, }) {
56
73
  toPaneConfig: EMPTY_MARKDOWN_PANE,
57
74
  };
58
75
  }
59
- if (segmentTransition) {
60
- return {
61
- fromIndex: segmentTransition.fromIndex,
62
- toIndex: segmentTransition.toIndex,
63
- fromPaneConfig: buildPaneConfig(items.find(item => item.index === segmentTransition.fromIndex), segmentTransition.fromMode),
64
- toPaneConfig: buildPaneConfig(items.find(item => item.index === segmentTransition.toIndex), segmentTransition.toMode),
65
- };
66
- }
67
- return {
68
- fromIndex: currentIndex,
69
- toIndex: currentIndex,
70
- fromPaneConfig: EMPTY_MARKDOWN_PANE,
71
- toPaneConfig: buildPaneConfig(activeItem, activeMode),
76
+ const fromItem = items.find(item => item.index === transition.fromIndex);
77
+ const toItem = items.find(item => item.index === transition.toIndex);
78
+ const fromPaneConfig = buildPaneConfig(fromItem, transition.fromMode);
79
+ const toPaneConfig = intraSegmentScroll
80
+ ? EMPTY_MARKDOWN_PANE
81
+ : buildPaneConfig(toItem, transition.toMode);
82
+ const paneState = {
83
+ fromIndex: transition.fromIndex,
84
+ toIndex: transition.toIndex,
85
+ fromPaneConfig,
86
+ toPaneConfig,
72
87
  };
73
- }, [items, activeItem, currentIndex, activeMode, segmentTransition]);
88
+ return paneState;
89
+ }, [items, currentIndex, transition, intraSegmentScroll, model]);
74
90
  const overlayHeight = Math.max(stageHeight, 0);
91
+ const transitionProgress = (_b = transition === null || transition === void 0 ? void 0 : transition.progress) !== null && _b !== void 0 ? _b : 0;
75
92
  useLayoutEffect(() => {
76
93
  var _a;
77
94
  const parent = (_a = overlayRef.current) === null || _a === void 0 ? void 0 : _a.parentElement;
@@ -91,21 +108,20 @@ export function ListStoryStageOverlay({ model, segmentTransition, }) {
91
108
  };
92
109
  }, [model, story]);
93
110
  useLayoutEffect(() => {
94
- if (!isTransitioning) {
95
- setTransitionTranslatePx(0);
96
- return;
97
- }
98
111
  const stack = stackRef.current;
99
112
  if (!stack) {
100
113
  return;
101
114
  }
102
115
  const measure = () => {
103
116
  const fromPane = stack.querySelector('[data-pane="from"]');
104
- const gap = stack.querySelector('.jgis-story-segment-transition-gap');
105
- if (!(fromPane instanceof HTMLElement) || !(gap instanceof HTMLElement)) {
117
+ if (!(fromPane instanceof HTMLElement)) {
106
118
  return;
107
119
  }
108
- const travel = fromPane.offsetHeight + gap.offsetHeight;
120
+ const gap = stack.querySelector('.jgis-story-segment-transition-gap');
121
+ const gapHeight = gap instanceof HTMLElement && !intraSegmentScroll
122
+ ? gap.offsetHeight
123
+ : 0;
124
+ const travel = fromPane.offsetHeight + gapHeight;
109
125
  setTransitionTranslatePx(prev => (prev === travel ? prev : travel));
110
126
  };
111
127
  measure();
@@ -114,19 +130,21 @@ export function ListStoryStageOverlay({ model, segmentTransition, }) {
114
130
  return () => {
115
131
  ro.disconnect();
116
132
  };
117
- }, [isTransitioning, fromIndex, toIndex, fromPaneConfig, toPaneConfig]);
133
+ }, [fromIndex, toIndex, fromPaneConfig, toPaneConfig, intraSegmentScroll]);
118
134
  const overlaySized = stageHeight > 0;
119
- if (!model || !story || !activeItem) {
135
+ if (!model || !story || !activeItem || !transition) {
120
136
  return null;
121
137
  }
122
- return (React.createElement("div", { ref: overlayRef, className: `jgis-story-stage-overlay${overlaySized ? ' jgis-story-stage-overlay--sized' : ''}${isTransitioning ? ' jgis-story-stage-overlay--transitioning' : ''}`, style: Object.assign(Object.assign(Object.assign({}, spectaPresentationStyle), { '--jgis-segment-transition-progress': transitionProgress }), (overlaySized
138
+ return (React.createElement("div", { ref: overlayRef, className: `jgis-story-stage-overlay${overlaySized ? ' jgis-story-stage-overlay--sized' : ''} jgis-story-stage-overlay--transitioning`, style: Object.assign(Object.assign(Object.assign({}, spectaPresentationStyle), { '--jgis-segment-transition-progress': transitionProgress }), (overlaySized
123
139
  ? {
124
140
  height: overlayHeight,
125
141
  '--jgis-handoff-gap-height': `${stageHeight}px`,
126
142
  '--jgis-transition-translate': `${transitionTranslatePx || stageHeight}px`,
127
143
  }
128
- : {})) }, isTransitioning ? (React.createElement("div", { ref: stackRef, className: "jgis-story-segment-transition-stack" },
129
- React.createElement(SegmentOverlayPane, { pane: "from", segmentIndex: fromIndex, config: fromPaneConfig, storyData: story, items: items }),
130
- React.createElement("div", { className: "jgis-story-segment-transition-gap", "aria-hidden": true }),
131
- React.createElement(SegmentOverlayPane, { pane: "to", segmentIndex: toIndex, config: toPaneConfig, storyData: story, items: items }))) : (React.createElement(SegmentOverlayPane, { pane: "to", segmentIndex: toIndex, config: toPaneConfig, storyData: story, items: items }))));
144
+ : {})) },
145
+ React.createElement("div", { ref: stackRef, className: "jgis-story-segment-transition-stack" },
146
+ React.createElement(SegmentOverlayPane, { pane: "from", segmentIndex: fromIndex, config: fromPaneConfig, storyData: story, items: items }),
147
+ !intraSegmentScroll ? (React.createElement(React.Fragment, null,
148
+ React.createElement("div", { className: "jgis-story-segment-transition-gap", "aria-hidden": true }),
149
+ React.createElement(SegmentOverlayPane, { pane: "to", segmentIndex: toIndex, config: toPaneConfig, storyData: story, items: items }))) : null)));
132
150
  }
@@ -0,0 +1,7 @@
1
+ import { IJupyterGISModel } from '@jupytergis/schema';
2
+ interface IListStoryTitleBarProps {
3
+ model: IJupyterGISModel;
4
+ isMobile: boolean;
5
+ }
6
+ export declare function ListStoryTitleBar({ model, isMobile, }: IListStoryTitleBarProps): JSX.Element;
7
+ export {};
@@ -0,0 +1,20 @@
1
+ import React, { useMemo } from 'react';
2
+ import { ListStoryTitleBarDesktop } from "./ListStoryTitleBarDesktop";
3
+ import { ListStoryTitleBarMobile } from "./ListStoryTitleBarMobile";
4
+ import { useListStoryScrollTrackContext } from "../context/ListStoryScrollTrackContext";
5
+ import { useCurrentSegmentIndex } from "../hooks/useCurrentSegmentIndex";
6
+ import { buildStorySegmentViewItems } from "../utils/storySegmentViewItems";
7
+ export function ListStoryTitleBar({ model, isMobile, }) {
8
+ const currentIndex = useCurrentSegmentIndex(model);
9
+ const { scrollToSegmentIndex } = useListStoryScrollTrackContext();
10
+ const segmentItems = useMemo(() => { var _a; return buildStorySegmentViewItems(model, (_a = model.getSelectedStory().story) !== null && _a !== void 0 ? _a : null); }, [model]);
11
+ const titleBarProps = {
12
+ segmentItems,
13
+ currentIndex,
14
+ onSegmentClick: scrollToSegmentIndex,
15
+ };
16
+ if (isMobile) {
17
+ return React.createElement(ListStoryTitleBarMobile, Object.assign({}, titleBarProps));
18
+ }
19
+ return React.createElement(ListStoryTitleBarDesktop, Object.assign({}, titleBarProps));
20
+ }
@@ -0,0 +1,2 @@
1
+ import type { IListStoryTitleBarContentProps } from "../types/types";
2
+ export declare function ListStoryTitleBarDesktop({ segmentItems, currentIndex, onSegmentClick, }: IListStoryTitleBarContentProps): JSX.Element;
@@ -0,0 +1,55 @@
1
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
2
+ import React, { useLayoutEffect, useRef, useState } from 'react';
3
+ import { Button } from "../../../shared/components/Button";
4
+ export function ListStoryTitleBarDesktop({ segmentItems, currentIndex, onSegmentClick, }) {
5
+ const segmentsRef = useRef(null);
6
+ const [hasOverflow, setHasOverflow] = useState(false);
7
+ const currentPosition = segmentItems.findIndex(item => item.index === currentIndex);
8
+ const hasPrev = currentPosition > 0;
9
+ const hasNext = currentPosition >= 0 && currentPosition < segmentItems.length - 1;
10
+ const goToAdjacentSegment = (direction) => {
11
+ const nextPosition = currentPosition + direction;
12
+ const nextItem = segmentItems[nextPosition];
13
+ if (!nextItem) {
14
+ return;
15
+ }
16
+ onSegmentClick(nextItem.index);
17
+ };
18
+ useLayoutEffect(() => {
19
+ const segments = segmentsRef.current;
20
+ if (!segments) {
21
+ return;
22
+ }
23
+ const update = () => {
24
+ setHasOverflow(segments.scrollWidth > segments.clientWidth);
25
+ };
26
+ update();
27
+ const ro = new ResizeObserver(update);
28
+ ro.observe(segments);
29
+ return () => {
30
+ ro.disconnect();
31
+ };
32
+ }, []);
33
+ useLayoutEffect(() => {
34
+ const segments = segmentsRef.current;
35
+ if (!segments) {
36
+ return;
37
+ }
38
+ const active = segments.querySelector('.jgis-story-title-bar-segment[data-state="active"]');
39
+ if (!active) {
40
+ return;
41
+ }
42
+ active.scrollIntoView({
43
+ behavior: 'smooth',
44
+ });
45
+ }, [currentIndex]);
46
+ return (React.createElement("nav", { className: "jgis-story-title-bar", "aria-label": "Story segments" },
47
+ hasOverflow ? (React.createElement(Button, { type: "button", variant: "ghost", size: "icon-sm", className: "jgis-story-title-bar-scroll-btn", "aria-label": "Previous segment", disabled: !hasPrev, onClick: () => goToAdjacentSegment(-1) },
48
+ React.createElement(ChevronLeft, null))) : null,
49
+ React.createElement("div", { ref: segmentsRef, className: "jgis-story-title-bar-segments", onWheelCapture: event => event.stopPropagation() }, segmentItems.map(item => {
50
+ const isActive = item.index === currentIndex;
51
+ return (React.createElement("button", { key: item.id, type: "button", className: "jgis-story-title-bar-label jgis-story-title-bar-segment", "data-state": isActive ? 'active' : 'inactive', "aria-current": isActive ? 'true' : undefined, "aria-label": `Go to ${item.layerName}`, onClick: () => onSegmentClick(item.index) }, item.layerName));
52
+ })),
53
+ hasOverflow ? (React.createElement(Button, { type: "button", variant: "ghost", size: "icon-sm", className: "jgis-story-title-bar-scroll-btn", "aria-label": "Next segment", disabled: !hasNext, onClick: () => goToAdjacentSegment(1) },
54
+ React.createElement(ChevronRight, null))) : null));
55
+ }
@@ -0,0 +1,2 @@
1
+ import type { IListStoryTitleBarContentProps } from "../types/types";
2
+ export declare function ListStoryTitleBarMobile({ segmentItems, currentIndex, onSegmentClick, }: IListStoryTitleBarContentProps): JSX.Element;
@@ -0,0 +1,41 @@
1
+ import { Menu } from 'lucide-react';
2
+ import React, { useRef, useState } from 'react';
3
+ import { Button } from "../../../shared/components/Button";
4
+ import { Popover, PopoverContent, PopoverTrigger, } from "../../../shared/components/Popover";
5
+ function getSlideDirection(prevSegmentId, currentPosition, segmentItems) {
6
+ if (!prevSegmentId || currentPosition < 0) {
7
+ return undefined;
8
+ }
9
+ const prevPosition = segmentItems.findIndex(item => item.id === prevSegmentId);
10
+ if (prevPosition < 0) {
11
+ return undefined;
12
+ }
13
+ return currentPosition > prevPosition ? 'next' : 'prev';
14
+ }
15
+ export function ListStoryTitleBarMobile({ segmentItems, currentIndex, onSegmentClick, }) {
16
+ var _a;
17
+ const [menuOpen, setMenuOpen] = useState(false);
18
+ const currentPosition = segmentItems.findIndex(item => item.index === currentIndex);
19
+ const activeSegment = currentPosition >= 0 ? segmentItems[currentPosition] : undefined;
20
+ const prevSegmentIdRef = useRef(undefined);
21
+ const slideDirectionRef = useRef(undefined);
22
+ const activeSegmentId = activeSegment === null || activeSegment === void 0 ? void 0 : activeSegment.id;
23
+ if (activeSegmentId !== prevSegmentIdRef.current) {
24
+ slideDirectionRef.current = getSlideDirection(prevSegmentIdRef.current, currentPosition, segmentItems);
25
+ prevSegmentIdRef.current = activeSegmentId;
26
+ }
27
+ const handleMenuSegmentClick = (index) => {
28
+ onSegmentClick(index);
29
+ setMenuOpen(false);
30
+ };
31
+ return (React.createElement("nav", { className: "jgis-story-title-bar jgis-story-title-bar--mobile", "aria-label": "Story segments" },
32
+ React.createElement(Popover, { open: menuOpen, onOpenChange: setMenuOpen },
33
+ React.createElement(PopoverTrigger, { asChild: true },
34
+ React.createElement(Button, { type: "button", variant: "ghost", className: "jgis-story-title-bar-menu-btn", "aria-label": "Open story menu" },
35
+ React.createElement(Menu, null))),
36
+ React.createElement(PopoverContent, { align: "center", side: "bottom", className: "jgis-story-title-bar-segment-menu" }, segmentItems.map(item => {
37
+ const isActive = item.index === currentIndex;
38
+ return (React.createElement("button", { key: item.id, type: "button", className: "jgis-story-title-bar-label jgis-story-title-bar-segment-menu-item", "data-state": isActive ? 'active' : 'inactive', "aria-current": isActive ? 'true' : undefined, onClick: () => handleMenuSegmentClick(item.index) }, item.layerName));
39
+ }))),
40
+ React.createElement("span", { key: activeSegment === null || activeSegment === void 0 ? void 0 : activeSegment.id, className: "jgis-story-title-bar-active-segment", "data-state": "active", "data-slide-direction": slideDirectionRef.current }, (_a = activeSegment === null || activeSegment === void 0 ? void 0 : activeSegment.layerName) !== null && _a !== void 0 ? _a : '')));
41
+ }
@@ -0,0 +1,13 @@
1
+ import { IJGISStoryMap, IJupyterGISModel } from '@jupytergis/schema';
2
+ import type { IListStorySegmentTransition } from "../types/types";
3
+ export interface ISpectaMobileListModeContentProps {
4
+ model: IJupyterGISModel;
5
+ storyData: IJGISStoryMap | null;
6
+ currentIndex: number;
7
+ setIndex: (index: number) => void;
8
+ onSegmentTransitionChange?: (payload: IListStorySegmentTransition | null) => void;
9
+ }
10
+ /**
11
+ * Mobile list story: full-viewport touch scroll on the virtual track drives
12
+ */
13
+ export declare function SpectaMobileListModeContent({ model, storyData, currentIndex, setIndex, onSegmentTransitionChange, }: ISpectaMobileListModeContentProps): JSX.Element;
@@ -0,0 +1,36 @@
1
+ import React, { useLayoutEffect, useMemo, useRef } from 'react';
2
+ import { ListStoryVirtualScrollTrack } from "./ListStoryVirtualScrollTrack";
3
+ import { useListStoryScrollTrackContext } from "../context/ListStoryScrollTrackContext";
4
+ import { useListStoryScroll } from "../hooks/useListStoryScroll";
5
+ import { getSpectaPresentationStyle } from "../utils/spectaPresentation";
6
+ import { buildStorySegmentViewItems } from "../utils/storySegmentViewItems";
7
+ import { STORY_TYPE } from "../../../types";
8
+ /**
9
+ * Mobile list story: full-viewport touch scroll on the virtual track drives
10
+ */
11
+ export function SpectaMobileListModeContent({ model, storyData, currentIndex, setIndex, onSegmentTransitionChange, }) {
12
+ const scrollContainerRef = useRef(null);
13
+ const { scrollTrackLayout, bindScrollTrackElement } = useListStoryScrollTrackContext();
14
+ const segmentViewItems = useMemo(() => buildStorySegmentViewItems(model, storyData), [model, storyData]);
15
+ const presentationStyle = getSpectaPresentationStyle(storyData);
16
+ const segmentTransitionSyncEnabled = Boolean(onSegmentTransitionChange) &&
17
+ (storyData === null || storyData === void 0 ? void 0 : storyData.storyType) === STORY_TYPE.verticalScroll;
18
+ useLayoutEffect(() => {
19
+ bindScrollTrackElement(scrollContainerRef.current);
20
+ return () => {
21
+ bindScrollTrackElement(null);
22
+ };
23
+ }, [bindScrollTrackElement]);
24
+ useListStoryScroll({
25
+ enabled: segmentTransitionSyncEnabled,
26
+ scrollContainerRef,
27
+ storyData,
28
+ scrollTrackLayout,
29
+ items: segmentViewItems,
30
+ currentIndex,
31
+ setIndex,
32
+ onSegmentTransitionChange,
33
+ });
34
+ return (React.createElement("div", { ref: scrollContainerRef, id: "jgis-story-segment-panel", className: "jgis-story-viewer-panel-specta-mod-vertical-scroll jgis-story-mobile-list-scroll", style: presentationStyle },
35
+ React.createElement(ListStoryVirtualScrollTrack, { scrollTrackLayout: scrollTrackLayout })));
36
+ }
@@ -0,0 +1,14 @@
1
+ import { IJGISStoryMap, IStorySegmentLayer } from '@jupytergis/schema';
2
+ import { RefObject } from 'react';
3
+ export interface ISpectaMobileSingleModeContentProps {
4
+ segmentContainerRef: RefObject<HTMLDivElement>;
5
+ storyData: IJGISStoryMap | null;
6
+ currentIndex: number;
7
+ activeSlide: IStorySegmentLayer['parameters'] | undefined;
8
+ layerName: string;
9
+ handlePrev: () => void;
10
+ handleNext: () => void;
11
+ hasPrev: boolean;
12
+ hasNext: boolean;
13
+ }
14
+ export declare function SpectaMobileSingleModeContent({ segmentContainerRef, storyData, currentIndex, activeSlide, layerName, handlePrev, handleNext, hasPrev, hasNext, }: ISpectaMobileSingleModeContentProps): JSX.Element;
@@ -0,0 +1,98 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import StoryViewerPanel from "../StoryViewerPanel";
3
+ import { getSpectaPresentationStyle } from "../utils/spectaPresentation";
4
+ import { Button } from "../../../shared/components/Button";
5
+ import { Drawer, DrawerContent, DrawerTrigger, } from "../../../shared/components/Drawer";
6
+ const MAIN_ID = 'jp-main-content-panel';
7
+ const SEGMENT_PANEL_ID = 'jgis-story-segment-panel';
8
+ const SEGMENT_HEADER_ID = 'jgis-story-segment-header';
9
+ const SNAP_FIRST_MIN = 0.3;
10
+ const SNAP_FIRST_MAX = 0.95;
11
+ const SNAP_FIRST_DEFAULT = 0.7;
12
+ /** Offset (px) for segment header height: margins from p and h1 in story content */
13
+ const SEGMENT_HEADER_OFFSET_PX = 16.8 * 2 + 18.76;
14
+ /**
15
+ * Compute the first snap point so that vaul's --snap-point-height (the
16
+ * transform offset) equals #jgis-story-segment-panel height minus #jgis-story-segment-header height.
17
+ * For a bottom drawer, offset = mainHeight * (1 - snapPoint), so
18
+ * snapPoint = (mainHeight - offset) / mainHeight.
19
+ */
20
+ function getFirstSnapFromSegmentHeader(mainEl, segmentPanelEl, segmentHeaderEl) {
21
+ const mainHeight = mainEl.getBoundingClientRect().height;
22
+ const segmentPanelHeight = segmentPanelEl.getBoundingClientRect().height;
23
+ const segmentHeaderHeight = segmentHeaderEl.getBoundingClientRect().height;
24
+ const offsetPx = segmentPanelHeight - segmentHeaderHeight - SEGMENT_HEADER_OFFSET_PX;
25
+ if (mainHeight <= 0) {
26
+ return SNAP_FIRST_DEFAULT;
27
+ }
28
+ const fraction = (mainHeight - offsetPx) / mainHeight;
29
+ const clamped = Math.max(SNAP_FIRST_MIN, Math.min(SNAP_FIRST_MAX, fraction));
30
+ return clamped;
31
+ }
32
+ export function SpectaMobileSingleModeContent({ segmentContainerRef, storyData, currentIndex, activeSlide, layerName, handlePrev, handleNext, hasPrev, hasNext, }) {
33
+ const [container, setContainer] = useState(null);
34
+ const [snapPoints, setSnapPoints] = useState([
35
+ SNAP_FIRST_DEFAULT,
36
+ 1,
37
+ ]);
38
+ const [snap, setSnap] = useState(snapPoints[0]);
39
+ const presentationStyle = getSpectaPresentationStyle(storyData);
40
+ useEffect(() => {
41
+ const isInSnapPoints = snapPoints.some(p => p === snap ||
42
+ (typeof p === 'number' &&
43
+ typeof snap === 'number' &&
44
+ Math.abs(p - snap) < 1e-9));
45
+ if (!isInSnapPoints && snapPoints.length > 0) {
46
+ setSnap(snapPoints[0]);
47
+ }
48
+ }, [snapPoints, snap]);
49
+ useEffect(() => {
50
+ const mainEl = document.getElementById(MAIN_ID);
51
+ setContainer(mainEl);
52
+ if (!mainEl) {
53
+ return;
54
+ }
55
+ const updateFirstSnap = () => {
56
+ const segmentPanelEl = document.getElementById(SEGMENT_PANEL_ID);
57
+ const segmentHeaderEl = document.getElementById(SEGMENT_HEADER_ID);
58
+ if (segmentPanelEl && segmentHeaderEl) {
59
+ const firstSnap = getFirstSnapFromSegmentHeader(mainEl, segmentPanelEl, segmentHeaderEl);
60
+ setSnapPoints([firstSnap, 1]);
61
+ }
62
+ };
63
+ const resizeObserver = new ResizeObserver(() => updateFirstSnap());
64
+ let observedPanelEl = null;
65
+ const syncHeaderObserver = () => {
66
+ const segmentPanelEl = document.getElementById(SEGMENT_PANEL_ID);
67
+ const segmentHeaderEl = document.getElementById(SEGMENT_HEADER_ID);
68
+ if (!segmentPanelEl ||
69
+ !segmentHeaderEl ||
70
+ segmentPanelEl === observedPanelEl) {
71
+ return;
72
+ }
73
+ if (observedPanelEl) {
74
+ resizeObserver.unobserve(observedPanelEl);
75
+ }
76
+ resizeObserver.observe(segmentPanelEl);
77
+ observedPanelEl = segmentPanelEl;
78
+ updateFirstSnap();
79
+ };
80
+ syncHeaderObserver();
81
+ const mutationObserver = new MutationObserver(syncHeaderObserver);
82
+ mutationObserver.observe(mainEl, {
83
+ childList: true,
84
+ subtree: true,
85
+ });
86
+ return () => {
87
+ resizeObserver.disconnect();
88
+ mutationObserver.disconnect();
89
+ };
90
+ }, []);
91
+ return (React.createElement("div", { className: "jgis-mobile-specta-trigger-wrapper" },
92
+ React.createElement(Drawer, { snapPoints: snapPoints, activeSnapPoint: snap, setActiveSnapPoint: setSnap, direction: "bottom", container: container, noBodyStyles: true },
93
+ React.createElement(DrawerTrigger, { asChild: true },
94
+ React.createElement(Button, null, "Open Story Panel")),
95
+ React.createElement(DrawerContent, { style: presentationStyle },
96
+ React.createElement("div", { id: SEGMENT_PANEL_ID, className: "jgis-story-viewer-panel" },
97
+ React.createElement(StoryViewerPanel, { isSpecta: true, isMobile: true, segmentContainerRef: segmentContainerRef, storyData: storyData, currentIndex: currentIndex, activeSlide: activeSlide, layerName: layerName, segmentNav: { handlePrev, handleNext, hasPrev, hasNext } }))))));
98
+ }
@@ -1,15 +1,19 @@
1
- import { IJGISStoryMap, IStorySegmentLayer } from '@jupytergis/schema';
2
- import React, { RefObject } from 'react';
1
+ import { IJGISStoryMap, IJupyterGISModel, IStorySegmentLayer } from '@jupytergis/schema';
2
+ import { RefObject } from 'react';
3
+ import type { IListStorySegmentTransition } from "../types/types";
3
4
  interface ISpectaMobileViewProps {
5
+ model: IJupyterGISModel;
4
6
  segmentContainerRef: RefObject<HTMLDivElement>;
5
7
  storyData: IJGISStoryMap | null;
6
8
  currentIndex: number;
9
+ setIndex: (index: number) => void;
7
10
  activeSlide: IStorySegmentLayer['parameters'] | undefined;
8
11
  layerName: string;
9
12
  handlePrev: () => void;
10
13
  handleNext: () => void;
11
14
  hasPrev: boolean;
12
15
  hasNext: boolean;
16
+ onSegmentTransitionChange?: (payload: IListStorySegmentTransition | null) => void;
13
17
  }
14
- export declare function SpectaMobileView({ segmentContainerRef, storyData, currentIndex, activeSlide, layerName, handlePrev, handleNext, hasPrev, hasNext, }: ISpectaMobileViewProps): React.JSX.Element;
18
+ export declare function SpectaMobileView({ model, segmentContainerRef, storyData, currentIndex, setIndex, activeSlide, layerName, handlePrev, handleNext, hasPrev, hasNext, onSegmentTransitionChange, }: ISpectaMobileViewProps): JSX.Element;
15
19
  export {};
@@ -1,100 +1,12 @@
1
- import React, { useEffect, useState } from 'react';
2
- import { getSpectaPresentationStyle } from "../utils/spectaPresentation";
3
- import { Button } from "../../../shared/components/Button";
4
- import { Drawer, DrawerContent, DrawerTrigger, } from "../../../shared/components/Drawer";
5
- import StoryViewerPanel from '../StoryViewerPanel';
6
- const MAIN_ID = 'jp-main-content-panel';
7
- const SEGMENT_PANEL_ID = 'jgis-story-segment-panel';
8
- const SEGMENT_HEADER_ID = 'jgis-story-segment-header';
9
- const SNAP_FIRST_MIN = 0.3;
10
- const SNAP_FIRST_MAX = 0.95;
11
- const SNAP_FIRST_DEFAULT = 0.7;
12
- /** Offset (px) for segment header height: margins from p and h1 in story content */
13
- const SEGMENT_HEADER_OFFSET_PX = 16.8 * 2 + 18.76;
14
- /**
15
- * Compute the first snap point so that vaul's --snap-point-height (the
16
- * transform offset) equals #jgis-story-segment-panel height minus #jgis-story-segment-header height.
17
- * For a bottom drawer, offset = mainHeight * (1 - snapPoint), so
18
- * snapPoint = (mainHeight - offset) / mainHeight.
19
- */
20
- function getFirstSnapFromSegmentHeader(mainEl, segmentPanelEl, segmentHeaderEl) {
21
- const mainHeight = mainEl.getBoundingClientRect().height;
22
- const segmentPanelHeight = segmentPanelEl.getBoundingClientRect().height;
23
- const segmentHeaderHeight = segmentHeaderEl.getBoundingClientRect().height;
24
- const offsetPx = segmentPanelHeight - segmentHeaderHeight - SEGMENT_HEADER_OFFSET_PX;
25
- if (mainHeight <= 0) {
26
- return SNAP_FIRST_DEFAULT;
27
- }
28
- const fraction = (mainHeight - offsetPx) / mainHeight;
29
- const clamped = Math.max(SNAP_FIRST_MIN, Math.min(SNAP_FIRST_MAX, fraction));
30
- return clamped;
31
- }
32
- export function SpectaMobileView({ segmentContainerRef, storyData, currentIndex, activeSlide, layerName, handlePrev, handleNext, hasPrev, hasNext, }) {
33
- const [container, setContainer] = useState(null);
34
- const [snapPoints, setSnapPoints] = useState([
35
- SNAP_FIRST_DEFAULT,
36
- 1,
37
- ]);
38
- const [snap, setSnap] = useState(snapPoints[0]);
39
- const presentationStyle = getSpectaPresentationStyle(storyData);
40
- // Keep active snap in sync with snapPoints so Vaul's --snap-point-height stays defined.
41
- useEffect(() => {
42
- const isInSnapPoints = snapPoints.some(p => p === snap ||
43
- (typeof p === 'number' &&
44
- typeof snap === 'number' &&
45
- Math.abs(p - snap) < 1e-9));
46
- if (!isInSnapPoints && snapPoints.length > 0) {
47
- setSnap(snapPoints[0]);
48
- }
49
- }, [snapPoints, snap]);
50
- // Observe #jgis-story-segment-panel (and re-attach when drawer reopens).
51
- useEffect(() => {
52
- const mainEl = document.getElementById(MAIN_ID);
53
- setContainer(mainEl);
54
- if (!mainEl) {
55
- return;
56
- }
57
- const updateFirstSnap = () => {
58
- const segmentPanelEl = document.getElementById(SEGMENT_PANEL_ID);
59
- const segmentHeaderEl = document.getElementById(SEGMENT_HEADER_ID);
60
- if (segmentPanelEl && segmentHeaderEl) {
61
- const firstSnap = getFirstSnapFromSegmentHeader(mainEl, segmentPanelEl, segmentHeaderEl);
62
- setSnapPoints([firstSnap, 1]);
63
- }
64
- };
65
- const resizeObserver = new ResizeObserver(() => updateFirstSnap());
66
- let observedPanelEl = null;
67
- const syncHeaderObserver = () => {
68
- const segmentPanelEl = document.getElementById(SEGMENT_PANEL_ID);
69
- const segmentHeaderEl = document.getElementById(SEGMENT_HEADER_ID);
70
- if (!segmentPanelEl ||
71
- !segmentHeaderEl ||
72
- segmentPanelEl === observedPanelEl) {
73
- return;
74
- }
75
- if (observedPanelEl) {
76
- resizeObserver.unobserve(observedPanelEl);
77
- }
78
- resizeObserver.observe(segmentPanelEl);
79
- observedPanelEl = segmentPanelEl;
80
- updateFirstSnap();
81
- };
82
- syncHeaderObserver();
83
- const mutationObserver = new MutationObserver(syncHeaderObserver);
84
- mutationObserver.observe(mainEl, {
85
- childList: true,
86
- subtree: true,
87
- });
88
- return () => {
89
- resizeObserver.disconnect();
90
- mutationObserver.disconnect();
91
- };
92
- }, []);
93
- return (React.createElement("div", { className: "jgis-mobile-specta-trigger-wrapper" },
94
- React.createElement(Drawer, { snapPoints: snapPoints, activeSnapPoint: snap, setActiveSnapPoint: setSnap, direction: "bottom", container: container, noBodyStyles: true },
95
- React.createElement(DrawerTrigger, { asChild: true },
96
- React.createElement(Button, null, "Open Story Panel")),
97
- React.createElement(DrawerContent, { style: presentationStyle },
98
- React.createElement("div", { id: SEGMENT_PANEL_ID, className: "jgis-story-viewer-panel" },
99
- React.createElement(StoryViewerPanel, { isSpecta: true, isMobile: true, segmentContainerRef: segmentContainerRef, storyData: storyData, currentIndex: currentIndex, activeSlide: activeSlide, layerName: layerName, segmentNav: { handlePrev, handleNext, hasPrev, hasNext } }))))));
1
+ import React from 'react';
2
+ import { SpectaMobileListModeContent } from "./SpectaMobileListModeContent";
3
+ import { SpectaMobileSingleModeContent } from "./SpectaMobileSingleModeContent";
4
+ import { STORY_TYPE } from "../../../types";
5
+ export function SpectaMobileView({ model, segmentContainerRef, storyData, currentIndex, setIndex, activeSlide, layerName, handlePrev, handleNext, hasPrev, hasNext, onSegmentTransitionChange, }) {
6
+ const viewMode = (storyData === null || storyData === void 0 ? void 0 : storyData.storyType) === STORY_TYPE.verticalScroll ? 'list' : 'single';
7
+ const renderModeContent = {
8
+ single: () => (React.createElement(SpectaMobileSingleModeContent, { segmentContainerRef: segmentContainerRef, storyData: storyData, currentIndex: currentIndex, activeSlide: activeSlide, layerName: layerName, handlePrev: handlePrev, handleNext: handleNext, hasPrev: hasPrev, hasNext: hasNext })),
9
+ list: () => (React.createElement(SpectaMobileListModeContent, { model: model, storyData: storyData, currentIndex: currentIndex, setIndex: setIndex, onSegmentTransitionChange: onSegmentTransitionChange })),
10
+ };
11
+ return renderModeContent[viewMode]();
100
12
  }
@@ -3,7 +3,11 @@ import React from 'react';
3
3
  import type { IListStoryScrollTrackLayout } from "../types/types";
4
4
  interface IListStoryScrollTrackContextValue {
5
5
  scrollTrackLayout: IListStoryScrollTrackLayout | null;
6
+ scrollTop: number;
6
7
  bindScrollTrackElement: (element: HTMLDivElement | null) => void;
8
+ scrollToSegmentIndex: (index: number, options?: {
9
+ behavior?: ScrollBehavior;
10
+ }) => void;
7
11
  }
8
12
  interface IListStoryScrollTrackProviderProps {
9
13
  model: IJupyterGISModel;