@protoyard/decks-ui 0.0.0-stage → 1.1.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/LICENSE +21 -0
- package/README.md +170 -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,12 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
/** The deck root the stage renders in. */
|
|
3
|
+
root: HTMLElement | undefined;
|
|
4
|
+
slideId: string;
|
|
5
|
+
/** `/deck/slides/3`: block pointers are `<slidePointer>/blocks/<n>`. */
|
|
6
|
+
slidePointer: string;
|
|
7
|
+
/** The props pointer of a catalog slide, or null for a free slide. */
|
|
8
|
+
propsPointer: string | null;
|
|
9
|
+
};
|
|
10
|
+
declare const BlockOutline: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type BlockOutline = ReturnType<typeof BlockOutline>;
|
|
12
|
+
export default BlockOutline;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
type AgentReferenceSource,
|
|
4
|
+
describeSlideForAgent,
|
|
5
|
+
formatAgentReference,
|
|
6
|
+
} from '@protoyard/decks/agent';
|
|
7
|
+
import { checkDeckStatic } from '@protoyard/decks/quality';
|
|
8
|
+
import type { Deck, DeckSlide } from '@protoyard/decks/schema';
|
|
9
|
+
import { copyDeckText as copyText } from '@texuli/slides/stage-core';
|
|
10
|
+
import { onMount, tick } from 'svelte';
|
|
11
|
+
import { Badge } from 'texuli/badge';
|
|
12
|
+
import { Button } from 'texuli/button';
|
|
13
|
+
import BlockOutline from './BlockOutline.svelte';
|
|
14
|
+
import { deckFileName, downloadText, inertJson } from './format.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* "Debug for agents": the slide on screen as a coding agent needs it. The
|
|
18
|
+
* record (id, position, step, layout, JSON pointers, blocks, text) comes from
|
|
19
|
+
* `describeSlideForAgent`, the findings from `checkDeckStatic`. Four actions
|
|
20
|
+
* copy or save it; outline mode draws each block's box and pointer on the
|
|
21
|
+
* stage. A side sheet, not a modal: the deck keeps its keys, D toggles it and
|
|
22
|
+
* Esc closes it first. Every string is rendered as text; nothing is fetched.
|
|
23
|
+
*/
|
|
24
|
+
let {
|
|
25
|
+
deck,
|
|
26
|
+
slideId,
|
|
27
|
+
step = 0,
|
|
28
|
+
root,
|
|
29
|
+
source = null,
|
|
30
|
+
onjump,
|
|
31
|
+
onclose,
|
|
32
|
+
onnotify,
|
|
33
|
+
}: {
|
|
34
|
+
deck: Deck;
|
|
35
|
+
slideId: string;
|
|
36
|
+
step?: number;
|
|
37
|
+
/** The deck root, for outline mode. */
|
|
38
|
+
root: HTMLElement | undefined;
|
|
39
|
+
/** Where the deck came from (a blueprint path, a prototype slug, a share id). */
|
|
40
|
+
source?: AgentReferenceSource | null;
|
|
41
|
+
onjump: (slideId: string) => void;
|
|
42
|
+
onclose: () => void;
|
|
43
|
+
/** Feedback for the deck's toast. */
|
|
44
|
+
onnotify: (message: string, tone?: 'info' | 'error') => void;
|
|
45
|
+
} = $props();
|
|
46
|
+
|
|
47
|
+
const issues = $derived(checkDeckStatic(deck).issues);
|
|
48
|
+
const record = $derived(
|
|
49
|
+
describeSlideForAgent(
|
|
50
|
+
deck,
|
|
51
|
+
{ slideId, step },
|
|
52
|
+
{
|
|
53
|
+
source,
|
|
54
|
+
findings: issues,
|
|
55
|
+
url: typeof window === 'undefined' ? null : window.location.href,
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
const rawSlide = $derived<DeckSlide | undefined>(deck.slides[record.slide.index]);
|
|
60
|
+
/** Slides other than this one that have findings, in deck order. */
|
|
61
|
+
const otherSlides = $derived.by(() => {
|
|
62
|
+
const counts = new Map<string, number>();
|
|
63
|
+
for (const issue of issues) {
|
|
64
|
+
if (issue.slideId && issue.slideId !== record.slide.id) {
|
|
65
|
+
counts.set(issue.slideId, (counts.get(issue.slideId) ?? 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return deck.slides
|
|
69
|
+
.filter((slide) => counts.has(slide.id))
|
|
70
|
+
.map((slide) => ({ id: slide.id, count: counts.get(slide.id) ?? 0 }));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
let outline = $state(false);
|
|
74
|
+
let closeButton = $state<HTMLElement | null>(null);
|
|
75
|
+
let opener: HTMLElement | null = null;
|
|
76
|
+
const uid = $props.id();
|
|
77
|
+
const titleId = `py-deck-debug-${uid}`;
|
|
78
|
+
|
|
79
|
+
onMount(() => {
|
|
80
|
+
opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
81
|
+
void tick().then(() => closeButton?.focus({ preventScroll: true }));
|
|
82
|
+
return () => {
|
|
83
|
+
if (opener?.isConnected) opener.focus({ preventScroll: true });
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
async function copy(text: string, what: string): Promise<void> {
|
|
88
|
+
if (await copyText(text)) onnotify(`${what} copied`);
|
|
89
|
+
else onnotify(`Could not copy the ${what.toLowerCase()}`, 'error');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function download(): void {
|
|
93
|
+
const ok = downloadText(inertJson(deck), deckFileName(record.deck.id));
|
|
94
|
+
onnotify(ok ? 'Deck JSON downloaded' : 'Could not save the deck JSON', ok ? 'info' : 'error');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Texuli outline buttons, kept at the 44 px touch target on phones (36 px from `sm`). */
|
|
98
|
+
const buttonClass = 'min-h-11 sm:min-h-9';
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
{#if outline}
|
|
102
|
+
<BlockOutline
|
|
103
|
+
{root}
|
|
104
|
+
slideId={record.slide.id}
|
|
105
|
+
slidePointer={record.slide.pointer}
|
|
106
|
+
propsPointer={record.slide.propsPointer}
|
|
107
|
+
/>
|
|
108
|
+
{/if}
|
|
109
|
+
|
|
110
|
+
<aside
|
|
111
|
+
class="absolute inset-y-0 right-0 z-40 flex w-full max-w-md flex-col border-l border-border bg-background text-foreground shadow-xl"
|
|
112
|
+
aria-labelledby={titleId}
|
|
113
|
+
data-py-deck-debug
|
|
114
|
+
data-tx-deck-chrome
|
|
115
|
+
>
|
|
116
|
+
<div class="flex items-center justify-between gap-3 border-b border-border px-4 py-3">
|
|
117
|
+
<h2 id={titleId} class="text-base font-semibold">Debug for agents</h2>
|
|
118
|
+
<Button bind:ref={closeButton} variant="outline" class={buttonClass} onclick={onclose}>Close</Button>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div class="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-4 py-4 text-sm">
|
|
122
|
+
<section aria-label="This slide">
|
|
123
|
+
<dl class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1">
|
|
124
|
+
<dt class="text-muted-foreground">Slide</dt>
|
|
125
|
+
<dd class="font-mono break-all" data-py-deck-debug-slide>{record.slide.id}</dd>
|
|
126
|
+
<dt class="text-muted-foreground">Position</dt>
|
|
127
|
+
<dd>{record.slide.hidden ? 'hidden' : `${record.slide.position} of ${record.slide.total}`}, step {record.slide.step} of {record.slide.steps}</dd>
|
|
128
|
+
<dt class="text-muted-foreground">Layout</dt>
|
|
129
|
+
<dd class="font-mono">{record.slide.layout}</dd>
|
|
130
|
+
<dt class="text-muted-foreground">Pointer</dt>
|
|
131
|
+
<dd class="font-mono break-all" data-py-deck-debug-pointer>{record.slide.pointer}</dd>
|
|
132
|
+
{#if record.slide.propsPointer}
|
|
133
|
+
<dt class="text-muted-foreground">Props</dt>
|
|
134
|
+
<dd class="font-mono break-all">{record.slide.propsPointer}</dd>
|
|
135
|
+
{/if}
|
|
136
|
+
<dt class="text-muted-foreground">Deck</dt>
|
|
137
|
+
<dd class="font-mono break-all">{record.deck.id ?? 'unnamed'}</dd>
|
|
138
|
+
<dt class="text-muted-foreground">Notes</dt>
|
|
139
|
+
<dd>{record.notes.present ? `${record.notes.characters} characters` : 'none'}</dd>
|
|
140
|
+
</dl>
|
|
141
|
+
</section>
|
|
142
|
+
|
|
143
|
+
<section aria-label="Actions" class="grid gap-2 sm:grid-cols-2">
|
|
144
|
+
<!-- The copy actions return their promise: Texuli's Button shows pending until it settles. -->
|
|
145
|
+
<Button variant="outline" class={buttonClass} data-py-deck-debug-action="reference" onclick={() => copy(formatAgentReference(record, 'line'), 'Agent reference')}>Copy agent reference</Button>
|
|
146
|
+
<Button variant="outline" class={buttonClass} data-py-deck-debug-action="prompt" onclick={() => copy(formatAgentReference(record, 'markdown'), 'Agent prompt')}>Copy as agent prompt</Button>
|
|
147
|
+
<Button variant="outline" class={buttonClass} data-py-deck-debug-action="slide-json" onclick={() => copy(inertJson(rawSlide ?? null), 'Slide JSON')}>Copy slide JSON</Button>
|
|
148
|
+
<Button variant="outline" class={buttonClass} data-py-deck-debug-action="download" onclick={download}>Download deck JSON</Button>
|
|
149
|
+
<Button
|
|
150
|
+
variant="outline"
|
|
151
|
+
class={`${buttonClass} col-span-full aria-pressed:border-primary aria-pressed:bg-primary/10`}
|
|
152
|
+
aria-pressed={outline}
|
|
153
|
+
data-py-deck-debug-action="outline"
|
|
154
|
+
onclick={() => (outline = !outline)}
|
|
155
|
+
>
|
|
156
|
+
Outline blocks
|
|
157
|
+
</Button>
|
|
158
|
+
</section>
|
|
159
|
+
|
|
160
|
+
<section aria-labelledby={`${titleId}-findings`}>
|
|
161
|
+
<h3 id={`${titleId}-findings`} class="mb-2 font-semibold">Checks on this slide</h3>
|
|
162
|
+
{#if record.findings.length === 0}
|
|
163
|
+
<p class="text-muted-foreground">No findings on this slide.</p>
|
|
164
|
+
{:else}
|
|
165
|
+
<ul class="space-y-2" data-py-deck-debug-findings>
|
|
166
|
+
{#each record.findings as finding, index (`${finding.rule}:${finding.pointer}:${index}`)}
|
|
167
|
+
<li class="rounded-md border border-border p-2">
|
|
168
|
+
<p class="flex flex-wrap items-center gap-2"><Badge variant={finding.severity === 'error' ? 'destructive' : 'warning'} label={finding.severity} /> <span class="font-mono">{finding.rule}</span></p>
|
|
169
|
+
<p class="font-mono text-xs break-all text-muted-foreground">{finding.pointer}</p>
|
|
170
|
+
<p>{finding.message}</p>
|
|
171
|
+
</li>
|
|
172
|
+
{/each}
|
|
173
|
+
</ul>
|
|
174
|
+
{/if}
|
|
175
|
+
{#if otherSlides.length > 0}
|
|
176
|
+
<h4 class="mt-3 mb-2 font-medium">Other slides with findings</h4>
|
|
177
|
+
<ul class="flex flex-wrap gap-2">
|
|
178
|
+
{#each otherSlides as other (other.id)}
|
|
179
|
+
<li>
|
|
180
|
+
<Button variant="outline" class={buttonClass} data-py-deck-debug-jump={other.id} onclick={() => onjump(other.id)}>
|
|
181
|
+
{other.id} ({other.count})
|
|
182
|
+
</Button>
|
|
183
|
+
</li>
|
|
184
|
+
{/each}
|
|
185
|
+
</ul>
|
|
186
|
+
{/if}
|
|
187
|
+
</section>
|
|
188
|
+
|
|
189
|
+
{#if record.blocks.length > 0}
|
|
190
|
+
<section aria-labelledby={`${titleId}-blocks`}>
|
|
191
|
+
<h3 id={`${titleId}-blocks`} class="mb-2 font-semibold">Blocks</h3>
|
|
192
|
+
<ul class="space-y-1 font-mono text-xs">
|
|
193
|
+
{#each record.blocks as block (block.pointer)}
|
|
194
|
+
<li class="break-all"><span class="text-muted-foreground">{block.pointer}</span> {block.type}{block.id ? ` #${block.id}` : ''}</li>
|
|
195
|
+
{/each}
|
|
196
|
+
</ul>
|
|
197
|
+
</section>
|
|
198
|
+
{/if}
|
|
199
|
+
|
|
200
|
+
<section aria-labelledby={`${titleId}-text`}>
|
|
201
|
+
<h3 id={`${titleId}-text`} class="mb-2 font-semibold">Slide text</h3>
|
|
202
|
+
<p class="whitespace-pre-wrap text-muted-foreground" data-py-deck-debug-text>{record.text || '(no text)'}</p>
|
|
203
|
+
</section>
|
|
204
|
+
</div>
|
|
205
|
+
</aside>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type AgentReferenceSource } from '@protoyard/decks/agent';
|
|
2
|
+
import type { Deck } from '@protoyard/decks/schema';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
deck: Deck;
|
|
5
|
+
slideId: string;
|
|
6
|
+
step?: number;
|
|
7
|
+
/** The deck root, for outline mode. */
|
|
8
|
+
root: HTMLElement | undefined;
|
|
9
|
+
/** Where the deck came from (a blueprint path, a prototype slug, a share id). */
|
|
10
|
+
source?: AgentReferenceSource | null;
|
|
11
|
+
onjump: (slideId: string) => void;
|
|
12
|
+
onclose: () => void;
|
|
13
|
+
/** Feedback for the deck's toast. */
|
|
14
|
+
onnotify: (message: string, tone?: 'info' | 'error') => void;
|
|
15
|
+
};
|
|
16
|
+
declare const DeckDebugPanel: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
17
|
+
type DeckDebugPanel = ReturnType<typeof DeckDebugPanel>;
|
|
18
|
+
export default DeckDebugPanel;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the deck's "Debug for agents" panel is available. Tiny on purpose:
|
|
3
|
+
* DeckApp imports this file directly, and the panel itself stays on its own
|
|
4
|
+
* lazy chunk (`./index.ts`) that loads only when it opens.
|
|
5
|
+
*/
|
|
6
|
+
/** `auto` (the default): dev builds or `?debug=1`. `on` and `off` are the host's choice. */
|
|
7
|
+
export type DeckDebugSetting = 'auto' | 'on' | 'off';
|
|
8
|
+
export interface DeckDebugContext {
|
|
9
|
+
/** True in a Vite dev server build. */
|
|
10
|
+
dev?: boolean;
|
|
11
|
+
/** The page's `location.search`. */
|
|
12
|
+
search?: string;
|
|
13
|
+
}
|
|
14
|
+
/** True for a Vite dev build (`import.meta.env.DEV`); false in production builds and exports. */
|
|
15
|
+
export declare function isDevBuild(): boolean;
|
|
16
|
+
/** `?debug=1` (or `true`) on the page URL. */
|
|
17
|
+
export declare function debugQueryOn(search: string): boolean;
|
|
18
|
+
/** The availability matrix: `off` always wins, `on` always shows, `auto` needs dev or `?debug=1`. */
|
|
19
|
+
export declare function deckDebugAvailable(setting?: DeckDebugSetting, context?: DeckDebugContext): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the deck's "Debug for agents" panel is available. Tiny on purpose:
|
|
3
|
+
* DeckApp imports this file directly, and the panel itself stays on its own
|
|
4
|
+
* lazy chunk (`./index.ts`) that loads only when it opens.
|
|
5
|
+
*/
|
|
6
|
+
/** True for a Vite dev build (`import.meta.env.DEV`); false in production builds and exports. */
|
|
7
|
+
export function isDevBuild() {
|
|
8
|
+
return import.meta.env?.DEV === true;
|
|
9
|
+
}
|
|
10
|
+
/** `?debug=1` (or `true`) on the page URL. */
|
|
11
|
+
export function debugQueryOn(search) {
|
|
12
|
+
try {
|
|
13
|
+
const value = new URLSearchParams(search).get('debug');
|
|
14
|
+
return value === '1' || value === 'true';
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** The availability matrix: `off` always wins, `on` always shows, `auto` needs dev or `?debug=1`. */
|
|
21
|
+
export function deckDebugAvailable(setting = 'auto', context = {}) {
|
|
22
|
+
if (setting === 'off')
|
|
23
|
+
return false;
|
|
24
|
+
if (setting === 'on')
|
|
25
|
+
return true;
|
|
26
|
+
return context.dev === true || debugQueryOn(context.search ?? '');
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text the debug panel copies or downloads. Deck data is untrusted: JSON is
|
|
3
|
+
* written with every invisible, control and bidirectional character escaped
|
|
4
|
+
* as `\uXXXX` (JSON-equivalent, so the data round-trips), so pasted slide JSON
|
|
5
|
+
* can never reorder or hide text in an editor or a prompt (Trojan Source).
|
|
6
|
+
*/
|
|
7
|
+
/** Pretty JSON with every invisible character escaped (the shared definition in `@protoyard/decks/text`). */
|
|
8
|
+
export { inertJson } from '@protoyard/decks/text';
|
|
9
|
+
/** A safe download name: `<slug>.deck.json`. */
|
|
10
|
+
export declare function deckFileName(deckId: string | null | undefined): string;
|
|
11
|
+
/** Saves `text` as a file in the browser (an object URL, no network). False where that is impossible. */
|
|
12
|
+
export declare function downloadText(text: string, fileName: string, doc?: Document): boolean;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text the debug panel copies or downloads. Deck data is untrusted: JSON is
|
|
3
|
+
* written with every invisible, control and bidirectional character escaped
|
|
4
|
+
* as `\uXXXX` (JSON-equivalent, so the data round-trips), so pasted slide JSON
|
|
5
|
+
* can never reorder or hide text in an editor or a prompt (Trojan Source).
|
|
6
|
+
*/
|
|
7
|
+
/** Pretty JSON with every invisible character escaped (the shared definition in `@protoyard/decks/text`). */
|
|
8
|
+
export { inertJson } from '@protoyard/decks/text';
|
|
9
|
+
/** A safe download name: `<slug>.deck.json`. */
|
|
10
|
+
export function deckFileName(deckId) {
|
|
11
|
+
const slug = (deckId ?? '')
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
14
|
+
.replace(/^-+|-+$/g, '')
|
|
15
|
+
.slice(0, 64);
|
|
16
|
+
return `${slug || 'deck'}.deck.json`;
|
|
17
|
+
}
|
|
18
|
+
/** Saves `text` as a file in the browser (an object URL, no network). False where that is impossible. */
|
|
19
|
+
export function downloadText(text, fileName, doc = document) {
|
|
20
|
+
const win = doc.defaultView;
|
|
21
|
+
if (!win || typeof win.URL?.createObjectURL !== 'function')
|
|
22
|
+
return false;
|
|
23
|
+
try {
|
|
24
|
+
const url = win.URL.createObjectURL(new Blob([text], { type: 'application/json' }));
|
|
25
|
+
const link = doc.createElement('a');
|
|
26
|
+
link.href = url;
|
|
27
|
+
link.download = fileName;
|
|
28
|
+
link.rel = 'noopener';
|
|
29
|
+
link.style.display = 'none';
|
|
30
|
+
doc.body.append(link);
|
|
31
|
+
link.click();
|
|
32
|
+
link.remove();
|
|
33
|
+
win.setTimeout(() => win.URL.revokeObjectURL(url), 0);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@protoyard/decks-ui/debug` — the "Debug for agents" panel (ADR-038 lazy
|
|
3
|
+
* chunk): DeckApp imports it only when the panel opens, so it adds nothing to
|
|
4
|
+
* the deck entry. Availability (`deckDebugAvailable`) lives in
|
|
5
|
+
* `./availability.ts`, which the entry imports directly.
|
|
6
|
+
*/
|
|
7
|
+
export { type DeckDebugContext, type DeckDebugSetting, debugQueryOn, deckDebugAvailable, isDevBuild, } from './availability.ts';
|
|
8
|
+
export { default as BlockOutline } from './BlockOutline.svelte';
|
|
9
|
+
export { default as DeckDebugPanel } from './DeckDebugPanel.svelte';
|
|
10
|
+
export { deckFileName, downloadText, inertJson } from './format.ts';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@protoyard/decks-ui/debug` — the "Debug for agents" panel (ADR-038 lazy
|
|
3
|
+
* chunk): DeckApp imports it only when the panel opens, so it adds nothing to
|
|
4
|
+
* the deck entry. Availability (`deckDebugAvailable`) lives in
|
|
5
|
+
* `./availability.ts`, which the entry imports directly.
|
|
6
|
+
*/
|
|
7
|
+
export { debugQueryOn, deckDebugAvailable, isDevBuild, } from "./availability.js";
|
|
8
|
+
export { default as BlockOutline } from './BlockOutline.svelte';
|
|
9
|
+
export { default as DeckDebugPanel } from './DeckDebugPanel.svelte';
|
|
10
|
+
export { deckFileName, downloadText, inertJson } from "./format.js";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type CanvasFit as DeckCanvasFit, fitDeckCanvas } from '@texuli/deck/engine';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* `@protoyard/decks-ui` root: small, eager helpers only. DeckApp, Deck Studio
|
|
3
|
+
* and the debug panel live on their own subpaths (`./app`, `./studio`,
|
|
4
|
+
* `./debug`) so each loads lazily (ADR-038). The stage itself is
|
|
5
|
+
* `@texuli/slides` (Texuli ADR-009).
|
|
6
|
+
*/
|
|
7
|
+
export { fitDeckCanvas } from '@texuli/deck/engine';
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { CustomSlide } from '@protoyard/decks/schema';
|
|
3
|
+
import { Button } from 'texuli/button';
|
|
4
|
+
import { type CustomSlideRegistry, customSlideLoader, frozenCopy } from './custom-slides.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A `layout: "custom"` slide. In a generated deck (level 2) it loads the
|
|
8
|
+
* component the prototype registered under `props.component` and renders it
|
|
9
|
+
* with `{ slide, step, stepCount, data }`, inside the slide canvas. A
|
|
10
|
+
* component that throws, or fails to load, shows an error card on this slide
|
|
11
|
+
* only; the deck keeps navigating. Without a registry (the player, level 1)
|
|
12
|
+
* or with `live` off (overview, print, previews) it shows a labelled card and
|
|
13
|
+
* runs no owned code.
|
|
14
|
+
*/
|
|
15
|
+
let {
|
|
16
|
+
slide,
|
|
17
|
+
step = 0,
|
|
18
|
+
stepCount = 0,
|
|
19
|
+
registry,
|
|
20
|
+
live = true,
|
|
21
|
+
}: {
|
|
22
|
+
slide: CustomSlide;
|
|
23
|
+
step?: number;
|
|
24
|
+
stepCount?: number;
|
|
25
|
+
/** The generated deck's `src/custom-slides.ts`; absent in the player. */
|
|
26
|
+
registry?: CustomSlideRegistry | undefined;
|
|
27
|
+
/** False in thumbnails and print: show the card, never run the component. */
|
|
28
|
+
live?: boolean;
|
|
29
|
+
} = $props();
|
|
30
|
+
|
|
31
|
+
const id = $derived(slide.props.component);
|
|
32
|
+
const loader = $derived(customSlideLoader(registry, id));
|
|
33
|
+
const load = $derived(live && loader ? loader() : null);
|
|
34
|
+
const ownSlide = $derived(frozenCopy(slide));
|
|
35
|
+
const data = $derived(frozenCopy(slide.props.data ?? {}));
|
|
36
|
+
const label = $derived(slide.title ?? id);
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
{#snippet card(heading: string, detail: string, tone: 'info' | 'error' = 'info')}
|
|
40
|
+
<div
|
|
41
|
+
class="grid size-full place-items-center p-16"
|
|
42
|
+
role={tone === 'error' ? 'alert' : 'note'}
|
|
43
|
+
data-py-deck-custom-card={tone}
|
|
44
|
+
>
|
|
45
|
+
<div class="max-w-4xl rounded-2xl border-2 border-dashed border-border bg-muted/40 p-8 text-center">
|
|
46
|
+
<p class="text-2xl font-semibold tracking-wide text-muted-foreground uppercase">{label}</p>
|
|
47
|
+
<p class={`mt-4 text-3xl font-semibold ${tone === 'error' ? 'text-destructive' : 'text-foreground'}`}>{heading}</p>
|
|
48
|
+
<p class="mt-4 text-2xl text-muted-foreground">{detail}</p>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
{/snippet}
|
|
52
|
+
|
|
53
|
+
<div class="size-full" data-py-deck-custom={id}>
|
|
54
|
+
{#if !live}
|
|
55
|
+
{@render card('Interactive slide', `Runs the "${id}" component on the slide itself.`)}
|
|
56
|
+
{:else if !registry}
|
|
57
|
+
{@render card(
|
|
58
|
+
'Interactive slide — open the generated deck to run it',
|
|
59
|
+
`This slide runs the "${id}" component, which only a generated deck has.`
|
|
60
|
+
)}
|
|
61
|
+
{:else if !loader}
|
|
62
|
+
{@render card(
|
|
63
|
+
'This custom slide is not available',
|
|
64
|
+
`No component named "${id}" is registered in this deck's src/custom-slides.ts.`,
|
|
65
|
+
'error'
|
|
66
|
+
)}
|
|
67
|
+
{:else if load}
|
|
68
|
+
{#await load}
|
|
69
|
+
<div class="grid size-full place-items-center" role="status" aria-label="Loading the slide">
|
|
70
|
+
<span class="text-2xl text-muted-foreground">Loading the slide…</span>
|
|
71
|
+
</div>
|
|
72
|
+
{:then module}
|
|
73
|
+
{@const Slide = module.default}
|
|
74
|
+
<svelte:boundary>
|
|
75
|
+
<!-- The component owns its clicks: they never advance the deck. -->
|
|
76
|
+
<div class="size-full" data-tx-deck-interactive>
|
|
77
|
+
<Slide slide={ownSlide} {step} {stepCount} {data} />
|
|
78
|
+
</div>
|
|
79
|
+
{#snippet failed(_error, reset)}
|
|
80
|
+
<div class="grid size-full place-items-center p-16" role="alert" data-py-deck-custom-card="error">
|
|
81
|
+
<div class="max-w-4xl rounded-2xl border-2 border-destructive bg-background p-8 text-center">
|
|
82
|
+
<p class="text-3xl font-semibold text-destructive">This slide stopped working</p>
|
|
83
|
+
<p class="mt-4 text-2xl text-muted-foreground">The "{id}" component failed. The rest of the deck still works.</p>
|
|
84
|
+
<Button variant="outline" size="lg" class="mt-6 text-2xl" onclick={reset}>Try again</Button>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
{/snippet}
|
|
88
|
+
</svelte:boundary>
|
|
89
|
+
{:catch}
|
|
90
|
+
{@render card(
|
|
91
|
+
'This slide could not be loaded',
|
|
92
|
+
`The "${id}" component did not load. The rest of the deck still works.`,
|
|
93
|
+
'error'
|
|
94
|
+
)}
|
|
95
|
+
{/await}
|
|
96
|
+
{/if}
|
|
97
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CustomSlide } from '@protoyard/decks/schema';
|
|
2
|
+
import { type CustomSlideRegistry } from './custom-slides.ts';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
slide: CustomSlide;
|
|
5
|
+
step?: number;
|
|
6
|
+
stepCount?: number;
|
|
7
|
+
/** The generated deck's `src/custom-slides.ts`; absent in the player. */
|
|
8
|
+
registry?: CustomSlideRegistry | undefined;
|
|
9
|
+
/** False in thumbnails and print: show the card, never run the component. */
|
|
10
|
+
live?: boolean;
|
|
11
|
+
};
|
|
12
|
+
declare const CustomSlideHost: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type CustomSlideHost = ReturnType<typeof CustomSlideHost>;
|
|
14
|
+
export default CustomSlideHost;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CustomSlide } from '@protoyard/decks/schema';
|
|
2
|
+
import type { Component } from 'svelte';
|
|
3
|
+
/**
|
|
4
|
+
* What an owned custom slide component receives: its own slide (a frozen
|
|
5
|
+
* copy), the current step, the number of steps and the slide's `data`. Never
|
|
6
|
+
* the raw deck or the runtime's write services; a prototype that wants them
|
|
7
|
+
* passes them to its component itself.
|
|
8
|
+
*/
|
|
9
|
+
export interface CustomSlideProps {
|
|
10
|
+
slide: Readonly<CustomSlide>;
|
|
11
|
+
/** 0 before the first reveal, up to `stepCount`. */
|
|
12
|
+
step: number;
|
|
13
|
+
/** The slide's reveal steps (its `steps` labels). */
|
|
14
|
+
stepCount: number;
|
|
15
|
+
/** `props.data`, bounded JSON (a frozen copy; `{}` when the slide has none). */
|
|
16
|
+
data: Readonly<Record<string, unknown>>;
|
|
17
|
+
}
|
|
18
|
+
export type CustomSlideComponent = Component<CustomSlideProps>;
|
|
19
|
+
/** A lazy loader: the component's module loads the first time its slide shows. */
|
|
20
|
+
export type CustomSlideLoader = () => Promise<{
|
|
21
|
+
default: CustomSlideComponent;
|
|
22
|
+
}>;
|
|
23
|
+
/** A generated deck's `src/custom-slides.ts`: component id → lazy loader. */
|
|
24
|
+
export type CustomSlideRegistry = Readonly<Record<string, CustomSlideLoader>>;
|
|
25
|
+
/** A deep-frozen copy, so a component can never change the deck's data. */
|
|
26
|
+
export declare function frozenCopy<T>(value: T): Readonly<T>;
|
|
27
|
+
/** The registry's loader for `id`, never one inherited from `Object.prototype`. */
|
|
28
|
+
export declare function customSlideLoader(registry: CustomSlideRegistry | undefined, id: string): CustomSlideLoader | null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** A deep-frozen copy, so a component can never change the deck's data. */
|
|
2
|
+
export function frozenCopy(value) {
|
|
3
|
+
const copy = structuredClone(value);
|
|
4
|
+
const freeze = (node) => {
|
|
5
|
+
if (node === null || typeof node !== 'object' || Object.isFrozen(node))
|
|
6
|
+
return;
|
|
7
|
+
Object.freeze(node);
|
|
8
|
+
for (const child of Object.values(node))
|
|
9
|
+
freeze(child);
|
|
10
|
+
};
|
|
11
|
+
freeze(copy);
|
|
12
|
+
return copy;
|
|
13
|
+
}
|
|
14
|
+
/** The registry's loader for `id`, never one inherited from `Object.prototype`. */
|
|
15
|
+
export function customSlideLoader(registry, id) {
|
|
16
|
+
if (!registry || !Object.hasOwn(registry, id))
|
|
17
|
+
return null;
|
|
18
|
+
const loader = registry[id];
|
|
19
|
+
return typeof loader === 'function' ? loader : null;
|
|
20
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@protoyard/decks-ui/layouts`: custom slides, the one slide kind a
|
|
3
|
+
* generated deck renders with its own code. The 20 catalog layouts render on
|
|
4
|
+
* the `@texuli/slides` stage (`@texuli/slides/layouts`).
|
|
5
|
+
*/
|
|
6
|
+
export { default as CustomSlideHost } from './CustomSlideHost.svelte';
|
|
7
|
+
export { type CustomSlideComponent, type CustomSlideLoader, type CustomSlideProps, type CustomSlideRegistry, customSlideLoader, frozenCopy, } from './custom-slides.ts';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@protoyard/decks-ui/layouts`: custom slides, the one slide kind a
|
|
3
|
+
* generated deck renders with its own code. The 20 catalog layouts render on
|
|
4
|
+
* the `@texuli/slides` stage (`@texuli/slides/layouts`).
|
|
5
|
+
*/
|
|
6
|
+
export { default as CustomSlideHost } from './CustomSlideHost.svelte';
|
|
7
|
+
export { customSlideLoader, frozenCopy, } from "./custom-slides.js";
|
package/dist/player.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|