react-cheminfo 0.7.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/citation/core/index.d.ts +1 -0
- package/lib/citation/core/index.d.ts.map +1 -1
- package/lib/citation/core/index.js +1 -0
- package/lib/citation/core/index.js.map +1 -1
- package/lib/citation/core/teachingPaper.d.ts +15 -0
- package/lib/citation/core/teachingPaper.d.ts.map +1 -0
- package/lib/citation/core/teachingPaper.js +32 -0
- package/lib/citation/core/teachingPaper.js.map +1 -0
- 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.d.ts +15 -2
- package/lib/pedagogy/ui/TutorialStepStrip.d.ts.map +1 -1
- package/lib/pedagogy/ui/TutorialStepStrip.js +19 -5
- package/lib/pedagogy/ui/TutorialStepStrip.js.map +1 -1
- package/lib/pedagogy/ui/index.d.ts +1 -1
- package/lib/pedagogy/ui/index.d.ts.map +1 -1
- package/lib/pedagogy/ui/index.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/citation/core/index.ts +1 -0
- package/src/citation/core/teachingPaper.ts +36 -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 +39 -10
- package/src/pedagogy/ui/index.ts +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,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
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
|
|
3
|
+
import { isModifiedClick } from '../../chrome/ui/navItem.ts';
|
|
4
|
+
import { pluralize } from '../../format/core/index.ts';
|
|
5
|
+
import type { TalkManifest, TalkSummary } from '../core/index.ts';
|
|
6
|
+
|
|
7
|
+
/** The talks to list, and what opening one means. */
|
|
8
|
+
export interface TalkListProps {
|
|
9
|
+
/** One manifest per publishing site. */
|
|
10
|
+
manifests: readonly TalkManifest[];
|
|
11
|
+
/** Called when a talk is picked. */
|
|
12
|
+
onOpen: (manifest: TalkManifest, talk: TalkSummary) => void;
|
|
13
|
+
/**
|
|
14
|
+
* The address a talk sits at, which keeps the card a real link — a middle
|
|
15
|
+
* click opens a tab of its own, and a crawler walks the list.
|
|
16
|
+
* @default undefined — the card is a link with no address
|
|
17
|
+
*/
|
|
18
|
+
renderHref?: (manifest: TalkManifest, talk: TalkSummary) => string;
|
|
19
|
+
/**
|
|
20
|
+
* How a site id is written above its talks.
|
|
21
|
+
* @default the id itself
|
|
22
|
+
*/
|
|
23
|
+
siteName?: (site: string) => string;
|
|
24
|
+
/**
|
|
25
|
+
* What an empty list says. A list that draws nothing at all reads as a
|
|
26
|
+
* broken page rather than as a site that has published no deck yet.
|
|
27
|
+
* @default 'No talk yet.'
|
|
28
|
+
*/
|
|
29
|
+
emptyNote?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The talks on offer: a site's own, or the whole family's.
|
|
34
|
+
*
|
|
35
|
+
* The site heading appears only once there is more than one site to tell
|
|
36
|
+
* apart, so the same component draws a single site's `/talks` page and the
|
|
37
|
+
* family-wide listing without either looking like the other one's leftovers.
|
|
38
|
+
* @param props - See {@link TalkListProps}.
|
|
39
|
+
* @returns The listing.
|
|
40
|
+
*/
|
|
41
|
+
export function TalkList(props: TalkListProps): ReactElement {
|
|
42
|
+
const {
|
|
43
|
+
manifests,
|
|
44
|
+
onOpen,
|
|
45
|
+
renderHref,
|
|
46
|
+
siteName,
|
|
47
|
+
emptyNote = 'No talk yet.',
|
|
48
|
+
} = props;
|
|
49
|
+
const groups = groupBySite(manifests);
|
|
50
|
+
const showSite = groups.length > 1;
|
|
51
|
+
|
|
52
|
+
if (groups.length === 0) {
|
|
53
|
+
return <p className="talk-list-empty">{emptyNote}</p>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="talk-list">
|
|
58
|
+
{groups.map((group) => (
|
|
59
|
+
<section key={group.site} className="talk-list-group">
|
|
60
|
+
{showSite ? (
|
|
61
|
+
<h2 className="talk-list-site">
|
|
62
|
+
{siteName === undefined ? group.site : siteName(group.site)}
|
|
63
|
+
</h2>
|
|
64
|
+
) : null}
|
|
65
|
+
<div className="talk-grid">
|
|
66
|
+
{group.entries.map(({ manifest, talk }) => (
|
|
67
|
+
<a
|
|
68
|
+
key={`${manifest.site}:${talk.id}`}
|
|
69
|
+
className="talk-card"
|
|
70
|
+
href={renderHref?.(manifest, talk)}
|
|
71
|
+
onClick={(event) => {
|
|
72
|
+
if (isModifiedClick(event)) return;
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
onOpen(manifest, talk);
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<div className="talk-card-title">{talk.title}</div>
|
|
78
|
+
{talk.subtitle === undefined ? null : (
|
|
79
|
+
<div className="talk-card-subtitle">{talk.subtitle}</div>
|
|
80
|
+
)}
|
|
81
|
+
<div className="talk-card-meta">{occasion(talk)}</div>
|
|
82
|
+
<div className="talk-card-count">
|
|
83
|
+
{`${talk.slideCount} ${pluralize(talk.slideCount, 'slide')}`}
|
|
84
|
+
</div>
|
|
85
|
+
</a>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
</section>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** One talk, together with the manifest that published it. */
|
|
95
|
+
interface TalkEntry {
|
|
96
|
+
/** The manifest the talk was listed in. */
|
|
97
|
+
manifest: TalkManifest;
|
|
98
|
+
/** The talk itself. */
|
|
99
|
+
talk: TalkSummary;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Every talk one site published, most recent first. */
|
|
103
|
+
interface TalkGroup {
|
|
104
|
+
/** The `ECOSYSTEM_SITES` id of the publishing site. */
|
|
105
|
+
site: string;
|
|
106
|
+
/** Its talks. */
|
|
107
|
+
entries: TalkEntry[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function groupBySite(manifests: readonly TalkManifest[]): TalkGroup[] {
|
|
111
|
+
const bySite = new Map<string, TalkEntry[]>();
|
|
112
|
+
for (const manifest of manifests) {
|
|
113
|
+
let entries = bySite.get(manifest.site);
|
|
114
|
+
if (entries === undefined) {
|
|
115
|
+
entries = [];
|
|
116
|
+
bySite.set(manifest.site, entries);
|
|
117
|
+
}
|
|
118
|
+
for (const talk of manifest.talks) entries.push({ manifest, talk });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const groups: TalkGroup[] = [];
|
|
122
|
+
for (const [site, entries] of bySite) {
|
|
123
|
+
// A site that has published nothing is not a heading over an empty row.
|
|
124
|
+
if (entries.length === 0) continue;
|
|
125
|
+
groups.push({ site, entries: entries.toSorted(byMostRecent) });
|
|
126
|
+
}
|
|
127
|
+
return groups;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The dates are ISO, so they order as text; a talk with no date sorts last
|
|
131
|
+
// rather than ahead of every dated one.
|
|
132
|
+
function byMostRecent(left: TalkEntry, right: TalkEntry): number {
|
|
133
|
+
const before = left.talk.date ?? '';
|
|
134
|
+
const after = right.talk.date ?? '';
|
|
135
|
+
if (before === after) return 0;
|
|
136
|
+
if (before === '') return 1;
|
|
137
|
+
if (after === '') return -1;
|
|
138
|
+
return before > after ? -1 : 1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function occasion(talk: TalkSummary): string {
|
|
142
|
+
const parts: string[] = [];
|
|
143
|
+
if (talk.event !== undefined) parts.push(talk.event);
|
|
144
|
+
if (talk.date !== undefined) parts.push(talk.date);
|
|
145
|
+
return parts.join(' · ');
|
|
146
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type { BackToSlidesProps } from './BackToSlides.tsx';
|
|
2
|
+
export { BackToSlides } from './BackToSlides.tsx';
|
|
3
|
+
export type { DemoLinkProps, RenderSlideLink } from './DemoLink.tsx';
|
|
4
|
+
export { DemoLink } from './DemoLink.tsx';
|
|
5
|
+
export type { EmbedSlideProps } from './EmbedSlide.tsx';
|
|
6
|
+
export { EmbedSlide } from './EmbedSlide.tsx';
|
|
7
|
+
export type { SectionSlideProps } from './SectionSlide.tsx';
|
|
8
|
+
export { SectionSlide } from './SectionSlide.tsx';
|
|
9
|
+
export type { SlideLayoutProps, SlideViewProps } from './Slide.tsx';
|
|
10
|
+
export { SlideView } from './Slide.tsx';
|
|
11
|
+
export type { SlideBodyProps } from './SlideBody.tsx';
|
|
12
|
+
export { SlideBody } from './SlideBody.tsx';
|
|
13
|
+
export type { SlideshowProps } from './Slideshow.tsx';
|
|
14
|
+
export { Slideshow } from './Slideshow.tsx';
|
|
15
|
+
export type { SlideshowBarProps } from './SlideshowBar.tsx';
|
|
16
|
+
export { SlideshowBar } from './SlideshowBar.tsx';
|
|
17
|
+
export type { TalkListProps } from './TalkList.tsx';
|
|
18
|
+
export { TalkList } from './TalkList.tsx';
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publish a site's talks so another site can list and play them.
|
|
3
|
+
*
|
|
4
|
+
* A deck is Markdown in the repository of the site whose subject it teaches,
|
|
5
|
+
* which is what keeps a talk reviewable. For learn.cheminfo.org to gather the
|
|
6
|
+
* family's talks it needs two static files per site and no API: a manifest of
|
|
7
|
+
* what exists, and the source of each deck. Both are written at build time and
|
|
8
|
+
* served with permissive CORS, because public teaching material is fetched from
|
|
9
|
+
* a page that is not ours.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { join, resolve } from 'node:path';
|
|
14
|
+
|
|
15
|
+
import type { Plugin } from 'vite';
|
|
16
|
+
|
|
17
|
+
import { siteById, siteUrl } from '../../ecosystem/core/index.ts';
|
|
18
|
+
import type { SiteId } from '../../ecosystem/core/sites.ts';
|
|
19
|
+
import type { Talk } from '../core/talk.ts';
|
|
20
|
+
import { parseTalk } from '../core/talk.ts';
|
|
21
|
+
import { buildTalkManifest } from '../core/talkManifest.ts';
|
|
22
|
+
|
|
23
|
+
/** What the build needs to know to publish the decks. */
|
|
24
|
+
export interface TalksOptions {
|
|
25
|
+
/** The site the talks belong to. */
|
|
26
|
+
site: SiteId;
|
|
27
|
+
/**
|
|
28
|
+
* Where the sources live, relative to the project root.
|
|
29
|
+
* @default 'src/talks'
|
|
30
|
+
*/
|
|
31
|
+
dir?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Where the site is served, used as the manifest's origin.
|
|
34
|
+
* @default `https://<the site's host>`
|
|
35
|
+
*/
|
|
36
|
+
origin?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One talk on disk: its id, its source, and what the source parses into. */
|
|
40
|
+
export interface LoadedTalk {
|
|
41
|
+
/** The file name without extension, or the folder name for `<id>/index.md`. */
|
|
42
|
+
id: string;
|
|
43
|
+
/** The Markdown as written, which is what other sites fetch. */
|
|
44
|
+
source: string;
|
|
45
|
+
/** The parsed talk, for the manifest. */
|
|
46
|
+
talk: Talk;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Write `talks.json` and `talks/<id>.md` into the build.
|
|
51
|
+
* @param options - Which site, and where its decks are.
|
|
52
|
+
* @returns The Vite plugin.
|
|
53
|
+
*/
|
|
54
|
+
export function cheminfoTalks(options: TalksOptions): Plugin {
|
|
55
|
+
const { site, dir = 'src/talks' } = options;
|
|
56
|
+
let root = process.cwd();
|
|
57
|
+
let outDir = 'dist';
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
name: 'cheminfo-talks',
|
|
61
|
+
apply: 'build',
|
|
62
|
+
configResolved(config) {
|
|
63
|
+
root = config.root;
|
|
64
|
+
outDir = config.build.outDir;
|
|
65
|
+
},
|
|
66
|
+
closeBundle() {
|
|
67
|
+
const talks = loadTalksFromDisk(join(root, dir));
|
|
68
|
+
const origin =
|
|
69
|
+
options.origin ?? siteUrl(siteById(site)).replace(/\/$/, '');
|
|
70
|
+
const manifest = buildTalkManifest({
|
|
71
|
+
site,
|
|
72
|
+
origin,
|
|
73
|
+
talks: talks.map((entry) => ({ id: entry.id, ...entry.talk })),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const target = resolve(root, outDir);
|
|
77
|
+
mkdirSync(join(target, 'talks'), { recursive: true });
|
|
78
|
+
writeFileSync(
|
|
79
|
+
join(target, 'talks.json'),
|
|
80
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
81
|
+
);
|
|
82
|
+
for (const entry of talks) {
|
|
83
|
+
writeFileSync(join(target, 'talks', `${entry.id}.md`), entry.source);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read every deck of a directory, in either of the two shapes a talk takes.
|
|
91
|
+
* @param directory - Absolute path of the talks directory.
|
|
92
|
+
* @returns The talks, sorted by id so a build is reproducible.
|
|
93
|
+
*/
|
|
94
|
+
export function loadTalksFromDisk(directory: string): LoadedTalk[] {
|
|
95
|
+
const loaded: LoadedTalk[] = [];
|
|
96
|
+
let entries: string[] = [];
|
|
97
|
+
try {
|
|
98
|
+
entries = readdirSync(directory);
|
|
99
|
+
} catch {
|
|
100
|
+
// A site with no talks yet still builds; it simply publishes an empty list.
|
|
101
|
+
return loaded;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
const path = join(directory, entry);
|
|
106
|
+
if (entry.endsWith('.md')) {
|
|
107
|
+
const source = readFileSync(path, 'utf8');
|
|
108
|
+
loaded.push({
|
|
109
|
+
id: entry.slice(0, -3),
|
|
110
|
+
source,
|
|
111
|
+
talk: parseTalk(source),
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const index = join(path, 'index.md');
|
|
116
|
+
let source: string;
|
|
117
|
+
try {
|
|
118
|
+
source = readFileSync(index, 'utf8');
|
|
119
|
+
} catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
loaded.push({ id: entry, source, talk: parseTalk(source) });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return loaded.toSorted((left, right) => left.id.localeCompare(right.id));
|
|
126
|
+
}
|
package/src/slides.ts
ADDED
|
@@ -179,7 +179,7 @@ const ROOT_STYLE: CSSProperties = {
|
|
|
179
179
|
position: 'relative',
|
|
180
180
|
overflow: 'hidden',
|
|
181
181
|
boxSizing: 'border-box',
|
|
182
|
-
border: '1px solid #d3d8de',
|
|
182
|
+
border: '1px solid var(--border, #d3d8de)',
|
|
183
183
|
borderRadius: 6,
|
|
184
184
|
background: '#fff',
|
|
185
185
|
};
|
|
@@ -191,6 +191,6 @@ const LOADING_STYLE: CSSProperties = {
|
|
|
191
191
|
display: 'flex',
|
|
192
192
|
alignItems: 'center',
|
|
193
193
|
justifyContent: 'center',
|
|
194
|
-
color: '#8a96a3',
|
|
194
|
+
color: 'var(--text-faint, #8a96a3)',
|
|
195
195
|
fontSize: 13,
|
|
196
196
|
};
|