@scenar/cli 0.1.20 → 0.2.1
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/example-bundle/assets/index-BLQ14SEo.js +147 -0
- package/example-bundle/assets/logo-f-5LjVqJ.png +0 -0
- package/example-bundle/assets/style-Dnzx_jUO.css +1 -0
- package/example-bundle/index.html +13 -0
- package/example-bundle/pack-manifest.json +36 -0
- package/example-bundle/scenario.json +9 -0
- package/package.json +17 -5
- package/src/__tests__/pack-generate-embed-entry.test.ts +59 -6
- package/src/__tests__/publish-command.test.ts +46 -0
- package/src/__tests__/publish-flow.test.ts +220 -0
- package/src/__tests__/read-viewport.test.ts +45 -0
- package/src/__tests__/run-publish.test.ts +92 -0
- package/src/__tests__/serve-command.test.ts +44 -0
- package/src/__tests__/serve-request-path.test.ts +67 -0
- package/src/__tests__/serve-static-server.test.ts +88 -0
- package/src/__tests__/try-command.test.ts +21 -0
- package/src/api.d.ts +31 -0
- package/src/api.d.ts.map +1 -0
- package/src/api.js +28 -0
- package/src/api.js.map +1 -0
- package/src/api.ts +55 -0
- package/src/bundle/read-viewport.d.ts +23 -0
- package/src/bundle/read-viewport.d.ts.map +1 -0
- package/src/bundle/read-viewport.js +25 -0
- package/src/bundle/read-viewport.js.map +1 -0
- package/src/bundle/read-viewport.ts +36 -0
- package/src/commands/deploy.d.ts.map +1 -1
- package/src/commands/deploy.js +8 -24
- package/src/commands/deploy.js.map +1 -1
- package/src/commands/deploy.ts +10 -28
- package/src/commands/narrate.d.ts.map +1 -1
- package/src/commands/narrate.js +39 -170
- package/src/commands/narrate.js.map +1 -1
- package/src/commands/narrate.ts +48 -289
- package/src/commands/pack.d.ts.map +1 -1
- package/src/commands/pack.js +12 -112
- package/src/commands/pack.js.map +1 -1
- package/src/commands/pack.ts +12 -128
- package/src/commands/publish.d.ts +3 -0
- package/src/commands/publish.d.ts.map +1 -0
- package/src/commands/publish.js +64 -0
- package/src/commands/publish.js.map +1 -0
- package/src/commands/publish.ts +86 -0
- package/src/commands/render.d.ts.map +1 -1
- package/src/commands/render.js +12 -194
- package/src/commands/render.js.map +1 -1
- package/src/commands/render.ts +19 -286
- package/src/commands/serve.d.ts +3 -0
- package/src/commands/serve.d.ts.map +1 -0
- package/src/commands/serve.js +91 -0
- package/src/commands/serve.js.map +1 -0
- package/src/commands/serve.ts +112 -0
- package/src/commands/try.d.ts +3 -0
- package/src/commands/try.d.ts.map +1 -0
- package/src/commands/try.js +99 -0
- package/src/commands/try.js.map +1 -0
- package/src/commands/try.ts +122 -0
- package/src/index.d.ts.map +1 -1
- package/src/index.js +6 -0
- package/src/index.js.map +1 -1
- package/src/index.ts +6 -0
- package/src/narrate/run-narrate.d.ts +43 -0
- package/src/narrate/run-narrate.d.ts.map +1 -0
- package/src/narrate/run-narrate.js +174 -0
- package/src/narrate/run-narrate.js.map +1 -0
- package/src/narrate/run-narrate.ts +298 -0
- package/src/pack/generate-embed-entry.d.ts +11 -0
- package/src/pack/generate-embed-entry.d.ts.map +1 -1
- package/src/pack/generate-embed-entry.js +81 -10
- package/src/pack/generate-embed-entry.js.map +1 -1
- package/src/pack/generate-embed-entry.ts +81 -10
- package/src/pack/run-pack.d.ts +34 -0
- package/src/pack/run-pack.d.ts.map +1 -0
- package/src/pack/run-pack.js +117 -0
- package/src/pack/run-pack.js.map +1 -0
- package/src/pack/run-pack.ts +163 -0
- package/src/publish/github.d.ts +11 -0
- package/src/publish/github.d.ts.map +1 -0
- package/src/publish/github.js +197 -0
- package/src/publish/github.js.map +1 -0
- package/src/publish/github.ts +216 -0
- package/src/publish/publish-flow.d.ts +110 -0
- package/src/publish/publish-flow.d.ts.map +1 -0
- package/src/publish/publish-flow.js +117 -0
- package/src/publish/publish-flow.js.map +1 -0
- package/src/publish/publish-flow.ts +199 -0
- package/src/publish/run-publish.d.ts +41 -0
- package/src/publish/run-publish.d.ts.map +1 -0
- package/src/publish/run-publish.js +77 -0
- package/src/publish/run-publish.js.map +1 -0
- package/src/publish/run-publish.ts +131 -0
- package/src/render/run-render.d.ts +31 -0
- package/src/render/run-render.d.ts.map +1 -0
- package/src/render/run-render.js +196 -0
- package/src/render/run-render.js.map +1 -0
- package/src/render/run-render.ts +287 -0
- package/src/serve/request-path.d.ts +23 -0
- package/src/serve/request-path.d.ts.map +1 -0
- package/src/serve/request-path.js +52 -0
- package/src/serve/request-path.js.map +1 -0
- package/src/serve/request-path.ts +55 -0
- package/src/serve/run-serve.d.ts +31 -0
- package/src/serve/run-serve.d.ts.map +1 -0
- package/src/serve/run-serve.js +35 -0
- package/src/serve/run-serve.js.map +1 -0
- package/src/serve/run-serve.ts +68 -0
- package/src/serve/static-server.d.ts +38 -0
- package/src/serve/static-server.d.ts.map +1 -0
- package/src/serve/static-server.js +91 -0
- package/src/serve/static-server.js.map +1 -0
- package/src/serve/static-server.ts +118 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { mkdir, writeFile, stat } from "node:fs/promises";
|
|
2
|
+
import { join, resolve, extname, basename, dirname } from "node:path";
|
|
3
|
+
import { loadScenarioYaml } from "../util/load-yaml.js";
|
|
4
|
+
import { loadStepsFromTs } from "../util/load-ts.js";
|
|
5
|
+
import { discoverScenarios } from "../util/discover-scenarios.js";
|
|
6
|
+
import {
|
|
7
|
+
computeHash,
|
|
8
|
+
loadCache,
|
|
9
|
+
saveCache,
|
|
10
|
+
fileExists,
|
|
11
|
+
isCached,
|
|
12
|
+
getCachedDuration,
|
|
13
|
+
buildCacheFile,
|
|
14
|
+
} from "../util/narration-cache.js";
|
|
15
|
+
import { validateScenario } from "../validate/scenario-validator.js";
|
|
16
|
+
import { resolveProvider } from "../tts/resolve-provider.js";
|
|
17
|
+
import type { TtsProvider } from "../tts/types.js";
|
|
18
|
+
|
|
19
|
+
/** Options for {@link runNarrate}. Paths may be relative; resolved here. */
|
|
20
|
+
export interface RunNarrateOptions {
|
|
21
|
+
/** A scenario YAML file, a .ts steps file, or a directory of scenarios. */
|
|
22
|
+
readonly target: string;
|
|
23
|
+
/** TTS provider name: echogarden, edge-tts, or openai (default echogarden). */
|
|
24
|
+
readonly tts?: string;
|
|
25
|
+
/** Output directory for audio (default: alongside the scenario). */
|
|
26
|
+
readonly out?: string;
|
|
27
|
+
/** Voice name (provider-specific). */
|
|
28
|
+
readonly voice?: string;
|
|
29
|
+
/** URL path prefix for the `src` fields written into the manifest. */
|
|
30
|
+
readonly baseUrl?: string;
|
|
31
|
+
/** Progress sink for per-step/per-scenario messages. */
|
|
32
|
+
readonly onLog?: (message: string) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Per-scenario narration outcome. */
|
|
36
|
+
export interface NarratedScenario {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly generated: number;
|
|
39
|
+
readonly cached: number;
|
|
40
|
+
/** True when the scenario had no narration text (nothing to synthesize). */
|
|
41
|
+
readonly skipped: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Aggregate result of a narrate run. */
|
|
45
|
+
export interface NarrateResult {
|
|
46
|
+
readonly mode: "file" | "directory";
|
|
47
|
+
readonly scenarios: NarratedScenario[];
|
|
48
|
+
readonly totalGenerated: number;
|
|
49
|
+
readonly totalCached: number;
|
|
50
|
+
readonly totalSkipped: number;
|
|
51
|
+
readonly errors: Array<{ id: string; message: string }>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface StepWithNarration {
|
|
55
|
+
index: number;
|
|
56
|
+
text: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function extractNarratedSteps(
|
|
60
|
+
steps: Array<{ narration?: string; narrationText?: string }>,
|
|
61
|
+
): StepWithNarration[] {
|
|
62
|
+
const result: StepWithNarration[] = [];
|
|
63
|
+
for (let i = 0; i < steps.length; i++) {
|
|
64
|
+
const text = steps[i]!.narration ?? steps[i]!.narrationText;
|
|
65
|
+
if (typeof text === "string" && text.length > 0) {
|
|
66
|
+
result.push({ index: i, text });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface RuntimeManifestEntry {
|
|
73
|
+
src: string;
|
|
74
|
+
durationMs: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface RuntimeManifest {
|
|
78
|
+
steps: (RuntimeManifestEntry | null)[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function buildRuntimeManifest(
|
|
82
|
+
totalSteps: number,
|
|
83
|
+
entries: Map<number, { src: string; durationMs: number }>,
|
|
84
|
+
): RuntimeManifest {
|
|
85
|
+
const steps: (RuntimeManifestEntry | null)[] = Array.from({ length: totalSteps }, () => null);
|
|
86
|
+
for (const [idx, entry] of entries) {
|
|
87
|
+
steps[idx] = entry;
|
|
88
|
+
}
|
|
89
|
+
return { steps };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function loadStepsFromFile(
|
|
93
|
+
filePath: string,
|
|
94
|
+
): Promise<{ steps: Array<{ narration?: string; narrationText?: string }>; totalSteps: number }> {
|
|
95
|
+
const ext = extname(filePath).toLowerCase();
|
|
96
|
+
|
|
97
|
+
if (ext === ".ts" || ext === ".tsx") {
|
|
98
|
+
const steps = await loadStepsFromTs(filePath);
|
|
99
|
+
return { steps, totalSteps: steps.length };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const scenario = await loadScenarioYaml(filePath);
|
|
103
|
+
const validation = validateScenario(scenario);
|
|
104
|
+
if (!validation.valid) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Scenario has ${validation.errors.length} error(s). Run 'scenar validate ${filePath}' for details.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const steps = (scenario as Record<string, unknown>)["steps"] as
|
|
111
|
+
| Array<Record<string, unknown>>
|
|
112
|
+
| undefined;
|
|
113
|
+
if (!steps) {
|
|
114
|
+
throw new Error("No steps found in scenario.");
|
|
115
|
+
}
|
|
116
|
+
return { steps: steps as Array<{ narrationText?: string }>, totalSteps: steps.length };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface GenerateOptions {
|
|
120
|
+
scenarioId: string;
|
|
121
|
+
narratedSteps: StepWithNarration[];
|
|
122
|
+
totalSteps: number;
|
|
123
|
+
outDir: string;
|
|
124
|
+
voice: string;
|
|
125
|
+
baseUrl?: string;
|
|
126
|
+
provider: TtsProvider;
|
|
127
|
+
onLog: (message: string) => void;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface GenerateStats {
|
|
131
|
+
generated: number;
|
|
132
|
+
cached: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function generateNarration(opts: GenerateOptions): Promise<GenerateStats> {
|
|
136
|
+
const { scenarioId, narratedSteps, totalSteps, outDir, voice, baseUrl, provider, onLog } = opts;
|
|
137
|
+
|
|
138
|
+
await mkdir(outDir, { recursive: true });
|
|
139
|
+
|
|
140
|
+
const existingCache = await loadCache(outDir);
|
|
141
|
+
const stats: GenerateStats = { generated: 0, cached: 0 };
|
|
142
|
+
const manifestEntries = new Map<number, { src: string; durationMs: number }>();
|
|
143
|
+
const cacheEntries = new Map<number, { hash: string; durationMs: number }>();
|
|
144
|
+
|
|
145
|
+
for (const step of narratedSteps) {
|
|
146
|
+
const hash = computeHash(step.text, voice);
|
|
147
|
+
const fileName = `step-${step.index}.mp3`;
|
|
148
|
+
const mp3Path = join(outDir, fileName);
|
|
149
|
+
|
|
150
|
+
const srcPrefix = baseUrl ? `${baseUrl.replace(/\/$/, "")}/${scenarioId}` : `.`;
|
|
151
|
+
const src = `${srcPrefix}/${fileName}`;
|
|
152
|
+
|
|
153
|
+
if (isCached(existingCache, step.index, hash, voice) && (await fileExists(mp3Path))) {
|
|
154
|
+
const durationMs = getCachedDuration(existingCache!, step.index);
|
|
155
|
+
manifestEntries.set(step.index, { src, durationMs });
|
|
156
|
+
cacheEntries.set(step.index, { hash, durationMs });
|
|
157
|
+
stats.cached++;
|
|
158
|
+
onLog(` step ${step.index}: cached`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
onLog(` step ${step.index}: generating...`);
|
|
163
|
+
const result = await provider.synthesize(step.text, { voice: voice || undefined });
|
|
164
|
+
|
|
165
|
+
await writeFile(mp3Path, result.audio);
|
|
166
|
+
manifestEntries.set(step.index, { src, durationMs: result.durationMs });
|
|
167
|
+
cacheEntries.set(step.index, { hash, durationMs: result.durationMs });
|
|
168
|
+
stats.generated++;
|
|
169
|
+
onLog(` step ${step.index}: ${result.durationMs}ms (${result.audio.length} bytes)`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const manifest = buildRuntimeManifest(totalSteps, manifestEntries);
|
|
173
|
+
await writeFile(join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
174
|
+
|
|
175
|
+
const cache = buildCacheFile(voice, totalSteps, cacheEntries);
|
|
176
|
+
await saveCache(outDir, cache);
|
|
177
|
+
|
|
178
|
+
onLog(`\x1b[32m+\x1b[0m ${scenarioId}: ${stats.generated} generated, ${stats.cached} cached`);
|
|
179
|
+
return stats;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Generate narration audio for a scenario file or a directory of scenarios —
|
|
184
|
+
* the pure orchestration behind `scenar narrate`. Resolves the TTS provider,
|
|
185
|
+
* synthesizes per-step audio (incremental via a content-hash cache), writes each
|
|
186
|
+
* scenario's manifest, and returns aggregate stats. No process/exit coupling.
|
|
187
|
+
*/
|
|
188
|
+
export async function runNarrate(options: RunNarrateOptions): Promise<NarrateResult> {
|
|
189
|
+
const onLog = options.onLog ?? (() => {});
|
|
190
|
+
const provider = await resolveProvider(options.tts ?? "echogarden");
|
|
191
|
+
const resolved = resolve(options.target);
|
|
192
|
+
const info = await stat(resolved);
|
|
193
|
+
|
|
194
|
+
return info.isDirectory()
|
|
195
|
+
? narrateDirectory(resolved, options, provider, onLog)
|
|
196
|
+
: narrateSingleFile(resolved, options, provider, onLog);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function narrateSingleFile(
|
|
200
|
+
filePath: string,
|
|
201
|
+
options: RunNarrateOptions,
|
|
202
|
+
provider: TtsProvider,
|
|
203
|
+
onLog: (message: string) => void,
|
|
204
|
+
): Promise<NarrateResult> {
|
|
205
|
+
const { steps, totalSteps } = await loadStepsFromFile(filePath);
|
|
206
|
+
const narratedSteps = extractNarratedSteps(steps);
|
|
207
|
+
const id = basename(dirname(filePath));
|
|
208
|
+
|
|
209
|
+
if (narratedSteps.length === 0) {
|
|
210
|
+
return {
|
|
211
|
+
mode: "file",
|
|
212
|
+
scenarios: [{ id, generated: 0, cached: 0, skipped: true }],
|
|
213
|
+
totalGenerated: 0,
|
|
214
|
+
totalCached: 0,
|
|
215
|
+
totalSkipped: 1,
|
|
216
|
+
errors: [],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const outDir = options.out ?? join(dirname(filePath), "narration");
|
|
221
|
+
const stats = await generateNarration({
|
|
222
|
+
scenarioId: id,
|
|
223
|
+
narratedSteps,
|
|
224
|
+
totalSteps,
|
|
225
|
+
outDir,
|
|
226
|
+
voice: options.voice ?? "",
|
|
227
|
+
baseUrl: options.baseUrl,
|
|
228
|
+
provider,
|
|
229
|
+
onLog,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
mode: "file",
|
|
234
|
+
scenarios: [{ id, generated: stats.generated, cached: stats.cached, skipped: false }],
|
|
235
|
+
totalGenerated: stats.generated,
|
|
236
|
+
totalCached: stats.cached,
|
|
237
|
+
totalSkipped: 0,
|
|
238
|
+
errors: [],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function narrateDirectory(
|
|
243
|
+
dirPath: string,
|
|
244
|
+
options: RunNarrateOptions,
|
|
245
|
+
provider: TtsProvider,
|
|
246
|
+
onLog: (message: string) => void,
|
|
247
|
+
): Promise<NarrateResult> {
|
|
248
|
+
const scenarios = await discoverScenarios(dirPath);
|
|
249
|
+
const result: NarratedScenario[] = [];
|
|
250
|
+
const errors: Array<{ id: string; message: string }> = [];
|
|
251
|
+
let totalGenerated = 0;
|
|
252
|
+
let totalCached = 0;
|
|
253
|
+
let totalSkipped = 0;
|
|
254
|
+
|
|
255
|
+
if (scenarios.length > 0) {
|
|
256
|
+
onLog(`Discovered ${scenarios.length} scenario(s)`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const scenario of scenarios) {
|
|
260
|
+
onLog(` ${scenario.id}`);
|
|
261
|
+
try {
|
|
262
|
+
const steps = await loadStepsFromTs(scenario.stepsPath);
|
|
263
|
+
const narratedSteps = extractNarratedSteps(steps);
|
|
264
|
+
|
|
265
|
+
if (narratedSteps.length === 0) {
|
|
266
|
+
onLog(" (no narration)");
|
|
267
|
+
totalSkipped++;
|
|
268
|
+
result.push({ id: scenario.id, generated: 0, cached: 0, skipped: true });
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const outDir = options.out
|
|
273
|
+
? join(options.out, scenario.id)
|
|
274
|
+
: join(dirPath, scenario.id, "narration");
|
|
275
|
+
|
|
276
|
+
const stats = await generateNarration({
|
|
277
|
+
scenarioId: scenario.id,
|
|
278
|
+
narratedSteps,
|
|
279
|
+
totalSteps: steps.length,
|
|
280
|
+
outDir,
|
|
281
|
+
voice: options.voice ?? "",
|
|
282
|
+
baseUrl: options.baseUrl,
|
|
283
|
+
provider,
|
|
284
|
+
onLog,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
totalGenerated += stats.generated;
|
|
288
|
+
totalCached += stats.cached;
|
|
289
|
+
result.push({ id: scenario.id, generated: stats.generated, cached: stats.cached, skipped: false });
|
|
290
|
+
} catch (error) {
|
|
291
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
292
|
+
onLog(` \x1b[31mfailed\x1b[0m: ${message}`);
|
|
293
|
+
errors.push({ id: scenario.id, message });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return { mode: "directory", scenarios: result, totalGenerated, totalCached, totalSkipped, errors };
|
|
298
|
+
}
|
|
@@ -30,6 +30,17 @@ export interface EmbedEntryInput {
|
|
|
30
30
|
* @scenar/react theme + styles so the bundle is self-contained, and scopes the
|
|
31
31
|
* tree with `SCENAR_CLASS` + `DemoViewport` exactly as the authoring surfaces do.
|
|
32
32
|
*
|
|
33
|
+
* Interactions: the entry wires the full host-side interaction system —
|
|
34
|
+
* `useStepInteractions` + `<Cursor>` + `<ViewportTransformLayer>` — so packed
|
|
35
|
+
* embeds run the same narration-synced cursor moves, clicks, typing, hovers,
|
|
36
|
+
* drags, scrolls, and viewport transitions as the in-app player. (`ScenarioPlayer`
|
|
37
|
+
* itself owns none of this; the host must wire it, per DemoViewport's contract.)
|
|
38
|
+
*
|
|
39
|
+
* Theme: the embed reads `?theme` from its own URL and applies the `dark` class
|
|
40
|
+
* alongside `SCENAR_CLASS` when `theme=dark` (matching the `.scenar.dark` token
|
|
41
|
+
* selector). The default is light, so existing embeds are unaffected; a host can
|
|
42
|
+
* theme-sync simply by framing `…/?theme=dark`.
|
|
43
|
+
*
|
|
33
44
|
* Pure function, no side effects — the caller writes the result to disk.
|
|
34
45
|
*/
|
|
35
46
|
export declare function generateEmbedEntry(input: EmbedEntryInput): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-embed-entry.d.ts","sourceRoot":"","sources":["../../../src/pack/generate-embed-entry.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAC;IACpB,yDAAyD;IACzD,cAAc,EAAE,MAAM,CAAC;IACvB,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,YAAY,EAAE,OAAO,CAAC;IACtB,+DAA+D;IAC/D,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,sEAAsE;IACtE,cAAc,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,WAAW,EAAE,MAAM,CAAC;CACrB;AAED
|
|
1
|
+
{"version":3,"file":"generate-embed-entry.d.ts","sourceRoot":"","sources":["../../../src/pack/generate-embed-entry.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAC;IACpB,yDAAyD;IACzD,cAAc,EAAE,MAAM,CAAC;IACvB,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,YAAY,EAAE,OAAO,CAAC;IACtB,+DAA+D;IAC/D,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,sEAAsE;IACtE,cAAc,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAiJjE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAgBnF"}
|
|
@@ -9,6 +9,17 @@ import { posix } from "node:path";
|
|
|
9
9
|
* @scenar/react theme + styles so the bundle is self-contained, and scopes the
|
|
10
10
|
* tree with `SCENAR_CLASS` + `DemoViewport` exactly as the authoring surfaces do.
|
|
11
11
|
*
|
|
12
|
+
* Interactions: the entry wires the full host-side interaction system —
|
|
13
|
+
* `useStepInteractions` + `<Cursor>` + `<ViewportTransformLayer>` — so packed
|
|
14
|
+
* embeds run the same narration-synced cursor moves, clicks, typing, hovers,
|
|
15
|
+
* drags, scrolls, and viewport transitions as the in-app player. (`ScenarioPlayer`
|
|
16
|
+
* itself owns none of this; the host must wire it, per DemoViewport's contract.)
|
|
17
|
+
*
|
|
18
|
+
* Theme: the embed reads `?theme` from its own URL and applies the `dark` class
|
|
19
|
+
* alongside `SCENAR_CLASS` when `theme=dark` (matching the `.scenar.dark` token
|
|
20
|
+
* selector). The default is light, so existing embeds are unaffected; a host can
|
|
21
|
+
* theme-sync simply by framing `…/?theme=dark`.
|
|
22
|
+
*
|
|
12
23
|
* Pure function, no side effects — the caller writes the result to disk.
|
|
13
24
|
*/
|
|
14
25
|
export function generateEmbedEntry(input) {
|
|
@@ -17,11 +28,24 @@ export function generateEmbedEntry(input) {
|
|
|
17
28
|
const renderImport = toPosix(input.renderFilePath).replace(/\.[^.]+$/, "");
|
|
18
29
|
const lines = [];
|
|
19
30
|
// --- Imports ---
|
|
31
|
+
// React hooks drive the host-side interaction system (the same one the
|
|
32
|
+
// in-app player wires): step tracking, cursor target, ripple/drag flags, and
|
|
33
|
+
// the viewport transform. Without these, the embed would render content but
|
|
34
|
+
// silently drop every cursor move and mid-step interaction.
|
|
35
|
+
lines.push(`import { useCallback, useRef, useState } from "react";`);
|
|
20
36
|
lines.push(`import { createRoot } from "react-dom/client";`);
|
|
21
|
-
const reactImports =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
37
|
+
const reactImports = [
|
|
38
|
+
"ScenarioPlayer",
|
|
39
|
+
"DemoViewport",
|
|
40
|
+
"SCENAR_CLASS",
|
|
41
|
+
"Cursor",
|
|
42
|
+
"useStepInteractions",
|
|
43
|
+
"ViewportTransformLayer",
|
|
44
|
+
"VIEWPORT_TRANSFORM_IDENTITY",
|
|
45
|
+
];
|
|
46
|
+
if (input.hasNarration)
|
|
47
|
+
reactImports.push("useNarrationManifest");
|
|
48
|
+
lines.push(`import { ${reactImports.join(", ")} } from "@scenar/react";`);
|
|
25
49
|
lines.push(`import "@scenar/react/theme.css";`);
|
|
26
50
|
lines.push(`import "@scenar/react/styles.css";`);
|
|
27
51
|
lines.push(`import { renderStep } from ${JSON.stringify(renderImport)};`);
|
|
@@ -54,26 +78,73 @@ export function generateEmbedEntry(input) {
|
|
|
54
78
|
lines.push(``);
|
|
55
79
|
lines.push(`const _resolveManifestUrl = () => "./narration/manifest.json";`);
|
|
56
80
|
}
|
|
81
|
+
// --- Theme: opt-in dark via ?theme=dark (default light; backward-compatible) ---
|
|
82
|
+
// Read once at module load from the embed's own URL. `theme=dark` adds the
|
|
83
|
+
// `dark` class next to SCENAR_CLASS so the `.scenar.dark` tokens apply; any
|
|
84
|
+
// other value (including absent) renders light, preserving prior behavior.
|
|
85
|
+
lines.push(``);
|
|
86
|
+
lines.push(`const _theme = (() => {`);
|
|
87
|
+
lines.push(` try { return new URLSearchParams(window.location.search).get("theme"); }`);
|
|
88
|
+
lines.push(` catch { return null; }`);
|
|
89
|
+
lines.push(`})();`);
|
|
90
|
+
lines.push(`const _rootClass = _theme === "dark" ? SCENAR_CLASS + " dark" : SCENAR_CLASS;`);
|
|
57
91
|
// --- App tree ---
|
|
92
|
+
// The structure mirrors the in-app demo wiring exactly (see DemoViewport's
|
|
93
|
+
// contract and ViewportTransformLayer's "Cursor must be a sibling" invariant):
|
|
94
|
+
// DemoViewport(containerRef)
|
|
95
|
+
// └ ViewportTransformLayer(transform)
|
|
96
|
+
// └ ScenarioPlayer(embed, onStepChange)
|
|
97
|
+
// └ Cursor(target, containerRef) ← sibling, not a child
|
|
98
|
+
// `useStepInteractions` reads the current step's `interactions` and drives the
|
|
99
|
+
// cursor / ripple / drag / viewport state, synced to narration duration.
|
|
100
|
+
const manifestExpr = input.hasNarration ? "_manifest" : "undefined";
|
|
58
101
|
lines.push(``);
|
|
59
102
|
lines.push(`function _App() {`);
|
|
60
103
|
if (input.hasNarration) {
|
|
61
104
|
lines.push(` const _manifest = useNarrationManifest(${JSON.stringify(input.scenarioId)}, _resolveManifestUrl);`);
|
|
62
105
|
}
|
|
106
|
+
lines.push(` const _containerRef = useRef<HTMLDivElement>(null);`);
|
|
107
|
+
lines.push(` const [_stepIndex, _setStepIndex] = useState(0);`);
|
|
108
|
+
lines.push(` const [_cursorTarget, _setCursorTarget] = useState<string | undefined>(undefined);`);
|
|
109
|
+
lines.push(` const [_showRipple, _setShowRipple] = useState(true);`);
|
|
110
|
+
lines.push(` const [_dragging, _setDragging] = useState(false);`);
|
|
111
|
+
lines.push(` const [_viewport, _setViewport] = useState(VIEWPORT_TRANSFORM_IDENTITY);`);
|
|
112
|
+
lines.push(``);
|
|
113
|
+
// Reset the cursor when the step changes so a prior step's target never
|
|
114
|
+
// lingers; the new step's interactions re-set it at their scheduled time.
|
|
115
|
+
lines.push(` const _handleStepChange = useCallback((_data: any, index: number) => {`);
|
|
116
|
+
lines.push(` _setStepIndex(index);`);
|
|
117
|
+
lines.push(` _setCursorTarget(undefined);`);
|
|
118
|
+
lines.push(` }, []);`);
|
|
119
|
+
lines.push(``);
|
|
120
|
+
lines.push(` useStepInteractions({`);
|
|
121
|
+
lines.push(` stepIndex: _stepIndex,`);
|
|
122
|
+
lines.push(` narrationManifest: ${manifestExpr},`);
|
|
123
|
+
lines.push(` containerRef: _containerRef,`);
|
|
124
|
+
lines.push(` setCursorTarget: _setCursorTarget,`);
|
|
125
|
+
lines.push(` setShowRipple: _setShowRipple,`);
|
|
126
|
+
lines.push(` setDragging: _setDragging,`);
|
|
127
|
+
lines.push(` setViewportTransform: _setViewport,`);
|
|
128
|
+
lines.push(` steps: _steps,`);
|
|
129
|
+
lines.push(` });`);
|
|
130
|
+
lines.push(``);
|
|
63
131
|
lines.push(` const _bundle = {`);
|
|
64
132
|
lines.push(` id: ${JSON.stringify(input.scenarioId)},`);
|
|
65
133
|
lines.push(` steps: _steps,`);
|
|
66
|
-
lines.push(` narrationManifest: ${
|
|
134
|
+
lines.push(` narrationManifest: ${manifestExpr},`);
|
|
67
135
|
lines.push(` };`);
|
|
68
136
|
const open = input.providersPath ? `<_Providers>` : ``;
|
|
69
137
|
const close = input.providersPath ? `</_Providers>` : ``;
|
|
70
138
|
lines.push(` return (`);
|
|
71
|
-
lines.push(` <div className={
|
|
139
|
+
lines.push(` <div className={_rootClass}>`);
|
|
72
140
|
lines.push(` ${open}`);
|
|
73
|
-
lines.push(` <DemoViewport canonicalWidth={${input.canonicalWidth}} shellHeight={${input.shellHeight}}>`);
|
|
74
|
-
lines.push(` <
|
|
75
|
-
lines.push(` {
|
|
76
|
-
lines.push(`
|
|
141
|
+
lines.push(` <DemoViewport containerRef={_containerRef} canonicalWidth={${input.canonicalWidth}} shellHeight={${input.shellHeight}}>`);
|
|
142
|
+
lines.push(` <ViewportTransformLayer transform={_viewport}>`);
|
|
143
|
+
lines.push(` <ScenarioPlayer bundle={_bundle} embed onStepChange={_handleStepChange}>`);
|
|
144
|
+
lines.push(` {(data: any, stepIndex: number) => renderStep(data, stepIndex)}`);
|
|
145
|
+
lines.push(` </ScenarioPlayer>`);
|
|
146
|
+
lines.push(` </ViewportTransformLayer>`);
|
|
147
|
+
lines.push(` <Cursor target={_cursorTarget} containerRef={_containerRef} showRipple={_showRipple} isDragging={_dragging} />`);
|
|
77
148
|
lines.push(` </DemoViewport>`);
|
|
78
149
|
lines.push(` ${close}`);
|
|
79
150
|
lines.push(` </div>`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-embed-entry.js","sourceRoot":"","sources":["../../../src/pack/generate-embed-entry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAyBlC
|
|
1
|
+
{"version":3,"file":"generate-embed-entry.js","sourceRoot":"","sources":["../../../src/pack/generate-embed-entry.ts"],"names":[],"mappings":"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"}
|
|
@@ -33,6 +33,17 @@ export interface EmbedEntryInput {
|
|
|
33
33
|
* @scenar/react theme + styles so the bundle is self-contained, and scopes the
|
|
34
34
|
* tree with `SCENAR_CLASS` + `DemoViewport` exactly as the authoring surfaces do.
|
|
35
35
|
*
|
|
36
|
+
* Interactions: the entry wires the full host-side interaction system —
|
|
37
|
+
* `useStepInteractions` + `<Cursor>` + `<ViewportTransformLayer>` — so packed
|
|
38
|
+
* embeds run the same narration-synced cursor moves, clicks, typing, hovers,
|
|
39
|
+
* drags, scrolls, and viewport transitions as the in-app player. (`ScenarioPlayer`
|
|
40
|
+
* itself owns none of this; the host must wire it, per DemoViewport's contract.)
|
|
41
|
+
*
|
|
42
|
+
* Theme: the embed reads `?theme` from its own URL and applies the `dark` class
|
|
43
|
+
* alongside `SCENAR_CLASS` when `theme=dark` (matching the `.scenar.dark` token
|
|
44
|
+
* selector). The default is light, so existing embeds are unaffected; a host can
|
|
45
|
+
* theme-sync simply by framing `…/?theme=dark`.
|
|
46
|
+
*
|
|
36
47
|
* Pure function, no side effects — the caller writes the result to disk.
|
|
37
48
|
*/
|
|
38
49
|
export function generateEmbedEntry(input: EmbedEntryInput): string {
|
|
@@ -43,11 +54,23 @@ export function generateEmbedEntry(input: EmbedEntryInput): string {
|
|
|
43
54
|
const lines: string[] = [];
|
|
44
55
|
|
|
45
56
|
// --- Imports ---
|
|
57
|
+
// React hooks drive the host-side interaction system (the same one the
|
|
58
|
+
// in-app player wires): step tracking, cursor target, ripple/drag flags, and
|
|
59
|
+
// the viewport transform. Without these, the embed would render content but
|
|
60
|
+
// silently drop every cursor move and mid-step interaction.
|
|
61
|
+
lines.push(`import { useCallback, useRef, useState } from "react";`);
|
|
46
62
|
lines.push(`import { createRoot } from "react-dom/client";`);
|
|
47
|
-
const reactImports =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
const reactImports = [
|
|
64
|
+
"ScenarioPlayer",
|
|
65
|
+
"DemoViewport",
|
|
66
|
+
"SCENAR_CLASS",
|
|
67
|
+
"Cursor",
|
|
68
|
+
"useStepInteractions",
|
|
69
|
+
"ViewportTransformLayer",
|
|
70
|
+
"VIEWPORT_TRANSFORM_IDENTITY",
|
|
71
|
+
];
|
|
72
|
+
if (input.hasNarration) reactImports.push("useNarrationManifest");
|
|
73
|
+
lines.push(`import { ${reactImports.join(", ")} } from "@scenar/react";`);
|
|
51
74
|
lines.push(`import "@scenar/react/theme.css";`);
|
|
52
75
|
lines.push(`import "@scenar/react/styles.css";`);
|
|
53
76
|
lines.push(`import { renderStep } from ${JSON.stringify(renderImport)};`);
|
|
@@ -86,26 +109,74 @@ export function generateEmbedEntry(input: EmbedEntryInput): string {
|
|
|
86
109
|
lines.push(`const _resolveManifestUrl = () => "./narration/manifest.json";`);
|
|
87
110
|
}
|
|
88
111
|
|
|
112
|
+
// --- Theme: opt-in dark via ?theme=dark (default light; backward-compatible) ---
|
|
113
|
+
// Read once at module load from the embed's own URL. `theme=dark` adds the
|
|
114
|
+
// `dark` class next to SCENAR_CLASS so the `.scenar.dark` tokens apply; any
|
|
115
|
+
// other value (including absent) renders light, preserving prior behavior.
|
|
116
|
+
lines.push(``);
|
|
117
|
+
lines.push(`const _theme = (() => {`);
|
|
118
|
+
lines.push(` try { return new URLSearchParams(window.location.search).get("theme"); }`);
|
|
119
|
+
lines.push(` catch { return null; }`);
|
|
120
|
+
lines.push(`})();`);
|
|
121
|
+
lines.push(`const _rootClass = _theme === "dark" ? SCENAR_CLASS + " dark" : SCENAR_CLASS;`);
|
|
122
|
+
|
|
89
123
|
// --- App tree ---
|
|
124
|
+
// The structure mirrors the in-app demo wiring exactly (see DemoViewport's
|
|
125
|
+
// contract and ViewportTransformLayer's "Cursor must be a sibling" invariant):
|
|
126
|
+
// DemoViewport(containerRef)
|
|
127
|
+
// └ ViewportTransformLayer(transform)
|
|
128
|
+
// └ ScenarioPlayer(embed, onStepChange)
|
|
129
|
+
// └ Cursor(target, containerRef) ← sibling, not a child
|
|
130
|
+
// `useStepInteractions` reads the current step's `interactions` and drives the
|
|
131
|
+
// cursor / ripple / drag / viewport state, synced to narration duration.
|
|
132
|
+
const manifestExpr = input.hasNarration ? "_manifest" : "undefined";
|
|
90
133
|
lines.push(``);
|
|
91
134
|
lines.push(`function _App() {`);
|
|
92
135
|
if (input.hasNarration) {
|
|
93
136
|
lines.push(` const _manifest = useNarrationManifest(${JSON.stringify(input.scenarioId)}, _resolveManifestUrl);`);
|
|
94
137
|
}
|
|
138
|
+
lines.push(` const _containerRef = useRef<HTMLDivElement>(null);`);
|
|
139
|
+
lines.push(` const [_stepIndex, _setStepIndex] = useState(0);`);
|
|
140
|
+
lines.push(` const [_cursorTarget, _setCursorTarget] = useState<string | undefined>(undefined);`);
|
|
141
|
+
lines.push(` const [_showRipple, _setShowRipple] = useState(true);`);
|
|
142
|
+
lines.push(` const [_dragging, _setDragging] = useState(false);`);
|
|
143
|
+
lines.push(` const [_viewport, _setViewport] = useState(VIEWPORT_TRANSFORM_IDENTITY);`);
|
|
144
|
+
lines.push(``);
|
|
145
|
+
// Reset the cursor when the step changes so a prior step's target never
|
|
146
|
+
// lingers; the new step's interactions re-set it at their scheduled time.
|
|
147
|
+
lines.push(` const _handleStepChange = useCallback((_data: any, index: number) => {`);
|
|
148
|
+
lines.push(` _setStepIndex(index);`);
|
|
149
|
+
lines.push(` _setCursorTarget(undefined);`);
|
|
150
|
+
lines.push(` }, []);`);
|
|
151
|
+
lines.push(``);
|
|
152
|
+
lines.push(` useStepInteractions({`);
|
|
153
|
+
lines.push(` stepIndex: _stepIndex,`);
|
|
154
|
+
lines.push(` narrationManifest: ${manifestExpr},`);
|
|
155
|
+
lines.push(` containerRef: _containerRef,`);
|
|
156
|
+
lines.push(` setCursorTarget: _setCursorTarget,`);
|
|
157
|
+
lines.push(` setShowRipple: _setShowRipple,`);
|
|
158
|
+
lines.push(` setDragging: _setDragging,`);
|
|
159
|
+
lines.push(` setViewportTransform: _setViewport,`);
|
|
160
|
+
lines.push(` steps: _steps,`);
|
|
161
|
+
lines.push(` });`);
|
|
162
|
+
lines.push(``);
|
|
95
163
|
lines.push(` const _bundle = {`);
|
|
96
164
|
lines.push(` id: ${JSON.stringify(input.scenarioId)},`);
|
|
97
165
|
lines.push(` steps: _steps,`);
|
|
98
|
-
lines.push(` narrationManifest: ${
|
|
166
|
+
lines.push(` narrationManifest: ${manifestExpr},`);
|
|
99
167
|
lines.push(` };`);
|
|
100
168
|
const open = input.providersPath ? `<_Providers>` : ``;
|
|
101
169
|
const close = input.providersPath ? `</_Providers>` : ``;
|
|
102
170
|
lines.push(` return (`);
|
|
103
|
-
lines.push(` <div className={
|
|
171
|
+
lines.push(` <div className={_rootClass}>`);
|
|
104
172
|
lines.push(` ${open}`);
|
|
105
|
-
lines.push(` <DemoViewport canonicalWidth={${input.canonicalWidth}} shellHeight={${input.shellHeight}}>`);
|
|
106
|
-
lines.push(` <
|
|
107
|
-
lines.push(` {
|
|
108
|
-
lines.push(`
|
|
173
|
+
lines.push(` <DemoViewport containerRef={_containerRef} canonicalWidth={${input.canonicalWidth}} shellHeight={${input.shellHeight}}>`);
|
|
174
|
+
lines.push(` <ViewportTransformLayer transform={_viewport}>`);
|
|
175
|
+
lines.push(` <ScenarioPlayer bundle={_bundle} embed onStepChange={_handleStepChange}>`);
|
|
176
|
+
lines.push(` {(data: any, stepIndex: number) => renderStep(data, stepIndex)}`);
|
|
177
|
+
lines.push(` </ScenarioPlayer>`);
|
|
178
|
+
lines.push(` </ViewportTransformLayer>`);
|
|
179
|
+
lines.push(` <Cursor target={_cursorTarget} containerRef={_containerRef} showRipple={_showRipple} isDragging={_dragging} />`);
|
|
109
180
|
lines.push(` </DemoViewport>`);
|
|
110
181
|
lines.push(` ${close}`);
|
|
111
182
|
lines.push(` </div>`);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type PackManifest } from "./pack-manifest.js";
|
|
2
|
+
/** Options for {@link runPack}. Paths may be relative; they are resolved here. */
|
|
3
|
+
export interface RunPackOptions {
|
|
4
|
+
/** Path to a scenario directory (must contain steps.ts + a renderStep export). */
|
|
5
|
+
readonly scenarioDir: string;
|
|
6
|
+
/** Output directory for the bundle (default: ./<scenario-id>-bundle). */
|
|
7
|
+
readonly outDir?: string;
|
|
8
|
+
/** Canonical viewport width in px (default: {@link DEFAULT_VIEWPORT}.width). */
|
|
9
|
+
readonly width?: number;
|
|
10
|
+
/** Shell/container height in px (default: {@link DEFAULT_VIEWPORT}.height). */
|
|
11
|
+
readonly shellHeight?: number;
|
|
12
|
+
/** Keep the generated entry directory for debugging (default: false). */
|
|
13
|
+
readonly keepTemp?: boolean;
|
|
14
|
+
/** Progress sink for mid-operation messages. */
|
|
15
|
+
readonly onLog?: (message: string) => void;
|
|
16
|
+
}
|
|
17
|
+
/** The outcome of a successful pack. */
|
|
18
|
+
export interface PackResult {
|
|
19
|
+
readonly scenarioId: string;
|
|
20
|
+
readonly outDir: string;
|
|
21
|
+
readonly renderFilePath: string;
|
|
22
|
+
readonly providersPath: string | null;
|
|
23
|
+
readonly hasNarration: boolean;
|
|
24
|
+
readonly manifest: PackManifest;
|
|
25
|
+
readonly totalBytes: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Bundle a scenario directory into a self-contained, hosted embed — the pure
|
|
29
|
+
* orchestration behind `scenar pack`, with no process/exit coupling so both the
|
|
30
|
+
* CLI command and the MCP server can call it. Throws on any failure (invalid
|
|
31
|
+
* directory, bundler error, allowlist violation); the caller formats it.
|
|
32
|
+
*/
|
|
33
|
+
export declare function runPack(options: RunPackOptions): Promise<PackResult>;
|
|
34
|
+
//# sourceMappingURL=run-pack.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-pack.d.ts","sourceRoot":"","sources":["../../../src/pack/run-pack.ts"],"names":[],"mappings":"AAMA,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,oBAAoB,CAAC;AAM5B,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,gDAAgD;IAChD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAED,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CAmF1E"}
|