@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.
- package/LICENSE +21 -0
- package/README.md +172 -2
- package/dist/app/CustomSlide.svelte +42 -0
- package/dist/app/CustomSlide.svelte.d.ts +60 -0
- package/dist/app/DeckApp.svelte +527 -0
- package/dist/app/DeckApp.svelte.d.ts +41 -0
- package/dist/app/FreeBlock.svelte +66 -0
- package/dist/app/FreeBlock.svelte.d.ts +10 -0
- package/dist/app/FreeSlide.svelte +135 -0
- package/dist/app/FreeSlide.svelte.d.ts +5 -0
- package/dist/app/context.d.ts +14 -0
- package/dist/app/context.js +11 -0
- package/dist/app/fit.d.ts +39 -0
- package/dist/app/fit.js +97 -0
- package/dist/app/free-prefetch.d.ts +3 -0
- package/dist/app/free-prefetch.js +13 -0
- package/dist/app/index.d.ts +6 -0
- package/dist/app/index.js +5 -0
- package/dist/app/interactive.d.ts +3 -0
- package/dist/app/interactive.js +25 -0
- package/dist/app/renderers.d.ts +14 -0
- package/dist/app/renderers.js +49 -0
- package/dist/app/stage-deck.d.ts +40 -0
- package/dist/app/stage-deck.js +65 -0
- package/dist/debug/BlockOutline.svelte +117 -0
- package/dist/debug/BlockOutline.svelte.d.ts +12 -0
- package/dist/debug/DeckDebugPanel.svelte +205 -0
- package/dist/debug/DeckDebugPanel.svelte.d.ts +18 -0
- package/dist/debug/availability.d.ts +19 -0
- package/dist/debug/availability.js +27 -0
- package/dist/debug/format.d.ts +12 -0
- package/dist/debug/format.js +39 -0
- package/dist/debug/index.d.ts +10 -0
- package/dist/debug/index.js +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -0
- package/dist/layouts/CustomSlideHost.svelte +97 -0
- package/dist/layouts/CustomSlideHost.svelte.d.ts +14 -0
- package/dist/layouts/custom-slides.d.ts +28 -0
- package/dist/layouts/custom-slides.js +20 -0
- package/dist/layouts/index.d.ts +7 -0
- package/dist/layouts/index.js +7 -0
- package/dist/player.css +6 -0
- package/dist/player.css.d.ts +1 -0
- package/dist/studio/DeckStudioPanel.svelte +261 -0
- package/dist/studio/DeckStudioPanel.svelte.d.ts +8 -0
- package/dist/studio/StudioIcon.svelte +9 -0
- package/dist/studio/StudioIcon.svelte.d.ts +6 -0
- package/dist/studio/index.d.ts +26 -0
- package/dist/studio/index.js +21 -0
- package/dist/studio/studio.svelte.d.ts +565 -0
- package/dist/studio/studio.svelte.js +85 -0
- package/dist/styles.css +5 -0
- package/dist/styles.css.d.ts +1 -0
- 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;
|