@protoyard/decks-ui 0.0.0-stage → 1.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -2
  3. package/dist/app/CustomSlide.svelte +42 -0
  4. package/dist/app/CustomSlide.svelte.d.ts +60 -0
  5. package/dist/app/DeckApp.svelte +527 -0
  6. package/dist/app/DeckApp.svelte.d.ts +41 -0
  7. package/dist/app/FreeBlock.svelte +66 -0
  8. package/dist/app/FreeBlock.svelte.d.ts +10 -0
  9. package/dist/app/FreeSlide.svelte +135 -0
  10. package/dist/app/FreeSlide.svelte.d.ts +5 -0
  11. package/dist/app/context.d.ts +14 -0
  12. package/dist/app/context.js +11 -0
  13. package/dist/app/fit.d.ts +39 -0
  14. package/dist/app/fit.js +97 -0
  15. package/dist/app/free-prefetch.d.ts +3 -0
  16. package/dist/app/free-prefetch.js +13 -0
  17. package/dist/app/index.d.ts +6 -0
  18. package/dist/app/index.js +5 -0
  19. package/dist/app/interactive.d.ts +3 -0
  20. package/dist/app/interactive.js +25 -0
  21. package/dist/app/renderers.d.ts +14 -0
  22. package/dist/app/renderers.js +49 -0
  23. package/dist/app/stage-deck.d.ts +40 -0
  24. package/dist/app/stage-deck.js +65 -0
  25. package/dist/debug/BlockOutline.svelte +117 -0
  26. package/dist/debug/BlockOutline.svelte.d.ts +12 -0
  27. package/dist/debug/DeckDebugPanel.svelte +205 -0
  28. package/dist/debug/DeckDebugPanel.svelte.d.ts +18 -0
  29. package/dist/debug/availability.d.ts +19 -0
  30. package/dist/debug/availability.js +27 -0
  31. package/dist/debug/format.d.ts +12 -0
  32. package/dist/debug/format.js +39 -0
  33. package/dist/debug/index.d.ts +10 -0
  34. package/dist/debug/index.js +10 -0
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +7 -0
  37. package/dist/layouts/CustomSlideHost.svelte +97 -0
  38. package/dist/layouts/CustomSlideHost.svelte.d.ts +14 -0
  39. package/dist/layouts/custom-slides.d.ts +28 -0
  40. package/dist/layouts/custom-slides.js +20 -0
  41. package/dist/layouts/index.d.ts +7 -0
  42. package/dist/layouts/index.js +7 -0
  43. package/dist/player.css +6 -0
  44. package/dist/player.css.d.ts +1 -0
  45. package/dist/studio/DeckStudioPanel.svelte +261 -0
  46. package/dist/studio/DeckStudioPanel.svelte.d.ts +8 -0
  47. package/dist/studio/StudioIcon.svelte +9 -0
  48. package/dist/studio/StudioIcon.svelte.d.ts +6 -0
  49. package/dist/studio/index.d.ts +26 -0
  50. package/dist/studio/index.js +21 -0
  51. package/dist/studio/studio.svelte.d.ts +565 -0
  52. package/dist/studio/studio.svelte.js +85 -0
  53. package/dist/styles.css +5 -0
  54. package/dist/styles.css.d.ts +1 -0
  55. package/package.json +106 -3
@@ -0,0 +1,527 @@
1
+ <script lang="ts">
2
+ import '@texuli/slides/styles.css';
3
+ import { Bug } from '@lucide/svelte';
4
+ import type { AgentReferenceSource } from '@protoyard/decks/agent';
5
+ import type { Deck } from '@protoyard/decks/schema';
6
+ import type { DeckStyleWarning } from '@protoyard/decks/style';
7
+ import { deckThemeKey } from '@protoyard/decks/style/key';
8
+ import {
9
+ type DeckController,
10
+ formatDeckHash,
11
+ parseDeckHash,
12
+ parseDeckRoute,
13
+ } from '@texuli/deck/engine';
14
+ import { DeckStageCore, type DeckThemeLoader } from '@texuli/slides/stage-core';
15
+ import { flushSync, onMount, untrack } from 'svelte';
16
+ import { Button, IconButton } from 'texuli/button';
17
+ import { LucideIcon } from 'texuli/icons/lucide';
18
+ import { type DeckDebugSetting, deckDebugAvailable, isDevBuild } from '../debug/availability.js';
19
+ import type { CustomSlideRegistry } from '../layouts/custom-slides.js';
20
+ import { setDeckHostContext } from './context.js';
21
+ import { createProtoyardRenderers } from './renderers.js';
22
+ import {
23
+ prepareStageDeck,
24
+ stageThemeKey,
25
+ themeKeyOf,
26
+ stageDeck as toStageDeck,
27
+ } from './stage-deck.js';
28
+
29
+ /**
30
+ * The one deck component the player's deck entry and generated deck
31
+ * prototypes mount (ADR-038), presented by the `@texuli/slides` stage core
32
+ * (`DeckStageCore`, Texuli ADR-009): the stage without its default renderer
33
+ * tables, which a Protoyard deck never uses, so the build emits none of
34
+ * their chunks or CSS. The stage owns the canvas, routing
35
+ * (`#/<slide-id>/<step>`), keys, touch, the toolbar, overview, presenter,
36
+ * blackout, laser, help, copy link, print and transitions. DeckApp adds what
37
+ * is Protoyard's own:
38
+ *
39
+ * - the Protoyard deck shape, migrated to the Texuli deck model and prepared
40
+ * once (`prepareDeckV2` with Protoyard's block check), so the stage paints
41
+ * it on its first render without validating it again;
42
+ * - Protoyard themes, palette seeds and font pairings (`@protoyard/themes`);
43
+ * - free slides through `@protoyard/blocks-ui` (persistent block state,
44
+ * embedded apps, the viewer id) and the generated deck's custom slides;
45
+ * - the "Debug for agents" panel (D, or its button in the stage toolbar's
46
+ * host tools) and the print page for `?print=1`.
47
+ */
48
+ let {
49
+ deck,
50
+ class: className = '',
51
+ syncHash = true,
52
+ autofocus = true,
53
+ namespace = 'deck',
54
+ mode = 'light',
55
+ fit = 'viewport',
56
+ debug = 'auto',
57
+ debugSource = null,
58
+ customSlides,
59
+ resolveAvatar,
60
+ }: {
61
+ /** Validated deck document (`DeckSchema`). */
62
+ deck: Deck;
63
+ class?: string;
64
+ /** When true (default), keep `location.hash` in sync with the slide and step. */
65
+ syncHash?: boolean;
66
+ /** Focus the active slide on mount so keyboard navigation works immediately. */
67
+ autofocus?: boolean;
68
+ /** Presenter channel, view-transition and first-visit hint namespace. */
69
+ namespace?: string;
70
+ /** Appearance mode for the deck's theme. Decks default to light for stable exports. */
71
+ mode?: 'light' | 'dark';
72
+ /**
73
+ * `viewport` (default): the deck takes the app height (`h-app`: the window,
74
+ * or the device screen inside a shell frame). `container`: it fills a parent
75
+ * the host has already sized.
76
+ */
77
+ fit?: 'viewport' | 'container';
78
+ /**
79
+ * The "Debug for agents" panel: `auto` (default) in dev builds or with
80
+ * `?debug=1`; `on` always; `off` never.
81
+ */
82
+ debug?: DeckDebugSetting;
83
+ /** Where the deck came from, for the agent reference (a blueprint path, a prototype slug). */
84
+ debugSource?: AgentReferenceSource | null;
85
+ /**
86
+ * A generated deck's own slide components (`src/custom-slides.ts`), for
87
+ * `layout: "custom"` slides. Without it (the player) they show a labelled card.
88
+ */
89
+ customSlides?: CustomSlideRegistry;
90
+ /** Resolves a library avatar id (`av-012`) to its image URL (team and people slides). */
91
+ resolveAvatar?: (id: string) => string | undefined;
92
+ } = $props();
93
+
94
+ const stage = $derived(toStageDeck(deck));
95
+ const renderers = $derived(createProtoyardRenderers(stage.freeSlides));
96
+
97
+ setDeckHostContext({
98
+ get customSlides() {
99
+ return customSlides;
100
+ },
101
+ get freeSlides() {
102
+ return stage.freeSlides;
103
+ },
104
+ });
105
+
106
+ /**
107
+ * The deck the stage presents: migrated to the deck model and validated once with
108
+ * Protoyard's block check. A prepared deck at the `developer` tier paints on the
109
+ * stage's first render, with no migration or validation chunk. A deck that does not
110
+ * prepare goes to the stage as it is, which shows its diagnostics instead of slides.
111
+ */
112
+ const prepared = $derived(prepareStageDeck(stage.input));
113
+ const stageDeck = $derived(prepared.ok ? prepared : stage.input);
114
+ /** What the print view paints: the prepared model v2 deck. */
115
+ const printDeck = $derived(prepared.ok ? prepared.deck : stage.input);
116
+
117
+ /**
118
+ * Style warnings per stage theme key. The stage caches loaded themes by key
119
+ * (the key carries the font pair, see `stageThemeKey`), so switching back to a
120
+ * key it already has does not call `loadTheme` again: the banner reads the
121
+ * warnings of the key the deck asks for now.
122
+ */
123
+ let warningsByKey = $state<Record<string, DeckStyleWarning[]>>({});
124
+ const styleWarnings = $derived(warningsByKey[stageThemeKey(deck.style)] ?? []);
125
+ const loadTheme: DeckThemeLoader = async (id) => {
126
+ // The theme registry and the palette engine load with the first theme, off the deck entry.
127
+ const { loadDeckTheme } = await import('@protoyard/decks/style');
128
+ const fontPair = deck.style?.fontPair;
129
+ const loaded = await loadDeckTheme(themeKeyOf(id, fontPair), fontPair);
130
+ warningsByKey = { ...warningsByKey, [id]: loaded.warnings };
131
+ return loaded.theme as unknown as Awaited<ReturnType<DeckThemeLoader>>;
132
+ };
133
+
134
+ const debugAvailable = $derived(
135
+ deckDebugAvailable(debug, {
136
+ dev: isDevBuild(),
137
+ search: typeof window === 'undefined' ? '' : window.location.search,
138
+ })
139
+ );
140
+ let debugOpen = $state(false);
141
+ let presenterOpen = $state(false);
142
+ let rootEl = $state<HTMLDivElement | undefined>();
143
+ let controller = $state.raw<DeckController | null>(null);
144
+ let slideId = $state(untrack(() => deck.slides[0]?.id ?? ''));
145
+ let step = $state(0);
146
+
147
+ const route = typeof window === 'undefined' ? null : parseDeckRoute('', window.location.search);
148
+ const printing = route?.flags.print === true;
149
+ /**
150
+ * The browser's own print (Ctrl/Cmd+P): the one-slide-per-page sheet replaces
151
+ * the stage on paper (see the print CSS below), and the deck root is released
152
+ * from its one-screen height so every page prints, but only once the sheet
153
+ * is ready (`data-print-ready`): until then the paper shows the stage as on
154
+ * screen, never a blank page. The print view's code is preloaded while the
155
+ * browser is idle (`preloadDeckPrintViewCore`), so the sheet mounted on
156
+ * `beforeprint` renders its pages at once; it still settles themes, images
157
+ * and fonts asynchronously after the browser has captured the page, so the
158
+ * first Ctrl/Cmd+P prints the current slide and says so. The sheet then stays
159
+ * mounted, laid out off-screen and hidden, so every later print has all the
160
+ * slides. It is mounted on the first print only: mounting every slide (and
161
+ * its embedded apps) a second time on load would cost every reader.
162
+ *
163
+ * The deck root is a print isolation target (`data-tx-print-isolate`) as well
164
+ * as the sheet: while the sheet is mounted, Texuli's isolation hides
165
+ * everything outside its targets, and the stage must stay printable until the
166
+ * sheet is ready. Outside the deck root (a host page, a shell frame) nothing
167
+ * prints.
168
+ */
169
+ let printSheet = $state(false);
170
+ /** The print that just ran had only the stage: the sheet was not ready yet. */
171
+ let printedStage = false;
172
+ let exporter = $state.raw<typeof import('@texuli/slides/export-core') | null>(null);
173
+ const printNotes = route?.flags.notes === true ? 'appendix' : 'none';
174
+
175
+ // The host attributes (`data-slide-id`, `data-slide-step`) and the debug panel follow the
176
+ // stage's controller: its first position (a deep link) and every move after it.
177
+ $effect(() => {
178
+ const stage = controller;
179
+ if (!stage) return;
180
+ const follow = (state: { slideId: string; step: number }) => {
181
+ slideId = state.slideId;
182
+ step = state.step;
183
+ };
184
+ follow(stage.getState());
185
+ return stage.subscribe((state) => follow(state));
186
+ });
187
+
188
+ function toggleDebug(): void {
189
+ if (!debugAvailable) return;
190
+ debugOpen = !debugOpen;
191
+ }
192
+
193
+ function jumpToSlide(id: string): void {
194
+ controller?.goTo(id);
195
+ }
196
+
197
+ function isEditable(target: EventTarget | null): boolean {
198
+ if (!(target instanceof HTMLElement)) return false;
199
+ return (
200
+ target.isContentEditable ||
201
+ target.closest('input, textarea, select, [contenteditable="true"]') !== null
202
+ );
203
+ }
204
+
205
+ /** D toggles the debug panel (where it is available); Esc closes it first, before the stage's layers. */
206
+ function onKeyDown(event: KeyboardEvent): void {
207
+ if (!debugAvailable || presenterOpen) return;
208
+ if (event.ctrlKey || event.metaKey || event.altKey || isEditable(event.target)) return;
209
+ if (event.key === 'd' || event.key === 'D') {
210
+ event.preventDefault();
211
+ toggleDebug();
212
+ } else if (event.key === 'Escape' && debugOpen) {
213
+ // The panel is the innermost layer: Esc closes it before any stage layer.
214
+ event.preventDefault();
215
+ event.stopImmediatePropagation();
216
+ debugOpen = false;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * A followed hash that names a step past the slide's last one (`#/loop/9`) shows
222
+ * the last step; write that position back so the address bar, a copied link
223
+ * and the presenter agree (the stage clamps the position but keeps the hash).
224
+ */
225
+ function canonicalHash(): void {
226
+ const stage = controller;
227
+ if (!syncHash || !stage || typeof window === 'undefined') return;
228
+ const state = stage.getState();
229
+ const asked = parseDeckHash(window.location.hash);
230
+ // Only a step past the slide's last one, once the stage has followed the hash to that slide.
231
+ if (!asked || asked.slideId !== state.slideId || asked.step <= state.stepCount) return;
232
+ const canonical = formatDeckHash(state.slideId, state.step);
233
+ if (window.location.hash !== canonical) history.replaceState(history.state, '', canonical);
234
+ }
235
+
236
+ $effect(() => {
237
+ if (!syncHash || !controller) return;
238
+ // After the stage's own hashchange handling (and a view transition's update).
239
+ const follow = () => setTimeout(canonicalHash, 50);
240
+ follow();
241
+ window.addEventListener('hashchange', follow);
242
+ return () => window.removeEventListener('hashchange', follow);
243
+ });
244
+
245
+ /**
246
+ * The print view module with everything it imports on first use (its body and slide
247
+ * painter, G81), so a view mounted after it renders its pages on the first render.
248
+ * DeckApp paints with its own renderer tables only, as the stage does: the core
249
+ * entry (G105) never references the stage's default tables, so the build emits
250
+ * none of their chunks or CSS.
251
+ */
252
+ async function loadPrintView(): Promise<typeof import('@texuli/slides/export-core')> {
253
+ const module = await import('@texuli/slides/export-core');
254
+ await module.preloadDeckPrintViewCore();
255
+ return module;
256
+ }
257
+
258
+ let toast = $state('');
259
+ let toastTimer: ReturnType<typeof setTimeout> | undefined;
260
+ function notify(message: string): void {
261
+ toast = message;
262
+ clearTimeout(toastTimer);
263
+ toastTimer = setTimeout(() => {
264
+ toast = '';
265
+ }, 2_500);
266
+ }
267
+
268
+ onMount(() => {
269
+ // The print view loads while the browser is idle, so Ctrl/Cmd+P finds it ready.
270
+ let warming = false;
271
+ const warm = () => {
272
+ if (warming) return;
273
+ warming = true;
274
+ void loadPrintView().then(
275
+ (module) => {
276
+ exporter = module;
277
+ },
278
+ () => {
279
+ warming = false;
280
+ }
281
+ );
282
+ };
283
+ const hasIdle = 'requestIdleCallback' in window;
284
+ const idle = hasIdle
285
+ ? window.requestIdleCallback(warm, { timeout: 4_000 })
286
+ : window.setTimeout(warm, 1_500);
287
+ const beforePrint = () => {
288
+ printSheet = true;
289
+ warm();
290
+ // The pages must be in the document before the browser captures it.
291
+ flushSync();
292
+ printedStage = !rootEl?.querySelector(
293
+ ':scope > .deck-app__print-sheet [data-print-ready="true"]'
294
+ );
295
+ };
296
+ const afterPrint = () => {
297
+ if (printedStage) {
298
+ notify(
299
+ 'Printed the current slide while the print view got ready. Print again for every slide.'
300
+ );
301
+ }
302
+ printedStage = false;
303
+ };
304
+ window.addEventListener('beforeprint', beforePrint);
305
+ window.addEventListener('afterprint', afterPrint);
306
+ // Capture: the debug panel answers before the stage's own key handling. Always registered:
307
+ // `debug` can turn on after mount, and the handler ignores keys while it is unavailable.
308
+ window.addEventListener('keydown', onKeyDown, true);
309
+ return () => {
310
+ if (hasIdle) window.cancelIdleCallback(idle);
311
+ else window.clearTimeout(idle);
312
+ window.removeEventListener('beforeprint', beforePrint);
313
+ window.removeEventListener('afterprint', afterPrint);
314
+ window.removeEventListener('keydown', onKeyDown, true);
315
+ clearTimeout(toastTimer);
316
+ };
317
+ });
318
+ </script>
319
+
320
+ {#if printing}
321
+ <!-- `?print=1` (the stage's print button opens it): one slide per page, ready for Save as PDF. -->
322
+ {#await loadPrintView()}
323
+ <p class="deck-app__print-status" role="status" data-py-deck-print-loading>Preparing the slides for printing…</p>
324
+ {:then exporter}
325
+ <div class="deck-app__print" data-py-deck-print data-tx-mode={mode}>
326
+ <exporter.DeckPrintViewCore
327
+ deck={printDeck as never}
328
+ notes={printNotes}
329
+ {renderers}
330
+ tier="developer"
331
+ {loadTheme}
332
+ {resolveAvatar}
333
+ />
334
+ </div>
335
+ {:catch}
336
+ <div class="deck-app__print-status" role="alert" data-py-deck-print-error>
337
+ <p>The print view could not be loaded. Check the connection, then try again.</p>
338
+ <Button variant="outline" class="justify-self-center" onclick={() => window.location.reload()}>Reload</Button>
339
+ </div>
340
+ {/await}
341
+ {:else}
342
+ <div
343
+ bind:this={rootEl}
344
+ class={`deck-app relative flex w-full min-h-0 flex-col overflow-hidden bg-background text-foreground ${fit === 'container' ? 'h-full' : 'h-app'} ${className}`}
345
+ data-py-deck-app
346
+ data-py-deck-fit={fit}
347
+ data-slide-id={slideId}
348
+ data-slide-step={step}
349
+ data-presenter={presenterOpen ? 'true' : undefined}
350
+ data-py-deck-print-ready={exporter ? 'true' : undefined}
351
+ data-tx-print-isolate="true"
352
+ data-py-deck-motion={deck.style?.motion ?? 'default'}
353
+ data-py-deck-theme={deckThemeKey(deck.style)}
354
+ data-py-deck-mode={mode}
355
+ data-tx-mode={mode}
356
+ >
357
+ <DeckStageCore
358
+ deck={stageDeck}
359
+ fit="container"
360
+ tier="developer"
361
+ trustBadge={false}
362
+ {syncHash}
363
+ {autofocus}
364
+ {namespace}
365
+ {loadTheme}
366
+ {resolveAvatar}
367
+ {renderers}
368
+ defaultRenderers={false}
369
+ presenterTimer
370
+ bind:presenter={presenterOpen}
371
+ bind:controller
372
+ tools={debugAvailable ? debugTool : undefined}
373
+ />
374
+
375
+ {#if styleWarnings.length > 0}
376
+ <p
377
+ class="shrink-0 border-t border-border bg-background px-3 py-1.5 text-xs text-muted-foreground"
378
+ role="status"
379
+ data-py-deck-style-warning
380
+ >
381
+ {styleWarnings.map((warning) => warning.message).join(' ')}
382
+ </p>
383
+ {/if}
384
+
385
+ {#if debugOpen && debugAvailable && !presenterOpen}
386
+ {#await import('../debug/DeckDebugPanel.svelte')}
387
+ <div class="absolute inset-y-0 right-0 z-40 grid w-full max-w-md place-items-center border-l border-border bg-background" role="status" data-py-deck-debug-loading data-tx-deck-chrome>
388
+ <span class="text-sm text-muted-foreground">Loading the debug panel…</span>
389
+ </div>
390
+ {:then mod}
391
+ <mod.default
392
+ {deck}
393
+ {slideId}
394
+ {step}
395
+ root={rootEl}
396
+ source={debugSource}
397
+ onjump={jumpToSlide}
398
+ onclose={() => {
399
+ debugOpen = false;
400
+ }}
401
+ onnotify={(message) => notify(message)}
402
+ />
403
+ {:catch}
404
+ <div class="absolute inset-y-0 right-0 z-40 grid w-full max-w-md place-items-center border-l border-border bg-background p-6" role="alert" data-tx-deck-chrome>
405
+ <p class="text-sm text-destructive">The debug panel could not be loaded.</p>
406
+ </div>
407
+ {/await}
408
+ {/if}
409
+
410
+ {#if printSheet && exporter}
411
+ <!-- Paper only: every visible slide, one per page. On screen it is laid out off-screen and
412
+ hidden, so it settles (images, fonts, lazy families) before the next print. -->
413
+ <div class="deck-app__print-sheet" data-py-deck-print data-tx-mode={mode}>
414
+ <exporter.DeckPrintViewCore
415
+ deck={printDeck as never}
416
+ {renderers}
417
+ tier="developer"
418
+ {loadTheme}
419
+ {resolveAvatar}
420
+ />
421
+ </div>
422
+ {/if}
423
+
424
+ <!-- The debug panel's copy and download results, visible and announced. -->
425
+ <p
426
+ class={`pointer-events-none absolute inset-x-0 bottom-20 z-50 mx-auto w-fit max-w-md rounded-md bg-foreground px-3 py-1.5 text-sm text-background ${toast ? '' : 'sr-only'}`}
427
+ role="status"
428
+ aria-live="polite"
429
+ aria-atomic="true"
430
+ data-py-deck-toast
431
+ >
432
+ {toast}
433
+ </p>
434
+ </div>
435
+ {/if}
436
+
437
+ {#snippet debugTool()}
438
+ <!-- With the debug panel available, the stage toolbar gains its button as a host tool (G82). -->
439
+ <IconButton
440
+ variant="ghost"
441
+ class="deck-app__debug"
442
+ label="Debug for agents"
443
+ aria-pressed={debugOpen}
444
+ aria-keyshortcuts="D"
445
+ title="Debug for agents (D)"
446
+ data-py-deck-tool="debug"
447
+ onclick={toggleDebug}
448
+ >
449
+ <LucideIcon icon={Bug} size="sm" />
450
+ </IconButton>
451
+ {/snippet}
452
+
453
+ <style>
454
+ .deck-app :global(.tx-deck-stage) {
455
+ flex: 1;
456
+ min-block-size: 0;
457
+ }
458
+
459
+ /*
460
+ * The debug button is a Texuli IconButton (its own size, hover and focus ring) in the
461
+ * stage toolbar; the pressed state shows while the panel is open. Global: the class
462
+ * lands on the IconButton's element, outside this component's scope.
463
+ */
464
+ .deck-app :global(.deck-app__debug[aria-pressed='true']) {
465
+ background: var(--tx-muted);
466
+ }
467
+
468
+ .deck-app__print {
469
+ min-block-size: 100%;
470
+ }
471
+
472
+ .deck-app__print-status {
473
+ display: grid;
474
+ gap: 0.75rem;
475
+ place-content: center;
476
+ min-block-size: 100dvh;
477
+ padding: 1.5rem;
478
+ color: var(--tx-foreground);
479
+ text-align: center;
480
+ }
481
+
482
+ /* Laid out (not `display: none`), so the print view can measure and settle; never seen or focused. */
483
+ @media screen {
484
+ .deck-app__print-sheet {
485
+ position: fixed;
486
+ inset-block-start: 0;
487
+ inset-inline-start: -200vw;
488
+ inline-size: 100vw;
489
+ visibility: hidden;
490
+ pointer-events: none;
491
+ }
492
+ }
493
+
494
+ /*
495
+ * Ctrl/Cmd+P with the sheet ready: the sheet alone goes on paper, and the
496
+ * deck root (and a viewport preset's or frame's locked app column) is
497
+ * released from its one-screen height, so every page prints. A sheet that is
498
+ * not ready yet stays off the paper and the stage prints instead. Global
499
+ * selectors: Svelte's scoping would drop rules about the sheet's content.
500
+ */
501
+ @media print {
502
+ :global(.deck-app__print-sheet:not(:has([data-print-ready='true']))) {
503
+ display: none !important;
504
+ }
505
+
506
+ :global(.deck-app:has(> .deck-app__print-sheet [data-print-ready='true'])) {
507
+ display: block !important;
508
+ block-size: auto !important;
509
+ max-block-size: none !important;
510
+ overflow: visible !important;
511
+ }
512
+
513
+ :global(.deck-app:has(> .deck-app__print-sheet [data-print-ready='true']) > :not(.deck-app__print-sheet)) {
514
+ display: none !important;
515
+ }
516
+
517
+ :global(html:has(.deck-app > .deck-app__print-sheet [data-print-ready='true'])),
518
+ :global(html:has(.deck-app > .deck-app__print-sheet [data-print-ready='true']) body),
519
+ :global(html:has(.deck-app > .deck-app__print-sheet [data-print-ready='true']) #app),
520
+ :global(html:has(.deck-app > .deck-app__print-sheet [data-print-ready='true']) [data-py-scroll-root]) {
521
+ block-size: auto !important;
522
+ max-block-size: none !important;
523
+ overflow: visible !important;
524
+ transform: none !important;
525
+ }
526
+ }
527
+ </style>
@@ -0,0 +1,41 @@
1
+ import '@texuli/slides/styles.css';
2
+ import type { AgentReferenceSource } from '@protoyard/decks/agent';
3
+ import type { Deck } from '@protoyard/decks/schema';
4
+ import { type DeckDebugSetting } from '../debug/availability.ts';
5
+ import type { CustomSlideRegistry } from '../layouts/custom-slides.ts';
6
+ type $$ComponentProps = {
7
+ /** Validated deck document (`DeckSchema`). */
8
+ deck: Deck;
9
+ class?: string;
10
+ /** When true (default), keep `location.hash` in sync with the slide and step. */
11
+ syncHash?: boolean;
12
+ /** Focus the active slide on mount so keyboard navigation works immediately. */
13
+ autofocus?: boolean;
14
+ /** Presenter channel, view-transition and first-visit hint namespace. */
15
+ namespace?: string;
16
+ /** Appearance mode for the deck's theme. Decks default to light for stable exports. */
17
+ mode?: 'light' | 'dark';
18
+ /**
19
+ * `viewport` (default): the deck takes the app height (`h-app`: the window,
20
+ * or the device screen inside a shell frame). `container`: it fills a parent
21
+ * the host has already sized.
22
+ */
23
+ fit?: 'viewport' | 'container';
24
+ /**
25
+ * The "Debug for agents" panel: `auto` (default) in dev builds or with
26
+ * `?debug=1`; `on` always; `off` never.
27
+ */
28
+ debug?: DeckDebugSetting;
29
+ /** Where the deck came from, for the agent reference (a blueprint path, a prototype slug). */
30
+ debugSource?: AgentReferenceSource | null;
31
+ /**
32
+ * A generated deck's own slide components (`src/custom-slides.ts`), for
33
+ * `layout: "custom"` slides. Without it (the player) they show a labelled card.
34
+ */
35
+ customSlides?: CustomSlideRegistry;
36
+ /** Resolves a library avatar id (`av-012`) to its image URL (team and people slides). */
37
+ resolveAvatar?: (id: string) => string | undefined;
38
+ };
39
+ declare const DeckApp: import("svelte").Component<$$ComponentProps, {}, "">;
40
+ type DeckApp = ReturnType<typeof DeckApp>;
41
+ export default DeckApp;
@@ -0,0 +1,66 @@
1
+ <script lang="ts">
2
+ import type { Block } from '@protoyard/blocks';
3
+ import { BlockRenderer, getBlockHost, setBlockHost } from '@protoyard/blocks-ui';
4
+ import { DECK_INTERACTIVE_ATTRIBUTE } from '@texuli/deck/engine';
5
+ import type { DeckStepContext } from '@texuli/slides/stage-core';
6
+ import { containsDeckControl } from './interactive.js';
7
+
8
+ /**
9
+ * One top-level block of a free slide. The `@texuli/slides` step context
10
+ * owns the reveal: the box-less `tx-deck-stage__fragment` wrapper carries its
11
+ * phase and effect, so the stage's fragment styles animate the block. The
12
+ * Protoyard block host passes the surrounding host's state, embeds, viewer
13
+ * and density through, lays the block out for the slide canvas, and moves
14
+ * code highlights with the step.
15
+ */
16
+ let { block, index, step }: { block: Block; index: number; step: DeckStepContext } = $props();
17
+
18
+ const path = $derived(['blocks', index] as const);
19
+ const parent = getBlockHost();
20
+ setBlockHost({
21
+ get stateHost() {
22
+ return parent.stateHost;
23
+ },
24
+ get mountEmbed() {
25
+ return parent.mountEmbed;
26
+ },
27
+ get density() {
28
+ return parent.density;
29
+ },
30
+ // A deck slide is a fixed-size canvas: blocks lay out for the slide, never the viewport.
31
+ get canvasMode() {
32
+ return 'slide' as const;
33
+ },
34
+ get documentId() {
35
+ return parent.documentId;
36
+ },
37
+ get viewerId() {
38
+ return parent.viewerId;
39
+ },
40
+ get onCtaAction() {
41
+ return parent.onCtaAction;
42
+ },
43
+ get step() {
44
+ return step.step;
45
+ },
46
+ // The wrapper reveals the block; nested blocks are never stepped on their own.
47
+ get fragment() {
48
+ return () => null;
49
+ },
50
+ get codeHighlight() {
51
+ return () => step.codeHighlight(path)?.index ?? 0;
52
+ },
53
+ });
54
+ const guard = $derived(containsDeckControl(block) ? { [DECK_INTERACTIVE_ATTRIBUTE]: '' } : {});
55
+ </script>
56
+
57
+ <div
58
+ class="tx-deck-stage__fragment"
59
+ data-tx-deck-item={index}
60
+ data-tx-deck-morph-id={`${block.type}-${index}`}
61
+ {...step.attributes(path)}
62
+ >
63
+ <div class="min-w-0" {...guard}>
64
+ <BlockRenderer blocks={[block]} />
65
+ </div>
66
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { Block } from '@protoyard/blocks';
2
+ import type { DeckStepContext } from '@texuli/slides/stage-core';
3
+ type $$ComponentProps = {
4
+ block: Block;
5
+ index: number;
6
+ step: DeckStepContext;
7
+ };
8
+ declare const FreeBlock: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type FreeBlock = ReturnType<typeof FreeBlock>;
10
+ export default FreeBlock;