diff --git a/.changeset/json-deck-prop.md b/.changeset/json-deck-prop.md
new file mode 100644
index 0000000..85f488d
--- /dev/null
+++ b/.changeset/json-deck-prop.md
@@ -0,0 +1,29 @@
+---
+"@textcortex/slidewise": minor
+---
+
+Add `jsonDeck` prop and expose `resolveJsonDeck` — the AI-facing entry point for feeding model-generated decks into the editor.
+
+**`SlidewiseEditor` / `Slidewise.Root`**
+
+- New top-level `jsonDeck?: Deck | string` prop. Pass either a parsed `Deck` object or a JSON string and Slidewise will `JSON.parse` (when needed) and run the value through `migrate()` before mounting — no manual normalisation required. Takes precedence over `deck` when both are provided.
+- `deck` is now optional; one of `deck` or `jsonDeck` must be supplied. Existing callers passing only `deck` are unaffected.
+
+**Why**
+
+This is the contract LLMs target when generating slides. The exported `Deck` TypeScript type is the JSON schema: hosts can prompt their model to emit a `Deck`-shaped object (or stringified JSON) and pipe it straight into `` without writing glue.
+
+**New export: `resolveJsonDeck(input: Deck | string): Deck`**
+
+Same parse + migrate helper Slidewise uses internally. Use it to validate AI output before passing it to the editor, or when building tools that emit `Deck` JSON outside of React.
+
+```tsx
+import { SlidewiseEditor, resolveJsonDeck } from "@textcortex/slidewise";
+
+// Pass JSON directly:
+
+
+// Or validate first:
+const deck = resolveJsonDeck(aiGeneratedJsonString);
+
+```
diff --git a/packages/slidewise/src/SlidewiseEditor.tsx b/packages/slidewise/src/SlidewiseEditor.tsx
index 199baf6..92b7f7d 100644
--- a/packages/slidewise/src/SlidewiseEditor.tsx
+++ b/packages/slidewise/src/SlidewiseEditor.tsx
@@ -30,8 +30,25 @@ export interface SlidewiseEditorProps {
* `onChange` — that would loop. Hold the deck in a stable ref, and
* only pass a new one when you intentionally want to reset the editor
* (e.g. discard changes, load a different file).
+ *
+ * One of `deck` or `jsonDeck` is required.
+ */
+ deck?: Deck;
+ /**
+ * Deck supplied as JSON — either a `Deck` object or a JSON string. This
+ * is the AI-facing entry point: feed model output directly without
+ * manually calling `JSON.parse` or `migrate()`. The value is parsed (if
+ * a string) and run through `migrate()` so older schema versions are
+ * upgraded transparently.
+ *
+ * Takes precedence over `deck` when both are provided. Pass a stable
+ * reference (or stable string) on subsequent renders — changing it
+ * resets the editor's internal state, same as swapping `deck`.
+ *
+ * The JSON shape is the public `Deck` type exported from this package.
+ * Use it as the schema your LLM targets when generating new decks.
*/
- deck: Deck;
+ jsonDeck?: Deck | string;
/** Fires after every committed mutation; receives the updated deck. */
onChange?: (deck: Deck) => void;
/** Fires when the user clicks "Save" in the top bar. */
@@ -159,6 +176,7 @@ export const SlidewiseEditor = forwardRef<
>(function SlidewiseEditor(
{
deck,
+ jsonDeck,
onChange,
onSave,
onExport,
@@ -199,6 +217,7 @@ export const SlidewiseEditor = forwardRef<
const rootProps: SlidewiseRootProps = {
deck,
+ jsonDeck,
onChange,
onSave,
onExport,
diff --git a/packages/slidewise/src/compound/SlidewiseRoot.tsx b/packages/slidewise/src/compound/SlidewiseRoot.tsx
index 795627d..061b484 100644
--- a/packages/slidewise/src/compound/SlidewiseRoot.tsx
+++ b/packages/slidewise/src/compound/SlidewiseRoot.tsx
@@ -3,6 +3,7 @@ import {
useEffect,
useId,
useImperativeHandle,
+ useMemo,
useRef,
useState,
type CSSProperties,
@@ -15,6 +16,7 @@ import {
useEditorStore,
} from "@/lib/StoreProvider";
import { collectFontFamilies, ensureGoogleFontsLoaded } from "@/lib/fonts";
+import { resolveJsonDeck } from "@/lib/schema/json";
import type { Deck } from "@/lib/types";
import { GridView } from "@/components/editor/GridView";
import { PlayMode } from "@/components/editor/PlayMode";
@@ -39,8 +41,22 @@ export interface SlidewiseRootProps {
* Deck to load on mount. Pass a new reference only when you intend to
* reset the editor's state (e.g. discard changes, load a different file)
* — passing a new reference on every `onChange` would loop.
+ *
+ * One of `deck` or `jsonDeck` is required.
*/
- deck: Deck;
+ deck?: Deck;
+ /**
+ * Deck supplied as JSON — either a `Deck` object or a JSON string. This is
+ * the AI-facing entry point: feed model output directly without manually
+ * calling `JSON.parse` or `migrate()`. The input is parsed (if a string)
+ * and run through `migrate()` to upgrade older schema versions.
+ *
+ * Takes precedence over `deck` when both are provided. Like `deck`, the
+ * value should only change when you intentionally want to reset the
+ * editor's state — pass a stable reference (or stable string) on subsequent
+ * renders to avoid re-loading on every commit.
+ */
+ jsonDeck?: Deck | string;
/** Fires after every committed mutation. */
onChange?: (deck: Deck) => void;
/** Fires when the user invokes save (top bar button or imperative API). */
@@ -222,15 +238,30 @@ export interface SlidewiseRootHandle {
* which is just `` rendering the standard layout.
*/
export const Root = forwardRef>(
- function SlidewiseRoot(props, ref) {
+ function SlidewiseRoot({ deck, jsonDeck, ...rest }, ref) {
+ const resolvedDeck = useMemo(
+ () => resolveInputDeck(deck, jsonDeck),
+ [deck, jsonDeck]
+ );
return (
-
-
+
+
);
}
);
+function resolveInputDeck(
+ deck: Deck | undefined,
+ jsonDeck: Deck | string | undefined
+): Deck {
+ if (jsonDeck !== undefined) return resolveJsonDeck(jsonDeck);
+ if (deck !== undefined) return deck;
+ throw new Error(
+ "Slidewise: requires either a `deck` or `jsonDeck` prop."
+ );
+}
+
function RootInner({
deck,
onChange,
@@ -258,7 +289,9 @@ function RootInner({
style,
children,
forwardedRef,
-}: PropsWithChildren & {
+}: PropsWithChildren> & {
+ /** Resolved deck — always provided by the outer `Root`. */
+ deck: Deck;
forwardedRef: Ref;
}) {
const store = useEditorStore();
diff --git a/packages/slidewise/src/index.ts b/packages/slidewise/src/index.ts
index 0a20f51..3ea03a4 100644
--- a/packages/slidewise/src/index.ts
+++ b/packages/slidewise/src/index.ts
@@ -92,6 +92,7 @@ export { parsePptx, serializeDeck } from "./lib/pptx";
export type { ParseDiagnostics, ParseResult } from "./lib/pptx/types";
export { migrate, CURRENT_DECK_VERSION } from "./lib/schema/migrate";
+export { resolveJsonDeck } from "./lib/schema/json";
export type {
Deck,
diff --git a/packages/slidewise/src/lib/schema/__tests__/json.test.ts b/packages/slidewise/src/lib/schema/__tests__/json.test.ts
new file mode 100644
index 0000000..fe416ef
--- /dev/null
+++ b/packages/slidewise/src/lib/schema/__tests__/json.test.ts
@@ -0,0 +1,39 @@
+import { describe, it, expect } from "vitest";
+import { resolveJsonDeck } from "../json";
+import { CURRENT_DECK_VERSION } from "../migrate";
+
+describe("schema/json.resolveJsonDeck", () => {
+ it("accepts a parsed Deck object and stamps the current version", () => {
+ const out = resolveJsonDeck({
+ title: "From AI",
+ slides: [{ id: "s1", background: "#FFFFFF", elements: [] }],
+ } as never);
+ expect(out.version).toBe(CURRENT_DECK_VERSION);
+ expect(out.title).toBe("From AI");
+ expect(out.slides).toHaveLength(1);
+ });
+
+ it("accepts a JSON string", () => {
+ const json = JSON.stringify({
+ version: CURRENT_DECK_VERSION,
+ title: "Stringified",
+ slides: [{ id: "s1", background: "#000", elements: [] }],
+ });
+ const out = resolveJsonDeck(json);
+ expect(out.title).toBe("Stringified");
+ expect(out.slides[0].id).toBe("s1");
+ });
+
+ it("throws on malformed JSON strings", () => {
+ expect(() => resolveJsonDeck("{ not valid")).toThrow();
+ });
+
+ it("rejects decks from a newer schema", () => {
+ const json = JSON.stringify({
+ version: CURRENT_DECK_VERSION + 1,
+ title: "Future",
+ slides: [],
+ });
+ expect(() => resolveJsonDeck(json)).toThrow(/newer than this build/);
+ });
+});
diff --git a/packages/slidewise/src/lib/schema/json.ts b/packages/slidewise/src/lib/schema/json.ts
new file mode 100644
index 0000000..54375ac
--- /dev/null
+++ b/packages/slidewise/src/lib/schema/json.ts
@@ -0,0 +1,18 @@
+import type { Deck } from "@/lib/types";
+import { migrate } from "./migrate";
+
+/**
+ * Normalise a host-supplied deck JSON — either a parsed `Deck` object or a
+ * JSON string — into a current-schema `Deck`. Runs the input through
+ * `migrate()` so older `version` stamps are upgraded and the basic shape is
+ * validated. This is the entry point hosts use when feeding AI-generated
+ * decks to ``.
+ *
+ * Throws if the string isn't valid JSON, or if the resulting object is
+ * missing the basic `Deck` shape / its `version` is newer than this build.
+ */
+export function resolveJsonDeck(input: Deck | string): Deck {
+ const parsed: unknown =
+ typeof input === "string" ? (JSON.parse(input) as unknown) : input;
+ return migrate(parsed);
+}