@typecaast/react 0.1.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.
@@ -0,0 +1,138 @@
1
+ import { CSSProperties, ReactNode } from 'react';
2
+ import { Config, ThemeMode, FitMode, Participant, Size } from '@typecaast/schema';
3
+ import { Skin } from '@typecaast/skin-kit';
4
+ import { SimState, Player, Capabilities, ResolvedTheme } from '@typecaast/core';
5
+
6
+ interface TypecaastProps {
7
+ config: Config;
8
+ skin: Skin;
9
+ /** Force a theme; otherwise resolved from `config.meta.theme`. */
10
+ theme?: ThemeMode;
11
+ autoplay?: boolean;
12
+ loop?: boolean;
13
+ rate?: number;
14
+ /** Container fit mode; defaults to `config.meta.fit`. */
15
+ fit?: FitMode;
16
+ /** Accessible label for the simulation. */
17
+ label?: string;
18
+ className?: string;
19
+ style?: CSSProperties;
20
+ }
21
+ /**
22
+ * Mounts the real-time player and renders the resolved skin from live state.
23
+ * The animated visuals are `aria-hidden`; an accessible transcript carries the
24
+ * conversation for screen readers, and `prefers-reduced-motion` snaps to the
25
+ * final state instead of animating (PLAN §20).
26
+ */
27
+ declare function Typecaast({ config, skin, theme, autoplay, loop, rate, fit, label, className, style, }: TypecaastProps): ReactNode;
28
+
29
+ interface UseTypecaastOptions {
30
+ /** Force a theme; otherwise resolved from `config.meta.theme`. */
31
+ theme?: ThemeMode;
32
+ autoplay?: boolean;
33
+ loop?: boolean;
34
+ rate?: number;
35
+ /** The active skin's capabilities, to drop what it can't render. */
36
+ capabilities?: Capabilities;
37
+ }
38
+ /** Imperative controls + live state returned by {@link useTypecaast}. */
39
+ interface TypecaastControls {
40
+ state: SimState;
41
+ /** Current playback time in ms (reactive). */
42
+ currentMs: number;
43
+ play(): void;
44
+ pause(): void;
45
+ seek(timeMs: number): void;
46
+ scrubTo(timeMs: number): void;
47
+ setRate(rate: number): void;
48
+ stepNext(): void;
49
+ stepPrev(): void;
50
+ duration: number;
51
+ rate: number;
52
+ playing: boolean;
53
+ /** Escape hatch to the underlying player. */
54
+ player: Player;
55
+ }
56
+ /**
57
+ * Mount a player for a config and expose live state + controls. The player
58
+ * owns the clock (rAF in the browser); this hook bridges its ticks into React
59
+ * state. The builder uses these controls for preview-as-you-go editing.
60
+ *
61
+ * In M1-UI the player runs over the mocked engine (see engine-adapter); the
62
+ * hook's surface is the final one and does not change when the real engine
63
+ * lands.
64
+ */
65
+ declare function useTypecaast(config: Config, options?: UseTypecaastOptions): TypecaastControls;
66
+
67
+ interface TypecaastStageProps {
68
+ state: SimState;
69
+ skin: Skin;
70
+ participants: Participant[];
71
+ /** Skin-specific options from `meta.skin.options`. */
72
+ options?: Record<string, unknown>;
73
+ }
74
+ /**
75
+ * Maps a `SimState` onto a skin's components: a `Frame` wrapping the thread
76
+ * items (Message / SystemMessage), the typing indicators, and the composer.
77
+ * Reactions render inside the skin's `Message` (it reads `message.reactions`).
78
+ */
79
+ declare function TypecaastStage({ state, skin, participants, options, }: TypecaastStageProps): ReactNode;
80
+
81
+ /**
82
+ * Resolve a theme mode to a concrete theme. `auto` falls back to `light`
83
+ * here; M1U.4 makes `auto` reactive against the host `prefers-color-scheme`
84
+ * via a hook layered on top of this.
85
+ */
86
+ declare function resolveTheme(mode: ThemeMode): ResolvedTheme;
87
+
88
+ /** Reactively tracks the host's `prefers-color-scheme: dark`. */
89
+ declare function usePrefersDark(): boolean;
90
+ /**
91
+ * Resolve a theme mode to a concrete theme. `light`/`dark` are forced; `auto`
92
+ * tracks the host `prefers-color-scheme` reactively and falls back to `light`
93
+ * when no preference signal is available.
94
+ */
95
+ declare function useResolvedTheme(mode: ThemeMode): ResolvedTheme;
96
+
97
+ /**
98
+ * Tracks `prefers-reduced-motion: reduce`. When true, the player snaps to the
99
+ * final state instead of animating (PLAN §20).
100
+ */
101
+ declare function useReducedMotion(): boolean;
102
+
103
+ type FontLoadState = "loading" | "loaded";
104
+ /**
105
+ * Load a skin's declared web fonts on mount so the live preview renders in the
106
+ * correct typeface (PLAN §19) — never relying on a host OS font. SSR-safe and
107
+ * a no-op off the DOM (resolves "loaded"). Re-runs if the skin's fonts change.
108
+ */
109
+ declare function useSkinFonts(skin: Skin): FontLoadState;
110
+
111
+ interface FitBoxProps {
112
+ fit: FitMode;
113
+ canvas: Size;
114
+ children: ReactNode;
115
+ className?: string;
116
+ style?: CSSProperties;
117
+ }
118
+ /**
119
+ * Applies the `fit` strategy between the authoring canvas and the host
120
+ * container (PLAN §7):
121
+ * - `reflow`: fills width; content re-wraps (container query / ResizeObserver).
122
+ * - `scale`: renders at exact canvas size, CSS-scaled to fit (layout preserved).
123
+ * - `fixed`: exact canvas size, clipped.
124
+ */
125
+ declare function FitBox({ fit, canvas, children, className, style, }: FitBoxProps): ReactNode;
126
+
127
+ interface TranscriptLine {
128
+ name: string;
129
+ text: string;
130
+ }
131
+ /**
132
+ * Build a plain-text transcript from the authored config — the accessible
133
+ * representation of the conversation for screen readers (PLAN §20). The config
134
+ * is already structured text, so this is a faithful, non-animated version.
135
+ */
136
+ declare function buildTranscript(config: Config): TranscriptLine[];
137
+
138
+ export { FitBox, type FitBoxProps, type FontLoadState, type TranscriptLine, Typecaast, type TypecaastControls, type TypecaastProps, TypecaastStage, type TypecaastStageProps, type UseTypecaastOptions, buildTranscript, resolveTheme, usePrefersDark, useReducedMotion, useResolvedTheme, useSkinFonts, useTypecaast };
@@ -0,0 +1,138 @@
1
+ import { CSSProperties, ReactNode } from 'react';
2
+ import { Config, ThemeMode, FitMode, Participant, Size } from '@typecaast/schema';
3
+ import { Skin } from '@typecaast/skin-kit';
4
+ import { SimState, Player, Capabilities, ResolvedTheme } from '@typecaast/core';
5
+
6
+ interface TypecaastProps {
7
+ config: Config;
8
+ skin: Skin;
9
+ /** Force a theme; otherwise resolved from `config.meta.theme`. */
10
+ theme?: ThemeMode;
11
+ autoplay?: boolean;
12
+ loop?: boolean;
13
+ rate?: number;
14
+ /** Container fit mode; defaults to `config.meta.fit`. */
15
+ fit?: FitMode;
16
+ /** Accessible label for the simulation. */
17
+ label?: string;
18
+ className?: string;
19
+ style?: CSSProperties;
20
+ }
21
+ /**
22
+ * Mounts the real-time player and renders the resolved skin from live state.
23
+ * The animated visuals are `aria-hidden`; an accessible transcript carries the
24
+ * conversation for screen readers, and `prefers-reduced-motion` snaps to the
25
+ * final state instead of animating (PLAN §20).
26
+ */
27
+ declare function Typecaast({ config, skin, theme, autoplay, loop, rate, fit, label, className, style, }: TypecaastProps): ReactNode;
28
+
29
+ interface UseTypecaastOptions {
30
+ /** Force a theme; otherwise resolved from `config.meta.theme`. */
31
+ theme?: ThemeMode;
32
+ autoplay?: boolean;
33
+ loop?: boolean;
34
+ rate?: number;
35
+ /** The active skin's capabilities, to drop what it can't render. */
36
+ capabilities?: Capabilities;
37
+ }
38
+ /** Imperative controls + live state returned by {@link useTypecaast}. */
39
+ interface TypecaastControls {
40
+ state: SimState;
41
+ /** Current playback time in ms (reactive). */
42
+ currentMs: number;
43
+ play(): void;
44
+ pause(): void;
45
+ seek(timeMs: number): void;
46
+ scrubTo(timeMs: number): void;
47
+ setRate(rate: number): void;
48
+ stepNext(): void;
49
+ stepPrev(): void;
50
+ duration: number;
51
+ rate: number;
52
+ playing: boolean;
53
+ /** Escape hatch to the underlying player. */
54
+ player: Player;
55
+ }
56
+ /**
57
+ * Mount a player for a config and expose live state + controls. The player
58
+ * owns the clock (rAF in the browser); this hook bridges its ticks into React
59
+ * state. The builder uses these controls for preview-as-you-go editing.
60
+ *
61
+ * In M1-UI the player runs over the mocked engine (see engine-adapter); the
62
+ * hook's surface is the final one and does not change when the real engine
63
+ * lands.
64
+ */
65
+ declare function useTypecaast(config: Config, options?: UseTypecaastOptions): TypecaastControls;
66
+
67
+ interface TypecaastStageProps {
68
+ state: SimState;
69
+ skin: Skin;
70
+ participants: Participant[];
71
+ /** Skin-specific options from `meta.skin.options`. */
72
+ options?: Record<string, unknown>;
73
+ }
74
+ /**
75
+ * Maps a `SimState` onto a skin's components: a `Frame` wrapping the thread
76
+ * items (Message / SystemMessage), the typing indicators, and the composer.
77
+ * Reactions render inside the skin's `Message` (it reads `message.reactions`).
78
+ */
79
+ declare function TypecaastStage({ state, skin, participants, options, }: TypecaastStageProps): ReactNode;
80
+
81
+ /**
82
+ * Resolve a theme mode to a concrete theme. `auto` falls back to `light`
83
+ * here; M1U.4 makes `auto` reactive against the host `prefers-color-scheme`
84
+ * via a hook layered on top of this.
85
+ */
86
+ declare function resolveTheme(mode: ThemeMode): ResolvedTheme;
87
+
88
+ /** Reactively tracks the host's `prefers-color-scheme: dark`. */
89
+ declare function usePrefersDark(): boolean;
90
+ /**
91
+ * Resolve a theme mode to a concrete theme. `light`/`dark` are forced; `auto`
92
+ * tracks the host `prefers-color-scheme` reactively and falls back to `light`
93
+ * when no preference signal is available.
94
+ */
95
+ declare function useResolvedTheme(mode: ThemeMode): ResolvedTheme;
96
+
97
+ /**
98
+ * Tracks `prefers-reduced-motion: reduce`. When true, the player snaps to the
99
+ * final state instead of animating (PLAN §20).
100
+ */
101
+ declare function useReducedMotion(): boolean;
102
+
103
+ type FontLoadState = "loading" | "loaded";
104
+ /**
105
+ * Load a skin's declared web fonts on mount so the live preview renders in the
106
+ * correct typeface (PLAN §19) — never relying on a host OS font. SSR-safe and
107
+ * a no-op off the DOM (resolves "loaded"). Re-runs if the skin's fonts change.
108
+ */
109
+ declare function useSkinFonts(skin: Skin): FontLoadState;
110
+
111
+ interface FitBoxProps {
112
+ fit: FitMode;
113
+ canvas: Size;
114
+ children: ReactNode;
115
+ className?: string;
116
+ style?: CSSProperties;
117
+ }
118
+ /**
119
+ * Applies the `fit` strategy between the authoring canvas and the host
120
+ * container (PLAN §7):
121
+ * - `reflow`: fills width; content re-wraps (container query / ResizeObserver).
122
+ * - `scale`: renders at exact canvas size, CSS-scaled to fit (layout preserved).
123
+ * - `fixed`: exact canvas size, clipped.
124
+ */
125
+ declare function FitBox({ fit, canvas, children, className, style, }: FitBoxProps): ReactNode;
126
+
127
+ interface TranscriptLine {
128
+ name: string;
129
+ text: string;
130
+ }
131
+ /**
132
+ * Build a plain-text transcript from the authored config — the accessible
133
+ * representation of the conversation for screen readers (PLAN §20). The config
134
+ * is already structured text, so this is a faithful, non-animated version.
135
+ */
136
+ declare function buildTranscript(config: Config): TranscriptLine[];
137
+
138
+ export { FitBox, type FitBoxProps, type FontLoadState, type TranscriptLine, Typecaast, type TypecaastControls, type TypecaastProps, TypecaastStage, type TypecaastStageProps, type UseTypecaastOptions, buildTranscript, resolveTheme, usePrefersDark, useReducedMotion, useResolvedTheme, useSkinFonts, useTypecaast };
package/dist/index.js ADDED
@@ -0,0 +1,386 @@
1
+ import { useSyncExternalStore, useMemo, useState, useEffect, useRef, useLayoutEffect } from 'react';
2
+ import { createPlayer, createEngine } from '@typecaast/core';
3
+ import { loadSkinFonts, ThemeProvider } from '@typecaast/skin-kit';
4
+ import { jsx, jsxs } from 'react/jsx-runtime';
5
+
6
+ // src/typecaast.tsx
7
+ function configToEngine(config, theme, capabilities) {
8
+ return createEngine(config, theme, capabilities);
9
+ }
10
+ var QUERY = "(prefers-color-scheme: dark)";
11
+ function getMql() {
12
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
13
+ return null;
14
+ }
15
+ return window.matchMedia(QUERY);
16
+ }
17
+ function subscribe(onChange) {
18
+ const mql = getMql();
19
+ if (!mql) return () => {
20
+ };
21
+ mql.addEventListener("change", onChange);
22
+ return () => mql.removeEventListener("change", onChange);
23
+ }
24
+ function getSnapshot() {
25
+ return getMql()?.matches ?? false;
26
+ }
27
+ function getServerSnapshot() {
28
+ return false;
29
+ }
30
+ function usePrefersDark() {
31
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
32
+ }
33
+ function useResolvedTheme(mode) {
34
+ const prefersDark = usePrefersDark();
35
+ if (mode === "light") return "light";
36
+ if (mode === "dark") return "dark";
37
+ return prefersDark ? "dark" : "light";
38
+ }
39
+
40
+ // src/use-typecaast.ts
41
+ function useTypecaast(config, options = {}) {
42
+ const {
43
+ theme,
44
+ autoplay = false,
45
+ loop = false,
46
+ rate = 1,
47
+ capabilities
48
+ } = options;
49
+ const resolved = useResolvedTheme(theme ?? config.meta.theme);
50
+ const player = useMemo(() => {
51
+ const engine = configToEngine(config, resolved, capabilities);
52
+ return createPlayer(engine.getStateAt, {
53
+ durationMs: engine.durationMs,
54
+ steps: engine.steps,
55
+ loop,
56
+ rate
57
+ });
58
+ }, [config, resolved, capabilities, loop, rate]);
59
+ const [state, setState] = useState(() => player.state);
60
+ const [currentMs, setCurrentMs] = useState(() => player.currentMs);
61
+ const [playing, setPlaying] = useState(() => player.playing);
62
+ useEffect(() => {
63
+ const sync = () => {
64
+ setState(player.state);
65
+ setCurrentMs(player.currentMs);
66
+ };
67
+ sync();
68
+ setPlaying(player.playing);
69
+ const offs = [
70
+ player.on("tick", sync),
71
+ player.on("seek", sync),
72
+ player.on("play", () => setPlaying(true)),
73
+ player.on("pause", () => setPlaying(false))
74
+ ];
75
+ if (autoplay) player.play();
76
+ return () => {
77
+ offs.forEach((off) => off());
78
+ player.destroy();
79
+ };
80
+ }, [player, autoplay]);
81
+ return {
82
+ state,
83
+ currentMs,
84
+ play: () => player.play(),
85
+ pause: () => player.pause(),
86
+ seek: (t) => player.seek(t),
87
+ scrubTo: (t) => player.scrubTo(t),
88
+ setRate: (r) => player.setRate(r),
89
+ stepNext: () => player.stepNext(),
90
+ stepPrev: () => player.stepPrev(),
91
+ duration: player.durationMs,
92
+ rate: player.rate,
93
+ playing,
94
+ player
95
+ };
96
+ }
97
+ function useSkinFonts(skin) {
98
+ const fonts = skin.meta.fonts;
99
+ const [state, setState] = useState(
100
+ () => fonts && fonts.length > 0 ? "loading" : "loaded"
101
+ );
102
+ useEffect(() => {
103
+ if (!fonts || fonts.length === 0) {
104
+ setState("loaded");
105
+ return;
106
+ }
107
+ let cancelled = false;
108
+ setState("loading");
109
+ loadSkinFonts(fonts).finally(() => {
110
+ if (!cancelled) setState("loaded");
111
+ });
112
+ return () => {
113
+ cancelled = true;
114
+ };
115
+ }, [fonts]);
116
+ return state;
117
+ }
118
+ var QUERY2 = "(prefers-reduced-motion: reduce)";
119
+ function getMql2() {
120
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
121
+ return null;
122
+ }
123
+ return window.matchMedia(QUERY2);
124
+ }
125
+ function subscribe2(onChange) {
126
+ const mql = getMql2();
127
+ if (!mql) return () => {
128
+ };
129
+ mql.addEventListener("change", onChange);
130
+ return () => mql.removeEventListener("change", onChange);
131
+ }
132
+ var getSnapshot2 = () => getMql2()?.matches ?? false;
133
+ var getServerSnapshot2 = () => false;
134
+ function useReducedMotion() {
135
+ return useSyncExternalStore(subscribe2, getSnapshot2, getServerSnapshot2);
136
+ }
137
+
138
+ // src/transcript.ts
139
+ function buildTranscript(config) {
140
+ const name = new Map(config.participants.map((p) => [p.id, p.name]));
141
+ const lines = [];
142
+ for (const step of config.timeline) {
143
+ let from;
144
+ let text;
145
+ if (step.type === "message" || step.type === "system") {
146
+ from = step.from;
147
+ text = step.text;
148
+ } else if (step.type === "composerType") {
149
+ from = step.from;
150
+ text = step.text;
151
+ }
152
+ if (text) {
153
+ lines.push({ name: from ? name.get(from) ?? from : "System", text });
154
+ }
155
+ }
156
+ return lines;
157
+ }
158
+ function TypecaastStage({
159
+ state,
160
+ skin,
161
+ participants,
162
+ options
163
+ }) {
164
+ const theme = state.theme;
165
+ const byId = useMemo(() => {
166
+ const map = /* @__PURE__ */ new Map();
167
+ for (const p of participants) map.set(p.id, p);
168
+ return map;
169
+ }, [participants]);
170
+ const { Frame, Message, SystemMessage, TypingIndicator, Composer } = skin.components;
171
+ const tokens = skin.tokens?.[theme];
172
+ const composerAuthor = state.composer.from ? byId.get(state.composer.from) : void 0;
173
+ return /* @__PURE__ */ jsx(ThemeProvider, { theme, tokens, children: /* @__PURE__ */ jsxs(Frame, { theme, options, children: [
174
+ /* @__PURE__ */ jsxs(
175
+ "div",
176
+ {
177
+ "data-typecaast-thread": "",
178
+ style: {
179
+ display: "flex",
180
+ flexDirection: "column",
181
+ justifyContent: "flex-end",
182
+ flex: "1 1 auto",
183
+ minHeight: 0,
184
+ overflow: "hidden"
185
+ },
186
+ children: [
187
+ state.messages.map((message, i) => {
188
+ const author = byId.get(message.from);
189
+ if (!author) return null;
190
+ if (message.variant === "system") {
191
+ return /* @__PURE__ */ jsx(
192
+ SystemMessage,
193
+ {
194
+ theme,
195
+ message,
196
+ author
197
+ },
198
+ message.id
199
+ );
200
+ }
201
+ const prev = state.messages[i - 1];
202
+ const previousAuthor = prev ? byId.get(prev.from) : void 0;
203
+ return /* @__PURE__ */ jsx(
204
+ Message,
205
+ {
206
+ theme,
207
+ message,
208
+ author,
209
+ previousAuthor
210
+ },
211
+ message.id
212
+ );
213
+ }),
214
+ state.typingIndicators.map((typing, i) => {
215
+ const author = byId.get(typing.from);
216
+ if (!author) return null;
217
+ return /* @__PURE__ */ jsx(
218
+ TypingIndicator,
219
+ {
220
+ theme,
221
+ typing,
222
+ author
223
+ },
224
+ `typing-${typing.from}-${i}`
225
+ );
226
+ })
227
+ ]
228
+ }
229
+ ),
230
+ composerAuthor ? /* @__PURE__ */ jsx(
231
+ Composer,
232
+ {
233
+ theme,
234
+ composer: state.composer,
235
+ author: composerAuthor
236
+ }
237
+ ) : null
238
+ ] }) });
239
+ }
240
+ function FitBox({
241
+ fit,
242
+ canvas,
243
+ children,
244
+ className,
245
+ style
246
+ }) {
247
+ const ref = useRef(null);
248
+ const [container, setContainer] = useState(
249
+ null
250
+ );
251
+ useLayoutEffect(() => {
252
+ const el = ref.current;
253
+ if (!el || typeof ResizeObserver === "undefined") return;
254
+ const ro = new ResizeObserver((entries) => {
255
+ const rect = entries[0]?.contentRect;
256
+ if (rect) setContainer({ w: rect.width, h: rect.height });
257
+ });
258
+ ro.observe(el);
259
+ return () => ro.disconnect();
260
+ }, []);
261
+ if (fit === "reflow") {
262
+ return /* @__PURE__ */ jsx(
263
+ "div",
264
+ {
265
+ ref,
266
+ className,
267
+ "data-fit": "reflow",
268
+ style: { width: "100%", ...style },
269
+ children
270
+ }
271
+ );
272
+ }
273
+ if (fit === "fixed") {
274
+ return /* @__PURE__ */ jsx(
275
+ "div",
276
+ {
277
+ ref,
278
+ className,
279
+ "data-fit": "fixed",
280
+ style: {
281
+ width: canvas.width,
282
+ height: canvas.height,
283
+ overflow: "hidden",
284
+ ...style
285
+ },
286
+ children
287
+ }
288
+ );
289
+ }
290
+ const scale = container ? Math.min(container.w / canvas.width, container.h / canvas.height) : 1;
291
+ return /* @__PURE__ */ jsx(
292
+ "div",
293
+ {
294
+ ref,
295
+ className,
296
+ "data-fit": "scale",
297
+ style: { width: "100%", height: "100%", overflow: "hidden", ...style },
298
+ children: /* @__PURE__ */ jsx(
299
+ "div",
300
+ {
301
+ "data-fit-canvas": "",
302
+ style: {
303
+ width: canvas.width,
304
+ height: canvas.height,
305
+ transform: `scale(${scale})`,
306
+ transformOrigin: "top left"
307
+ },
308
+ children
309
+ }
310
+ )
311
+ }
312
+ );
313
+ }
314
+ var SR_ONLY = {
315
+ position: "absolute",
316
+ width: 1,
317
+ height: 1,
318
+ padding: 0,
319
+ margin: -1,
320
+ overflow: "hidden",
321
+ clipPath: "inset(50%)",
322
+ whiteSpace: "nowrap",
323
+ border: 0
324
+ };
325
+ function Typecaast({
326
+ config,
327
+ skin,
328
+ theme,
329
+ autoplay,
330
+ loop,
331
+ rate,
332
+ fit,
333
+ label,
334
+ className,
335
+ style
336
+ }) {
337
+ const reduced = useReducedMotion();
338
+ const tc = useTypecaast(config, {
339
+ theme,
340
+ autoplay: autoplay && !reduced,
341
+ loop: loop && !reduced,
342
+ rate,
343
+ capabilities: skin.meta.capabilities
344
+ });
345
+ const fonts = useSkinFonts(skin);
346
+ useEffect(() => {
347
+ if (reduced) tc.seek(tc.duration);
348
+ }, [reduced, tc]);
349
+ const transcript = useMemo(() => buildTranscript(config), [config]);
350
+ return /* @__PURE__ */ jsxs(
351
+ "div",
352
+ {
353
+ className,
354
+ style: { position: "relative", ...style },
355
+ "data-typecaast": "",
356
+ "data-fonts": fonts,
357
+ role: "figure",
358
+ "aria-label": label ?? `Chat simulation (${skin.meta.name})`,
359
+ children: [
360
+ /* @__PURE__ */ jsx("ol", { style: SR_ONLY, children: transcript.map((line, i) => /* @__PURE__ */ jsxs("li", { children: [
361
+ line.name,
362
+ ": ",
363
+ line.text
364
+ ] }, i)) }),
365
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: { height: "100%" }, children: /* @__PURE__ */ jsx(FitBox, { fit: fit ?? config.meta.fit, canvas: config.meta.canvas, children: /* @__PURE__ */ jsx(
366
+ TypecaastStage,
367
+ {
368
+ state: tc.state,
369
+ skin,
370
+ participants: config.participants,
371
+ options: config.meta.skin.options
372
+ }
373
+ ) }) })
374
+ ]
375
+ }
376
+ );
377
+ }
378
+
379
+ // src/resolve-theme.ts
380
+ function resolveTheme(mode) {
381
+ return mode === "dark" ? "dark" : mode === "light" ? "light" : "light";
382
+ }
383
+
384
+ export { FitBox, Typecaast, TypecaastStage, buildTranscript, resolveTheme, usePrefersDark, useReducedMotion, useResolvedTheme, useSkinFonts, useTypecaast };
385
+ //# sourceMappingURL=index.js.map
386
+ //# sourceMappingURL=index.js.map