@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,261 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { deckStudioOptions, loadDeckStudioThemes } from '@protoyard/decks/patch';
|
|
3
|
+
import { onMount } from 'svelte';
|
|
4
|
+
import { Button, buttonVariants } from 'texuli/button';
|
|
5
|
+
import { Checkbox } from 'texuli/choice';
|
|
6
|
+
import { cx } from 'texuli/cx';
|
|
7
|
+
import { Field } from 'texuli/field';
|
|
8
|
+
import { Input, Textarea } from 'texuli/input';
|
|
9
|
+
import { SelectContent, SelectItem, SelectRoot, SelectTrigger, SelectValue } from 'texuli/select';
|
|
10
|
+
import type { DeckStudio } from './studio.svelte.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The Deck Studio DevTools tab: deck style, the current slide, and the
|
|
14
|
+
* export. Every control writes to the studio's in-memory edits; the host's
|
|
15
|
+
* `DeckApp` renders `studio.preview`, so changes show live.
|
|
16
|
+
*/
|
|
17
|
+
let { studio }: { studio: DeckStudio; [key: string]: unknown } = $props();
|
|
18
|
+
|
|
19
|
+
// The choices load with this lazy panel: the theme list starts with the
|
|
20
|
+
// built-ins and the library catalog, and grows to the whole library (Texuli's
|
|
21
|
+
// expanded set) once its metadata loads.
|
|
22
|
+
const options = deckStudioOptions();
|
|
23
|
+
let themes = $state(options.themes);
|
|
24
|
+
onMount(() => {
|
|
25
|
+
let active = true;
|
|
26
|
+
void loadDeckStudioThemes().then((all) => {
|
|
27
|
+
if (active) themes = [...all];
|
|
28
|
+
});
|
|
29
|
+
return () => {
|
|
30
|
+
active = false;
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
const style = $derived(studio.preview.style);
|
|
34
|
+
const slide = $derived(studio.slide);
|
|
35
|
+
let copying = $state(false);
|
|
36
|
+
let status = $state('');
|
|
37
|
+
let exportField = $state<HTMLElement | null>(null);
|
|
38
|
+
|
|
39
|
+
const seedHex = $derived(
|
|
40
|
+
style.paletteSeed === undefined
|
|
41
|
+
? ''
|
|
42
|
+
: `#${style.paletteSeed.toString(16).padStart(6, '0').slice(-6)}`
|
|
43
|
+
);
|
|
44
|
+
const downloadHref = $derived(
|
|
45
|
+
`data:application/json;charset=utf-8,${encodeURIComponent(studio.patchJson)}`
|
|
46
|
+
);
|
|
47
|
+
const operationCount = $derived(studio.patch.length);
|
|
48
|
+
|
|
49
|
+
/** The "use the default" choice of a select, which removes the field. */
|
|
50
|
+
const DEFAULT = '__default__';
|
|
51
|
+
const orDefault = (value: string | undefined | null) => value ?? DEFAULT;
|
|
52
|
+
const orNull = (value: string) => (value === DEFAULT ? null : value);
|
|
53
|
+
|
|
54
|
+
interface Choice {
|
|
55
|
+
value: string;
|
|
56
|
+
label: string;
|
|
57
|
+
}
|
|
58
|
+
const withDefault = (label: string, values: readonly Choice[]): Choice[] => [
|
|
59
|
+
{ value: DEFAULT, label },
|
|
60
|
+
...values,
|
|
61
|
+
];
|
|
62
|
+
const plain = (values: readonly string[]): Choice[] =>
|
|
63
|
+
values.map((value) => ({ value, label: value }));
|
|
64
|
+
/** A slide background authored as a spec object (not a preset) keeps a choice of its own. */
|
|
65
|
+
const CUSTOM = '__custom__';
|
|
66
|
+
const slideBackground = $derived(
|
|
67
|
+
slide?.background === undefined
|
|
68
|
+
? DEFAULT
|
|
69
|
+
: typeof slide.background === 'string'
|
|
70
|
+
? slide.background
|
|
71
|
+
: CUSTOM
|
|
72
|
+
);
|
|
73
|
+
const slideBackgrounds = $derived([
|
|
74
|
+
...withDefault('Deck default', plain(options.backgrounds)),
|
|
75
|
+
...(slideBackground === CUSTOM ? [{ value: CUSTOM, label: 'Custom (deck JSON)' }] : []),
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
function setSeed(raw: string) {
|
|
79
|
+
const match = /^#?([0-9a-f]{6})$/i.exec(raw.trim());
|
|
80
|
+
if (raw.trim() === '') studio.setStyle('paletteSeed', null);
|
|
81
|
+
else if (match) studio.setStyle('paletteSeed', Number.parseInt(match[1] as string, 16));
|
|
82
|
+
else status = 'Use a six-digit hex color for the palette seed, such as #2563eb.';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function copyPatch() {
|
|
86
|
+
copying = true;
|
|
87
|
+
status = '';
|
|
88
|
+
try {
|
|
89
|
+
await navigator.clipboard.writeText(studio.patchJson);
|
|
90
|
+
status = 'Patch copied.';
|
|
91
|
+
} catch {
|
|
92
|
+
(exportField as HTMLTextAreaElement | null)?.select();
|
|
93
|
+
status = 'Copying is blocked here: the patch is selected, press Ctrl or ⌘ + C.';
|
|
94
|
+
} finally {
|
|
95
|
+
copying = false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The download is a data: URL, which Texuli's Button href policy refuses: an anchor styled as one.
|
|
100
|
+
const linkButton = cx(
|
|
101
|
+
buttonVariants({ variant: 'outline', size: 'sm' }),
|
|
102
|
+
'aria-disabled:pointer-events-none aria-disabled:opacity-60'
|
|
103
|
+
);
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
{#snippet choice(
|
|
107
|
+
label: string,
|
|
108
|
+
value: string,
|
|
109
|
+
items: readonly Choice[],
|
|
110
|
+
onchange: (value: string) => void,
|
|
111
|
+
disabled = false
|
|
112
|
+
)}
|
|
113
|
+
<Field {label} {disabled} class="gap-1 text-[12px]">
|
|
114
|
+
{#snippet children(control)}
|
|
115
|
+
<SelectRoot {value} items={[...items]} {disabled} onValueChange={(next) => onchange(next)}>
|
|
116
|
+
<SelectTrigger {...control.props} size="sm" class="w-full">
|
|
117
|
+
<SelectValue />
|
|
118
|
+
</SelectTrigger>
|
|
119
|
+
<SelectContent>
|
|
120
|
+
{#each items as item (item.value)}
|
|
121
|
+
<SelectItem value={item.value} label={item.label}>{item.label}</SelectItem>
|
|
122
|
+
{/each}
|
|
123
|
+
</SelectContent>
|
|
124
|
+
</SelectRoot>
|
|
125
|
+
{/snippet}
|
|
126
|
+
</Field>
|
|
127
|
+
{/snippet}
|
|
128
|
+
|
|
129
|
+
<div class="grid gap-4 p-3" data-py-deck-studio>
|
|
130
|
+
<section class="grid gap-2" aria-labelledby="py-ds-style">
|
|
131
|
+
<h3 id="py-ds-style" class="text-[12px] font-semibold uppercase tracking-wide text-muted-foreground">Deck style</h3>
|
|
132
|
+
{@render choice(
|
|
133
|
+
'Theme',
|
|
134
|
+
orDefault(style.theme),
|
|
135
|
+
withDefault('Default', themes.map((theme) => ({ value: theme.id, label: theme.label }))),
|
|
136
|
+
(value) => studio.setStyle('theme', orNull(value))
|
|
137
|
+
)}
|
|
138
|
+
<Field label="Palette seed (replaces the theme)" class="gap-1 text-[12px]">
|
|
139
|
+
{#snippet children(control)}
|
|
140
|
+
<Input
|
|
141
|
+
{...control.props}
|
|
142
|
+
type="text"
|
|
143
|
+
inputmode="text"
|
|
144
|
+
placeholder="#2563eb"
|
|
145
|
+
value={seedHex}
|
|
146
|
+
onchange={(e) => setSeed(e.currentTarget.value)}
|
|
147
|
+
/>
|
|
148
|
+
{/snippet}
|
|
149
|
+
</Field>
|
|
150
|
+
{@render choice(
|
|
151
|
+
'Font pair',
|
|
152
|
+
orDefault(style.fontPair),
|
|
153
|
+
withDefault('Theme default', options.fontPairs.map((pair) => ({ value: pair.id, label: pair.label }))),
|
|
154
|
+
(value) => studio.setStyle('fontPair', orNull(value))
|
|
155
|
+
)}
|
|
156
|
+
<div class="grid grid-cols-2 gap-2">
|
|
157
|
+
{@render choice('Aspect', style.aspect ?? '16:9', plain(options.aspects), (value) =>
|
|
158
|
+
studio.setStyle('aspect', value as never)
|
|
159
|
+
)}
|
|
160
|
+
{@render choice('Motion', style.motion ?? 'full', plain(options.motions), (value) =>
|
|
161
|
+
studio.setStyle('motion', value as never)
|
|
162
|
+
)}
|
|
163
|
+
{@render choice(
|
|
164
|
+
'Deck background',
|
|
165
|
+
orDefault(style.background),
|
|
166
|
+
withDefault('Default', plain(options.backgrounds)),
|
|
167
|
+
(value) => studio.setStyle('background', orNull(value))
|
|
168
|
+
)}
|
|
169
|
+
{@render choice('Progress', style.progress ?? 'bottom', plain(options.progress), (value) =>
|
|
170
|
+
studio.setStyle('progress', value as never)
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
<Checkbox
|
|
174
|
+
label="Slide numbers"
|
|
175
|
+
class="text-[12px]"
|
|
176
|
+
checked={style.slideNumbers !== false}
|
|
177
|
+
onCheckedChange={(checked) => studio.setStyle('slideNumbers', checked ? null : false)}
|
|
178
|
+
/>
|
|
179
|
+
</section>
|
|
180
|
+
|
|
181
|
+
<section class="grid gap-2" aria-labelledby="py-ds-slide">
|
|
182
|
+
<h3 id="py-ds-slide" class="text-[12px] font-semibold uppercase tracking-wide text-muted-foreground">Slide</h3>
|
|
183
|
+
{@render choice(
|
|
184
|
+
'Edit slide',
|
|
185
|
+
studio.slideId,
|
|
186
|
+
studio.preview.slides.map((item, index) => ({
|
|
187
|
+
value: item.id,
|
|
188
|
+
label: `${index + 1}. ${item.title ?? item.id}`,
|
|
189
|
+
})),
|
|
190
|
+
(value) => studio.selectSlide(value)
|
|
191
|
+
)}
|
|
192
|
+
{#if slide}
|
|
193
|
+
{@render choice(
|
|
194
|
+
'Layout',
|
|
195
|
+
slide.layout,
|
|
196
|
+
plain(studio.layoutChoices),
|
|
197
|
+
(value) => studio.setSlide('layout', value),
|
|
198
|
+
studio.layoutChoices.length < 2
|
|
199
|
+
)}
|
|
200
|
+
<div class="grid grid-cols-2 gap-2">
|
|
201
|
+
{@render choice(
|
|
202
|
+
'Slide background',
|
|
203
|
+
slideBackground,
|
|
204
|
+
slideBackgrounds,
|
|
205
|
+
(value) => {
|
|
206
|
+
if (value !== CUSTOM) studio.setSlide('background', orNull(value));
|
|
207
|
+
}
|
|
208
|
+
)}
|
|
209
|
+
{@render choice(
|
|
210
|
+
'Slide transition',
|
|
211
|
+
orDefault(slide.transition),
|
|
212
|
+
withDefault('Deck default', plain(options.transitions)),
|
|
213
|
+
(value) => studio.setSlide('transition', orNull(value) as never)
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
<Checkbox
|
|
217
|
+
label="Hide this slide"
|
|
218
|
+
class="text-[12px]"
|
|
219
|
+
checked={slide.hidden === true}
|
|
220
|
+
onCheckedChange={(checked) => studio.setSlide('hidden', checked)}
|
|
221
|
+
/>
|
|
222
|
+
{/if}
|
|
223
|
+
</section>
|
|
224
|
+
|
|
225
|
+
<section class="grid gap-2" aria-labelledby="py-ds-export">
|
|
226
|
+
<h3 id="py-ds-export" class="text-[12px] font-semibold uppercase tracking-wide text-muted-foreground">Export</h3>
|
|
227
|
+
{#if studio.issues.length > 0}
|
|
228
|
+
<ul class="grid gap-1 text-[12px] text-destructive" role="alert">
|
|
229
|
+
{#each studio.issues as issue (issue.jsonPath + issue.message)}
|
|
230
|
+
<li><code>{issue.jsonPath}</code>: {issue.message}</li>
|
|
231
|
+
{/each}
|
|
232
|
+
</ul>
|
|
233
|
+
{/if}
|
|
234
|
+
<p class="text-[12px] text-muted-foreground">
|
|
235
|
+
{operationCount === 0 ? 'No changes yet.' : `${operationCount} JSON Patch operation${operationCount === 1 ? '' : 's'} (RFC 6902) for the source deck.`}
|
|
236
|
+
</p>
|
|
237
|
+
<Textarea
|
|
238
|
+
bind:ref={exportField}
|
|
239
|
+
class="h-32 font-mono text-[11px]"
|
|
240
|
+
readonly
|
|
241
|
+
aria-label="Deck patch (JSON)"
|
|
242
|
+
value={studio.patchJson}
|
|
243
|
+
/>
|
|
244
|
+
<div class="flex flex-wrap gap-2">
|
|
245
|
+
<Button
|
|
246
|
+
variant="outline"
|
|
247
|
+
size="sm"
|
|
248
|
+
loading={copying}
|
|
249
|
+
autoLoading={false}
|
|
250
|
+
loadingLabel="Copying…"
|
|
251
|
+
disabled={operationCount === 0}
|
|
252
|
+
onclick={copyPatch}
|
|
253
|
+
>
|
|
254
|
+
Copy patch
|
|
255
|
+
</Button>
|
|
256
|
+
<a class={linkButton} href={downloadHref} download="deck-studio.patch.json" aria-disabled={operationCount === 0}>Download patch</a>
|
|
257
|
+
<Button variant="outline" size="sm" disabled={operationCount === 0} onclick={() => studio.reset()}>Reset</Button>
|
|
258
|
+
</div>
|
|
259
|
+
<p class="min-h-4 text-[12px]" role="status" aria-live="polite">{status}</p>
|
|
260
|
+
</section>
|
|
261
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DeckStudio } from './studio.svelte.ts';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
studio: DeckStudio;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
};
|
|
6
|
+
declare const DeckStudioPanel: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type DeckStudioPanel = ReturnType<typeof DeckStudioPanel>;
|
|
8
|
+
export default DeckStudioPanel;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Presentation } from '@lucide/svelte';
|
|
3
|
+
import { LucideIcon } from 'texuli/icons/lucide';
|
|
4
|
+
|
|
5
|
+
/** The presentation-screen glyph for the DevTools tab (Lucide, through Texuli's icon bridge). */
|
|
6
|
+
let { class: className = '' }: { class?: string } = $props();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<LucideIcon icon={Presentation} class={className} />
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
import type { DeckStudio } from './studio.svelte.ts';
|
|
3
|
+
/**
|
|
4
|
+
* `@protoyard/decks-ui/studio` — Deck Studio (ADR-038), a lazy DevTools tab
|
|
5
|
+
* that edits the deck style and the current slide with a live preview and
|
|
6
|
+
* exports RFC 6902 patches for the source deck JSON.
|
|
7
|
+
*
|
|
8
|
+
* const studio = createDeckStudio(deck);
|
|
9
|
+
* // render <DeckApp deck={studio.preview} />, and in mountPrototype:
|
|
10
|
+
* devtoolsTabs: [deckStudioTab(studio)]
|
|
11
|
+
*/
|
|
12
|
+
export { createDeckStudio, type DeckStudio, type DeckStudioOptions, prefixDeckPatch, } from './studio.svelte.ts';
|
|
13
|
+
/** A DevTools tab entry (structurally `DevtoolsTab` from @protoyard/devtools). */
|
|
14
|
+
export interface DeckStudioTab {
|
|
15
|
+
id: 'deck-studio';
|
|
16
|
+
label: string;
|
|
17
|
+
icon: Component<{
|
|
18
|
+
class?: string;
|
|
19
|
+
}>;
|
|
20
|
+
load: () => Promise<unknown>;
|
|
21
|
+
props: {
|
|
22
|
+
studio: DeckStudio;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** The tab for `mountPrototype({ devtoolsTabs })`. The panel loads only when the tab opens. */
|
|
26
|
+
export declare function deckStudioTab(studio: DeckStudio): DeckStudioTab;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import StudioIcon from './StudioIcon.svelte';
|
|
2
|
+
/**
|
|
3
|
+
* `@protoyard/decks-ui/studio` — Deck Studio (ADR-038), a lazy DevTools tab
|
|
4
|
+
* that edits the deck style and the current slide with a live preview and
|
|
5
|
+
* exports RFC 6902 patches for the source deck JSON.
|
|
6
|
+
*
|
|
7
|
+
* const studio = createDeckStudio(deck);
|
|
8
|
+
* // render <DeckApp deck={studio.preview} />, and in mountPrototype:
|
|
9
|
+
* devtoolsTabs: [deckStudioTab(studio)]
|
|
10
|
+
*/
|
|
11
|
+
export { createDeckStudio, prefixDeckPatch, } from "./studio.svelte.js";
|
|
12
|
+
/** The tab for `mountPrototype({ devtoolsTabs })`. The panel loads only when the tab opens. */
|
|
13
|
+
export function deckStudioTab(studio) {
|
|
14
|
+
return {
|
|
15
|
+
id: 'deck-studio',
|
|
16
|
+
label: 'Deck',
|
|
17
|
+
icon: StudioIcon,
|
|
18
|
+
load: async () => (await import('./DeckStudioPanel.svelte')).default,
|
|
19
|
+
props: { studio },
|
|
20
|
+
};
|
|
21
|
+
}
|