@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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -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,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,6 @@
1
+ type $$ComponentProps = {
2
+ class?: string;
3
+ };
4
+ declare const StudioIcon: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type StudioIcon = ReturnType<typeof StudioIcon>;
6
+ export default StudioIcon;
@@ -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
+ }