spectacle
Version:
ReactJS Powered Presentation Framework
1,731 lines (1,688 loc) • 134 kB
JavaScript
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __objRest = (source, exclude) => {
var target = {};
for (var prop in source)
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
target[prop] = source[prop];
if (source != null && __getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(source)) {
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
target[prop] = source[prop];
}
return target;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/components/deck/index.tsx
import { Fragment as Fragment3 } from "react";
// src/components/deck/default-deck.tsx
import { useRef as useRef4, useCallback as useCallback5, useEffect as useEffect8 } from "react";
// src/components/deck/deck.tsx
import {
useState as useState4,
useEffect as useEffect6,
forwardRef,
useMemo as useMemo2,
useCallback as useCallback3,
createContext,
useImperativeHandle,
useId as useId2
} from "react";
import styled2, { ThemeProvider } from "styled-components";
// src/hooks/use-slides.tsx
import { useState, useEffect, useId } from "react";
import { jsx } from "react/jsx-runtime";
var PLACEHOLDER_CLASS_NAME = "spectacle-v7-slide";
function useCollectSlides() {
const [initialized, setInitialized] = useState(false);
const [slideContainer, setSlideContainer] = useState();
const [slideIds, setSlideIds] = useState([]);
const [slideIdsOfSlidesWithTemplates, setSlideIdsOfSlidesWithTemplates] = useState(/* @__PURE__ */ new Set());
useEffect(() => {
if (!slideContainer)
return;
const slides = slideContainer.getElementsByClassName(
PLACEHOLDER_CLASS_NAME
);
const nextSlideIds = [];
const nextSlideIdsOfSlidesWithTemplates = /* @__PURE__ */ new Set();
for (const placeholderNode of slides) {
const { slideId, slideHasTemplate } = placeholderNode.dataset;
if (slideId !== void 0) {
nextSlideIds.push(slideId);
if (slideHasTemplate === "true") {
nextSlideIdsOfSlidesWithTemplates.add(slideId);
}
}
}
setSlideIds(nextSlideIds);
setSlideIdsOfSlidesWithTemplates(nextSlideIdsOfSlidesWithTemplates);
setInitialized(true);
}, [slideContainer]);
return [
setSlideContainer,
slideIds,
slideIdsOfSlidesWithTemplates,
initialized
];
}
function useSlide(doesSlideHaveTemplate, userProvidedId) {
const id = useId();
const [slideId] = useState(userProvidedId || id);
return {
slideId,
placeholder: /* @__PURE__ */ jsx(
"div",
{
className: PLACEHOLDER_CLASS_NAME,
"data-slide-id": slideId,
"data-slide-has-template": doesSlideHaveTemplate
}
)
};
}
// src/hooks/use-aspect-ratio-fitting.ts
import { useRef, useState as useState2, useCallback, useEffect as useEffect2 } from "react";
import useResizeObserver from "use-resize-observer";
function useAspectRatioFitting({
targetWidth = 1366,
targetHeight = 768
}) {
const containerRef = useRef(null);
const [scaleFactor, setScaleFactor] = useState2(1);
const [transformOrigin, setTransformOrigin] = useState2({ x: 0, y: 0 });
const recalculate = useCallback(
({ width, height }) => {
const containerWidth = Number(width) || 0.01;
const containerHeight = Number(height) || 0.01;
const containerRatio = containerWidth / containerHeight;
const targetRatio = targetWidth / targetHeight;
const useVertical = containerRatio > targetRatio;
const scaleFactor2 = useVertical ? containerHeight / targetHeight : containerWidth / targetWidth;
const scaledWidth = targetWidth * scaleFactor2;
const scaledHeight = targetHeight * scaleFactor2;
let x0 = 0;
if (useVertical) {
x0 = 0.5 * (containerWidth - scaledWidth);
x0 /= 1 - scaleFactor2;
}
let y0 = 0;
if (!useVertical) {
y0 = 0.5 * (containerHeight - scaledHeight);
y0 /= 1 - scaleFactor2;
}
setScaleFactor(scaleFactor2);
setTransformOrigin({ x: x0, y: y0 });
},
[targetWidth, targetHeight]
);
useEffect2(() => {
if (!containerRef || !containerRef.current)
return;
const rects = containerRef.current.getClientRects();
recalculate(rects[0]);
}, [targetWidth, targetHeight, recalculate]);
useResizeObserver({
ref: containerRef,
onResize: recalculate
});
const styles = {
position: "relative",
width: targetWidth,
height: targetHeight,
scaleFactor,
transform: `scale(${scaleFactor})`,
transformOrigin: `${transformOrigin.x}px ${transformOrigin.y}px`
};
return [containerRef, styles];
}
// src/hooks/use-deck-state.ts
import { useReducer, useMemo } from "react";
import { merge } from "merge-anything";
// src/utils/clamp.ts
function toFiniteNumber(value) {
if (!value || isNaN(value)) {
return 0;
} else if (value === Infinity || value === -Infinity) {
const sign = value < 0 ? -1 : 1;
return sign * Number.MAX_SAFE_INTEGER;
}
return value;
}
function clamp(number, lower, upper) {
if (isNaN(number)) {
return NaN;
}
let finiteNumber = toFiniteNumber(number);
if (finiteNumber === finiteNumber) {
if (upper !== void 0) {
finiteNumber = finiteNumber <= upper ? finiteNumber : upper;
}
if (lower !== void 0) {
finiteNumber = finiteNumber >= lower ? finiteNumber : lower;
}
}
return finiteNumber;
}
// src/hooks/use-deck-state.ts
var GOTO_FINAL_STEP = null;
var initialDeckState = {
initialized: false,
navigationDirection: 0,
pendingView: {
slideIndex: 0,
stepIndex: 0
},
activeView: {
slideIndex: 0,
stepIndex: 0
}
};
function deckReducer(state, { type, payload = {} }) {
var _a;
switch (type) {
case "INITIALIZE_TO":
return {
navigationDirection: 0,
activeView: merge(state.activeView, payload),
pendingView: merge(state.pendingView, payload),
initialized: true
};
case "SKIP_TO":
const navigationDirection = (() => {
if ("slideIndex" in payload && payload.slideIndex) {
return clamp(payload.slideIndex - state.activeView.slideIndex, -1, 1);
}
return null;
})();
return __spreadProps(__spreadValues({}, state), {
navigationDirection: navigationDirection || state.navigationDirection,
pendingView: merge(state.pendingView, payload)
});
case "STEP_FORWARD":
return __spreadProps(__spreadValues({}, state), {
navigationDirection: 1,
pendingView: merge(state.pendingView, {
stepIndex: state.pendingView.stepIndex + 1
})
});
case "STEP_BACKWARD":
return __spreadProps(__spreadValues({}, state), {
navigationDirection: -1,
pendingView: merge(state.pendingView, {
stepIndex: state.pendingView.stepIndex - 1
})
});
case "ADVANCE_SLIDE":
return __spreadProps(__spreadValues({}, state), {
navigationDirection: 1,
pendingView: merge(state.pendingView, {
stepIndex: 0,
slideIndex: state.pendingView.slideIndex + 1
})
});
case "REGRESS_SLIDE":
return __spreadProps(__spreadValues({}, state), {
navigationDirection: -1,
pendingView: merge(state.pendingView, {
stepIndex: (_a = payload == null ? void 0 : payload.stepIndex) != null ? _a : GOTO_FINAL_STEP,
slideIndex: state.pendingView.slideIndex - 1
})
});
case "COMMIT_TRANSITION":
const pendingView = merge(state.pendingView, payload);
return __spreadProps(__spreadValues({}, state), {
pendingView,
activeView: merge(state.activeView, pendingView)
});
case "CANCEL_TRANSITION":
return __spreadProps(__spreadValues({}, state), {
pendingView: merge(state.pendingView, state.activeView)
});
default:
return state;
}
}
function useDeckState(userProvidedInitialState) {
const [
{ initialized, navigationDirection, pendingView, activeView },
dispatch
] = useReducer(deckReducer, {
initialized: initialDeckState.initialized,
navigationDirection: initialDeckState.navigationDirection,
pendingView: __spreadValues(__spreadValues({}, initialDeckState.pendingView), userProvidedInitialState),
activeView: __spreadValues(__spreadValues({}, initialDeckState.activeView), userProvidedInitialState)
});
const actions = useMemo(
() => ({
initializeTo: (payload) => dispatch({ type: "INITIALIZE_TO", payload }),
skipTo: (payload) => dispatch({ type: "SKIP_TO", payload }),
stepForward: () => dispatch({ type: "STEP_FORWARD" }),
stepBackward: () => dispatch({ type: "STEP_BACKWARD" }),
advanceSlide: () => dispatch({ type: "ADVANCE_SLIDE" }),
regressSlide: (payload) => dispatch({ type: "REGRESS_SLIDE", payload }),
commitTransition: (payload) => dispatch({ type: "COMMIT_TRANSITION", payload }),
cancelTransition: () => dispatch({ type: "CANCEL_TRANSITION" })
}),
[dispatch]
);
return __spreadValues({
initialized,
navigationDirection,
pendingView,
activeView
}, actions);
}
// src/hooks/use-mousetrap.ts
import { useEffect as useEffect3 } from "react";
import Mousetrap from "mousetrap";
function useMousetrap(keybinds, deps) {
useEffect3(() => {
for (const combo in keybinds) {
const callback = keybinds[combo];
if (typeof callback !== "function") {
throw new TypeError(
`Expected type 'function' in useMousetrap for combo '${combo}', but got ${typeof callback}`
);
}
Mousetrap.bind(combo, callback);
}
return () => {
for (const combo in keybinds) {
Mousetrap.unbind(combo);
}
};
}, [keybinds, ...deps]);
}
// src/hooks/use-location-sync.ts
import { useState as useState3, useEffect as useEffect4, useCallback as useCallback2 } from "react";
import { createBrowserHistory } from "history";
import QS from "query-string";
import isEqual from "react-fast-compare";
import { mergeAndCompare, merge as merge2 } from "merge-anything";
function defaultMergeLocation(object, ...sources) {
return mergeAndCompare(
(left, right, key) => {
switch (key) {
case "search":
if (!left)
return right;
return "?" + QS.stringify(__spreadValues(__spreadValues({}, QS.parse(left)), QS.parse(right)));
default:
return merge2(left, right);
}
},
object,
...sources
);
}
function useLocationSync({
setState,
mapStateToLocation: mapStateToLocation2,
mapLocationToState: mapLocationToState2,
disableInteractivity = false,
mergeLocation = defaultMergeLocation,
historyFactory = createBrowserHistory
}) {
const [history] = useState3(() => {
return typeof document !== "undefined" ? historyFactory() : null;
});
const [initialized, setInitialized] = useState3(false);
useEffect4(() => {
if (!initialized && disableInteractivity)
return;
return history == null ? void 0 : history.listen(({ location }) => {
setState(mapLocationToState2(location));
});
}, [
disableInteractivity,
initialized,
history,
setState,
mapLocationToState2
]);
const syncLocation = useCallback2(
(defaultState) => {
if (disableInteractivity || !history) {
return defaultState;
}
const { location } = history;
const initialState = merge2(
defaultState,
mapLocationToState2(location)
);
const nextLocation = mergeLocation(
{},
location,
mapStateToLocation2(initialState)
);
history.replace(nextLocation);
setInitialized(true);
return initialState;
},
[
history,
mapLocationToState2,
mapStateToLocation2,
disableInteractivity,
mergeLocation
]
);
const setLocation = useCallback2(
(state) => {
if (!initialized || !history)
return;
const { location } = history;
const nextLocation = mergeLocation(
{},
location,
mapStateToLocation2(state)
);
if (!isEqual(location, nextLocation)) {
history.push(nextLocation);
}
},
[history, initialized, mergeLocation, mapStateToLocation2]
);
return [syncLocation, setLocation];
}
// src/theme/default-theme.ts
var defaultTheme = {
size: {
width: 1366,
height: 768,
maxCodePaneHeight: 200
},
colors: {
primary: "#ebe5da",
secondary: "#fc6986",
tertiary: "#1e2852",
quaternary: "#ffc951",
quinary: "#8bddfd"
},
fonts: {
header: '"Helvetica Neue", Helvetica, Arial, sans-serif',
text: '"Helvetica Neue", Helvetica, Arial, sans-serif',
monospace: '"Consolas", "Menlo", monospace'
},
fontSizes: {
h1: "72px",
h2: "64px",
h3: "56px",
text: "44px",
monospace: "20px"
},
space: [16, 24, 32]
};
var default_theme_default = defaultTheme;
// src/theme/print-theme.ts
var printTheme = {
colors: {
primary: "#777",
secondary: "#000",
tertiary: "#fff",
quaternary: "#000000",
quinary: "#000000"
}
};
var print_theme_default = printTheme;
// src/theme/index.ts
var mergeKeys = (base, override) => Object.keys(override || {}).reduce(
(merged, key) => {
merged[key] = __spreadValues(__spreadValues({}, merged[key]), override[key]);
return merged;
},
__spreadValues({}, base)
);
function mergeTheme({ theme, printMode }) {
const merged = mergeKeys(default_theme_default, theme);
return printMode ? mergeKeys(merged, print_theme_default) : merged;
}
// src/location-map-fns/query-string.ts
var query_string_exports = {};
__export(query_string_exports, {
mapLocationToState: () => mapLocationToState,
mapStateToLocation: () => mapStateToLocation
});
import {
parse as parseQS,
stringify as stringifyQS
} from "query-string";
function mapLocationToState(location) {
const { search: queryString } = location;
const { slideIndex: rawSlideIndex, stepIndex: rawStepIndex } = parseQS(queryString);
const nextState = {};
if (rawSlideIndex === void 0) {
return nextState;
}
nextState.slideIndex = Number(rawSlideIndex);
if (isNaN(nextState.slideIndex)) {
throw new Error(
`Invalid slide index in URL query string: '${queryString}'`
);
}
if (rawStepIndex === "final") {
nextState.stepIndex = GOTO_FINAL_STEP;
} else if (rawStepIndex !== void 0) {
nextState.stepIndex = Number(rawStepIndex);
if (isNaN(nextState.stepIndex)) {
throw new Error(
`Invalid step index in URL query string: '${queryString}'`
);
}
}
return nextState;
}
function mapStateToLocation(state) {
const { slideIndex, stepIndex } = state;
const query = {};
if (typeof slideIndex !== "number") {
return query;
}
query.slideIndex = String(slideIndex);
if (typeof stepIndex === "number") {
query.stepIndex = String(stepIndex);
} else if (stepIndex === GOTO_FINAL_STEP) {
query.stepIndex = "final";
}
return {
search: "?" + stringifyQS(query)
};
}
// src/components/deck/deck-styles.ts
function overviewFrameStyle({
overviewScale,
nativeSlideWidth,
nativeSlideHeight
}) {
return {
margin: "1rem",
width: `${overviewScale * nativeSlideWidth}px`,
height: `${overviewScale / (nativeSlideWidth / nativeSlideHeight) * nativeSlideWidth}px`,
display: "block",
transform: "none",
position: "relative"
};
}
function overviewWrapperStyle({
overviewScale
}) {
return {
width: `${100 / overviewScale}%`,
height: `${100 / overviewScale}%`,
transform: `scale(${overviewScale})`,
transformOrigin: "0px 0px",
position: "absolute"
};
}
function printFrameStyle({
nativeSlideWidth,
nativeSlideHeight,
printScale
}) {
return {
margin: "0",
width: `${printScale * nativeSlideWidth}px`,
height: `${printScale / (nativeSlideWidth / nativeSlideHeight) * nativeSlideWidth}px`,
display: "block",
transform: "none",
position: "relative",
breakAfter: "page"
};
}
function printWrapperStyle({
printScale
}) {
return {
width: `${100 / printScale}%`,
height: `${100 / printScale}%`,
transform: `scale(${printScale})`,
transformOrigin: "0px 0px",
position: "absolute"
};
}
// src/utils/use-auto-play.ts
import { useEffect as useEffect5, useRef as useRef2 } from "react";
var useAutoPlay = ({
enabled = false,
loop = false,
stepForward,
interval = 1e3
}) => {
const stepFn = useRef2(stepForward);
stepFn.current = stepForward;
useEffect5(() => {
if (enabled) {
const id = setInterval(() => {
stepFn.current();
}, interval);
return () => clearInterval(id);
}
}, [enabled, interval, loop]);
};
// src/components/transitions/index.ts
var STAGE_RIGHT = "translateX(-100%)";
var CENTER_STAGE = "translateX(0%)";
var STAGE_LEFT = "translateX(100%)";
var fadeTransition = {
from: {
opacity: 0
},
enter: {
opacity: 1
},
leave: {
opacity: 0
}
};
var slideTransition = {
from: {
transform: STAGE_LEFT
},
enter: {
transform: CENTER_STAGE
},
leave: {
transform: STAGE_RIGHT
}
};
var defaultTransition = slideTransition;
// src/components/template-wrapper.tsx
import styled from "styled-components";
var TemplateWrapper = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
`;
var template_wrapper_default = TemplateWrapper;
// src/components/deck/deck.tsx
import { useRegisterActions } from "kbar";
// src/utils/constants.ts
var SYSTEM_FONT = '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", Helvetica, sans-serif';
var KEYBOARD_SHORTCUTS = {
DEFAULT_MODE: "mod+shift+d",
PRESENTER_MODE: "mod+shift+p",
OVERVIEW_MODE: "mod+shift+o",
PRINT_MODE: "mod+shift+r",
EXPORT_MODE: "mod+shift+e",
TAB_FORWARD_OVERVIEW_MODE: "tab",
TAB_BACKWARD_OVERVIEW_MODE: "shift+tab",
SELECT_SLIDE_OVERVIEW_MODE: "enter",
NEXT_SLIDE: "right",
PREVIOUS_SLIDE: "left"
};
var KEYBOARD_SHORTCUTS_IDS = {
DEFAULT_MODE: "DEFAULT_MODE",
PRESENTER_MODE: "PRESENTER_MODE",
OVERVIEW_MODE: "OVERVIEW_MODE",
PRINT_MODE: "PRINT_MODE",
EXPORT_MODE: "EXPORT_MODE",
TAB_FORWARD_OVERVIEW_MODE: "TAB_FORWARD_OVERVIEW_MODE",
TAB_BACKWARD_OVERVIEW_MODE: "TAB_BACKWARD_OVERVIEW_MODE",
SELECT_SLIDE_OVERVIEW_MODE: "SELECT_SLIDE_OVERVIEW_MODE",
NEXT_SLIDE: "NEXT_SLIDE",
PREVIOUS_SLIDE: "PREVIOUS_SLIDE"
};
var SPECTACLE_MODES = {
DEFAULT_MODE: "DEFAULT_MODE",
PRESENTER_MODE: "PRESENTER_MODE",
OVERVIEW_MODE: "OVERVIEW_MODE",
PRINT_MODE: "PRINT_MODE",
EXPORT_MODE: "EXPORT_MODE"
};
// src/components/deck/deck.tsx
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
var DeckContext = createContext(null);
DeckContext.displayName = "DeckContext";
var noop = () => {
};
var DEFAULT_PRINT_SCALE = 1;
var DEFAULT_OVERVIEW_SCALE = 0.25;
var Portal = styled2.div(
({ fitAspectRatioStyle, overviewMode, printMode }) => [
!printMode && { overflow: "hidden" },
!printMode && fitAspectRatioStyle,
overviewMode && {
display: "flex",
flexWrap: "wrap",
justifyContent: "flex-start",
alignItems: "flex-start",
alignContent: "flex-start",
transform: "scale(1)",
overflowY: "scroll",
width: "100%",
height: "100%"
},
printMode && {
display: "block"
}
]
);
var DeckInternal = forwardRef(
(_a, ref) => {
var _b = _a, {
id: userProvidedId,
className = "",
backdropStyle: userProvidedBackdropStyle,
overviewMode = false,
printMode = false,
exportMode = false,
overviewScale = DEFAULT_OVERVIEW_SCALE,
printScale = DEFAULT_PRINT_SCALE,
template,
theme: _c = {}
} = _b, _d = _c, {
Backdrop: UserProvidedBackdropComponent,
backdropStyle: themeProvidedBackdropStyle = {
position: "fixed",
top: 0,
left: 0,
width: "100vw",
height: "100vh"
},
suppressBackdropFallback: themeSuppressBackdropFallback
} = _d, restTheme = __objRest(_d, [
"Backdrop",
"backdropStyle",
"suppressBackdropFallback"
]), {
onSlideClick = noop,
onMobileSlide = noop,
disableInteractivity = false,
notePortalNode,
useAnimations = true,
children,
onActiveStateChange: onActiveStateChangeExternal = noop,
initialState: initialDeckState2 = {
slideIndex: 0,
stepIndex: 0
},
suppressBackdropFallback = false,
autoPlay = false,
autoPlayLoop = false,
autoPlayInterval = 1e3,
transition = defaultTransition,
backgroundImage
} = _b;
const id = useId2();
const [deckId] = useState4(userProvidedId || id);
const {
width: nativeSlideWidth = default_theme_default.size.width,
height: nativeSlideHeight = default_theme_default.size.height
} = restTheme.size || {};
const {
initialized,
pendingView,
activeView,
navigationDirection,
initializeTo,
skipTo,
stepForward,
stepBackward,
advanceSlide,
regressSlide,
commitTransition,
cancelTransition
} = useDeckState(initialDeckState2);
const [
setPlaceholderContainer,
slideIds,
slideIdsWithTemplates,
slideIdsInitialized
] = useCollectSlides();
useImperativeHandle(
ref,
() => ({
initialized,
activeView,
initializeTo,
skipTo,
stepForward,
stepBackward,
advanceSlide,
regressSlide,
numberOfSlides: slideIds.length
}),
[
initialized,
activeView,
initializeTo,
skipTo,
stepForward,
stepBackward,
advanceSlide,
regressSlide,
slideIds
]
);
useRegisterActions(
!disableInteractivity ? [
{
id: KEYBOARD_SHORTCUTS_IDS.NEXT_SLIDE,
name: "Next Slide",
keywords: "next",
perform: () => stepForward(),
section: "Slide"
},
{
id: KEYBOARD_SHORTCUTS_IDS.PREVIOUS_SLIDE,
name: "Previous Slide",
keywords: "previous",
perform: () => stepBackward(),
section: "Slide"
},
{
id: "Restart Presentation",
name: "Restart Presentation",
keywords: "restart",
perform: () => skipTo({
slideIndex: 0,
stepIndex: 0
}),
section: "Slide"
}
] : []
);
useMousetrap(
disableInteractivity ? {} : {
left: () => stepBackward(),
right: () => stepForward()
},
[]
);
const [syncLocation, onActiveStateChange] = useLocationSync(__spreadValues({
disableInteractivity,
setState: skipTo
}, query_string_exports));
useEffect6(() => {
if (!initialized)
return;
onActiveStateChange(activeView);
onActiveStateChangeExternal(activeView);
}, [
initialized,
activeView,
onActiveStateChange,
onActiveStateChangeExternal
]);
useEffect6(() => {
const initialView = syncLocation({
slideIndex: 0,
stepIndex: 0
});
initializeTo(initialView);
}, [initializeTo, syncLocation]);
useAutoPlay({
enabled: autoPlay,
loop: autoPlayLoop,
interval: autoPlayInterval,
stepForward
});
const handleSlideClick = useCallback3(
(e, slideId) => {
const slideIndex = slideIds.indexOf(slideId);
onSlideClick(e, slideIndex);
},
[onSlideClick, slideIds]
);
const activeSlideId = slideIds[activeView.slideIndex];
const pendingSlideId = slideIds[pendingView.slideIndex];
const fullyInitialized = initialized && slideIdsInitialized;
const [slidePortalNode, setSlidePortalNode] = useState4();
const [backdropRef, fitAspectRatioStyle] = useAspectRatioFitting({
targetWidth: nativeSlideWidth,
targetHeight: nativeSlideHeight
});
const frameStyle = useMemo2(() => {
const options = {
printScale,
overviewScale,
nativeSlideWidth,
nativeSlideHeight
};
if (overviewMode) {
return overviewFrameStyle(options);
} else if (printMode) {
return printFrameStyle(options);
}
return {};
}, [
nativeSlideHeight,
nativeSlideWidth,
overviewMode,
overviewScale,
printMode,
printScale
]);
const wrapperStyle = useMemo2(() => {
if (overviewMode) {
return overviewWrapperStyle({ overviewScale });
} else if (printMode) {
return printWrapperStyle({ printScale });
}
return {};
}, [overviewMode, overviewScale, printMode, printScale]);
const backdropStyle = __spreadValues(__spreadValues({}, themeProvidedBackdropStyle), userProvidedBackdropStyle);
const BackdropComponent = UserProvidedBackdropComponent || "div";
if (!backdropStyle["background"] && !backdropStyle["backgroundColor"] && !UserProvidedBackdropComponent && !suppressBackdropFallback && !themeSuppressBackdropFallback) {
backdropStyle["backgroundColor"] = "black";
}
const doesCurrentSlideHaveItsOwnTemplate = slideIdsWithTemplates.has(activeSlideId);
const templateElement = typeof template === "function" ? template({
slideNumber: activeView.slideIndex + 1,
numberOfSlides: slideIds.length
}) : template;
return /* @__PURE__ */ jsx2(
ThemeProvider,
{
theme: mergeTheme({
theme: restTheme,
printMode: printMode && !exportMode
}),
children: /* @__PURE__ */ jsx2(
BackdropComponent,
{
ref: backdropRef,
className,
style: __spreadProps(__spreadValues({}, backdropStyle), {
overflow: "hidden"
}),
children: /* @__PURE__ */ jsxs(
DeckContext.Provider,
{
value: {
deckId,
slideCount: slideIds.length,
slideIds,
useAnimations,
slidePortalNode,
onSlideClick: handleSlideClick,
onMobileSlide,
theme: restTheme,
autoPlayLoop,
navigationDirection,
frameOverrideStyle: frameStyle,
wrapperOverrideStyle: wrapperStyle,
backdropNode: backdropRef.current,
notePortalNode,
initialized: fullyInitialized,
activeView: __spreadProps(__spreadValues({}, activeView), {
slideId: activeSlideId
}),
pendingView: __spreadProps(__spreadValues({}, pendingView), {
slideId: pendingSlideId
}),
skipTo,
stepForward,
stepBackward,
advanceSlide,
regressSlide,
commitTransition,
cancelTransition,
transition,
template,
backgroundImage,
inOverviewMode: overviewMode,
inPrintMode: printMode
},
children: [
/* @__PURE__ */ jsx2(
Portal,
{
ref: setSlidePortalNode,
overviewMode,
printMode,
fitAspectRatioStyle,
children: !doesCurrentSlideHaveItsOwnTemplate && !overviewMode && !printMode && /* @__PURE__ */ jsx2(
template_wrapper_default,
{
style: __spreadProps(__spreadValues({}, wrapperStyle), {
// Slides are appended to the parent as they are portaled in and end up later in
// the source order. Adding zIndex to the template to overlay the sibling slides
// once they have been portaled in.
zIndex: 1
}),
children: templateElement
}
)
}
),
/* @__PURE__ */ jsx2("div", { ref: setPlaceholderContainer, style: { display: "none" }, children })
]
}
)
}
)
}
);
}
);
DeckInternal.displayName = "Deck";
var Deck = DeckInternal;
Deck.displayName = "Deck";
// src/hooks/use-broadcast-channel.ts
import { useCallback as useCallback4, useEffect as useEffect7, useId as useId3, useRef as useRef3 } from "react";
import { BroadcastChannel as BroadcastChannelPolyfill } from "broadcast-channel";
var noop2 = () => {
};
var safeWindow = {};
if (typeof window !== "undefined") {
safeWindow = window;
}
var BroadcastChannel = safeWindow.BroadcastChannel || BroadcastChannelPolyfill;
function useBroadcastChannel(channelName, onMessage = noop2, deps = []) {
const broadcasterId = useId3();
const channel = useRef3();
useEffect7(() => {
channel.current = new BroadcastChannel(channelName);
return () => {
var _a;
(_a = channel.current) == null ? void 0 : _a.close();
};
}, [channelName]);
const postMessage = useCallback4(
(type, payload = {}) => {
var _a;
const message = {
type,
payload,
meta: { sender: broadcasterId }
};
const rawMessage = JSON.stringify(message);
(_a = channel.current) == null ? void 0 : _a.postMessage(rawMessage);
},
[broadcasterId]
);
const userMessageHandlerRef = useRef3(onMessage);
useEffect7(() => {
userMessageHandlerRef.current = onMessage;
}, [...deps, postMessage]);
useEffect7(() => {
var _a;
if (!channel.current)
return;
const messageHandler = (event) => {
const rawMessage = event.data;
const message = JSON.parse(rawMessage);
userMessageHandlerRef.current(message);
};
(_a = channel.current) == null ? void 0 : _a.addEventListener("message", messageHandler);
return () => {
var _a2;
(_a2 = channel.current) == null ? void 0 : _a2.removeEventListener("message", messageHandler);
};
}, [postMessage]);
return [postMessage, broadcasterId];
}
// src/components/deck/default-deck.tsx
import { jsx as jsx3 } from "react/jsx-runtime";
var DefaultDeck = (props) => {
const _a = props, {
overviewMode = false,
printMode = false,
exportMode = false,
toggleMode,
children
} = _a, rest = __objRest(_a, [
"overviewMode",
"printMode",
"exportMode",
"toggleMode",
"children"
]);
const deck = useRef4(null);
const [postMessage] = useBroadcastChannel(
"spectacle_presenter_bus",
(message) => {
if (message.type !== "SYNC")
return;
const nextView = message.payload;
if (deck.current.initialized) {
deck.current.skipTo(nextView);
} else {
deck.current.initializeTo(nextView);
}
}
);
useEffect8(() => {
postMessage("SYNC_REQUEST");
}, [postMessage]);
useMousetrap(
overviewMode ? {
[KEYBOARD_SHORTCUTS.TAB_FORWARD_OVERVIEW_MODE]: () => deck.current.advanceSlide(),
[KEYBOARD_SHORTCUTS.TAB_BACKWARD_OVERVIEW_MODE]: () => deck.current.regressSlide({
stepIndex: 0
}),
[KEYBOARD_SHORTCUTS.SELECT_SLIDE_OVERVIEW_MODE]: () => toggleMode({
newMode: SPECTACLE_MODES.DEFAULT_MODE
})
} : {},
[]
);
const onSlideClick = useCallback5(
(e, slideIndex) => {
if (overviewMode) {
toggleMode({
e,
newMode: SPECTACLE_MODES.DEFAULT_MODE,
senderSlideIndex: +slideIndex
});
}
},
[overviewMode, toggleMode]
);
const onMobileSlide = (e) => {
if (navigator.maxTouchPoints < 1 || !deck.current)
return;
switch (e.dir) {
case "Left":
deck.current.stepForward();
break;
case "Right":
deck.current.regressSlide();
break;
}
};
return /* @__PURE__ */ jsx3(
DeckInternal,
__spreadProps(__spreadValues({
overviewMode,
onSlideClick,
onMobileSlide,
printMode,
exportMode,
ref: deck
}, rest), {
children
})
);
};
var default_deck_default = DefaultDeck;
// src/components/presenter-mode/index.tsx
import {
useRef as useRef7,
useCallback as useCallback7,
useState as useState8,
useEffect as useEffect10
} from "react";
import styled7 from "styled-components";
// src/components/presenter-mode/components.tsx
import styled3 from "styled-components";
var PresenterDeckContainer = styled3.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: row;
background-color: #181818;
overflow: hidden;
color: white;
`;
var NotesColumn = styled3.div`
padding: 0;
display: flex;
flex-direction: column;
width: 50%;
border-right: 1px solid black;
`;
var PreviewColumn = styled3.div`
background-color: black;
display: flex;
flex-direction: column;
height: 100%;
width: 50%;
> :first-child {
margin-bottom: 0.5em;
}
`;
var SlideContainer = styled3.div`
display: flex;
flex-direction: column;
height: calc(50% - 1em);
width: 100%;
overflow: hidden;
`;
var SlideWrapper = styled3.div`
flex: 1;
width: 100%;
position: relative;
.spectacle-fullscreen-button {
display: none;
}
${({ small }) => small && `flex: 0.8;`}
`;
var SlideCountLabel = styled3.span`
background: hsla(0, 0%, 100%, 0.1);
border-radius: 4px;
font-size: 0.7em;
padding: 1px 4px;
`;
var NotesContainer = styled3.div`
border-top: 1px solid black;
overflow-y: scroll;
flex: 1;
::-webkit-scrollbar {
width: 10px;
}
/* Track */
::-webkit-scrollbar-track {
background-color: #111;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #333;
border-radius: 10px;
}
`;
var deckBackdropStyles = {
currentSlide: {
width: "50vw",
height: "50vh",
left: "50vw",
top: "7vh"
},
nextSlide: {
width: "50vw",
height: "33vh",
top: "60vh",
left: "50vw"
}
};
// src/components/layout-primitives.ts
import styled4 from "styled-components";
import {
compose,
grid,
flexbox,
layout,
position,
border,
color,
space
} from "styled-system";
var containerPrintStyle = `
@media print {
height: inherit;
}
`;
var Box = styled4.div(
compose(layout, space, position, color, border),
containerPrintStyle
);
var FlexBox = styled4.div.attrs((props) => __spreadValues({
alignItems: "center",
justifyContent: "center",
display: "flex"
}, props))(
compose(layout, space, position, color, border, flexbox),
containerPrintStyle
);
var Grid = styled4.div.attrs((props) => __spreadValues({
display: "grid"
}, props))(compose(layout, grid, position), containerPrintStyle);
// src/components/presenter-mode/timer.tsx
import { useState as useState7, useCallback as useCallback6 } from "react";
// src/components/typography.tsx
import styled5 from "styled-components";
import {
color as color2,
typography,
space as space2,
compose as compose2,
system
} from "styled-system";
import {
useRef as useRef5,
useState as useState5
} from "react";
import useResizeObserver2 from "use-resize-observer";
import { jsx as jsx4 } from "react/jsx-runtime";
var decoration = system({ textDecoration: true });
var Text = styled5.div.attrs((props) => __spreadValues({
color: "primary",
fontFamily: "text",
fontSize: "text",
textAlign: "left",
padding: 0,
margin: 0
}, props))(compose2(color2, typography, space2));
var CodeSpan = styled5.code.attrs((props) => __spreadValues({
fontFamily: "monospace",
fontSize: "text"
}, props))(compose2(color2, typography, space2));
var Link = styled5.a.attrs(
(props) => __spreadValues({
fontFamily: "text",
fontSize: "text",
textDecoration: "underline",
color: "quaternary"
}, props)
)(
compose2(color2, typography, space2, decoration)
);
var Heading = styled5.div.attrs((props) => __spreadValues({
color: "secondary",
fontFamily: "header",
fontSize: "h1",
fontWeight: "bold",
textAlign: "center",
margin: 1,
padding: 0
}, props))(compose2(color2, typography, space2));
var Quote = styled5(
Text
).attrs((props) => __spreadValues({
color: "primary",
fontFamily: "text",
fontSize: "text",
textAlign: "left",
fontStyle: "italic",
padding: "16px 0 16px 8px",
margin: 0
}, props))`
border-left: 1px solid
${({ theme, borderColor }) => borderColor || theme.colors.secondary};
div {
margin: 0;
}
`;
var listStyle = system({
listStyleType: true
});
var OrderedList = styled5.ol.attrs(
(props) => __spreadValues({
color: "primary",
fontFamily: "text",
fontSize: "text",
textAlign: "left",
margin: 0
}, props)
)(
compose2(color2, typography, space2, listStyle)
);
var UnorderedList = styled5.ul.attrs(
(props) => __spreadValues({
color: "primary",
fontFamily: "text",
fontSize: "text",
textAlign: "left",
margin: 0
}, props)
)(
compose2(color2, typography, space2, listStyle)
);
var ListItem = styled5.li.attrs((props) => __spreadValues({
margin: 0
}, props))(compose2(color2, typography, space2));
var FitContainer = styled5.div`
width: 100%;
display: flex;
align-items: center;
justify-content: center;
`;
var ScalableText = styled5(
Text
).attrs((props) => __spreadValues({
textAlign: "center"
}, props))`
transform-origin: center;
transform: scale(${(props) => props.scale || 1});
white-space: nowrap;
`;
var FitText = (props) => {
const containerRef = useRef5(null);
const textRef = useRef5(null);
const [scale, setScale] = useState5(1);
useResizeObserver2({
ref: containerRef,
onResize: () => {
if (!containerRef.current || !textRef.current)
return;
const containerWidth = containerRef.current.offsetWidth;
const textWidth = textRef.current.offsetWidth;
if (textWidth === 0)
return;
const newScale = Math.min(containerWidth / textWidth);
setScale(newScale);
}
});
return /* @__PURE__ */ jsx4(FitContainer, { ref: containerRef, children: /* @__PURE__ */ jsx4(ScalableText, __spreadProps(__spreadValues({}, props), { ref: textRef, scale })) });
};
// src/components/internal-button.ts
import styled6 from "styled-components";
var InternalButton = styled6("button")`
background: #333;
border: 1px solid hsla(0, 0%, 0%, 0.4);
border-radius: 2px;
color: #fff;
box-shadow: inset 1px 1px 0 hsla(0, 0%, 100%, 0.1),
1px 1px 0 hsla(0, 0%, 0%, 0.1);
padding: 3px 20px;
font-size: 14px;
font-weight: bold;
font-family: ${SYSTEM_FONT};
&:active {
box-shadow: inset 1px 1px 0 hsla(0, 0%, 0%, 0.25),
1px 1px 0 hsla(0, 0%, 0%, 0.1);
}
`;
var internal_button_default = InternalButton;
// src/utils/use-timer.ts
import { useState as useState6, useRef as useRef6, useEffect as useEffect9 } from "react";
var useTimer = (handler, period, isActive) => {
const [timeDelay, setTimeDelay] = useState6(1);
const initialTime = useRef6();
const callBack = useRef6();
useEffect9(() => {
callBack.current = handler;
}, [handler]);
useEffect9(() => {
if (isActive) {
initialTime.current = (/* @__PURE__ */ new Date()).getTime();
const timer = setInterval(() => {
const currentTime = (/* @__PURE__ */ new Date()).getTime();
const delay = currentTime - initialTime.current;
initialTime.current = currentTime;
setTimeDelay(delay / 1e3);
callBack.current(timeDelay);
}, period);
return () => {
clearInterval(timer);
};
}
}, [period, isActive, timeDelay]);
};
// src/components/presenter-mode/timer.tsx
import { useRegisterActions as useRegisterActions2 } from "kbar";
import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
var Timer = () => {
const [timer, setTimer] = useState7(0);
const [timerStarted, setTimerStarted] = useState7(false);
const addToTimer = useCallback6((v) => setTimer((s) => s + v), []);
const toggleTimer = useCallback6(() => setTimerStarted((s) => !s), []);
const resetTimer = useCallback6(() => setTimer(0), []);
useTimer(addToTimer, 1e3, timerStarted);
const minutes = Math.floor(Math.round(timer) / 60);
useRegisterActions2([
{
id: "Start/Pause Timer",
name: "Start/Pause Timer",
keywords: "start pause",
perform: toggleTimer,
section: "Timer"
},
{
id: "Restart Timer",
name: "Restart Timer",
keywords: "restart",
perform: resetTimer,
section: "Timer"
}
]);
return /* @__PURE__ */ jsxs2(FlexBox, { children: [
/* @__PURE__ */ jsx5(FlexBox, { justifyContent: "flex-start", flex: 1, children: /* @__PURE__ */ jsx5(
Text,
{
fontFamily: SYSTEM_FONT,
fontWeight: "bold",
fontSize: "2vw",
textAlign: "left",
children: `${String(minutes).padStart(2, "0")}:${String(
Math.round(timer) - minutes * 60
).padStart(2, "0")}`
}
) }),
/* @__PURE__ */ jsx5(internal_button_default, { onClick: toggleTimer, children: timerStarted ? "Stop Timer" : "Start Timer" }),
/* @__PURE__ */ jsx5(Box, { width: 8 }),
/* @__PURE__ */ jsx5(internal_button_default, { onClick: resetTimer, children: "Reset" })
] });
};
// src/components/presenter-mode/index.tsx
import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
var endOfNextSlide = ({ slideIndex }) => ({
slideIndex: slideIndex + 1,
stepIndex: GOTO_FINAL_STEP
});
var PreviewSlideWrapper = styled7.div(
({ visible }) => ({
visibility: visible ? "visible" : "hidden"
})
);
var PresenterMode = (props) => {
const { children, theme, backgroundImage, template } = props;
const deck = useRef7(null);
const previewDeck = useRef7(null);
const [notePortalNode, setNotePortalNode] = useState8();
const [showFinalSlide, setShowFinalSlide] = useState8(true);
const [postMessage] = useBroadcastChannel(
"spectacle_presenter_bus",
(message) => {
if (message.type === "SYNC_REQUEST") {
postMessage("SYNC", deck.current.activeView);
}
}
);
const [syncLocation, setLocation] = useLocationSync(__spreadValues({
setState: (state) => deck.current.skipTo(state)
}, query_string_exports));
const onActiveStateChange = useCallback7(
(activeView) => {
var _a, _b;
setLocation(activeView);
postMessage("SYNC", activeView);
setShowFinalSlide(
(((_a = deck.current) == null ? void 0 : _a.numberOfSlides) || 0) - 1 !== ((_b = deck == null ? void 0 : deck.current) == null ? void 0 : _b.activeView.slideIndex)
);
previewDeck.current.skipTo(endOfNextSlide(activeView));
},
[postMessage, setLocation]
);
useEffect10(() => {
const initialView = syncLocation({
slideIndex: 0,
stepIndex: 0
});
deck.current.initializeTo(initialView);
postMessage("SYNC", initialView);
previewDeck.current.initializeTo(endOfNextSlide(initialView));
}, [postMessage, syncLocation]);
return /* @__PURE__ */ jsxs3(PresenterDeckContainer, { children: [
/* @__PURE__ */ jsxs3(NotesColumn, { children: [
/* @__PURE__ */ jsxs3(FlexBox, { justifyContent: "space-between", paddingTop: 15, paddingX: 15, children: [
/* @__PURE__ */ jsx6(SpectacleLogo, { size: 60 }),
/* @__PURE__ */ jsx6(FlexBox, { width: 0.75, flexDirection: "column", alignItems: "flex-end", children: /* @__PURE__ */ jsxs3(
Text,
{
"data-testid": "use-browser-tab-text",
fontSize: 15,
fontFamily: SYSTEM_FONT,
textAlign: "right",
padding: "0px",
margin: "0px 0px 10px",
children: [
"Open a second browser tab at ",
window.location.host,
" to use as the audience deck."
]
}
) })
] }),
/* @__PURE__ */ jsx6(Box, { paddingX: 15, paddingY: 10, children: /* @__PURE__ */ jsx6(Timer, {}) }),
/* @__PURE__ */ jsx6(NotesContainer, { children: /* @__PURE__ */ jsx6(
Text,
{
ref: setNotePortalNode,
fontFamily: SYSTEM_FONT,
lineHeight: "1.5em",
fontSize: "1.5vw",
padding: 15
}
) })
] }),
/* @__PURE__ */ jsxs3(PreviewColumn, { children: [
/* @__PURE__ */ jsx6(
DeckInternal,
{
notePortalNode,
backdropStyle: deckBackdropStyles.currentSlide,
onActiveStateChange,
ref: deck,
theme,
backgroundImage,
template,
children
}
),
/* @__PURE__ */ jsx6(PreviewSlideWrapper, { visible: showFinalSlide, children: /* @__PURE__ */ jsx6(
DeckInternal,
{
disableInteractivity: true,
useAnimations: false,
backdropStyle: deckBackdropStyles.nextSlide,
ref: previewDeck,
theme,
backgroundImage,
template,
children
}
) })
] })
] });
};
var presenter_mode_default = PresenterMode;
// src/components/print-mode/index.tsx
import styled9, { createGlobalStyle } from "styled-components";
// src/components/slide/slide.tsx
import {
createContext as createContext2,
useCallback as useCallback8,
useContext as useContext2,
useEffect as useEffect12,
useMemo as useMemo3,
useState as useState10
} from "react";
import ReactDOM from "react-dom";
import styled8, { css, ThemeContext } from "styled-components";
import {
background,
color as color3,
space as space3
} from "styled-system";
import { animated, useSpring } from "react-spring";
// src/hooks/use-steps.tsx
import { useState as useState9, useContext, useRef as useRef8, useEffect as useEffect11, useId as useId4 } from "react";
// src/utils/sort-by.ts
function sortByKeyComparator(key) {
return (lhs, rhs) => {
if (lhs[key] < rhs[key]) {
return -1;
} else if (lhs[key] > rhs[key]) {
return 1;
}
return 0;
};
}
// src/hooks/use-steps.tsx
import { jsx as jsx7 } from "react/jsx-runtime";
var PLACEHOLDER_CLASS_NAME2 = "step-placeholder";
function useSteps(numSteps = 1, {
id: userProvidedId,
priority,
stepIndex
} = {}) {
const id = useId4();
const [stepId] = useState9(userProvidedId || id);
const slideContext = useContext(SlideContext);
if (slideContext === null) {
throw new Error(
"`useSteps` must be called within a SlideContext.Provider. Did you call `useSteps` in a component that was not placed inside a <Slide>?"
);
}
const { activeStepIndex, activationThresholds } = slideContext;
let relStep;
if (activationThresholds === null) {
relStep = 0;
} else {
const threshold = activationThresholds[stepId];
relStep = activeStepIndex - threshold;
relStep = clamp(relStep, -1, numSteps - 1);
}
const isActive = relStep >= 0;
const placeholderRef = useRef8(null);
useEffect11(() => {
if (!placeholderRef.current) {
console.warn(
`A placeholder ref does not appear to be present in the DOM for stepper element with id '${stepId}'. (Did you forget to render it?)`
);
}
});
const placeholderProps = {
ref: placeholderRef,
className: PLACEHOLDER_CLASS_NAME2,
style: { display: "none" },
"data-step-id": stepId,
"data-step-count": numSteps
};
if (priority !== void 0) {
placeholderProps["data-priority"] = priority;
} else if (stepIndex !== void 0) {
console.warn(
"`options.stepIndex` option to `useSteps` is deprecated- please use `priority` option instead."
);
placeholderProps["data-priority"] = stepIndex;
}
return {
stepId,
isActive,
step: relStep,
placeholder: /* @__PURE__ */ jsx7("div", __spreadValues({}, placeholderProps))
};
}
function useCollectSteps() {
const [stepContainer, setStepContainer] = useState9();
const [activationThresholds, setActivationThresholds] = useState9({});
const [finalStepIndex, setFinalStepIndex] = useState9(0);
useEffect11(() => {
if (!stepContainer)
return;
const placeholderNodes = stepContainer.getElementsByClassName(
PLACEHOLDER_CLASS_NAME2
);
const [thresholds, numSteps] = [...placeholderNodes].map((node, index) => {
const dataset = node.dataset;
const id = dataset.stepId;
let stepCount = Number(dataset.stepCount);
if (isNaN(stepCount)) {
stepCount = 1;
}
let priority = Number(dataset.priority);
if (isNaN(priority)) {
priority = index;
}
return {
id,