spectacle
Version:
ReactJS Powered Presentation Framework
1 lines • 242 kB
Source Map (JSON)
{"version":3,"sources":["../src/components/deck/index.tsx","../src/components/deck/default-deck.tsx","../src/components/deck/deck.tsx","../src/hooks/use-slides.tsx","../src/hooks/use-aspect-ratio-fitting.ts","../src/hooks/use-deck-state.ts","../src/utils/clamp.ts","../src/hooks/use-mousetrap.ts","../src/hooks/use-location-sync.ts","../src/theme/default-theme.ts","../src/theme/print-theme.ts","../src/theme/index.ts","../src/location-map-fns/query-string.ts","../src/components/deck/deck-styles.ts","../src/utils/use-auto-play.ts","../src/components/transitions/index.ts","../src/components/template-wrapper.tsx","../src/utils/constants.ts","../src/hooks/use-broadcast-channel.ts","../src/components/presenter-mode/index.tsx","../src/components/presenter-mode/components.tsx","../src/components/layout-primitives.ts","../src/components/presenter-mode/timer.tsx","../src/components/typography.tsx","../src/components/internal-button.ts","../src/utils/use-timer.ts","../src/components/print-mode/index.tsx","../src/components/slide/slide.tsx","../src/hooks/use-steps.tsx","../src/utils/sort-by.ts","../src/hooks/use-modes.ts","../src/components/command-bar/index.tsx","../src/components/command-bar/command-bar-actions.tsx","../src/components/command-bar/search/index.tsx","../src/components/command-bar/results/index.tsx","../src/utils/platform-keys.ts","../src/components/appear.tsx","../src/components/code-pane.tsx","../src/utils/indent-normalizer.ts","../src/components/table.tsx","../src/components/image.ts","../src/components/notes.tsx","../src/components/progress.tsx","../src/components/animated-progress.tsx","../src/components/fullscreen.tsx","../src/hooks/use-full-screen.ts","../src/components/slide-layout.tsx","../src/components/default-template.tsx","../src/utils/remark-rehype-presenter-notes.ts","../src/components/markdown/markdown.tsx","../src/utils/mdx-component-mapper.tsx","../src/utils/separate-sections-from-json.ts","../src/components/markdown/markdown-layout-containers.tsx","../src/components/markdown/markdown-slide-renderer.tsx","../src/utils/remark-rehype-directive.ts","../src/components/logo.tsx","../src/utils/notes.ts"],"sourcesContent":["import { Fragment } from 'react';\nimport DefaultDeck from './default-deck';\nimport PresenterMode from '../presenter-mode';\nimport PrintMode from '../print-mode';\nimport useMousetrap from '../../hooks/use-mousetrap';\nimport { KEYBOARD_SHORTCUTS, SPECTACLE_MODES } from '../../utils/constants';\nimport { DeckProps } from './deck';\nimport useModes, { ModeActions } from '../../hooks/use-modes';\nimport CommandBar from '../command-bar';\n\nconst View = ({\n getCurrentMode,\n toggleMode,\n ...props\n}: ModeActions & DeckProps) => {\n const mode = getCurrentMode();\n switch (mode) {\n case SPECTACLE_MODES.DEFAULT_MODE:\n return <DefaultDeck {...props} toggleMode={toggleMode} />;\n\n case SPECTACLE_MODES.PRESENTER_MODE:\n return <PresenterMode {...props} />;\n\n /**\n * Print mode and export mode are identical except for the theme\n * that is used. Print mode uses the print theme which is usually\n * monotone and export mode uses the default theme.\n */\n case SPECTACLE_MODES.PRINT_MODE:\n return <PrintMode {...props} />;\n\n case SPECTACLE_MODES.EXPORT_MODE:\n return <PrintMode {...props} exportMode />;\n\n case SPECTACLE_MODES.OVERVIEW_MODE:\n return <DefaultDeck overviewMode toggleMode={toggleMode} {...props} />;\n\n default:\n return <Fragment />;\n }\n};\n\nconst SpectacleDeck = (props: DeckProps): JSX.Element => {\n const { toggleMode, getCurrentMode } = useModes();\n\n useMousetrap(\n {\n [KEYBOARD_SHORTCUTS.PRESENTER_MODE]: (e) =>\n e && toggleMode({ e, newMode: SPECTACLE_MODES.PRESENTER_MODE }),\n [KEYBOARD_SHORTCUTS.PRINT_MODE]: (e) =>\n e && toggleMode({ e, newMode: SPECTACLE_MODES.PRINT_MODE }),\n [KEYBOARD_SHORTCUTS.EXPORT_MODE]: (e) =>\n e && toggleMode({ e, newMode: SPECTACLE_MODES.EXPORT_MODE }),\n [KEYBOARD_SHORTCUTS.OVERVIEW_MODE]: (e) =>\n e && toggleMode({ e, newMode: SPECTACLE_MODES.OVERVIEW_MODE })\n },\n []\n );\n\n return (\n <CommandBar>\n <View\n getCurrentMode={getCurrentMode}\n toggleMode={toggleMode}\n {...props}\n />\n </CommandBar>\n );\n};\n\nexport default SpectacleDeck;\n","import { useRef, useCallback, useEffect } from 'react';\nimport { DeckInternal, DeckInternalProps, DeckProps, DeckRef } from './deck';\nimport useBroadcastChannel from '../../hooks/use-broadcast-channel';\nimport useMousetrap from '../../hooks/use-mousetrap';\nimport {\n KEYBOARD_SHORTCUTS,\n SPECTACLE_MODES,\n ToggleModeParams\n} from '../../utils/constants';\n\n/**\n * Spectacle DefaultDeck is a wrapper around the Deck component that adds Broadcast channel support\n * for audience and presenter modes. This is intentionally not built into the base Deck component\n * to allow for extensibility outside of core Spectacle functionality.\n */\nconst DefaultDeck = (props: DefaultDeckProps): JSX.Element => {\n const {\n overviewMode = false,\n printMode = false,\n exportMode = false,\n toggleMode,\n children,\n ...rest\n } = props;\n const deck = useRef<DeckRef>(null);\n\n const [postMessage] = useBroadcastChannel(\n 'spectacle_presenter_bus',\n (message) => {\n if (message.type !== 'SYNC') return;\n const nextView = message.payload;\n if (deck.current!.initialized) {\n deck.current!.skipTo(nextView);\n } else {\n deck.current!.initializeTo(nextView);\n }\n }\n );\n\n useEffect(() => {\n postMessage('SYNC_REQUEST');\n }, [postMessage]);\n\n useMousetrap(\n overviewMode\n ? {\n [KEYBOARD_SHORTCUTS.TAB_FORWARD_OVERVIEW_MODE]: () =>\n deck.current!.advanceSlide(),\n [KEYBOARD_SHORTCUTS.TAB_BACKWARD_OVERVIEW_MODE]: () =>\n deck.current!.regressSlide({\n stepIndex: 0\n }),\n [KEYBOARD_SHORTCUTS.SELECT_SLIDE_OVERVIEW_MODE]: () =>\n toggleMode({\n newMode: SPECTACLE_MODES.DEFAULT_MODE\n })\n }\n : {},\n []\n );\n\n const onSlideClick = useCallback<\n NonNullable<DeckInternalProps['onSlideClick']>\n >(\n (e, slideIndex) => {\n if (overviewMode) {\n toggleMode({\n e,\n newMode: SPECTACLE_MODES.DEFAULT_MODE,\n senderSlideIndex: +slideIndex\n });\n }\n },\n [overviewMode, toggleMode]\n );\n\n const onMobileSlide: DeckInternalProps['onMobileSlide'] = (e) => {\n if (navigator.maxTouchPoints < 1 || !deck.current) return;\n switch (e.dir) {\n case 'Left':\n deck.current.stepForward();\n break;\n case 'Right':\n deck.current.regressSlide();\n break;\n }\n };\n\n return (\n <DeckInternal\n overviewMode={overviewMode}\n onSlideClick={onSlideClick}\n onMobileSlide={onMobileSlide}\n printMode={printMode}\n exportMode={exportMode}\n ref={deck}\n {...rest}\n >\n {children}\n </DeckInternal>\n );\n};\n\nexport default DefaultDeck;\n\ntype DefaultDeckProps = DeckProps & {\n toggleMode(args: ToggleModeParams): void;\n overviewMode?: boolean;\n printMode?: boolean;\n exportMode?: boolean;\n};\n","import {\n useState,\n useEffect,\n forwardRef,\n useMemo,\n useCallback,\n createContext,\n ElementType,\n useImperativeHandle,\n FC,\n RefAttributes,\n ReactNode,\n CSSProperties,\n useId\n} from 'react';\nimport styled, { CSSObject, ThemeProvider } from 'styled-components';\nimport { useCollectSlides } from '../../hooks/use-slides';\nimport useAspectRatioFitting from '../../hooks/use-aspect-ratio-fitting';\nimport useDeckState, {\n DeckStateAndActions,\n DeckView\n} from '../../hooks/use-deck-state';\nimport useMousetrap from '../../hooks/use-mousetrap';\nimport useLocationSync from '../../hooks/use-location-sync';\nimport { mergeTheme } from '../../theme';\nimport * as queryStringMapFns from '../../location-map-fns/query-string';\nimport {\n overviewFrameStyle,\n overviewWrapperStyle,\n printFrameStyle,\n printWrapperStyle\n} from './deck-styles';\nimport { useAutoPlay } from '../../utils/use-auto-play';\nimport defaultTheme, {\n SpectacleThemeOverrides\n} from '../../theme/default-theme';\nimport { defaultTransition, SlideTransition } from '../transitions';\nimport { SwipeEventData } from 'react-swipeable';\nimport { MarkdownComponentMap } from '../../utils/mdx-component-mapper';\nimport TemplateWrapper from '../template-wrapper';\nimport { useRegisterActions } from 'kbar';\nimport { KEYBOARD_SHORTCUTS_IDS } from '../../utils/constants';\n\nexport type DeckContextType = {\n deckId: string | number;\n slideCount: number;\n slideIds: SlideId[];\n useAnimations: boolean;\n autoPlayLoop: boolean;\n navigationDirection: number;\n slidePortalNode: HTMLDivElement;\n onSlideClick(e: MouseEvent, slideId: SlideId): void;\n onMobileSlide(eventData: SwipeEventData): void;\n theme?: SpectacleThemeOverrides & MarkdownThemeOverrides;\n frameOverrideStyle: CSSProperties;\n wrapperOverrideStyle: CSSProperties;\n backdropNode: HTMLDivElement;\n notePortalNode: HTMLDivElement;\n initialized: boolean;\n activeView: {\n slideId: SlideId;\n slideIndex: number;\n stepIndex: number;\n };\n pendingView: {\n slideId: SlideId;\n slideIndex: number;\n stepIndex: number;\n };\n skipTo(options: { slideIndex: number; stepIndex: number }): void;\n stepForward(): void;\n stepBackward(): void;\n advanceSlide(): void;\n regressSlide(): void;\n commitTransition(newView?: { stepIndex: number }): void;\n cancelTransition(): void;\n template: TemplateFn | ReactNode;\n transition: SlideTransition;\n backgroundImage?: string;\n inOverviewMode: boolean;\n inPrintMode: boolean;\n};\n\nexport const DeckContext = createContext<DeckContextType>(null as any);\nDeckContext.displayName = 'DeckContext';\nconst noop = () => {};\n\n/**\n * By default, Spectacle will maintain a 100% zoom on print/export mode. This can be customized if the\n * user wants to select a different paper size.\n */\nconst DEFAULT_PRINT_SCALE = 1.0;\nconst DEFAULT_OVERVIEW_SCALE = 0.25;\n\ntype PortalProps = {\n fitAspectRatioStyle: CSSObject;\n overviewMode: boolean;\n printMode: boolean;\n};\nconst Portal = styled.div<PortalProps>(\n ({ fitAspectRatioStyle, overviewMode, printMode }) => [\n !printMode && { overflow: 'hidden' },\n !printMode && fitAspectRatioStyle,\n overviewMode && {\n display: 'flex',\n flexWrap: 'wrap',\n justifyContent: 'flex-start',\n alignItems: 'flex-start',\n alignContent: 'flex-start',\n transform: 'scale(1)',\n overflowY: 'scroll',\n width: '100%',\n height: '100%'\n },\n printMode && {\n display: 'block'\n }\n ]\n);\n\nexport const DeckInternal = forwardRef<DeckRef, DeckInternalProps>(\n (\n {\n id: userProvidedId,\n className = '',\n backdropStyle: userProvidedBackdropStyle,\n overviewMode = false,\n printMode = false,\n exportMode = false,\n overviewScale = DEFAULT_OVERVIEW_SCALE,\n printScale = DEFAULT_PRINT_SCALE,\n template,\n theme: {\n Backdrop: UserProvidedBackdropComponent,\n backdropStyle: themeProvidedBackdropStyle = {\n position: 'fixed',\n top: 0,\n left: 0,\n width: '100vw',\n height: '100vh'\n } as CSSObject,\n suppressBackdropFallback: themeSuppressBackdropFallback,\n ...restTheme\n } = {},\n\n onSlideClick = noop,\n onMobileSlide = noop,\n\n disableInteractivity = false,\n notePortalNode,\n useAnimations = true,\n children,\n onActiveStateChange: onActiveStateChangeExternal = noop,\n initialState: initialDeckState = {\n slideIndex: 0,\n stepIndex: 0\n },\n suppressBackdropFallback = false,\n autoPlay = false,\n autoPlayLoop = false,\n autoPlayInterval = 1000,\n transition = defaultTransition,\n backgroundImage\n },\n ref\n ) => {\n const id = useId();\n const [deckId] = useState(userProvidedId || id);\n const {\n width: nativeSlideWidth = defaultTheme.size.width,\n height: nativeSlideHeight = defaultTheme.size.height\n } = restTheme.size || {};\n\n const {\n initialized,\n pendingView,\n activeView,\n navigationDirection,\n\n initializeTo,\n skipTo,\n stepForward,\n stepBackward,\n advanceSlide,\n regressSlide,\n commitTransition,\n cancelTransition\n } = useDeckState(initialDeckState);\n\n const [\n setPlaceholderContainer,\n slideIds,\n slideIdsWithTemplates,\n slideIdsInitialized\n ] = useCollectSlides();\n\n // It really is much easier to just expose methods to the outside world that\n // drive the presentation through its state rather than trying to implement a\n // declarative API.\n useImperativeHandle(\n ref,\n () => ({\n initialized,\n activeView,\n initializeTo,\n skipTo,\n stepForward,\n stepBackward,\n advanceSlide,\n regressSlide,\n numberOfSlides: slideIds.length\n }),\n [\n initialized,\n activeView,\n initializeTo,\n skipTo,\n stepForward,\n stepBackward,\n advanceSlide,\n regressSlide,\n slideIds\n ]\n );\n\n useRegisterActions(\n !disableInteractivity\n ? [\n {\n id: KEYBOARD_SHORTCUTS_IDS.NEXT_SLIDE,\n name: 'Next Slide',\n keywords: 'next',\n perform: () => stepForward(),\n section: 'Slide'\n },\n {\n id: KEYBOARD_SHORTCUTS_IDS.PREVIOUS_SLIDE,\n name: 'Previous Slide',\n keywords: 'previous',\n perform: () => stepBackward(),\n section: 'Slide'\n },\n {\n id: 'Restart Presentation',\n name: 'Restart Presentation',\n keywords: 'restart',\n perform: () =>\n skipTo({\n slideIndex: 0,\n stepIndex: 0\n }),\n section: 'Slide'\n }\n ]\n : []\n );\n useMousetrap(\n disableInteractivity\n ? {}\n : {\n left: () => stepBackward(),\n right: () => stepForward()\n },\n []\n );\n\n const [syncLocation, onActiveStateChange] = useLocationSync({\n disableInteractivity,\n setState: skipTo,\n ...queryStringMapFns\n });\n\n useEffect(() => {\n if (!initialized) return;\n onActiveStateChange(activeView);\n onActiveStateChangeExternal(activeView);\n }, [\n initialized,\n activeView,\n onActiveStateChange,\n onActiveStateChangeExternal\n ]);\n\n useEffect(() => {\n const initialView = syncLocation({\n slideIndex: 0,\n stepIndex: 0\n });\n initializeTo(initialView);\n }, [initializeTo, syncLocation]);\n\n useAutoPlay({\n enabled: autoPlay,\n loop: autoPlayLoop,\n interval: autoPlayInterval,\n stepForward\n });\n\n const handleSlideClick = useCallback<\n NonNullable<DeckInternalProps['onSlideClick']>\n >(\n (e, slideId) => {\n const slideIndex = slideIds.indexOf(slideId);\n onSlideClick(e, slideIndex);\n },\n [onSlideClick, slideIds]\n );\n\n const activeSlideId = slideIds[activeView.slideIndex];\n const pendingSlideId = slideIds[pendingView.slideIndex];\n\n const fullyInitialized = initialized && slideIdsInitialized;\n\n // Slides don't actually render their content to their position in the DOM-\n // they render to this `portalNode` element. The only thing they actually\n // render to their \"natural\" DOM location is a placeholder node which we use\n // below to enumerate them.\n //\n // The main reason for this is so that we can be absolutely sure that no\n // intermediate areas of the tree end up breaking styling, while still\n // allowing users to organize their slides via component nesting:\n //\n // const ContentSlides = () => (\n // <>\n // <Slide>First Slide</Slide>\n // <p>This text will never appear, because it's not part of a Slide.<p>\n // <Slide>Second Slide</Slide>\n // </>\n // );\n //\n // const Presentation = () => (\n // <Deck>\n // <Slide>Title Slide</Slide>\n // <ContentSlides />\n // <Slide>Conclusion Slide</Slide>\n // </Deck>\n // );\n const [slidePortalNode, setSlidePortalNode] =\n useState<HTMLDivElement | null>();\n\n const [backdropRef, fitAspectRatioStyle] = useAspectRatioFitting({\n targetWidth: nativeSlideWidth,\n targetHeight: nativeSlideHeight\n });\n\n const frameStyle = useMemo(() => {\n const options = {\n printScale,\n overviewScale,\n nativeSlideWidth,\n nativeSlideHeight\n };\n if (overviewMode) {\n return overviewFrameStyle(options);\n } else if (printMode) {\n return printFrameStyle(options);\n }\n return {};\n }, [\n nativeSlideHeight,\n nativeSlideWidth,\n overviewMode,\n overviewScale,\n printMode,\n printScale\n ]);\n\n const wrapperStyle = useMemo(() => {\n if (overviewMode) {\n return overviewWrapperStyle({ overviewScale });\n } else if (printMode) {\n return printWrapperStyle({ printScale });\n }\n return {};\n }, [overviewMode, overviewScale, printMode, printScale]);\n\n // Try to be intelligent about the backdrop background color: we have to use\n // inline styles, which will take precedence over all other styles. So, we do\n // as much as we can here to detect if a backdrop color has been provided, or\n // if the user has provided a custom backdrop component (in which case they're\n // responsible for styling it properly.) If we don't detect an appropriate\n // case, then we apply the inline style.\n //\n // Yes, this is slightly awkward, but IMO adding an additional `<div>` element\n // would be even more awkward.\n const backdropStyle = {\n ...themeProvidedBackdropStyle,\n ...userProvidedBackdropStyle\n };\n const BackdropComponent = UserProvidedBackdropComponent || 'div';\n\n if (\n !backdropStyle['background'] &&\n !backdropStyle['backgroundColor'] &&\n !UserProvidedBackdropComponent &&\n !suppressBackdropFallback &&\n !themeSuppressBackdropFallback\n ) {\n backdropStyle['backgroundColor'] = 'black';\n }\n\n const doesCurrentSlideHaveItsOwnTemplate =\n slideIdsWithTemplates.has(activeSlideId);\n\n const templateElement: ReactNode =\n typeof template === 'function'\n ? template({\n slideNumber: activeView.slideIndex + 1,\n numberOfSlides: slideIds.length\n })\n : template;\n\n return (\n <ThemeProvider\n theme={mergeTheme({\n theme: restTheme,\n printMode: printMode && !exportMode\n })}\n >\n <BackdropComponent\n ref={backdropRef}\n className={className}\n style={{\n ...backdropStyle,\n overflow: 'hidden'\n }}\n >\n <DeckContext.Provider\n value={{\n deckId,\n slideCount: slideIds.length,\n slideIds,\n useAnimations,\n slidePortalNode: slidePortalNode!,\n onSlideClick: handleSlideClick,\n onMobileSlide: onMobileSlide,\n theme: restTheme,\n autoPlayLoop,\n navigationDirection,\n\n frameOverrideStyle: frameStyle,\n wrapperOverrideStyle: wrapperStyle,\n\n backdropNode: backdropRef.current!,\n notePortalNode: notePortalNode!,\n initialized: fullyInitialized,\n activeView: {\n ...activeView,\n slideId: activeSlideId\n },\n pendingView: {\n ...pendingView,\n slideId: pendingSlideId\n },\n skipTo,\n stepForward,\n stepBackward,\n advanceSlide,\n regressSlide,\n commitTransition,\n cancelTransition,\n transition,\n template,\n backgroundImage,\n inOverviewMode: overviewMode,\n inPrintMode: printMode\n }}\n >\n <Portal\n ref={setSlidePortalNode}\n overviewMode={overviewMode}\n printMode={printMode}\n fitAspectRatioStyle={fitAspectRatioStyle}\n >\n {!doesCurrentSlideHaveItsOwnTemplate &&\n !overviewMode &&\n !printMode && (\n <TemplateWrapper\n style={{\n ...wrapperStyle,\n // Slides are appended to the parent as they are portaled in and end up later in\n // the source order. Adding zIndex to the template to overlay the sibling slides\n // once they have been portaled in.\n zIndex: 1\n }}\n >\n {templateElement}\n </TemplateWrapper>\n )}\n </Portal>\n <div ref={setPlaceholderContainer} style={{ display: 'none' }}>\n {children}\n </div>\n </DeckContext.Provider>\n </BackdropComponent>\n </ThemeProvider>\n );\n }\n);\nDeckInternal.displayName = 'Deck';\n\nexport const Deck = DeckInternal as FC<DeckProps & RefAttributes<DeckRef>>;\n\nDeck.displayName = 'Deck';\n\nexport type TemplateFn = (options: {\n slideNumber: number;\n numberOfSlides: number;\n}) => ReactNode;\nexport type SlideId = string | number;\ntype MarkdownThemeOverrides = {\n markdownComponentMap?: MarkdownComponentMap;\n};\ntype BackdropOverrides = {\n Backdrop?: ElementType;\n backdropStyle?: CSSObject;\n /**\n * @deprecated set a value to one of the `Backdrop`,\n * `backdropStyle.background`, or `backdropStyle.backgroundColor` properties\n * inside the `theme` prop object instead\n */\n suppressBackdropFallback?: boolean;\n};\n\nexport type DeckRef = Omit<\n DeckStateAndActions,\n | 'cancelTransition'\n | 'commitTransition'\n | 'navigationDirection'\n | 'pendingView'\n> & {\n numberOfSlides: number;\n};\nexport type DeckProps = {\n id?: string | number;\n className?: string;\n children: ReactNode;\n autoPlay?: boolean;\n autoPlayLoop?: boolean;\n autoPlayInterval?: number;\n theme?: SpectacleThemeOverrides & MarkdownThemeOverrides & BackdropOverrides;\n template?: TemplateFn | ReactNode;\n printScale?: number;\n overviewScale?: number;\n transition?: SlideTransition;\n /**\n * @deprecated set a value to one of the `Backdrop`,\n * `backdropStyle.background`, or `backdropStyle.backgroundColor` properties\n * inside the `theme` prop object instead\n */\n suppressBackdropFallback?: boolean;\n backgroundImage?: string;\n};\n/**\n * These types are only used internally,\n * and are not officially part of the public API\n */\nexport type DeckInternalProps = DeckProps & {\n initialState?: DeckView;\n printMode?: boolean;\n exportMode?: boolean;\n overviewMode?: boolean;\n onSlideClick?(e: Event, slideId: SlideId): void;\n onMobileSlide?(eventData: SwipeEventData): void;\n disableInteractivity?: boolean;\n useAnimations?: boolean;\n notePortalNode?: HTMLDivElement | null;\n /** @deprecated use the backdropStyle property inside the `theme` prop object instead */\n backdropStyle?: Partial<CSSStyleDeclaration>;\n onActiveStateChange?: (activeView: DeckView) => void;\n backgroundImage?: string;\n};\n\nexport default Deck;\n","import { useState, useEffect, useId } from 'react';\nimport { SlideId } from '../components/deck/deck';\n\nexport const PLACEHOLDER_CLASS_NAME = 'spectacle-v7-slide';\n\n// After the initial render pass, this hook actually goes and looks for\n// <Slide> elements rendered lower in the tree. Slides decide on an ID for\n// themselves and communicate via the `data-slide-key` element on their\n// placeholder.\nexport function useCollectSlides() {\n const [initialized, setInitialized] = useState(false);\n const [slideContainer, setSlideContainer] = useState<HTMLElement | null>();\n const [slideIds, setSlideIds] = useState<SlideId[]>([]);\n const [slideIdsOfSlidesWithTemplates, setSlideIdsOfSlidesWithTemplates] =\n useState<Set<SlideId>>(new Set());\n\n useEffect(() => {\n if (!slideContainer) return;\n const slides = slideContainer.getElementsByClassName(\n PLACEHOLDER_CLASS_NAME\n ) as unknown as Iterable<HTMLElement>;\n\n const nextSlideIds: SlideId[] = [];\n const nextSlideIdsOfSlidesWithTemplates: Set<SlideId> = new Set();\n for (const placeholderNode of slides) {\n const { slideId, slideHasTemplate } = placeholderNode.dataset;\n if (slideId !== undefined) {\n nextSlideIds.push(slideId);\n if (slideHasTemplate === 'true') {\n nextSlideIdsOfSlidesWithTemplates.add(slideId);\n }\n }\n }\n setSlideIds(nextSlideIds);\n setSlideIdsOfSlidesWithTemplates(nextSlideIdsOfSlidesWithTemplates);\n setInitialized(true);\n }, [slideContainer]);\n\n return [\n setSlideContainer,\n slideIds,\n slideIdsOfSlidesWithTemplates,\n initialized\n ] as const;\n}\n\nexport function useSlide(\n doesSlideHaveTemplate: boolean,\n userProvidedId?: SlideId\n) {\n const id = useId();\n const [slideId] = useState<SlideId>(userProvidedId || id);\n return {\n slideId,\n placeholder: (\n <div\n className={PLACEHOLDER_CLASS_NAME}\n data-slide-id={slideId}\n data-slide-has-template={doesSlideHaveTemplate}\n />\n )\n };\n}\n","import { useRef, useState, useCallback, useEffect } from 'react';\nimport useResizeObserver from 'use-resize-observer';\nimport { CSSObject } from 'styled-components';\n\ntype ResizeHandler = NonNullable<\n NonNullable<Parameters<typeof useResizeObserver>[0]>['onResize']\n>;\n\n// Returns an offset and scaling factor which, when applied to `element`, will\n// make it properly fit into `container` at the given aspect ratio.\nexport default function useAspectRatioFitting({\n targetWidth = 1366,\n targetHeight = 768\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [scaleFactor, setScaleFactor] = useState(1);\n const [transformOrigin, setTransformOrigin] = useState({ x: 0, y: 0 });\n\n const recalculate = useCallback<ResizeHandler>(\n ({ width, height }) => {\n const containerWidth = Number(width) || 0.01;\n const containerHeight = Number(height) || 0.01;\n\n const containerRatio = containerWidth / containerHeight;\n const targetRatio = targetWidth / targetHeight;\n const useVertical = containerRatio > targetRatio;\n\n const scaleFactor = useVertical\n ? containerHeight / targetHeight\n : containerWidth / targetWidth;\n\n const scaledWidth = targetWidth * scaleFactor;\n const scaledHeight = targetHeight * scaleFactor;\n\n let x0 = 0;\n if (useVertical) {\n x0 = 0.5 * (containerWidth - scaledWidth);\n x0 /= 1 - scaleFactor;\n }\n\n let y0 = 0;\n if (!useVertical) {\n y0 = 0.5 * (containerHeight - scaledHeight);\n y0 /= 1 - scaleFactor;\n }\n\n setScaleFactor(scaleFactor);\n setTransformOrigin({ x: x0, y: y0 });\n },\n [targetWidth, targetHeight]\n );\n\n // recalculate sizes on the initial pass, and each time the target size\n // changes. (our measurements aren't as accurate as `useResizeObserver`, but\n // we only need to get them close because it'll do them again anyways.)\n useEffect(() => {\n if (!containerRef || !containerRef.current) return;\n const rects = containerRef.current.getClientRects();\n recalculate(rects[0]);\n }, [targetWidth, targetHeight, recalculate]);\n\n useResizeObserver({\n ref: containerRef,\n onResize: recalculate\n });\n\n const styles: CSSObject = {\n position: 'relative',\n width: targetWidth,\n height: targetHeight,\n scaleFactor,\n transform: `scale(${scaleFactor})`,\n transformOrigin: `${transformOrigin.x}px ${transformOrigin.y}px`\n };\n\n return [containerRef, styles] as const;\n}\n","import { useReducer, useMemo } from 'react';\nimport { merge } from 'merge-anything';\nimport { SlideId } from '../components/deck/deck';\nimport clamp from '../utils/clamp';\n\nexport const GOTO_FINAL_STEP = null as unknown as number;\n\nexport type DeckView = {\n slideId?: SlideId;\n slideIndex: number;\n stepIndex: number;\n};\nexport type DeckState = {\n initialized: boolean;\n navigationDirection: number;\n activeView: DeckView;\n pendingView: DeckView;\n};\n\nexport const initialDeckState: DeckState = {\n initialized: false,\n navigationDirection: 0,\n pendingView: {\n slideIndex: 0,\n stepIndex: 0\n },\n activeView: {\n slideIndex: 0,\n stepIndex: 0\n }\n};\n\ntype ReducerActions =\n | { type: 'INITIALIZE_TO'; payload: Partial<DeckView> }\n | { type: 'SKIP_TO'; payload: Partial<DeckView> }\n | { type: 'STEP_FORWARD'; payload?: undefined }\n | { type: 'STEP_BACKWARD'; payload?: undefined }\n | { type: 'ADVANCE_SLIDE'; payload?: undefined }\n | { type: 'REGRESS_SLIDE'; payload?: Pick<DeckView, 'stepIndex'> }\n | { type: 'COMMIT_TRANSITION'; payload?: DeckView }\n | { type: 'CANCEL_TRANSITION'; payload?: undefined };\n\nfunction deckReducer(state: DeckState, { type, payload = {} }: ReducerActions) {\n switch (type) {\n case 'INITIALIZE_TO':\n return {\n navigationDirection: 0,\n activeView: merge(state.activeView, payload),\n pendingView: merge(state.pendingView, payload),\n initialized: true\n };\n case 'SKIP_TO':\n const navigationDirection = (() => {\n if ('slideIndex' in payload && payload.slideIndex) {\n return clamp(payload.slideIndex - state.activeView.slideIndex, -1, 1);\n }\n return null;\n })();\n return {\n ...state,\n navigationDirection: navigationDirection || state.navigationDirection,\n pendingView: merge(state.pendingView, payload)\n };\n case 'STEP_FORWARD':\n return {\n ...state,\n navigationDirection: 1,\n pendingView: merge(state.pendingView, {\n stepIndex: state.pendingView.stepIndex + 1\n })\n };\n case 'STEP_BACKWARD':\n return {\n ...state,\n navigationDirection: -1,\n pendingView: merge(state.pendingView, {\n stepIndex: state.pendingView.stepIndex - 1\n })\n };\n case 'ADVANCE_SLIDE':\n return {\n ...state,\n navigationDirection: 1,\n pendingView: merge(state.pendingView, {\n stepIndex: 0,\n slideIndex: state.pendingView.slideIndex + 1\n })\n };\n case 'REGRESS_SLIDE':\n return {\n ...state,\n navigationDirection: -1,\n pendingView: merge(state.pendingView, {\n stepIndex: payload?.stepIndex ?? GOTO_FINAL_STEP,\n slideIndex: state.pendingView.slideIndex - 1\n })\n };\n case 'COMMIT_TRANSITION':\n const pendingView = merge(state.pendingView, payload);\n return {\n ...state,\n pendingView,\n activeView: merge(state.activeView, pendingView)\n };\n case 'CANCEL_TRANSITION':\n return {\n ...state,\n pendingView: merge(state.pendingView, state.activeView)\n };\n default:\n return state;\n }\n}\n\nexport default function useDeckState(userProvidedInitialState: DeckView) {\n const [\n { initialized, navigationDirection, pendingView, activeView },\n dispatch\n ] = useReducer(deckReducer, {\n initialized: initialDeckState.initialized,\n navigationDirection: initialDeckState.navigationDirection,\n pendingView: {\n ...initialDeckState.pendingView,\n ...userProvidedInitialState\n },\n activeView: {\n ...initialDeckState.activeView,\n ...userProvidedInitialState\n }\n });\n const actions = useMemo(\n () => ({\n initializeTo: (payload: Partial<DeckView>) =>\n dispatch({ type: 'INITIALIZE_TO', payload }),\n skipTo: (payload: Partial<DeckView>) =>\n dispatch({ type: 'SKIP_TO', payload }),\n stepForward: () => dispatch({ type: 'STEP_FORWARD' }),\n stepBackward: () => dispatch({ type: 'STEP_BACKWARD' }),\n advanceSlide: () => dispatch({ type: 'ADVANCE_SLIDE' }),\n regressSlide: (payload?: Pick<DeckView, 'stepIndex'>) =>\n dispatch({ type: 'REGRESS_SLIDE', payload }),\n commitTransition: (payload?: DeckView) =>\n dispatch({ type: 'COMMIT_TRANSITION', payload }),\n cancelTransition: () => dispatch({ type: 'CANCEL_TRANSITION' })\n }),\n [dispatch]\n );\n\n return {\n initialized,\n navigationDirection,\n pendingView,\n activeView,\n ...actions\n };\n}\n\nexport type DeckStateAndActions = ReturnType<typeof useDeckState>;\n","export function toFiniteNumber(value: number) {\n if (!value || isNaN(value)) {\n return 0;\n } else if (value === Infinity || value === -Infinity) {\n const sign = value < 0 ? -1 : 1;\n return sign * Number.MAX_SAFE_INTEGER;\n }\n return value;\n}\n\nexport default function clamp(number: number, lower?: number, upper?: number) {\n if (isNaN(number)) {\n return NaN;\n }\n let finiteNumber = toFiniteNumber(number);\n if (finiteNumber === finiteNumber) {\n if (upper !== undefined) {\n finiteNumber = finiteNumber <= upper ? finiteNumber : upper;\n }\n if (lower !== undefined) {\n finiteNumber = finiteNumber >= lower ? finiteNumber : lower;\n }\n }\n return finiteNumber;\n}\n","import { useEffect } from 'react';\nimport Mousetrap, { ExtendedKeyboardEvent } from 'mousetrap';\n\n/*\n * Hook for binding functions to keyboard bindings. Will throw an error if the\n * value of the keybind combination is not a function.\n */\nexport default function useMousetrap(\n keybinds: Record<string, (e?: ExtendedKeyboardEvent) => void>,\n deps: any[]\n): void {\n useEffect(() => {\n for (const combo in keybinds) {\n const callback = keybinds[combo];\n if (typeof callback !== 'function') {\n throw new TypeError(\n `Expected type 'function' in useMousetrap for combo '${combo}', but got ${typeof callback}`\n );\n }\n Mousetrap.bind(combo, callback);\n }\n return () => {\n for (const combo in keybinds) {\n Mousetrap.unbind(combo);\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [keybinds, ...deps]);\n}\n","import { useState, useEffect, useCallback } from 'react';\nimport { createBrowserHistory, Location } from 'history';\nimport QS from 'query-string';\nimport isEqual from 'react-fast-compare';\nimport { mergeAndCompare, merge } from 'merge-anything';\nimport { DeckView } from './use-deck-state';\nimport {\n mapLocationToState,\n mapStateToLocation,\n SlideState\n} from '../location-map-fns/query-string';\n\n// Needed to properly merge query strings. (Hook consumers can also provide\n// their own merge function if necessary)\nfunction defaultMergeLocation(\n object: Partial<Location>,\n ...sources: Partial<Location>[]\n): Partial<Location> {\n return mergeAndCompare(\n (left, right, key) => {\n switch (key) {\n case 'search':\n if (!left) return right;\n return (\n '?' +\n QS.stringify({\n ...QS.parse(left),\n ...QS.parse(right)\n })\n );\n default:\n return merge(left, right);\n }\n },\n object,\n ...sources\n );\n}\n\ntype LocationStateOptions = {\n setState(state: Partial<DeckView>): void;\n mapStateToLocation: typeof mapStateToLocation;\n mapLocationToState: typeof mapLocationToState;\n mergeLocation?: typeof defaultMergeLocation;\n historyFactory?: typeof createBrowserHistory;\n disableInteractivity?: boolean;\n};\n\n// Hook to keep some external state synchronized with the history location.\nexport default function useLocationSync({\n setState,\n mapStateToLocation,\n mapLocationToState,\n disableInteractivity = false,\n mergeLocation = defaultMergeLocation,\n historyFactory = createBrowserHistory\n}: LocationStateOptions) {\n const [history] = useState(() => {\n return typeof document !== 'undefined' ? historyFactory() : null;\n });\n const [initialized, setInitialized] = useState(false);\n\n // \"down-sync\" from location to state\n useEffect(() => {\n if (!initialized && disableInteractivity) return;\n return history?.listen(({ location }) => {\n setState(mapLocationToState(location));\n });\n }, [\n disableInteractivity,\n initialized,\n history,\n setState,\n mapLocationToState\n ]);\n\n const syncLocation = useCallback(\n (defaultState: DeckView): DeckView => {\n if (disableInteractivity || !history) {\n return defaultState;\n }\n // perform initial two-way sync between location and state (state wins)\n const { location } = history;\n const initialState: DeckView = merge(\n defaultState,\n mapLocationToState(location)\n );\n const nextLocation = mergeLocation(\n {},\n location,\n mapStateToLocation(initialState)\n );\n history.replace(nextLocation);\n setInitialized(true);\n return initialState;\n },\n [\n history,\n mapLocationToState,\n mapStateToLocation,\n disableInteractivity,\n mergeLocation\n ]\n );\n\n const setLocation = useCallback(\n (state: SlideState) => {\n if (!initialized || !history) return;\n // perform one-way sync to history\n const { location } = history;\n const nextLocation = mergeLocation(\n {},\n location,\n mapStateToLocation(state)\n );\n if (!isEqual(location, nextLocation)) {\n history.push(nextLocation);\n }\n },\n [history, initialized, mergeLocation, mapStateToLocation]\n );\n\n return [syncLocation, setLocation] as const;\n}\n","import { DeepPartial } from '../types/deep-partial';\n\nconst defaultTheme = {\n size: {\n width: 1366,\n height: 768,\n maxCodePaneHeight: 200\n },\n colors: {\n primary: '#ebe5da',\n secondary: '#fc6986',\n tertiary: '#1e2852',\n quaternary: '#ffc951',\n quinary: '#8bddfd'\n },\n fonts: {\n header: '\"Helvetica Neue\", Helvetica, Arial, sans-serif',\n text: '\"Helvetica Neue\", Helvetica, Arial, sans-serif',\n monospace: '\"Consolas\", \"Menlo\", monospace'\n },\n fontSizes: {\n h1: '72px',\n h2: '64px',\n h3: '56px',\n text: '44px',\n monospace: '20px'\n },\n space: [16, 24, 32]\n};\n\nexport type SpectacleTheme = typeof defaultTheme;\nexport type SpectacleThemeOverrides = DeepPartial<SpectacleTheme>;\n\nexport default defaultTheme;\n","import { SpectacleThemeOverrides } from './default-theme';\n\nconst printTheme: SpectacleThemeOverrides = {\n colors: {\n primary: '#777',\n secondary: '#000',\n tertiary: '#fff',\n quaternary: '#000000',\n quinary: '#000000'\n }\n};\nexport default printTheme;\n","import defaultTheme, {\n SpectacleTheme,\n SpectacleThemeOverrides\n} from './default-theme';\nimport printTheme from './print-theme';\n\nconst mergeKeys = (\n base: SpectacleTheme,\n override: SpectacleThemeOverrides\n): SpectacleTheme =>\n (Object.keys(override || {}) as Array<keyof SpectacleTheme>).reduce(\n (merged, key) => {\n merged[key] = { ...merged[key], ...override[key] } as any;\n return merged;\n },\n { ...base }\n );\n\ntype MergeOptions = { theme: SpectacleThemeOverrides; printMode?: boolean };\n\nexport function mergeTheme({ theme, printMode }: MergeOptions) {\n const merged = mergeKeys(defaultTheme, theme);\n return printMode ? mergeKeys(merged, printTheme) : merged;\n}\n","import { GOTO_FINAL_STEP } from '../hooks/use-deck-state';\nimport {\n parse as parseQS,\n ParsedQuery,\n stringify as stringifyQS\n} from 'query-string';\n\nexport type SlideState = {\n slideIndex?: number;\n stepIndex?: number | typeof GOTO_FINAL_STEP;\n};\n\nexport function mapLocationToState(\n location: Pick<Location, 'search'>\n): SlideState {\n const { search: queryString } = location;\n\n const { slideIndex: rawSlideIndex, stepIndex: rawStepIndex } =\n parseQS(queryString);\n\n const nextState: SlideState = {};\n\n if (rawSlideIndex === undefined) {\n return nextState;\n }\n\n nextState.slideIndex = Number(rawSlideIndex);\n if (isNaN(nextState.slideIndex)) {\n throw new Error(\n `Invalid slide index in URL query string: '${queryString}'`\n );\n }\n\n if (rawStepIndex === 'final') {\n nextState.stepIndex = GOTO_FINAL_STEP;\n } else if (rawStepIndex !== undefined) {\n nextState.stepIndex = Number(rawStepIndex);\n if (isNaN(nextState.stepIndex)) {\n throw new Error(\n `Invalid step index in URL query string: '${queryString}'`\n );\n }\n }\n\n return nextState;\n}\n\nexport function mapStateToLocation(state: SlideState) {\n const { slideIndex, stepIndex } = state;\n const query: ParsedQuery = {};\n if (typeof slideIndex !== 'number') {\n return query;\n }\n query.slideIndex = String(slideIndex);\n if (typeof stepIndex === 'number') {\n query.stepIndex = String(stepIndex);\n } else if (stepIndex === GOTO_FINAL_STEP) {\n query.stepIndex = 'final';\n }\n return {\n search: '?' + stringifyQS(query)\n };\n}\n","import { CSSProperties } from 'react';\n\nexport function overviewFrameStyle({\n overviewScale,\n nativeSlideWidth,\n nativeSlideHeight\n}: {\n overviewScale: number;\n nativeSlideWidth: number;\n nativeSlideHeight: number;\n}): CSSProperties {\n return {\n margin: '1rem',\n width: `${overviewScale * nativeSlideWidth}px`,\n height: `${\n (overviewScale / (nativeSlideWidth / nativeSlideHeight)) *\n nativeSlideWidth\n }px`,\n display: 'block',\n transform: 'none',\n position: 'relative'\n };\n}\n\nexport function overviewWrapperStyle({\n overviewScale\n}: {\n overviewScale: number;\n}): CSSProperties {\n return {\n width: `${100 / overviewScale}%`,\n height: `${100 / overviewScale}%`,\n transform: `scale(${overviewScale})`,\n transformOrigin: '0px 0px',\n position: 'absolute'\n };\n}\n\nexport function printFrameStyle({\n nativeSlideWidth,\n nativeSlideHeight,\n printScale\n}: {\n nativeSlideWidth: number;\n nativeSlideHeight: number;\n printScale: number;\n}): CSSProperties {\n return {\n margin: '0',\n width: `${printScale * nativeSlideWidth}px`,\n height: `${\n (printScale / (nativeSlideWidth / nativeSlideHeight)) * nativeSlideWidth\n }px`,\n display: 'block',\n transform: 'none',\n position: 'relative',\n breakAfter: 'page'\n };\n}\n\nexport function printWrapperStyle({\n printScale\n}: {\n printScale: number;\n}): CSSProperties {\n return {\n width: `${100 / printScale}%`,\n height: `${100 / printScale}%`,\n transform: `scale(${printScale})`,\n transformOrigin: '0px 0px',\n position: 'absolute'\n };\n}\n","import { useEffect, useRef } from 'react';\nimport { DeckStateAndActions } from '../hooks/use-deck-state';\n\nexport type AutoPlayOptions = {\n enabled?: boolean;\n loop?: boolean;\n stepForward: DeckStateAndActions['stepForward'];\n interval?: number;\n};\n\nexport const useAutoPlay = ({\n enabled = false,\n loop = false,\n stepForward,\n interval = 1000\n}: AutoPlayOptions) => {\n const stepFn = useRef(stepForward);\n stepFn.current = stepForward;\n\n useEffect(() => {\n if (enabled) {\n const id = setInterval(() => {\n stepFn.current();\n }, interval);\n\n return () => clearInterval(id);\n }\n }, [enabled, interval, loop]);\n};\n","import { CSSObject } from 'styled-components';\n\nconst STAGE_RIGHT = 'translateX(-100%)';\nconst CENTER_STAGE = 'translateX(0%)';\nconst STAGE_LEFT = 'translateX(100%)';\n\nexport type SlideTransition = {\n from?: CSSObject;\n leave?: CSSObject;\n enter?: CSSObject;\n};\n\nexport const fadeTransition: SlideTransition = {\n from: {\n opacity: 0\n },\n enter: {\n opacity: 1\n },\n leave: {\n opacity: 0\n }\n};\n\nexport const slideTransition: SlideTransition = {\n from: {\n transform: STAGE_LEFT\n },\n enter: {\n transform: CENTER_STAGE\n },\n leave: {\n transform: STAGE_RIGHT\n }\n};\n\nexport const defaultTransition = slideTransition;\n","import styled from 'styled-components';\n\nexport const TemplateWrapper = styled.div`\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n pointer-events: none;\n`;\n\nexport default TemplateWrapper;\n","export const DEFAULT_SLIDE_ELEMENT_INDEX = -1;\nexport const DEFAULT_SLIDE_INDEX = 0;\nexport const SYSTEM_FONT =\n '-apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Helvetica Neue\", Helvetica, sans-serif';\n\nexport const KEYBOARD_SHORTCUTS = {\n DEFAULT_MODE: 'mod+shift+d',\n PRESENTER_MODE: 'mod+shift+p',\n OVERVIEW_MODE: 'mod+shift+o',\n PRINT_MODE: 'mod+shift+r',\n EXPORT_MODE: 'mod+shift+e',\n TAB_FORWARD_OVERVIEW_MODE: 'tab',\n TAB_BACKWARD_OVERVIEW_MODE: 'shift+tab',\n SELECT_SLIDE_OVERVIEW_MODE: 'enter',\n NEXT_SLIDE: 'right',\n PREVIOUS_SLIDE: 'left'\n};\nexport type KeyboardShortcutTypes = keyof typeof KEYBOARD_SHORTCUTS;\n\nexport const KEYBOARD_SHORTCUTS_IDS = {\n DEFAULT_MODE: 'DEFAULT_MODE',\n PRESENTER_MODE: 'PRESENTER_MODE',\n OVERVIEW_MODE: 'OVERVIEW_MODE',\n PRINT_MODE: 'PRINT_MODE',\n EXPORT_MODE: 'EXPORT_MODE',\n TAB_FORWARD_OVERVIEW_MODE: 'TAB_FORWARD_OVERVIEW_MODE',\n TAB_BACKWARD_OVERVIEW_MODE: 'TAB_BACKWARD_OVERVIEW_MODE',\n SELECT_SLIDE_OVERVIEW_MODE: 'SELECT_SLIDE_OVERVIEW_MODE',\n NEXT_SLIDE: 'NEXT_SLIDE',\n PREVIOUS_SLIDE: 'PREVIOUS_SLIDE'\n};\n\nexport const SPECTACLE_MODES = {\n DEFAULT_MODE: 'DEFAULT_MODE',\n PRESENTER_MODE: 'PRESENTER_MODE',\n OVERVIEW_MODE: 'OVERVIEW_MODE',\n PRINT_MODE: 'PRINT_MODE',\n EXPORT_MODE: 'EXPORT_MODE'\n} as const;\ntype ValuesOf<T> = T[keyof T];\nexport type SpectacleMode = ValuesOf<typeof SPECTACLE_MODES>;\n\nexport type ModeSearchParams = {\n presenterMode?: boolean;\n overviewMode?: boolean;\n printMode?: boolean;\n exportMode?: boolean;\n};\n\nexport type ToggleModeParams = {\n newMode: SpectacleMode;\n senderSlideIndex?: number;\n e?: Event;\n};\n","import { useCallback, useEffect, useId, useRef } from 'react';\nimport { BroadcastChannel as BroadcastChannelPolyfill } from 'broadcast-channel';\nimport { DeckView } from './use-deck-state';\n\nconst noop = () => {};\nlet safeWindow: any = {};\nif (typeof window !== 'undefined') {\n safeWindow = window;\n}\nconst BroadcastChannel =\n safeWindow.BroadcastChannel || BroadcastChannelPolyfill;\n\ntype MessageCallback = (message: MessageTypes) => void;\n\ntype MessageTypes =\n | { type: 'SYNC'; payload: Partial<DeckView> }\n | { type: 'SYNC_REQUEST'; payload?: never };\n\nexport default function useBroadcastChannel(\n channelName: string,\n onMessage: MessageCallback = noop,\n deps = []\n) {\n const broadcasterId = useId();\n const channel = useRef<BroadcastChannel>();\n\n useEffect(() => {\n channel.current = new BroadcastChannel(channelName);\n\n return () => {\n channel.current?.close();\n };\n }, [channelName]);\n\n const postMessage = useCallback(\n <TType extends MessageTypes['type']>(\n type: TType,\n payload: MessageTypes['payload'] = {}\n ) => {\n const message = {\n type,\n payload,\n meta: { sender: broadcasterId }\n };\n const rawMessage = JSON.stringify(message);\n channel.current?.postMessage(rawMessage);\n },\n [broadcasterId]\n );\n\n // Avoid constantly modifying the 'message' listener in the effect below\n const userMessageHandlerRef = useRef(onMessage);\n useEffect(() => {\n userMessageHandlerRef.current = onMessage;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [...deps, postMessage]);\n\n useEffect(() => {\n if (!channel.current) return;\n const messageHandler = (event: MessageEvent<string>) => {\n const rawMessage = event.data;\n const message = JSON.parse(rawMessage);\n userMessageHandlerRef.current(message);\n };\n channel.current?.addEventListener('message', messageHandler);\n return () => {\n channel.current?.removeEventListener('message', messageHandler);\n };\n }, [postMessage]);\n\n return [postMessage, broadcasterId] as const;\n}\n","import {\n useRef,\n useCallback,\n useState,\n useEffect,\n ReactNode,\n ReactElement\n} from 'react';\nimport styled from 'styled-components';\nimport { DeckInternal, DeckRef, TemplateFn } from '../deck/deck';\nimport { Text, SpectacleLogo } from '../../index';\nimport {\n PresenterDeckContainer,\n NotesColumn,\n PreviewColumn,\n deckBackdropStyles,\n NotesContainer\n} from './components';\nim