T45a Site · Stack: Vite 8, React 19 and TanStack Router under pnpm; routes, theme, fonts, the manifest
This commit is contained in:
1 parent
e3957f7816
commit
167055fdae
32 files changed
+699
-1309
No files matched your search
@@ -0,0 +1,94 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Icon } from "./icons";
|
||||
import { mode, useMode } from "./theme";
|
||||
|
||||
const SPIN_AFTER = 400;
|
||||
const GIVE_UP = 20000;
|
||||
|
||||
type State = "starting" | "slow" | "ready" | "failed";
|
||||
|
||||
/** One story live from the gallery, themed with the site, no frame around it. */
|
||||
export function Live({ page, story, title }: { page: string; story: string; title: string }) {
|
||||
const theme = useMode();
|
||||
const frame = useRef<HTMLIFrameElement>(null);
|
||||
const [state, setState] = useState<State>("starting");
|
||||
const [full, setFull] = useState(false);
|
||||
const [run, setRun] = useState(0);
|
||||
const [start] = useState(mode);
|
||||
const src = (t: string) => `/gallery/?page=${page}&story=${story}&theme=${t}`;
|
||||
|
||||
useEffect(() => {
|
||||
setState("starting");
|
||||
const slow = window.setTimeout(() => setState((s) => (s === "starting" ? "slow" : s)), SPIN_AFTER);
|
||||
const failed = window.setTimeout(() => {
|
||||
setState((s) => (s === "ready" ? s : "failed"));
|
||||
console.error(`live: ${page}/${story} sent no ready in ${GIVE_UP} ms`);
|
||||
}, GIVE_UP);
|
||||
const ready = (event: MessageEvent) => {
|
||||
if (event.source !== frame.current?.contentWindow || event.data?.ely !== "ready") return;
|
||||
clearTimeout(slow);
|
||||
clearTimeout(failed);
|
||||
setState("ready");
|
||||
frame.current?.contentWindow?.postMessage({ ely: "theme", theme: mode() }, location.origin);
|
||||
};
|
||||
addEventListener("message", ready);
|
||||
return () => {
|
||||
clearTimeout(slow);
|
||||
clearTimeout(failed);
|
||||
removeEventListener("message", ready);
|
||||
};
|
||||
}, [page, story, run]);
|
||||
|
||||
useEffect(() => {
|
||||
frame.current?.contentWindow?.postMessage({ ely: "theme", theme }, location.origin);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!full) return;
|
||||
document.body.style.overflow = "hidden";
|
||||
const escape = (event: KeyboardEvent) => event.key === "Escape" && setFull(false);
|
||||
addEventListener("keydown", escape);
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
removeEventListener("keydown", escape);
|
||||
};
|
||||
}, [full]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<a className="skip-live" href="#after-live">
|
||||
Skip the live example
|
||||
</a>
|
||||
<div className={full ? "live full" : "live"}>
|
||||
<iframe key={run} ref={frame} title={`${title}, live`} src={src(start)} />
|
||||
{state !== "ready" && (
|
||||
<div className="live-state" role="status">
|
||||
{state === "slow" && <span className="spin" />}
|
||||
{state === "failed" ? (
|
||||
<>
|
||||
Couldn't start the example.
|
||||
<button className="text" onClick={() => setRun((n) => n + 1)}>
|
||||
Reload
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
"Starting…"
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="live-tools">
|
||||
<button className="tool" aria-label="Reload the example" onClick={() => setRun((n) => n + 1)}>
|
||||
<Icon name="Refresh" />
|
||||
</button>
|
||||
<a className="tool" href={src(theme)} target="_blank" rel="noopener" aria-label="Open the example alone">
|
||||
<Icon name="ArrowRightUp" />
|
||||
</a>
|
||||
<button className="tool" aria-label={full ? "Leave full screen" : "Full screen"} onClick={() => setFull(!full)}>
|
||||
<Icon name={full ? "Minimize" : "Maximize"} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span id="after-live" tabIndex={-1} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user