react-cheminfo 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -2
- package/bin/check-tokens.mjs +105 -0
- package/lib/about/core/about.d.ts +95 -0
- package/lib/about/core/about.d.ts.map +1 -0
- package/lib/about/core/about.js +78 -0
- package/lib/about/core/about.js.map +1 -0
- package/lib/about/core/index.d.ts +3 -0
- package/lib/about/core/index.d.ts.map +1 -0
- package/lib/about/core/index.js +2 -0
- package/lib/about/core/index.js.map +1 -0
- package/lib/about/ui/AboutCitations.d.ts +14 -0
- package/lib/about/ui/AboutCitations.d.ts.map +1 -0
- package/lib/about/ui/AboutCitations.js +40 -0
- package/lib/about/ui/AboutCitations.js.map +1 -0
- package/lib/about/ui/AboutPage.d.ts +32 -0
- package/lib/about/ui/AboutPage.d.ts.map +1 -0
- package/lib/about/ui/AboutPage.js +70 -0
- package/lib/about/ui/AboutPage.js.map +1 -0
- package/lib/about/ui/AboutSection.d.ts +23 -0
- package/lib/about/ui/AboutSection.d.ts.map +1 -0
- package/lib/about/ui/AboutSection.js +28 -0
- package/lib/about/ui/AboutSection.js.map +1 -0
- package/lib/about/ui/index.d.ts +7 -0
- package/lib/about/ui/index.d.ts.map +1 -0
- package/lib/about/ui/index.js +4 -0
- package/lib/about/ui/index.js.map +1 -0
- package/lib/chrome/ui/index.d.ts +1 -0
- package/lib/chrome/ui/index.d.ts.map +1 -1
- package/lib/chrome/ui/index.js +3 -0
- package/lib/chrome/ui/index.js.map +1 -1
- package/lib/clipboard/ui/CodeBlock.d.ts.map +1 -1
- package/lib/clipboard/ui/CodeBlock.js +4 -1
- package/lib/clipboard/ui/CodeBlock.js.map +1 -1
- package/lib/color/ui/ColorScaleLegend.js +1 -1
- package/lib/color/ui/ColorScaleLegend.js.map +1 -1
- package/lib/core.d.ts +3 -0
- package/lib/core.d.ts.map +1 -1
- package/lib/core.js +5 -0
- package/lib/core.js.map +1 -1
- package/lib/credits/core/credits.d.ts +36 -0
- package/lib/credits/core/credits.d.ts.map +1 -1
- package/lib/credits/core/credits.js +42 -0
- package/lib/credits/core/credits.js.map +1 -1
- package/lib/credits/ui/CreditsList.js +1 -1
- package/lib/credits/ui/CreditsList.js.map +1 -1
- package/lib/delimited/ui/DelimitedTextPanel.js +1 -1
- package/lib/delimited/ui/DelimitedTextPanel.js.map +1 -1
- package/lib/ecosystem/core/sites.d.ts +1 -1
- package/lib/ecosystem/core/sites.d.ts.map +1 -1
- package/lib/ecosystem/core/sites.js +10 -0
- package/lib/ecosystem/core/sites.js.map +1 -1
- package/lib/ecosystem/core/tokens.d.ts.map +1 -1
- package/lib/ecosystem/core/tokens.js +1 -0
- package/lib/ecosystem/core/tokens.js.map +1 -1
- package/lib/ecosystem/ui/EcosystemLinks.js +2 -2
- package/lib/ecosystem/ui/EcosystemLinks.js.map +1 -1
- package/lib/ecosystem/ui/EcosystemMenu.js +1 -1
- package/lib/ecosystem/ui/EcosystemMenu.js.map +1 -1
- package/lib/ecosystem/ui/SiteTile.d.ts.map +1 -1
- package/lib/ecosystem/ui/SiteTile.js +5 -5
- package/lib/ecosystem/ui/SiteTile.js.map +1 -1
- package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
- package/lib/ecosystem/ui/glyphs.js +3 -0
- package/lib/ecosystem/ui/glyphs.js.map +1 -1
- package/lib/orbital/core/index.d.ts +1 -1
- package/lib/orbital/core/index.d.ts.map +1 -1
- package/lib/orbital/core/index.js +1 -1
- package/lib/orbital/core/index.js.map +1 -1
- package/lib/orbital/core/realHarmonics.d.ts +5 -2
- package/lib/orbital/core/realHarmonics.d.ts.map +1 -1
- package/lib/orbital/core/realHarmonics.js.map +1 -1
- package/lib/orbital/ui/AtomicOrbitalCanvas.d.ts +18 -0
- package/lib/orbital/ui/AtomicOrbitalCanvas.d.ts.map +1 -1
- package/lib/orbital/ui/AtomicOrbitalCanvas.js +84 -7
- package/lib/orbital/ui/AtomicOrbitalCanvas.js.map +1 -1
- package/lib/orbital/ui/AtomicOrbitalViewer.d.ts +13 -0
- package/lib/orbital/ui/AtomicOrbitalViewer.d.ts.map +1 -1
- package/lib/orbital/ui/AtomicOrbitalViewer.js +1 -1
- package/lib/orbital/ui/AtomicOrbitalViewer.js.map +1 -1
- package/lib/orbital/ui/camera.d.ts +15 -0
- package/lib/orbital/ui/camera.d.ts.map +1 -1
- package/lib/orbital/ui/camera.js +68 -0
- package/lib/orbital/ui/camera.js.map +1 -1
- package/lib/orbital/ui/renderVolume.d.ts.map +1 -1
- package/lib/orbital/ui/renderVolume.js +8 -1
- package/lib/orbital/ui/renderVolume.js.map +1 -1
- package/lib/orbital/ui/serial.d.ts +25 -0
- package/lib/orbital/ui/serial.d.ts.map +1 -0
- package/lib/orbital/ui/serial.js +25 -0
- package/lib/orbital/ui/serial.js.map +1 -0
- package/lib/orbital/ui/viewer.d.ts +23 -0
- package/lib/orbital/ui/viewer.d.ts.map +1 -1
- package/lib/orbital/ui/viewer.js +70 -17
- package/lib/orbital/ui/viewer.js.map +1 -1
- package/lib/pedagogy/ui/ExerciseProgressHeader.js +1 -1
- package/lib/pedagogy/ui/ExerciseProgressHeader.js.map +1 -1
- package/lib/pedagogy/ui/GlossaryText.js +2 -2
- package/lib/pedagogy/ui/GlossaryText.js.map +1 -1
- package/lib/pedagogy/ui/ReferenceSectionBlock.js +5 -2
- package/lib/pedagogy/ui/ReferenceSectionBlock.js.map +1 -1
- package/lib/pedagogy/ui/SyntaxTooltip.js +2 -2
- package/lib/pedagogy/ui/SyntaxTooltip.js.map +1 -1
- package/lib/pedagogy/ui/TutorialStepStrip.js +4 -1
- package/lib/pedagogy/ui/TutorialStepStrip.js.map +1 -1
- package/lib/periodic/ui/CategoryLegend.js +1 -1
- package/lib/periodic/ui/CategoryLegend.js.map +1 -1
- package/lib/periodic/ui/PeriodicTableChrome.js +2 -2
- package/lib/periodic/ui/PeriodicTableChrome.js.map +1 -1
- package/lib/seo/vite/ogCard.d.ts.map +1 -1
- package/lib/seo/vite/ogCard.js +1 -0
- package/lib/seo/vite/ogCard.js.map +1 -1
- package/lib/share/ui/ShareDialogContent.d.ts.map +1 -1
- package/lib/share/ui/ShareDialogContent.js +5 -2
- package/lib/share/ui/ShareDialogContent.js.map +1 -1
- package/lib/share/ui/SharePartOptions.js +1 -1
- package/lib/share/ui/SharePartOptions.js.map +1 -1
- package/lib/slides/core/demoLinks.d.ts +43 -0
- package/lib/slides/core/demoLinks.d.ts.map +1 -0
- package/lib/slides/core/demoLinks.js +104 -0
- package/lib/slides/core/demoLinks.js.map +1 -0
- package/lib/slides/core/index.d.ts +13 -0
- package/lib/slides/core/index.d.ts.map +1 -0
- package/lib/slides/core/index.js +7 -0
- package/lib/slides/core/index.js.map +1 -0
- package/lib/slides/core/sectionHeading.d.ts +19 -0
- package/lib/slides/core/sectionHeading.d.ts.map +1 -0
- package/lib/slides/core/sectionHeading.js +19 -0
- package/lib/slides/core/sectionHeading.js.map +1 -0
- package/lib/slides/core/slideNavigation.d.ts +38 -0
- package/lib/slides/core/slideNavigation.d.ts.map +1 -0
- package/lib/slides/core/slideNavigation.js +65 -0
- package/lib/slides/core/slideNavigation.js.map +1 -0
- package/lib/slides/core/talk.d.ts +82 -0
- package/lib/slides/core/talk.d.ts.map +1 -0
- package/lib/slides/core/talk.js +134 -0
- package/lib/slides/core/talk.js.map +1 -0
- package/lib/slides/core/talkManifest.d.ts +90 -0
- package/lib/slides/core/talkManifest.d.ts.map +1 -0
- package/lib/slides/core/talkManifest.js +109 -0
- package/lib/slides/core/talkManifest.js.map +1 -0
- package/lib/slides/core/talkOrigin.d.ts +50 -0
- package/lib/slides/core/talkOrigin.d.ts.map +1 -0
- package/lib/slides/core/talkOrigin.js +62 -0
- package/lib/slides/core/talkOrigin.js.map +1 -0
- package/lib/slides/ui/BackToSlides.d.ts +26 -0
- package/lib/slides/ui/BackToSlides.d.ts.map +1 -0
- package/lib/slides/ui/BackToSlides.js +24 -0
- package/lib/slides/ui/BackToSlides.js.map +1 -0
- package/lib/slides/ui/DemoLink.d.ts +42 -0
- package/lib/slides/ui/DemoLink.d.ts.map +1 -0
- package/lib/slides/ui/DemoLink.js +17 -0
- package/lib/slides/ui/DemoLink.js.map +1 -0
- package/lib/slides/ui/EmbedSlide.d.ts +19 -0
- package/lib/slides/ui/EmbedSlide.d.ts.map +1 -0
- package/lib/slides/ui/EmbedSlide.js +56 -0
- package/lib/slides/ui/EmbedSlide.js.map +1 -0
- package/lib/slides/ui/SectionSlide.d.ts +18 -0
- package/lib/slides/ui/SectionSlide.d.ts.map +1 -0
- package/lib/slides/ui/SectionSlide.js +18 -0
- package/lib/slides/ui/SectionSlide.js.map +1 -0
- package/lib/slides/ui/Slide.d.ts +52 -0
- package/lib/slides/ui/Slide.d.ts.map +1 -0
- package/lib/slides/ui/Slide.js +58 -0
- package/lib/slides/ui/Slide.js.map +1 -0
- package/lib/slides/ui/SlideBody.d.ts +32 -0
- package/lib/slides/ui/SlideBody.d.ts.map +1 -0
- package/lib/slides/ui/SlideBody.js +27 -0
- package/lib/slides/ui/SlideBody.js.map +1 -0
- package/lib/slides/ui/Slideshow.d.ts +93 -0
- package/lib/slides/ui/Slideshow.d.ts.map +1 -0
- package/lib/slides/ui/Slideshow.js +122 -0
- package/lib/slides/ui/Slideshow.js.map +1 -0
- package/lib/slides/ui/SlideshowBar.d.ts +33 -0
- package/lib/slides/ui/SlideshowBar.d.ts.map +1 -0
- package/lib/slides/ui/SlideshowBar.js +20 -0
- package/lib/slides/ui/SlideshowBar.js.map +1 -0
- package/lib/slides/ui/TalkList.d.ts +37 -0
- package/lib/slides/ui/TalkList.d.ts.map +1 -0
- package/lib/slides/ui/TalkList.js +68 -0
- package/lib/slides/ui/TalkList.js.map +1 -0
- package/lib/slides/ui/index.d.ts +19 -0
- package/lib/slides/ui/index.d.ts.map +1 -0
- package/lib/slides/ui/index.js +10 -0
- package/lib/slides/ui/index.js.map +1 -0
- package/lib/slides/vite/index.d.ts +3 -0
- package/lib/slides/vite/index.d.ts.map +1 -0
- package/lib/slides/vite/index.js +2 -0
- package/lib/slides/vite/index.js.map +1 -0
- package/lib/slides/vite/talks.d.ts +50 -0
- package/lib/slides/vite/talks.d.ts.map +1 -0
- package/lib/slides/vite/talks.js +87 -0
- package/lib/slides/vite/talks.js.map +1 -0
- package/lib/slides.d.ts +3 -0
- package/lib/slides.d.ts.map +1 -0
- package/lib/slides.js +3 -0
- package/lib/slides.js.map +1 -0
- package/lib/structure/ui/StructureEditor.js +2 -2
- package/lib/structure/ui/StructureEditor.js.map +1 -1
- package/lib/structure/ui/StructurePlaceholder.js +1 -1
- package/lib/structure/ui/StructurePlaceholder.js.map +1 -1
- package/lib/tokens/core/checkTokens.d.ts +51 -0
- package/lib/tokens/core/checkTokens.d.ts.map +1 -0
- package/lib/tokens/core/checkTokens.js +188 -0
- package/lib/tokens/core/checkTokens.js.map +1 -0
- package/lib/tokens/core/colors.d.ts +42 -0
- package/lib/tokens/core/colors.d.ts.map +1 -0
- package/lib/tokens/core/colors.js +119 -0
- package/lib/tokens/core/colors.js.map +1 -0
- package/lib/tokens/core/index.d.ts +3 -0
- package/lib/tokens/core/index.d.ts.map +1 -0
- package/lib/tokens/core/index.js +2 -0
- package/lib/tokens/core/index.js.map +1 -0
- package/lib/tokens/core/rules.d.ts +23 -0
- package/lib/tokens/core/rules.d.ts.map +1 -0
- package/lib/tokens/core/rules.js +90 -0
- package/lib/tokens/core/rules.js.map +1 -0
- package/lib/ui.d.ts +1 -0
- package/lib/ui.d.ts.map +1 -1
- package/lib/ui.js +1 -0
- package/lib/ui.js.map +1 -1
- package/lib/vite.d.ts +1 -0
- package/lib/vite.d.ts.map +1 -1
- package/lib/vite.js +1 -0
- package/lib/vite.js.map +1 -1
- package/package.json +11 -3
- package/src/about/core/about.ts +176 -0
- package/src/about/core/index.ts +2 -0
- package/src/about/ui/AboutCitations.tsx +79 -0
- package/src/about/ui/AboutPage.tsx +177 -0
- package/src/about/ui/AboutSection.tsx +56 -0
- package/src/about/ui/index.ts +6 -0
- package/src/chrome/ui/index.ts +3 -0
- package/src/clipboard/ui/CodeBlock.tsx +4 -1
- package/src/color/ui/ColorScaleLegend.tsx +1 -1
- package/src/core.ts +5 -0
- package/src/credits/core/credits.ts +42 -0
- package/src/credits/ui/CreditsList.tsx +1 -1
- package/src/delimited/ui/DelimitedTextPanel.tsx +1 -1
- package/src/ecosystem/core/sites.ts +12 -0
- package/src/ecosystem/core/tokens.ts +1 -0
- package/src/ecosystem/ui/EcosystemLinks.tsx +2 -2
- package/src/ecosystem/ui/EcosystemMenu.tsx +1 -1
- package/src/ecosystem/ui/SiteTile.tsx +7 -5
- package/src/ecosystem/ui/glyphs.tsx +40 -0
- package/src/orbital/core/index.ts +4 -1
- package/src/orbital/core/realHarmonics.ts +5 -2
- package/src/orbital/ui/AtomicOrbitalCanvas.tsx +126 -4
- package/src/orbital/ui/AtomicOrbitalViewer.tsx +14 -1
- package/src/orbital/ui/camera.ts +81 -0
- package/src/orbital/ui/renderVolume.ts +8 -1
- package/src/orbital/ui/serial.ts +35 -0
- package/src/orbital/ui/viewer.ts +72 -13
- package/src/pedagogy/ui/ExerciseProgressHeader.tsx +1 -1
- package/src/pedagogy/ui/GlossaryText.tsx +2 -2
- package/src/pedagogy/ui/ReferenceSectionBlock.tsx +5 -2
- package/src/pedagogy/ui/SyntaxTooltip.tsx +2 -2
- package/src/pedagogy/ui/TutorialStepStrip.tsx +4 -1
- package/src/periodic/ui/CategoryLegend.tsx +1 -1
- package/src/periodic/ui/PeriodicTableChrome.tsx +2 -2
- package/src/seo/vite/ogCard.ts +1 -0
- package/src/share/ui/ShareDialogContent.tsx +5 -2
- package/src/share/ui/SharePartOptions.tsx +1 -1
- package/src/slides/core/demoLinks.ts +113 -0
- package/src/slides/core/index.ts +22 -0
- package/src/slides/core/sectionHeading.ts +29 -0
- package/src/slides/core/slideNavigation.ts +77 -0
- package/src/slides/core/talk.ts +203 -0
- package/src/slides/core/talkManifest.ts +158 -0
- package/src/slides/core/talkOrigin.ts +83 -0
- package/src/slides/ui/BackToSlides.tsx +49 -0
- package/src/slides/ui/DemoLink.tsx +67 -0
- package/src/slides/ui/EmbedSlide.tsx +85 -0
- package/src/slides/ui/SectionSlide.tsx +44 -0
- package/src/slides/ui/Slide.tsx +143 -0
- package/src/slides/ui/SlideBody.tsx +64 -0
- package/src/slides/ui/Slideshow.tsx +282 -0
- package/src/slides/ui/SlideshowBar.tsx +83 -0
- package/src/slides/ui/TalkList.tsx +146 -0
- package/src/slides/ui/index.ts +18 -0
- package/src/slides/vite/index.ts +2 -0
- package/src/slides/vite/talks.ts +126 -0
- package/src/slides.ts +2 -0
- package/src/structure/ui/StructureEditor.tsx +2 -2
- package/src/structure/ui/StructurePlaceholder.tsx +1 -1
- package/src/tokens/core/checkTokens.ts +242 -0
- package/src/tokens/core/colors.ts +122 -0
- package/src/tokens/core/index.ts +6 -0
- package/src/tokens/core/rules.ts +103 -0
- package/src/ui.ts +1 -0
- package/src/vite.ts +1 -0
- package/styles/chrome.css +1 -0
- package/styles/slides.css +667 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { ComponentType, ReactElement } from 'react';
|
|
2
|
+
import { Fragment } from 'react';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
Slide as SlideData,
|
|
6
|
+
TalkMeta,
|
|
7
|
+
TalkOrigin,
|
|
8
|
+
} from '../core/index.ts';
|
|
9
|
+
import { demoLabel, splitDemoLinks } from '../core/index.ts';
|
|
10
|
+
|
|
11
|
+
import type { RenderSlideLink } from './DemoLink.tsx';
|
|
12
|
+
import { DemoLink } from './DemoLink.tsx';
|
|
13
|
+
import { EmbedSlide } from './EmbedSlide.tsx';
|
|
14
|
+
import { SectionSlide } from './SectionSlide.tsx';
|
|
15
|
+
import { SlideBody } from './SlideBody.tsx';
|
|
16
|
+
|
|
17
|
+
/** What every layout — the generic ones and a site's own — is handed. */
|
|
18
|
+
export interface SlideLayoutProps {
|
|
19
|
+
/** The slide to draw. */
|
|
20
|
+
slide: SlideData;
|
|
21
|
+
/** How the publishing site names the talk in its addresses. */
|
|
22
|
+
talkId: string;
|
|
23
|
+
/** Which slide of the talk this is, zero-based. */
|
|
24
|
+
slideIndex: number;
|
|
25
|
+
/**
|
|
26
|
+
* Which site published the deck, written into the way back.
|
|
27
|
+
* @default undefined — the site playing it
|
|
28
|
+
*/
|
|
29
|
+
site?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Where the publishing site lives, which a relative address in the deck is
|
|
32
|
+
* resolved against.
|
|
33
|
+
* @default undefined — the addresses are this site's own
|
|
34
|
+
*/
|
|
35
|
+
talkOrigin?: string;
|
|
36
|
+
/** What the talk's front-matter said, for a layout that draws from it. */
|
|
37
|
+
meta: TalkMeta;
|
|
38
|
+
/**
|
|
39
|
+
* The site's own link, used for the in-app links of the slide.
|
|
40
|
+
* @default undefined — a plain anchor
|
|
41
|
+
*/
|
|
42
|
+
renderLink?: RenderSlideLink;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A slide, and the layouts available to draw it. */
|
|
46
|
+
export interface SlideViewProps extends SlideLayoutProps {
|
|
47
|
+
/**
|
|
48
|
+
* The layouts this site adds, by the name a slide writes in its
|
|
49
|
+
* `<!-- layout: … -->` marker. An entry here also replaces a generic layout
|
|
50
|
+
* of the same name.
|
|
51
|
+
* @default undefined
|
|
52
|
+
*/
|
|
53
|
+
layouts?: Record<string, ComponentType<SlideLayoutProps>>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Draw one slide with the layout it asks for.
|
|
58
|
+
*
|
|
59
|
+
* The layout name is not a closed list — a deck is written before the site
|
|
60
|
+
* knows how to draw it — so a name neither the site nor the player recognises
|
|
61
|
+
* falls back to the plain content layout. A talk must never fail to open
|
|
62
|
+
* because one of its slides asks for something that is not there yet.
|
|
63
|
+
* @param props - See {@link SlideViewProps}.
|
|
64
|
+
* @returns The slide.
|
|
65
|
+
*/
|
|
66
|
+
export function SlideView(props: SlideViewProps): ReactElement {
|
|
67
|
+
const { layouts, ...layoutProps } = props;
|
|
68
|
+
const { slide } = layoutProps;
|
|
69
|
+
|
|
70
|
+
const Custom = layouts?.[slide.layout];
|
|
71
|
+
if (Custom !== undefined) return <Custom {...layoutProps} />;
|
|
72
|
+
if (slide.layout === 'section') return <SectionSlide body={slide.body} />;
|
|
73
|
+
if (slide.layout === 'embed') return <EmbedSlide slide={slide} />;
|
|
74
|
+
|
|
75
|
+
return <GenericSlide {...layoutProps} />;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The layouts the player draws itself, for any deck on any site. */
|
|
79
|
+
const GENERIC_LAYOUTS = new Set(['content', 'title', 'quote', 'thanks']);
|
|
80
|
+
|
|
81
|
+
function GenericSlide(props: SlideLayoutProps): ReactElement {
|
|
82
|
+
const { slide, talkId, slideIndex, meta, renderLink, site, talkOrigin } =
|
|
83
|
+
props;
|
|
84
|
+
const layout = GENERIC_LAYOUTS.has(slide.layout) ? slide.layout : 'content';
|
|
85
|
+
const { body, demos } = splitDemoLinks(slide.body);
|
|
86
|
+
const origin: TalkOrigin =
|
|
87
|
+
site === undefined
|
|
88
|
+
? { talkId, slide: slideIndex }
|
|
89
|
+
: { talkId, slide: slideIndex, site };
|
|
90
|
+
const logos = layout === 'title' ? (meta.logos ?? []) : [];
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className={`slide slide--${layout}`}>
|
|
94
|
+
<div className="slide-content">
|
|
95
|
+
<SlideBody body={body} origin={origin} renderLink={renderLink} />
|
|
96
|
+
{logos.length === 0 ? null : (
|
|
97
|
+
<div className="slide-logos">
|
|
98
|
+
{logos.map((logo) => (
|
|
99
|
+
<img key={logo} className="slide-logo" src={logo} alt="" />
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
{demos.length === 0 ? null : (
|
|
104
|
+
<div className="slide-demos">
|
|
105
|
+
<span className="slide-demos-label">Live demos:</span>
|
|
106
|
+
{demos.map((demo, index) => (
|
|
107
|
+
<Fragment key={demo.href}>
|
|
108
|
+
{index === 0 ? null : (
|
|
109
|
+
<span className="slide-demos-separator" aria-hidden="true">
|
|
110
|
+
|
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
<DemoLink
|
|
114
|
+
href={resolveDemoHref(demo.href, talkOrigin)}
|
|
115
|
+
origin={origin}
|
|
116
|
+
renderLink={renderLink}
|
|
117
|
+
>
|
|
118
|
+
{demoLabel(demo.label)}
|
|
119
|
+
</DemoLink>
|
|
120
|
+
</Fragment>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Where a demo link actually points.
|
|
131
|
+
*
|
|
132
|
+
* A deck writes `/mf-finder?mass=300`, meaning its own site. Played there it is
|
|
133
|
+
* exactly that; played on another site of the family it has to be resolved
|
|
134
|
+
* against the site that wrote it, or the link lands on a page that does not
|
|
135
|
+
* exist.
|
|
136
|
+
* @param href - The address the slide wrote.
|
|
137
|
+
* @param talkOrigin - Where the publishing site lives, when it is not this one.
|
|
138
|
+
* @returns The address to open.
|
|
139
|
+
*/
|
|
140
|
+
function resolveDemoHref(href: string, talkOrigin?: string): string {
|
|
141
|
+
if (talkOrigin === undefined || !href.startsWith('/')) return href;
|
|
142
|
+
return `${talkOrigin.replace(/\/$/, '')}${href}`;
|
|
143
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import Markdown from 'react-markdown';
|
|
3
|
+
import rehypeRaw from 'rehype-raw';
|
|
4
|
+
|
|
5
|
+
import type { TalkOrigin } from '../core/index.ts';
|
|
6
|
+
|
|
7
|
+
import type { RenderSlideLink } from './DemoLink.tsx';
|
|
8
|
+
import { DemoLink } from './DemoLink.tsx';
|
|
9
|
+
|
|
10
|
+
/** The Markdown of a slide, and where its links go. */
|
|
11
|
+
export interface SlideBodyProps {
|
|
12
|
+
/** The Markdown source of the slide, or of one part of it. */
|
|
13
|
+
body: string;
|
|
14
|
+
/**
|
|
15
|
+
* Which slide the body belongs to, carried by every in-app link it holds so
|
|
16
|
+
* the tool a demo opens can offer the way back.
|
|
17
|
+
* @default undefined
|
|
18
|
+
*/
|
|
19
|
+
origin?: TalkOrigin;
|
|
20
|
+
/**
|
|
21
|
+
* The site's own link, used for the in-app links of the body.
|
|
22
|
+
* @default undefined — a plain anchor
|
|
23
|
+
*/
|
|
24
|
+
renderLink?: RenderSlideLink;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Render a slide's Markdown.
|
|
29
|
+
*
|
|
30
|
+
* A talk is written as prose, so raw HTML in the source is kept: a slide
|
|
31
|
+
* routinely holds a `<sub>`, a `<br>` or a figure the Markdown syntax cannot
|
|
32
|
+
* express. A link to an in-app route becomes a demo link; every other link
|
|
33
|
+
* leaves the deck in a tab of its own, so a click during a talk never loses the
|
|
34
|
+
* slide it was made from.
|
|
35
|
+
* @param props - See {@link SlideBodyProps}.
|
|
36
|
+
* @returns The rendered body.
|
|
37
|
+
*/
|
|
38
|
+
export function SlideBody(props: SlideBodyProps): ReactElement {
|
|
39
|
+
const { body, origin, renderLink } = props;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Markdown
|
|
43
|
+
rehypePlugins={[rehypeRaw]}
|
|
44
|
+
components={{
|
|
45
|
+
a({ href, children }) {
|
|
46
|
+
if (href?.startsWith('/') === true) {
|
|
47
|
+
return (
|
|
48
|
+
<DemoLink href={href} origin={origin} renderLink={renderLink}>
|
|
49
|
+
{children}
|
|
50
|
+
</DemoLink>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return (
|
|
54
|
+
<a href={href} target="_blank" rel="noreferrer">
|
|
55
|
+
{children}
|
|
56
|
+
</a>
|
|
57
|
+
);
|
|
58
|
+
},
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
{body}
|
|
62
|
+
</Markdown>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CSSProperties,
|
|
3
|
+
ComponentType,
|
|
4
|
+
MouseEvent,
|
|
5
|
+
ReactElement,
|
|
6
|
+
} from 'react';
|
|
7
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { useContainerSize } from '../../hooks/ui/useContainerSize.ts';
|
|
10
|
+
import type { Talk } from '../core/index.ts';
|
|
11
|
+
import { clampSlideIndex, slideActionForKey } from '../core/index.ts';
|
|
12
|
+
|
|
13
|
+
import type { RenderSlideLink } from './DemoLink.tsx';
|
|
14
|
+
import type { SlideLayoutProps } from './Slide.tsx';
|
|
15
|
+
import { SlideView } from './Slide.tsx';
|
|
16
|
+
import { SlideshowBar } from './SlideshowBar.tsx';
|
|
17
|
+
|
|
18
|
+
/** The talk being presented, and everything the site keeps a hand on. */
|
|
19
|
+
export interface SlideshowProps {
|
|
20
|
+
/** The parsed talk. */
|
|
21
|
+
talk: Talk;
|
|
22
|
+
/** Which slide is showing, zero-based; clamped into the talk before drawing. */
|
|
23
|
+
index: number;
|
|
24
|
+
/** Called with the slide to show; the site writes it wherever it keeps it. */
|
|
25
|
+
onIndex: (index: number) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Called when the presenter leaves the talk. The bar carries no Exit button
|
|
28
|
+
* when it is left out.
|
|
29
|
+
* @default undefined
|
|
30
|
+
*/
|
|
31
|
+
onExit?: () => void;
|
|
32
|
+
/**
|
|
33
|
+
* The layouts this site adds, by the name a slide writes in its
|
|
34
|
+
* `<!-- layout: … -->` marker.
|
|
35
|
+
* @default undefined
|
|
36
|
+
*/
|
|
37
|
+
layouts?: Record<string, ComponentType<SlideLayoutProps>>;
|
|
38
|
+
/**
|
|
39
|
+
* The site's own link, used for the in-app links the slides hold.
|
|
40
|
+
* @default undefined — a plain anchor
|
|
41
|
+
*/
|
|
42
|
+
renderLink?: RenderSlideLink;
|
|
43
|
+
/**
|
|
44
|
+
* How the publishing site names this talk in its addresses, carried by every
|
|
45
|
+
* demo link so the tool it opens can offer the way back.
|
|
46
|
+
* @default '' — the demo links carry no talk
|
|
47
|
+
*/
|
|
48
|
+
talkId?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Which site published the deck. A deck played somewhere else — as
|
|
51
|
+
* learn.cheminfo.org plays the family's — says so in the way back, so the
|
|
52
|
+
* link returns to the player it came from rather than to this one.
|
|
53
|
+
* @default undefined — the deck belongs to the site playing it
|
|
54
|
+
*/
|
|
55
|
+
site?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Where the publishing site lives, e.g. `https://www.chemcalc.org`. A deck
|
|
58
|
+
* written for one site holds addresses relative to it, so playing it
|
|
59
|
+
* elsewhere has to resolve them against their own origin rather than against
|
|
60
|
+
* the page.
|
|
61
|
+
* @default undefined — the addresses are this site's own
|
|
62
|
+
*/
|
|
63
|
+
talkOrigin?: string;
|
|
64
|
+
/**
|
|
65
|
+
* What the bar reads.
|
|
66
|
+
* @default the talk's own title
|
|
67
|
+
*/
|
|
68
|
+
title?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Class the player carries, in addition to `slideshow`.
|
|
71
|
+
* @default undefined
|
|
72
|
+
*/
|
|
73
|
+
className?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Fullscreen owned by the site rather than by the player.
|
|
76
|
+
*
|
|
77
|
+
* A deck that links into the live tool wants the whole app fullscreen, not
|
|
78
|
+
* the player: the player unmounts on the way to the demo, and the browser
|
|
79
|
+
* would leave fullscreen with it. A site in that shape passes what it
|
|
80
|
+
* fullscreens and how to toggle it, and the player's key and button drive
|
|
81
|
+
* that instead of their own element.
|
|
82
|
+
* @default undefined — the player fullscreens itself
|
|
83
|
+
*/
|
|
84
|
+
fullscreen?: {
|
|
85
|
+
/** Whether the site is presenting. */
|
|
86
|
+
isFullscreen: boolean;
|
|
87
|
+
/** Called when `f` or the bar's button asks to change that. */
|
|
88
|
+
onToggle: () => void;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Slides are authored on this canvas, and it is scaled to whatever it is played on. */
|
|
93
|
+
const SLIDE_WIDTH = 1920;
|
|
94
|
+
const SLIDE_HEIGHT = 1080;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The conference player: one talk, one slide at a time, on a canvas scaled to
|
|
98
|
+
* fill whatever screen it is given.
|
|
99
|
+
*
|
|
100
|
+
* The index is controlled, because the site owns the address — the slide lives
|
|
101
|
+
* in a query parameter so a demo link can return to the exact one — and the
|
|
102
|
+
* player therefore never touches history itself.
|
|
103
|
+
*
|
|
104
|
+
* Arrow keys, space and PageUp/PageDown navigate, `Home` and `End` jump to the
|
|
105
|
+
* ends, `f` toggles fullscreen, `b` and `w` blank the screen to black and to
|
|
106
|
+
* white the way a projector remote does, and `n` shows the presenter's notes.
|
|
107
|
+
* @param props - See {@link SlideshowProps}.
|
|
108
|
+
* @returns The player.
|
|
109
|
+
*/
|
|
110
|
+
export function Slideshow(props: SlideshowProps): ReactElement {
|
|
111
|
+
const {
|
|
112
|
+
talk,
|
|
113
|
+
index,
|
|
114
|
+
onIndex,
|
|
115
|
+
onExit,
|
|
116
|
+
layouts,
|
|
117
|
+
renderLink,
|
|
118
|
+
talkId = '',
|
|
119
|
+
site,
|
|
120
|
+
talkOrigin,
|
|
121
|
+
fullscreen,
|
|
122
|
+
title,
|
|
123
|
+
className,
|
|
124
|
+
} = props;
|
|
125
|
+
|
|
126
|
+
const total = talk.slides.length;
|
|
127
|
+
const current = clampSlideIndex(index, total);
|
|
128
|
+
const slide = talk.slides[current];
|
|
129
|
+
|
|
130
|
+
const playerRef = useRef<HTMLDivElement>(null);
|
|
131
|
+
const stageRef = useRef<HTMLDivElement>(null);
|
|
132
|
+
const { width, height } = useContainerSize(stageRef);
|
|
133
|
+
const scale =
|
|
134
|
+
width === 0 || height === 0
|
|
135
|
+
? 1
|
|
136
|
+
: Math.min(width / SLIDE_WIDTH, height / SLIDE_HEIGHT);
|
|
137
|
+
|
|
138
|
+
const [blank, setBlank] = useState<'black' | 'white' | null>(null);
|
|
139
|
+
const [showNotes, setShowNotes] = useState(false);
|
|
140
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
141
|
+
|
|
142
|
+
const goTo = useCallback(
|
|
143
|
+
(next: number) => {
|
|
144
|
+
setBlank(null);
|
|
145
|
+
onIndex(clampSlideIndex(next, total));
|
|
146
|
+
},
|
|
147
|
+
[onIndex, total],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const ownFullscreen = fullscreen === undefined;
|
|
151
|
+
const toggleOwn = useCallback(() => {
|
|
152
|
+
const element = playerRef.current;
|
|
153
|
+
if (element === null) return;
|
|
154
|
+
if (document.fullscreenElement === element) {
|
|
155
|
+
void document.exitFullscreen().catch(() => undefined);
|
|
156
|
+
} else {
|
|
157
|
+
void element.requestFullscreen().catch(() => undefined);
|
|
158
|
+
}
|
|
159
|
+
}, []);
|
|
160
|
+
const toggleFullscreen = fullscreen?.onToggle ?? toggleOwn;
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!ownFullscreen) return;
|
|
164
|
+
function onFullscreenChange(): void {
|
|
165
|
+
setIsFullscreen(document.fullscreenElement === playerRef.current);
|
|
166
|
+
}
|
|
167
|
+
document.addEventListener('fullscreenchange', onFullscreenChange);
|
|
168
|
+
return () => {
|
|
169
|
+
document.removeEventListener('fullscreenchange', onFullscreenChange);
|
|
170
|
+
};
|
|
171
|
+
}, [ownFullscreen]);
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
function onKeyDown(event: KeyboardEvent): void {
|
|
175
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
176
|
+
// A player embedded in a page shares the document with its fields, and
|
|
177
|
+
// space is both "next slide" and a character.
|
|
178
|
+
if (isTextEntry(event.target)) return;
|
|
179
|
+
const action = slideActionForKey(event.key, current, total);
|
|
180
|
+
if (action === null) return;
|
|
181
|
+
event.preventDefault();
|
|
182
|
+
switch (action.kind) {
|
|
183
|
+
case 'go':
|
|
184
|
+
goTo(action.index);
|
|
185
|
+
break;
|
|
186
|
+
case 'blank':
|
|
187
|
+
setBlank((shown) => (shown === action.color ? null : action.color));
|
|
188
|
+
break;
|
|
189
|
+
case 'fullscreen':
|
|
190
|
+
toggleFullscreen();
|
|
191
|
+
break;
|
|
192
|
+
case 'notes':
|
|
193
|
+
setShowNotes((shown) => !shown);
|
|
194
|
+
break;
|
|
195
|
+
default:
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
document.addEventListener('keydown', onKeyDown);
|
|
200
|
+
return () => {
|
|
201
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
202
|
+
};
|
|
203
|
+
}, [current, total, goTo, toggleFullscreen]);
|
|
204
|
+
|
|
205
|
+
function onStageClick(event: MouseEvent<HTMLDivElement>): void {
|
|
206
|
+
// A demo link or a control handles its own click without also stepping on.
|
|
207
|
+
const target = event.target as Element | null;
|
|
208
|
+
if (target !== null && target.closest('a, button') !== null) return;
|
|
209
|
+
goTo(current + 1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const progress = total > 1 ? (current / (total - 1)) * 100 : 100;
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div
|
|
216
|
+
ref={playerRef}
|
|
217
|
+
className={
|
|
218
|
+
className === undefined ? 'slideshow' : `slideshow ${className}`
|
|
219
|
+
}
|
|
220
|
+
>
|
|
221
|
+
{/* Clicking the stage steps on, the way a projector remote does; the
|
|
222
|
+
keyboard equivalent is the arrow keys the player already listens for. */}
|
|
223
|
+
<div className="slideshow-stage" ref={stageRef} onClick={onStageClick}>
|
|
224
|
+
<div className="slide-canvas" style={{ transform: `scale(${scale})` }}>
|
|
225
|
+
{slide === undefined ? null : (
|
|
226
|
+
// Keyed on the index so an animated slide replays from its start
|
|
227
|
+
// every time it is navigated to.
|
|
228
|
+
<SlideView
|
|
229
|
+
key={current}
|
|
230
|
+
slide={slide}
|
|
231
|
+
talkId={talkId}
|
|
232
|
+
site={site}
|
|
233
|
+
talkOrigin={talkOrigin}
|
|
234
|
+
slideIndex={current}
|
|
235
|
+
meta={talk.meta}
|
|
236
|
+
layouts={layouts}
|
|
237
|
+
renderLink={renderLink}
|
|
238
|
+
/>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
{blank === null ? null : (
|
|
242
|
+
// Clicking the blank screen brings the slide back, as does pressing
|
|
243
|
+
// the key that blanked it.
|
|
244
|
+
<div
|
|
245
|
+
className={`slideshow-blank slideshow-blank--${blank}`}
|
|
246
|
+
onClick={(event) => {
|
|
247
|
+
event.stopPropagation();
|
|
248
|
+
setBlank(null);
|
|
249
|
+
}}
|
|
250
|
+
/>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
{showNotes && slide?.notes !== undefined ? (
|
|
254
|
+
<div className="slideshow-notes no-print">{slide.notes}</div>
|
|
255
|
+
) : null}
|
|
256
|
+
<SlideshowBar
|
|
257
|
+
title={title ?? talk.meta.title}
|
|
258
|
+
index={current}
|
|
259
|
+
total={total}
|
|
260
|
+
isFullscreen={fullscreen?.isFullscreen ?? isFullscreen}
|
|
261
|
+
onGo={goTo}
|
|
262
|
+
onToggleFullscreen={toggleFullscreen}
|
|
263
|
+
onExit={onExit}
|
|
264
|
+
/>
|
|
265
|
+
<div
|
|
266
|
+
className="slideshow-progress"
|
|
267
|
+
style={{ '--progress': `${progress}%` } as CSSProperties}
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const TEXT_ENTRY_TAGS = new Set(['INPUT', 'TEXTAREA', 'SELECT']);
|
|
274
|
+
|
|
275
|
+
function isTextEntry(target: EventTarget | null): boolean {
|
|
276
|
+
if (target === null) return false;
|
|
277
|
+
const element = target as { tagName?: unknown; isContentEditable?: unknown };
|
|
278
|
+
if (element.isContentEditable === true) return true;
|
|
279
|
+
return (
|
|
280
|
+
typeof element.tagName === 'string' && TEXT_ENTRY_TAGS.has(element.tagName)
|
|
281
|
+
);
|
|
282
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Button } from '@blueprintjs/core';
|
|
2
|
+
import type { ReactElement } from 'react';
|
|
3
|
+
|
|
4
|
+
/** Where the talk stands, and what the presenter may do about it. */
|
|
5
|
+
export interface SlideshowBarProps {
|
|
6
|
+
/** What the bar reads. */
|
|
7
|
+
title: string;
|
|
8
|
+
/** Which slide is showing, zero-based. */
|
|
9
|
+
index: number;
|
|
10
|
+
/** How many slides the talk holds. */
|
|
11
|
+
total: number;
|
|
12
|
+
/** Whether the player is presenting on the whole screen. */
|
|
13
|
+
isFullscreen: boolean;
|
|
14
|
+
/** Called with the slide to show. */
|
|
15
|
+
onGo: (index: number) => void;
|
|
16
|
+
/** Called to enter or leave fullscreen. */
|
|
17
|
+
onToggleFullscreen: () => void;
|
|
18
|
+
/**
|
|
19
|
+
* Called when the presenter leaves the talk. The bar carries no Exit button
|
|
20
|
+
* when it is left out.
|
|
21
|
+
* @default undefined
|
|
22
|
+
*/
|
|
23
|
+
onExit?: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The bar under the stage: the way out, the talk's name, and where in it the
|
|
28
|
+
* presenter stands.
|
|
29
|
+
*
|
|
30
|
+
* It is chrome rather than the talk, so it is dropped from a printed handout
|
|
31
|
+
* along with the rest of the site's.
|
|
32
|
+
* @param props - See {@link SlideshowBarProps}.
|
|
33
|
+
* @returns The bar.
|
|
34
|
+
*/
|
|
35
|
+
export function SlideshowBar(props: SlideshowBarProps): ReactElement {
|
|
36
|
+
const {
|
|
37
|
+
title,
|
|
38
|
+
index,
|
|
39
|
+
total,
|
|
40
|
+
isFullscreen,
|
|
41
|
+
onGo,
|
|
42
|
+
onToggleFullscreen,
|
|
43
|
+
onExit,
|
|
44
|
+
} = props;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="slideshow-bar no-print">
|
|
48
|
+
{onExit === undefined ? null : (
|
|
49
|
+
<Button variant="minimal" icon="cross" text="Exit" onClick={onExit} />
|
|
50
|
+
)}
|
|
51
|
+
<div className="slideshow-title">{title}</div>
|
|
52
|
+
<div className="slideshow-controls">
|
|
53
|
+
<Button
|
|
54
|
+
variant="minimal"
|
|
55
|
+
icon="chevron-left"
|
|
56
|
+
aria-label="Previous slide"
|
|
57
|
+
disabled={index === 0}
|
|
58
|
+
onClick={() => {
|
|
59
|
+
onGo(index - 1);
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
<span className="slideshow-counter">
|
|
63
|
+
{`${total === 0 ? 0 : index + 1} / ${total}`}
|
|
64
|
+
</span>
|
|
65
|
+
<Button
|
|
66
|
+
variant="minimal"
|
|
67
|
+
icon="chevron-right"
|
|
68
|
+
aria-label="Next slide"
|
|
69
|
+
disabled={index >= total - 1}
|
|
70
|
+
onClick={() => {
|
|
71
|
+
onGo(index + 1);
|
|
72
|
+
}}
|
|
73
|
+
/>
|
|
74
|
+
<Button
|
|
75
|
+
variant="minimal"
|
|
76
|
+
icon={isFullscreen ? 'minimize' : 'fullscreen'}
|
|
77
|
+
aria-label="Toggle fullscreen"
|
|
78
|
+
onClick={onToggleFullscreen}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|