Skip to content

Commit 5b0e30c

Browse files
committed
fix(demo): bundle asciinema CSS statically so the player loads in production
The dynamic `import("asciinema-player/dist/bundle/asciinema-player.css")` caused Vite to emit the asciinema CSS as a separate chunk that was not served from GitHub Pages (404). The preload helper rejected, the catch in DemoSection set status="error", and the demo silently fell back to hero.svg. Make the CSS a top-level static import so it folds into the main page CSS bundle. Also invert the cast theme relative to the page theme (dark cast on light page, light cast on dark page) by passing `theme` to asciinema-player's `create()` instead of wrapping the host element with a `.ap-theme-*` class, and update the unit tests accordingly.
1 parent 2ca2ed0 commit 5b0e30c

3 files changed

Lines changed: 41 additions & 24 deletions

File tree

src/components/DemoSection.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { useEffect, useRef, useState } from "react";
2+
import "asciinema-player/dist/bundle/asciinema-player.css";
23

34
type PlayerStatus = "loading" | "ready" | "error";
45
type Theme = "light" | "dark";
6+
type DemoTheme = "opentaint-light" | "opentaint-dark";
57

68
type PlayerHandle = { dispose: () => void };
79

@@ -16,8 +18,8 @@ type PlayerModule = {
1618
const HERO_SRC = "/demo/hero.cast";
1719
const HERO_FALLBACK_SRC = "/demo/hero.svg";
1820

19-
const themeClass = (theme: Theme) =>
20-
theme === "dark" ? "ap-theme-opentaint-dark" : "ap-theme-opentaint-light";
21+
const demoThemeFor = (pageTheme: Theme): DemoTheme =>
22+
pageTheme === "dark" ? "opentaint-light" : "opentaint-dark";
2123

2224
const readInitialTheme = (): Theme =>
2325
typeof document !== "undefined" && document.documentElement.classList.contains("dark")
@@ -37,21 +39,23 @@ export function DemoSection() {
3739
const heroRef = useRef<HTMLDivElement | null>(null);
3840
const heroHandleRef = useRef<PlayerHandle | null>(null);
3941

42+
const demoTheme = demoThemeFor(theme);
43+
4044
useEffect(() => {
4145
if (reducedMotion) return;
4246

4347
let cancelled = false;
4448
const load = async () => {
4549
try {
4650
const mod = (await import("asciinema-player")) as PlayerModule;
47-
await import("asciinema-player/dist/bundle/asciinema-player.css");
48-
if (cancelled || !heroRef.current || heroHandleRef.current) return;
51+
if (cancelled || !heroRef.current) return;
4952
heroHandleRef.current = mod.create(HERO_SRC, heroRef.current, {
5053
autoPlay: true,
5154
loop: true,
5255
preload: true,
5356
controls: false,
5457
poster: "npt:0:0.1",
58+
theme: demoTheme,
5559
terminalFontFamily: "'JetBrains Mono', monospace",
5660
});
5761
setStatus("ready");
@@ -66,7 +70,7 @@ export function DemoSection() {
6670
heroHandleRef.current?.dispose();
6771
heroHandleRef.current = null;
6872
};
69-
}, [reducedMotion]);
73+
}, [reducedMotion, demoTheme]);
7074

7175
useEffect(() => {
7276
if (typeof document === "undefined") return;
@@ -85,10 +89,8 @@ export function DemoSection() {
8589
return () => mq.removeEventListener?.("change", onChange);
8690
}, []);
8791

88-
const wrapper = themeClass(theme);
89-
9092
return (
91-
<div className={`mx-auto max-w-[68.4rem] ${wrapper}`}>
93+
<div className="mx-auto max-w-[68.4rem]">
9294
<div className="overflow-hidden sm:rounded-md sm:border sm:border-border sm:bg-secondary/50">
9395
{reducedMotion || status === "error" ? (
9496
<img
@@ -101,7 +103,6 @@ export function DemoSection() {
101103
<div
102104
ref={heroRef}
103105
data-testid="demo-hero-player"
104-
className={wrapper}
105106
aria-label="OpenTaint scan demo, running continuously"
106107
/>
107108
)}

src/components/__tests__/DemoSection.test.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ const flushAsync = async () => {
4949
});
5050
};
5151

52+
const lastCreateOptions = () => {
53+
const call = createPlayerMock.mock.calls.at(-1);
54+
return (call?.[2] ?? {}) as Record<string, unknown>;
55+
};
56+
5257
describe("DemoSection", () => {
5358
it("mounts the hero player", async () => {
5459
render(<DemoSection />);
@@ -57,31 +62,39 @@ describe("DemoSection", () => {
5762
expect(createPlayerMock).toHaveBeenCalledTimes(1);
5863
});
5964

60-
it("applies the dark theme class when the document has .dark", async () => {
65+
it("uses the dark cast theme on a light page", async () => {
66+
render(<DemoSection />);
67+
await flushAsync();
68+
expect(lastCreateOptions().theme).toBe("opentaint-dark");
69+
});
70+
71+
it("uses the light cast theme on a dark page", async () => {
6172
document.documentElement.classList.add("dark");
6273
render(<DemoSection />);
6374
await flushAsync();
64-
const hero = screen.getByTestId("demo-hero-player");
65-
expect(hero.className).toContain("ap-theme-opentaint-dark");
75+
expect(lastCreateOptions().theme).toBe("opentaint-light");
6676
});
6777

68-
it("swaps the theme class when [data-theme-toggle] is clicked", async () => {
78+
it("re-creates the player with the inverse theme when [data-theme-toggle] is clicked", async () => {
6979
const toggle = document.createElement("button");
7080
toggle.setAttribute("data-theme-toggle", "");
7181
document.body.appendChild(toggle);
7282

7383
render(<DemoSection />);
7484
await flushAsync();
75-
const hero = screen.getByTestId("demo-hero-player");
76-
expect(hero.className).toContain("ap-theme-opentaint-light");
85+
expect(lastCreateOptions().theme).toBe("opentaint-dark");
86+
const firstHandle = playerInstances.at(-1);
7787

7888
await act(async () => {
7989
document.documentElement.classList.add("dark");
8090
toggle.click();
8191
await Promise.resolve();
8292
});
93+
await flushAsync();
8394

84-
expect(hero.className).toContain("ap-theme-opentaint-dark");
95+
expect(firstHandle?.dispose).toHaveBeenCalled();
96+
expect(createPlayerMock).toHaveBeenCalledTimes(2);
97+
expect(lastCreateOptions().theme).toBe("opentaint-light");
8598
toggle.remove();
8699
});
87100

src/styles/demo-theme.css

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
/*
2-
* Custom asciinema-player themes that match the site's light/dark palette.
3-
* Applied via wrapper class: .ap-theme-opentaint-light or .ap-theme-opentaint-dark.
2+
* Custom asciinema-player themes that intentionally contrast with the site
3+
* theme: the dark cast is shown on the light page and vice versa.
4+
*
5+
* Asciinema-player applies `asciinema-player-theme-<name>` to its inner
6+
* `.ap-player` element when `create(..., { theme: "<name>" })` is used.
47
* See src/components/DemoSection.tsx for the selector logic.
58
*/
69

7-
.ap-theme-opentaint-light {
8-
--term-color-background: hsl(var(--card));
9-
--term-color-foreground: hsl(var(--foreground));
10+
.ap-player.asciinema-player-theme-opentaint-light {
11+
--term-color-background: hsl(0 0% 100%);
12+
--term-color-foreground: hsl(215 14% 8%);
1013
--term-color-0: #3f3f46;
1114
--term-color-1: #dc2626;
1215
--term-color-2: #16a34a;
@@ -25,9 +28,9 @@
2528
--term-color-15: #27272a;
2629
}
2730

28-
.ap-theme-opentaint-dark {
29-
--term-color-background: hsl(var(--card));
30-
--term-color-foreground: hsl(var(--foreground));
31+
.ap-player.asciinema-player-theme-opentaint-dark {
32+
--term-color-background: hsl(215 10% 7%);
33+
--term-color-foreground: hsl(215 6% 92%);
3134
--term-color-0: #52525b;
3235
--term-color-1: #f87171;
3336
--term-color-2: #4ade80;

0 commit comments

Comments
 (0)