@open-slide/core 0.0.11 → 0.0.13

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 (89) hide show
  1. package/dist/{build-DHiRlpjn.js → build-DC3FTpWO.js} +2 -1
  2. package/dist/cli/bin.js +43 -4
  3. package/dist/{config-LZM903FE.js → config-Cuw0mC5h.js} +592 -63
  4. package/dist/design-BUML7uvZ.js +35 -0
  5. package/dist/{dev-B3JzCYn7.js → dev-BuWsdYvn.js} +2 -1
  6. package/dist/index.d.ts +55 -4
  7. package/dist/index.js +110 -1
  8. package/dist/{preview-UikovHEt.js → preview-CIcG-lP3.js} +2 -1
  9. package/dist/sync-3oqN1WyK.js +139 -0
  10. package/dist/sync-B4eLo2H6.js +3 -0
  11. package/dist/vite/index.d.ts +1 -1
  12. package/dist/vite/index.js +2 -1
  13. package/package.json +2 -1
  14. package/skills/apply-comments/SKILL.md +83 -0
  15. package/skills/create-slide/SKILL.md +81 -0
  16. package/skills/create-theme/SKILL.md +194 -0
  17. package/skills/slide-authoring/SKILL.md +288 -0
  18. package/src/app/{App.tsx → app.tsx} +8 -6
  19. package/src/app/components/{AssetView.tsx → asset-view.tsx} +41 -33
  20. package/src/app/components/{ClickNavZones.tsx → click-nav-zones.tsx} +1 -1
  21. package/src/app/components/history-provider.tsx +120 -0
  22. package/src/app/components/image-placeholder.tsx +121 -0
  23. package/src/app/components/inspector/{CommentWidget.tsx → comment-widget.tsx} +1 -1
  24. package/src/app/components/inspector/{InspectOverlay.tsx → inspect-overlay.tsx} +1 -1
  25. package/src/app/components/inspector/{InspectorPanel.tsx → inspector-panel.tsx} +164 -212
  26. package/src/app/components/inspector/{InspectorProvider.tsx → inspector-provider.tsx} +186 -18
  27. package/src/app/components/inspector/save-bar.tsx +47 -0
  28. package/src/app/components/panel/panel-fields.tsx +60 -0
  29. package/src/app/components/panel/panel-shell.tsx +78 -0
  30. package/src/app/components/panel/save-card.tsx +139 -0
  31. package/src/app/components/pdf-progress-toast.tsx +25 -0
  32. package/src/app/components/player.tsx +341 -0
  33. package/src/app/components/present/blackout-overlay.tsx +18 -0
  34. package/src/app/components/present/control-bar.tsx +204 -0
  35. package/src/app/components/present/help-overlay.tsx +56 -0
  36. package/src/app/components/present/jump-input.tsx +74 -0
  37. package/src/app/components/present/laser-pointer.tsx +40 -0
  38. package/src/app/components/present/overview-grid.tsx +184 -0
  39. package/src/app/components/present/progress-bar.tsx +26 -0
  40. package/src/app/components/present/use-idle.ts +44 -0
  41. package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
  42. package/src/app/components/present/use-presenter-channel.ts +71 -0
  43. package/src/app/components/present/use-touch-swipe.ts +63 -0
  44. package/src/app/components/sidebar/{FolderItem.tsx → folder-item.tsx} +62 -27
  45. package/src/app/components/sidebar/{IconPicker.tsx → icon-picker.tsx} +13 -10
  46. package/src/app/components/sidebar/{Sidebar.tsx → sidebar.tsx} +40 -34
  47. package/src/app/components/{SlideCanvas.tsx → slide-canvas.tsx} +35 -10
  48. package/src/app/components/style-panel/design-provider.tsx +139 -0
  49. package/src/app/components/style-panel/style-panel.tsx +326 -0
  50. package/src/app/components/style-panel/use-design.ts +112 -0
  51. package/src/app/components/theme-toggle.tsx +57 -0
  52. package/src/app/components/thumbnail-rail.tsx +151 -0
  53. package/src/app/components/ui/button.tsx +51 -19
  54. package/src/app/components/ui/card.tsx +1 -1
  55. package/src/app/components/ui/dialog.tsx +25 -9
  56. package/src/app/components/ui/dropdown-menu.tsx +29 -12
  57. package/src/app/components/ui/input.tsx +13 -9
  58. package/src/app/components/ui/popover.tsx +5 -2
  59. package/src/app/components/ui/progress.tsx +2 -2
  60. package/src/app/components/ui/select.tsx +11 -5
  61. package/src/app/components/ui/separator.tsx +1 -1
  62. package/src/app/components/ui/slider.tsx +4 -4
  63. package/src/app/components/ui/sonner.tsx +11 -1
  64. package/src/app/components/ui/tabs.tsx +6 -6
  65. package/src/app/components/ui/textarea.tsx +11 -7
  66. package/src/app/components/ui/toggle-group.tsx +2 -2
  67. package/src/app/components/ui/toggle.tsx +6 -6
  68. package/src/app/components/ui/tooltip.tsx +5 -2
  69. package/src/app/lib/design.ts +64 -0
  70. package/src/app/lib/export-html.ts +10 -1
  71. package/src/app/lib/export-pdf.ts +7 -0
  72. package/src/app/lib/folders.ts +1 -1
  73. package/src/app/lib/inspector/{useEditor.ts → use-editor.ts} +2 -1
  74. package/src/app/lib/sdk.ts +5 -0
  75. package/src/app/lib/slides.ts +1 -1
  76. package/src/app/lib/utils.ts +1 -1
  77. package/src/app/main.tsx +5 -2
  78. package/src/app/routes/{Home.tsx → home.tsx} +266 -97
  79. package/src/app/routes/presenter.tsx +400 -0
  80. package/src/app/routes/slide.tsx +519 -0
  81. package/src/app/styles.css +338 -67
  82. package/src/app/components/PdfProgressToast.tsx +0 -23
  83. package/src/app/components/Player.tsx +0 -100
  84. package/src/app/components/ThumbnailRail.tsx +0 -68
  85. package/src/app/components/inspector/SaveBar.tsx +0 -77
  86. package/src/app/routes/Slide.tsx +0 -478
  87. /package/dist/{config-SXL5qIl6.d.ts → config-DweCbRkQ.d.ts} +0 -0
  88. /package/src/app/lib/inspector/{useComments.ts → use-comments.ts} +0 -0
  89. /package/src/app/lib/{useWheelPageNavigation.ts → use-wheel-page-navigation.ts} +0 -0
@@ -6,10 +6,11 @@ import {
6
6
  Bold,
7
7
  ImageIcon,
8
8
  Italic,
9
- Trash2,
10
9
  X,
11
10
  } from 'lucide-react';
12
11
  import { useCallback, useEffect, useRef, useState } from 'react';
12
+ import { Field, NumberField, Section } from '@/components/panel/panel-fields';
13
+ import { PANEL_TRANSITION_MS, PanelShell, useAnimatedOpen } from '@/components/panel/panel-shell';
13
14
  import { Button } from '@/components/ui/button';
14
15
  import {
15
16
  Dialog,
@@ -19,8 +20,6 @@ import {
19
20
  DialogTitle,
20
21
  } from '@/components/ui/dialog';
21
22
  import { Input } from '@/components/ui/input';
22
- import { Label } from '@/components/ui/label';
23
- import { ScrollArea } from '@/components/ui/scroll-area';
24
23
  import {
25
24
  Select,
26
25
  SelectContent,
@@ -35,13 +34,9 @@ import { Toggle } from '@/components/ui/toggle';
35
34
  import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
36
35
  import { type AssetEntry, useAssets } from '@/lib/assets';
37
36
  import { findSlideSource } from '@/lib/inspector/fiber';
38
- import type { SlideComment } from '@/lib/inspector/useComments';
39
- import type { EditOp } from '@/lib/inspector/useEditor';
37
+ import type { EditOp } from '@/lib/inspector/use-editor';
40
38
  import { cn } from '@/lib/utils';
41
- import { type SelectedTarget, useInspector } from './InspectorProvider';
42
-
43
- const PANEL_W = 340;
44
- const PANEL_TRANSITION_MS = 280;
39
+ import { type SelectedTarget, useInspector } from './inspector-provider';
45
40
 
46
41
  type ElementSnapshot = {
47
42
  fontSize: number;
@@ -54,10 +49,11 @@ type ElementSnapshot = {
54
49
  letterSpacing: number;
55
50
  text: string | null;
56
51
  imageSrc: string | null;
52
+ placeholder: { hint: string; width?: number; height?: number } | null;
57
53
  };
58
54
 
59
55
  export function InspectorPanel() {
60
- const { active, slideId, selected, setSelected, bufferOps, pendingCount, comments, add, remove } =
56
+ const { active, slideId, selected, setSelected, bufferOps, pendingCount, add, applyEdit } =
61
57
  useInspector();
62
58
  const [snapshot, setSnapshot] = useState<ElementSnapshot | null>(null);
63
59
  const reloadCounter = useReloadCounter();
@@ -116,24 +112,15 @@ export function InspectorPanel() {
116
112
  );
117
113
 
118
114
  // `pinned` keeps the last selection rendered through the close-out
119
- // animation; `animVisible` lags one frame so the width transition
120
- // fires on the 0 → PANEL_W flip.
115
+ // animation so the panel's contents don't blank out before it collapses.
121
116
  const targetOpen = active && !!selected && !!snapshot;
122
117
  const [pinned, setPinned] = useState<{ s: SelectedTarget; n: ElementSnapshot } | null>(null);
123
- const [animVisible, setAnimVisible] = useState(false);
118
+ const animVisible = useAnimatedOpen(targetOpen && !!pinned);
124
119
 
125
120
  useEffect(() => {
126
121
  if (selected && snapshot) setPinned({ s: selected, n: snapshot });
127
122
  }, [selected, snapshot]);
128
123
 
129
- useEffect(() => {
130
- if (targetOpen && pinned) {
131
- const id = requestAnimationFrame(() => setAnimVisible(true));
132
- return () => cancelAnimationFrame(id);
133
- }
134
- setAnimVisible(false);
135
- }, [targetOpen, pinned]);
136
-
137
124
  useEffect(() => {
138
125
  if (!targetOpen && pinned) {
139
126
  const t = setTimeout(() => setPinned(null), PANEL_TRANSITION_MS);
@@ -145,114 +132,91 @@ export function InspectorPanel() {
145
132
  const { s: pinSelected, n: pinSnapshot } = pinned;
146
133
 
147
134
  return (
148
- <aside
149
- data-inspector-ui
150
- className="flex h-full shrink-0 justify-end overflow-hidden bg-card transition-[width,border-left-width] ease-out"
151
- style={{
152
- width: animVisible ? PANEL_W : 0,
153
- borderLeftWidth: animVisible ? 1 : 0,
154
- transitionDuration: `${PANEL_TRANSITION_MS}ms`,
155
- }}
156
- >
157
- <div style={{ width: PANEL_W }} className="flex h-full shrink-0 flex-col">
158
- <header className="flex shrink-0 items-center justify-between gap-2 border-b px-3 py-2.5">
135
+ <PanelShell
136
+ uiAttr="inspector"
137
+ animVisible={animVisible}
138
+ header={
139
+ <>
159
140
  <div className="flex min-w-0 items-center gap-2">
160
- <span className="rounded-md bg-muted px-1.5 py-0.5 font-mono text-[11px] text-foreground">
141
+ <span className="font-heading text-[12px] font-semibold tracking-tight">Inspect</span>
142
+ <span aria-hidden className="h-3 w-px bg-hairline" />
143
+ <span className="rounded-[3px] border border-hairline bg-card px-1.5 py-px font-mono text-[10.5px] text-foreground/85">
161
144
  &lt;{pinSelected.anchor.tagName.toLowerCase()}&gt;
162
145
  </span>
163
146
  </div>
164
147
  <Button
165
148
  variant="ghost"
166
- size="icon"
167
- className="size-7 text-muted-foreground hover:text-foreground"
149
+ size="icon-sm"
150
+ className="text-muted-foreground hover:text-foreground"
168
151
  onClick={() => setSelected(null)}
169
152
  aria-label="Deselect"
170
153
  >
171
154
  <X className="size-3.5" />
172
155
  </Button>
173
- </header>
174
-
175
- <ScrollArea className="flex flex-1 flex-col">
176
- <div className="flex min-h-full flex-col">
177
- {pinSnapshot.text !== null && (
178
- <Section title="Content">
179
- <ContentField snapshot={pinSnapshot} apply={apply} />
180
- </Section>
181
- )}
182
-
183
- <Separator />
184
-
185
- <Section title="Typography">
186
- <FontSizeField snapshot={pinSnapshot} apply={apply} />
187
- <FontWeightField snapshot={pinSnapshot} apply={apply} />
188
- <StyleToggles snapshot={pinSnapshot} apply={apply} />
189
- <LineHeightField snapshot={pinSnapshot} apply={apply} />
190
- <LetterSpacingField snapshot={pinSnapshot} apply={apply} />
191
- <TextAlignField snapshot={pinSnapshot} apply={apply} />
192
- </Section>
193
-
194
- <Separator />
195
-
196
- <Section title="Color">
197
- <ColorField
198
- label="Text"
199
- value={pinSnapshot.color}
200
- onChange={(v) => apply([{ kind: 'set-style', key: 'color', value: v }])}
201
- clearable={false}
202
- />
203
- <ColorField
204
- label="Background"
205
- value={pinSnapshot.backgroundColor ?? '#ffffff'}
206
- dim={!pinSnapshot.backgroundColor}
207
- onChange={(v) => apply([{ kind: 'set-style', key: 'backgroundColor', value: v }])}
208
- onClear={() => apply([{ kind: 'set-style', key: 'backgroundColor', value: null }])}
209
- clearable
210
- />
211
- </Section>
212
-
213
- {pinSnapshot.imageSrc !== null && (
214
- <>
215
- <Separator />
216
- <Section title="Image">
217
- <ImageField slideId={slideId} src={pinSnapshot.imageSrc} apply={apply} />
218
- </Section>
219
- </>
220
- )}
221
-
222
- <Separator />
223
-
224
- <div className="mt-auto">
225
- <CommentsSection
226
- comments={comments}
227
- selected={pinSelected}
228
- onAdd={add}
229
- onRemove={remove}
230
- />
231
- </div>
232
- </div>
233
- </ScrollArea>
234
- </div>
235
- </aside>
236
- );
237
- }
156
+ </>
157
+ }
158
+ footer={<CommentsSection selected={pinSelected} onAdd={add} />}
159
+ >
160
+ {pinSnapshot.text !== null && (
161
+ <Section title="Content">
162
+ <ContentField snapshot={pinSnapshot} apply={apply} />
163
+ </Section>
164
+ )}
238
165
 
239
- function Section({ title, children }: { title: string; children: React.ReactNode }) {
240
- return (
241
- <section className="px-4 py-4">
242
- <div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
243
- {title}
244
- </div>
245
- <div className="flex flex-col gap-3">{children}</div>
246
- </section>
247
- );
248
- }
166
+ <Separator />
167
+
168
+ <Section title="Typography">
169
+ <FontSizeField snapshot={pinSnapshot} apply={apply} />
170
+ <FontWeightField snapshot={pinSnapshot} apply={apply} />
171
+ <StyleToggles snapshot={pinSnapshot} apply={apply} />
172
+ <LineHeightField snapshot={pinSnapshot} apply={apply} />
173
+ <LetterSpacingField snapshot={pinSnapshot} apply={apply} />
174
+ <TextAlignField snapshot={pinSnapshot} apply={apply} />
175
+ </Section>
176
+
177
+ <Separator />
178
+
179
+ <Section title="Color">
180
+ <ColorField
181
+ label="Text"
182
+ value={pinSnapshot.color}
183
+ onChange={(v) => apply([{ kind: 'set-style', key: 'color', value: v }])}
184
+ clearable={false}
185
+ />
186
+ <ColorField
187
+ label="Background"
188
+ value={pinSnapshot.backgroundColor ?? '#ffffff'}
189
+ dim={!pinSnapshot.backgroundColor}
190
+ onChange={(v) => apply([{ kind: 'set-style', key: 'backgroundColor', value: v }])}
191
+ onClear={() => apply([{ kind: 'set-style', key: 'backgroundColor', value: null }])}
192
+ clearable
193
+ />
194
+ </Section>
249
195
 
250
- function Field({ label, children }: { label: string; children: React.ReactNode }) {
251
- return (
252
- <div className="grid grid-cols-[80px_1fr] items-center gap-3">
253
- <Label className="text-[11px] font-normal text-muted-foreground">{label}</Label>
254
- <div className="flex min-w-0 items-center gap-2">{children}</div>
255
- </div>
196
+ {pinSnapshot.imageSrc !== null && (
197
+ <>
198
+ <Separator />
199
+ <Section title="Image">
200
+ <ImageField slideId={slideId} src={pinSnapshot.imageSrc} apply={apply} />
201
+ </Section>
202
+ </>
203
+ )}
204
+
205
+ {pinSnapshot.placeholder && (
206
+ <>
207
+ <Separator />
208
+ <Section title="Image placeholder">
209
+ <PlaceholderField
210
+ slideId={slideId}
211
+ hint={pinSnapshot.placeholder.hint}
212
+ line={pinSelected.line}
213
+ column={pinSelected.column}
214
+ applyEdit={applyEdit}
215
+ />
216
+ </Section>
217
+ </>
218
+ )}
219
+ </PanelShell>
256
220
  );
257
221
  }
258
222
 
@@ -604,40 +568,6 @@ function ColorField({
604
568
  );
605
569
  }
606
570
 
607
- function NumberField({
608
- value,
609
- onChange,
610
- min,
611
- max,
612
- step = 1,
613
- suffix,
614
- }: {
615
- value: number;
616
- onChange: (n: number) => void;
617
- min?: number;
618
- max?: number;
619
- step?: number;
620
- suffix?: string;
621
- }) {
622
- return (
623
- <div className="flex h-8 shrink-0 items-center rounded-md border bg-background pr-2 shadow-xs focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50">
624
- <input
625
- type="number"
626
- value={value}
627
- onChange={(e) => {
628
- const n = Number(e.target.value);
629
- if (Number.isFinite(n)) onChange(n);
630
- }}
631
- min={min}
632
- max={max}
633
- step={step}
634
- className="h-full w-12 bg-transparent px-2 text-right text-[11px] tabular-nums outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
635
- />
636
- {suffix && <span className="text-[10px] text-muted-foreground">{suffix}</span>}
637
- </div>
638
- );
639
- }
640
-
641
571
  function ImageField({
642
572
  slideId,
643
573
  src,
@@ -694,6 +624,61 @@ function ImageField({
694
624
  );
695
625
  }
696
626
 
627
+ function PlaceholderField({
628
+ slideId,
629
+ hint,
630
+ line,
631
+ column,
632
+ applyEdit,
633
+ }: {
634
+ slideId: string;
635
+ hint: string;
636
+ line: number;
637
+ column: number;
638
+ applyEdit: (line: number, column: number, ops: EditOp[]) => Promise<void>;
639
+ }) {
640
+ const [open, setOpen] = useState(false);
641
+ const [submitting, setSubmitting] = useState(false);
642
+ return (
643
+ <div className="space-y-2">
644
+ <p className="text-[11px] leading-relaxed text-muted-foreground">
645
+ Hint: <span className="font-medium text-foreground">{hint}</span>
646
+ </p>
647
+ <Button
648
+ type="button"
649
+ variant="outline"
650
+ size="sm"
651
+ className="w-full"
652
+ disabled={submitting}
653
+ onClick={() => setOpen(true)}
654
+ >
655
+ <ImageIcon className="size-3.5" />
656
+ Replace…
657
+ </Button>
658
+ {open && (
659
+ <AssetPickerDialog
660
+ slideId={slideId}
661
+ onClose={() => setOpen(false)}
662
+ onPick={async (asset) => {
663
+ setOpen(false);
664
+ setSubmitting(true);
665
+ try {
666
+ await applyEdit(line, column, [
667
+ {
668
+ kind: 'replace-placeholder-with-image',
669
+ assetPath: `./assets/${asset.name}`,
670
+ },
671
+ ]);
672
+ } finally {
673
+ setSubmitting(false);
674
+ }
675
+ }}
676
+ />
677
+ )}
678
+ </div>
679
+ );
680
+ }
681
+
697
682
  function AssetPickerDialog({
698
683
  slideId,
699
684
  onClose,
@@ -757,15 +742,11 @@ function AssetPickerDialog({
757
742
  }
758
743
 
759
744
  function CommentsSection({
760
- comments,
761
745
  selected,
762
746
  onAdd,
763
- onRemove,
764
747
  }: {
765
- comments: SlideComment[];
766
748
  selected: { line: number; column: number };
767
749
  onAdd: (line: number, column: number, text: string) => Promise<void>;
768
- onRemove: (id: string) => Promise<void>;
769
750
  }) {
770
751
  const [draft, setDraft] = useState('');
771
752
  const [submitting, setSubmitting] = useState(false);
@@ -783,68 +764,29 @@ function CommentsSection({
783
764
  };
784
765
 
785
766
  return (
786
- <Section title={comments.length ? `Comments · ${comments.length}` : 'Comments'}>
767
+ <Section title="Note for the agent">
787
768
  <div className="flex flex-col gap-2">
788
- <Textarea
789
- value={draft}
790
- onChange={(e) => setDraft(e.target.value)}
791
- onKeyDown={(e) => {
792
- if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
793
- e.preventDefault();
794
- submit();
795
- }
796
- }}
797
- placeholder="Describe a change for the agent…"
798
- className="min-h-16 resize-none text-xs"
799
- />
800
- <div className="flex items-center justify-between">
801
- <span className="text-[10px] text-muted-foreground">⌘/Ctrl + Enter</span>
802
- <Button
803
- size="sm"
804
- disabled={submitting || !draft.trim()}
805
- onClick={submit}
806
- className="h-7 px-2.5 text-[11px]"
807
- >
808
- Add comment
769
+ <div className="comment-cue rounded-[6px]">
770
+ <Textarea
771
+ value={draft}
772
+ onChange={(e) => setDraft(e.target.value)}
773
+ onKeyDown={(e) => {
774
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
775
+ e.preventDefault();
776
+ submit();
777
+ }
778
+ }}
779
+ placeholder="Describe a change for the agent…"
780
+ className="min-h-16 resize-none text-[12px]"
781
+ />
782
+ </div>
783
+ <div className="flex items-center justify-between gap-2">
784
+ <span className="font-mono text-[10.5px] text-muted-foreground/70">⌘↵ to send</span>
785
+ <Button size="sm" variant="brand" disabled={submitting || !draft.trim()} onClick={submit}>
786
+ Add note
809
787
  </Button>
810
788
  </div>
811
789
  </div>
812
-
813
- {comments.length === 0 ? (
814
- <p className="text-[11px] text-muted-foreground">No comments yet.</p>
815
- ) : (
816
- <>
817
- <ul className="flex flex-col gap-1">
818
- {comments.map((c) => (
819
- <li
820
- key={c.id}
821
- className="group flex items-start gap-2 rounded-md border bg-background px-2.5 py-2 transition-colors hover:bg-muted/40"
822
- >
823
- <div className="min-w-0 flex-1">
824
- <div className="font-mono text-[10px] text-muted-foreground">line {c.line}</div>
825
- <div className="mt-0.5 text-xs leading-relaxed break-words">{c.note}</div>
826
- </div>
827
- <Button
828
- variant="ghost"
829
- size="icon"
830
- className="size-6 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 hover:text-destructive"
831
- onClick={() => onRemove(c.id)}
832
- aria-label="Delete comment"
833
- >
834
- <Trash2 className="size-3.5" />
835
- </Button>
836
- </li>
837
- ))}
838
- </ul>
839
- <p className="text-[10px] text-muted-foreground">
840
- Run{' '}
841
- <code className="rounded bg-muted px-1 py-0.5 font-mono text-foreground">
842
- /apply-comments
843
- </code>{' '}
844
- to apply.
845
- </p>
846
- </>
847
- )}
848
790
  </Section>
849
791
  );
850
792
  }
@@ -856,6 +798,15 @@ function readSnapshot(el: HTMLElement): ElementSnapshot {
856
798
  el.tagName === 'IMG'
857
799
  ? (el as HTMLImageElement).currentSrc || (el as HTMLImageElement).src || null
858
800
  : null;
801
+ const ph = el.dataset.slidePlaceholder ?? null;
802
+ const placeholder =
803
+ ph !== null
804
+ ? {
805
+ hint: ph,
806
+ width: el.dataset.placeholderW ? Number(el.dataset.placeholderW) : undefined,
807
+ height: el.dataset.placeholderH ? Number(el.dataset.placeholderH) : undefined,
808
+ }
809
+ : null;
859
810
 
860
811
  return {
861
812
  fontSize: parseFloat(cs.fontSize) || 16,
@@ -868,6 +819,7 @@ function readSnapshot(el: HTMLElement): ElementSnapshot {
868
819
  letterSpacing: parseLetterSpacing(cs.letterSpacing),
869
820
  text,
870
821
  imageSrc,
822
+ placeholder,
871
823
  };
872
824
  }
873
825