UNPKG

spectacle

Version:
1,731 lines (1,688 loc) 134 kB
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,