@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.
- package/lib/commands/index.js +1 -2
- package/lib/constants.js +4 -0
- package/lib/features/layers/forms/layer/index.d.ts +0 -1
- package/lib/features/layers/forms/layer/index.js +0 -1
- package/lib/features/layers/symbology/Grammar.js +101 -20
- package/lib/features/layers/symbology/components/MappingRow.js +32 -28
- package/lib/features/layers/symbology/components/ScaleEditor.js +3 -3
- package/lib/features/layers/symbology/components/color_ramp/ColorRampControls.js +1 -1
- package/lib/features/layers/symbology/components/color_stops/StopContainer.js +1 -1
- package/lib/features/layers/symbology/components/color_stops/StopRow.js +13 -1
- package/lib/features/layers/symbology/grammarToOLStyle.js +22 -2
- package/lib/features/layers/symbology/styleBuilder.d.ts +3 -0
- package/lib/features/layers/symbology/styleBuilder.js +15 -2
- package/lib/features/layers/symbology/symbologyDialog.js +3 -5
- package/lib/features/story/SpectaPanel.js +1 -1
- package/lib/features/story/components/ListStoryStageOverlay.d.ts +0 -1
- package/lib/features/story/components/ListStoryStageOverlay.js +52 -34
- package/lib/features/story/components/ListStoryTitleBar.d.ts +7 -0
- package/lib/features/story/components/ListStoryTitleBar.js +20 -0
- package/lib/features/story/components/ListStoryTitleBarDesktop.d.ts +2 -0
- package/lib/features/story/components/ListStoryTitleBarDesktop.js +55 -0
- package/lib/features/story/components/ListStoryTitleBarMobile.d.ts +2 -0
- package/lib/features/story/components/ListStoryTitleBarMobile.js +41 -0
- package/lib/features/story/components/SpectaMobileListModeContent.d.ts +13 -0
- package/lib/features/story/components/SpectaMobileListModeContent.js +36 -0
- package/lib/features/story/components/SpectaMobileSingleModeContent.d.ts +14 -0
- package/lib/features/story/components/SpectaMobileSingleModeContent.js +98 -0
- package/lib/features/story/components/SpectaMobileView.d.ts +7 -3
- package/lib/features/story/components/SpectaMobileView.js +11 -99
- package/lib/features/story/context/ListStoryScrollTrackContext.d.ts +4 -0
- package/lib/features/story/context/ListStoryScrollTrackContext.js +50 -6
- package/lib/features/story/hooks/useStoryMap.js +1 -16
- package/lib/features/story/types/types.d.ts +5 -0
- package/lib/features/story/utils/computeListStoryScrollState.d.ts +2 -0
- package/lib/features/story/utils/computeListStoryScrollState.js +34 -25
- package/lib/mainview/components/MainViewMapSurface.d.ts +15 -0
- package/lib/mainview/components/MainViewMapSurface.js +13 -0
- package/lib/mainview/components/MainViewOverlayLayer.d.ts +9 -0
- package/lib/mainview/components/MainViewOverlayLayer.js +11 -0
- package/lib/mainview/components/MainViewSidePanels.d.ts +17 -0
- package/lib/mainview/components/MainViewSidePanels.js +10 -0
- package/lib/mainview/components/MainViewSpectaPanel.d.ts +17 -0
- package/lib/mainview/components/MainViewSpectaPanel.js +8 -0
- package/lib/mainview/components/MainViewStoryStage.d.ts +13 -0
- package/lib/mainview/components/MainViewStoryStage.js +17 -0
- package/lib/mainview/components/PositionedFloater.d.ts +10 -0
- package/lib/mainview/components/PositionedFloater.js +7 -0
- package/lib/mainview/mainView.d.ts +3 -7
- package/lib/mainview/mainView.js +84 -164
- package/lib/shared/formbuilder/formselectors.js +1 -4
- package/lib/types.js +0 -1
- package/package.json +2 -2
- package/style/base.css +18 -4
- package/style/layerBrowser.css +3 -3
- package/style/storyPanel.css +192 -2
- package/style/symbologyDialog.css +269 -32
- package/lib/features/layers/forms/layer/heatmapLayerForm.d.ts +0 -3
- package/lib/features/layers/forms/layer/heatmapLayerForm.js +0 -96
- package/lib/features/layers/symbology/Heatmap.d.ts +0 -4
- 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
|
|
49
|
-
|
|
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 || !
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
if (!(fromPane instanceof HTMLElement) || !(gap instanceof HTMLElement)) {
|
|
117
|
+
if (!(fromPane instanceof HTMLElement)) {
|
|
106
118
|
return;
|
|
107
119
|
}
|
|
108
|
-
const
|
|
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
|
-
}, [
|
|
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' : ''}
|
|
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
|
-
: {})) },
|
|
129
|
-
React.createElement(
|
|
130
|
-
|
|
131
|
-
|
|
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,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,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,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
|
|
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):
|
|
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
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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;
|