@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.
- package/LICENSE +201 -0
- package/dist/index.cjs +397 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +138 -0
- package/dist/index.d.ts +138 -0
- package/dist/index.js +386 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|