@hyperframes/studio 0.6.112 → 0.6.114

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.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * setSlideshowManifest — Studio persist helper for the slideshow JSON island.
3
+ *
4
+ * The island is a `<script type="application/hyperframes-slideshow+json">` node
5
+ * embedded in the composition HTML. Because <script> nodes are not tracked by
6
+ * the SDK element tree (they have no hf-id), we cannot use a `setText` dispatch
7
+ * op. Instead we:
8
+ * 1. Call `sdkSession.serialize()` to get the current HTML.
9
+ * 2. Replace or insert the island with the new manifest JSON.
10
+ * 3. Write via `persistSdkSerialize` — the same low-level writer used by the
11
+ * other SDK-cutover paths (sdkDeletePersist, sdkTimingPersist, etc.).
12
+ *
13
+ * Inserting when absent: if no island exists in the serialized HTML we insert
14
+ * one before `</body>` (or, if no </body>, append to the end of the document).
15
+ * This means callers do NOT need to pre-scaffold the island; Task 11 / the panel
16
+ * can call persistSlideshowManifest on a fresh composition.
17
+ */
18
+
19
+ import type { SlideshowManifest } from "@hyperframes/core/slideshow";
20
+ import {
21
+ SLIDESHOW_ISLAND_TYPE,
22
+ SLIDESHOW_MANIFEST_VERSION,
23
+ parseSlideshowManifest,
24
+ slideshowIslandRegex,
25
+ } from "@hyperframes/core/slideshow";
26
+ import type { Composition } from "@hyperframes/sdk";
27
+ import type { CutoverDeps } from "./sdkCutover";
28
+ import { persistSdkSerialize } from "./sdkCutover";
29
+
30
+ // Matches ALL <script type="application/hyperframes-slideshow+json"> ... </script>
31
+ // blocks (global + case-insensitive) so we can strip every stale island in one pass.
32
+ const ISLAND_RE = slideshowIslandRegex("gi");
33
+
34
+ export function buildSlideshowIslandHtml(manifest: SlideshowManifest): string {
35
+ // Stamp the schema version (preserve an existing one) so future schema
36
+ // changes can detect and migrate older islands.
37
+ const versioned: SlideshowManifest = {
38
+ version: manifest.version ?? SLIDESHOW_MANIFEST_VERSION,
39
+ ...manifest,
40
+ };
41
+ // Escape `<` and `>` so that a manifest field containing `</script>` cannot
42
+ // break out of the script tag. JSON.parse round-trips </> unchanged.
43
+ const json = JSON.stringify(versioned, null, 2).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
44
+ return `<script type="${SLIDESHOW_ISLAND_TYPE}">\n${json}\n</script>`;
45
+ }
46
+
47
+ export interface PersistSlideshowArgs {
48
+ manifest: SlideshowManifest;
49
+ /** Live SDK Composition session — used only to read the current serialized HTML. */
50
+ sdkSession: Pick<Composition, "serialize">;
51
+ /** Exact on-disk bytes for the undo-history `before` baseline. */
52
+ originalContent: string;
53
+ targetPath: string;
54
+ deps: CutoverDeps;
55
+ /** Optional label override (default: "Edit slideshow"). */
56
+ label?: string;
57
+ /**
58
+ * When provided, threads a coalesceKey into recordEdit so rapid writes
59
+ * (e.g. per-keystroke notes changes) collapse to a single undo entry.
60
+ */
61
+ coalesceKey?: string;
62
+ }
63
+
64
+ export async function persistSlideshowManifest(args: PersistSlideshowArgs): Promise<void> {
65
+ const { manifest, sdkSession, originalContent, targetPath, deps, label, coalesceKey } = args;
66
+
67
+ const islandHtml = buildSlideshowIslandHtml(manifest);
68
+
69
+ // Write-time validation: confirm the island we just built round-trips to a
70
+ // valid manifest before touching disk, so a malformed edit can't corrupt the
71
+ // composition. parseSlideshowManifest throws on a structurally-invalid island.
72
+ try {
73
+ if (!parseSlideshowManifest(islandHtml)) {
74
+ throw new Error("built island did not parse back to a manifest");
75
+ }
76
+ } catch (err) {
77
+ throw new Error(`refusing to persist invalid slideshow manifest: ${(err as Error).message}`);
78
+ }
79
+
80
+ const current = sdkSession.serialize();
81
+
82
+ // Strip ALL existing islands (handles the case where two stale islands
83
+ // accumulated) then insert exactly one fresh island.
84
+ const stripped = current.replace(ISLAND_RE, "");
85
+
86
+ let after: string;
87
+ const bodyClose = stripped.lastIndexOf("</body>");
88
+ if (bodyClose !== -1) {
89
+ after = stripped.slice(0, bodyClose) + islandHtml + "\n" + stripped.slice(bodyClose);
90
+ } else {
91
+ after = stripped + "\n" + islandHtml;
92
+ }
93
+
94
+ // No-op gate: if the rewritten HTML is byte-identical to the current serialized
95
+ // HTML, skip the write — avoids a spurious disk write and a no-op undo entry.
96
+ if (after === current) return;
97
+
98
+ await persistSdkSerialize(after, targetPath, originalContent, deps, {
99
+ label: label ?? "Edit slideshow",
100
+ ...(coalesceKey ? { coalesceKey } : {}),
101
+ });
102
+ }
@@ -13,7 +13,7 @@ export interface AppToast {
13
13
  tone: "error" | "info";
14
14
  }
15
15
 
16
- export type RightPanelTab = "layers" | "design" | "renders" | "block-params";
16
+ export type RightPanelTab = "layers" | "design" | "renders" | "block-params" | "slideshow";
17
17
  export type RightInspectorPane = "layers" | "design";
18
18
 
19
19
  export interface RightInspectorPanes {
@@ -204,6 +204,7 @@ export function clampNumber(value: number, min: number, max: number): number {
204
204
  return Math.min(Math.max(value, min), max);
205
205
  }
206
206
 
207
+ // fallow-ignore-next-line unused-export
207
208
  export { COMPOSITION_ROOT_OPEN_TAG_RE } from "./compositionPatterns";
208
209
 
209
210
  export function collectHtmlIds(source: string): string[] {