Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/diagram-primitive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@textcortex/slidewise": minor
---

First-class **diagram** element (P3 / F3). A new `DiagramElement` models
process / timeline / funnel / matrix / cycle / list visuals as an ordered set
of labelled `nodes` instead of a hand-placed cluster of shapes and lines. The
renderer and the PPTX writer share one layout function (`layoutDiagram`, also
exported), so the on-canvas preview and the saved file can't drift. On export a
diagram serialises to a single labelled `<p:grpSp>` of real shapes + connectors
— so it stays grouped and editable in PowerPoint (move/resize as one unit)
rather than collapsing to anonymous floating shapes. Exposed via the
`DiagramElement` / `DiagramNode` / `DiagramKind` types and the `layoutDiagram`
helper.
26 changes: 26 additions & 0 deletions .changeset/host-generation-feedback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
"@textcortex/slidewise": minor
---

Host deck-generation ergonomics (review follow-ups for layout instantiation):

- **`summarizeLayouts(deck, options)`** — `{ compact: true }` returns the minimal
`{ id, name?, role, fillable }` shape (no geometry) for a tight model-context
budget; `{ dedupe: true }` collapses layouts that share a role + full
placeholder-slot inventory (text **and** non-text chart/picture/table slots)
into one representative carrying the rest as `aliases`, so an 85-layout
template surfaces as its handful of distinct kinds — and a chart-bearing
layout never collapses into a text-only twin. Composable.
- **Robust `sourceLayoutId` resolution** — a host-authored slide's
`sourceLayoutId` now resolves from `deck.layouts` **or**, when the host didn't
carry the layouts array, by the `ppt/slideLayouts/<id>.xml` id convention
against the `{ source }` archive. When it resolves to nothing, `serializeDeck`
emits a structured `{ code: "layout-unresolved", slideIndex, layoutId }`
warning instead of silently falling back.
- **Richer `chrome-skipped` warning** — now carries the detected `sourceAspect`
and `outputAspect` so a host can explain *why* chrome was dropped.

Plus README: the host "author-a-slide" contract (`{ sourceLayoutId,
background: "transparent", elements }` without a JS call), the recipe for
placing non-text slots from `summarizeLayouts` geometry, and the server-side
`layoutDiagram` render recipe with its DOM-free guarantee.
18 changes: 18 additions & 0 deletions .changeset/image-crop-radius.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@textcortex/slidewise": minor
---

Round-trip fidelity fixes (P4):

- **Image `crop` / `radius`** now round-trip. Previously an image's `crop`
(`<a:srcRect>`) was read on import but silently dropped on export, and corner
`radius` was neither parsed nor written. `serializeDeck` now routes any image
carrying a `crop` or `radius` through a dedicated `<p:pic>` writer (emitting
`<a:srcRect>` and `roundRect` geometry) instead of pptxgenjs — whose
cover/contain sizing emits its own `<a:srcRect>` and would fight a user crop —
and `parsePptx` reads a rounded picture's corner radius back. Plain
(uncropped, square-cornered) images keep the existing path unchanged.
- **Text-run letter-case (`cap`)** now round-trips. A run's `cap`
(`"all"` / `"small"`, OOXML `<a:rPr cap>`) was parsed on import but dropped on
export (pptxgenjs has no `cap` option). It's now re-applied per run in
post-process, so all-caps / small-caps styling survives a save.
8 changes: 8 additions & 0 deletions .changeset/layout-instantiation-hardening.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@textcortex/slidewise": minor
---

Harden layout instantiation for AI deck generation (P1 / F1):

- **Layout-instantiated slides now inherit their layout's background.** A slide minted by `addSlideFromLayout` with the default `transparent` background no longer serialises an explicit `<a:noFill/>` `<p:bg>` — that empty background was overriding the layout/master/theme inheritance, so instantiated slides lost their on-brand background. They now stay `<p:bg>`-less and paint from their `sourceLayoutId` layout's chrome (matching the source-slide guarantee for cloned/reordered slides).
- **Layout-selection metadata.** `DeckLayout.type` now carries the raw OOXML `<p:sldLayout type>` role, and the new `summarizeLayouts(deck)` returns a compact, model-context-friendly layout menu (friendly `role` label, fillable `fills` keys, per-placeholder kind/category/geometry) so a host can have a model pick a layout per slide. `placeholderKey(ph)` exposes the exact `fills` key for a placeholder.
13 changes: 13 additions & 0 deletions .changeset/non-16-9-diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@textcortex/slidewise": minor
---

Machine-readable serialization diagnostics (P2 / B3). `serializeDeck` now
accepts `SerializeOptions.onWarning`, a callback invoked with a structured
`SerializeWarning` when the output degrades. The key case is
`"chrome-skipped"` — emitted when a `source` template's masters / layouts /
theme / fonts can't be carried over because its slide size is unreadable, so
the deck falls back to generic chrome. Hosts can now detect and surface the
degradation instead of only seeing a console line. (Non-16:9 sizing for 4:3 /
16:10 / custom templates already drives the output slide size; this adds the
escape-hatch signal when it can't.)
191 changes: 191 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,203 @@ const pptx: Blob = await serializeDeck(deck); // export
const safe: Deck = migrate(unknownDeckJson); // normalise an external deck
```

`serializeDeck(deck, { source })` reproduces a source template's slide size
(16:9, 4:3, 16:10, or custom) and carries over its masters / layouts / theme /
fonts. Degradations are reported through an optional `onWarning` diagnostics
sink (structured `SerializeWarning`s) so the host can surface them rather than
ship a silently off-brand deck:

```ts
await serializeDeck(deck, {
source,
onWarning: (w) => {
switch (w.code) {
case "chrome-skipped": // size unreadable → generic chrome
notifyHost(`${w.message} (source ${w.sourceAspect}, output ${w.outputAspect})`);
break;
case "layout-unresolved": // a slide's sourceLayoutId matched nothing
notifyHost(`slide ${w.slideIndex}: layout ${w.layoutId} not found`);
break;
case "element-write-failed": // one element threw; rest of slide intact
notifyHost(`${w.elementType} ${w.elementId} skipped`);
break;
}
},
});
```

`migrate()` runs every external deck (PPTX import, JSON import, localStorage
hydration, host props) through the schema migration chain so the rest of the
editor only sees current-shape decks. It throws if the input was written by a
newer Slidewise than the host has installed — pin the version range you can
support.

### Generating slides from the template's layouts

`parsePptx` exposes the source template's master layouts on `deck.layouts`.
`addSlideFromLayout(deck, layoutId, opts)` mints a fresh slide bound to one of
them — the unlock for generating a deck with more slides than the template
hand-authored, using the template's own layout variety. The new slide carries
`sourceLayoutId`, so `serializeDeck(deck, { source })` paints its
background / fonts / theme / footer chrome from that layout (not from output
position), exactly like a cloned source slide.

```ts
import {
parsePptx,
serializeDeck,
summarizeLayouts,
addSlideFromLayout,
} from "@textcortex/slidewise";

const deck = await parsePptx(blob);

// 1. Show a model a compact menu of the available layouts. The shape is
// structured (not a string) so you can trim it to your context budget.
// For large templates (e.g. 85 layouts) pass options:
// summarizeLayouts(deck, { compact: true }) // { id, name?, role, fillable }, no geometry
// summarizeLayouts(deck, { dedupe: true }) // collapse layouts with the same role + full
// // slot inventory (text AND chart/image/table);
// // others → `aliases`. A chart-bearing layout
// // never collapses into a text-only twin.
// summarizeLayouts(deck, { compact: true, dedupe: true }) // both
const menu = summarizeLayouts(deck);
// [
// { id: "slideLayout2", name: "Title and Content", type: "obj",
// role: "Title and content", fillable: ["title", "body:1"],
// placeholders: [
// { key: "title", type: "title", category: "text", fillable: true, x, y, w, h },
// { key: "body:1", type: "body", idx: 1, category: "text", fillable: true, x, y, w, h },
// ] },
// ...
// ]

// 2. Instantiate a slide from the chosen layout, filling its text placeholders.
const next = addSlideFromLayout(deck, "slideLayout2", {
fills: { title: "Q3 Results", "body:1": "Revenue up 24%" },
});

const pptx = await serializeDeck(next, { source: blob });
```

**The `fills` contract.** `fills` is keyed by placeholder, resolved
most-specific-first: `"type:idx"` (e.g. `"body:1"`), then the bare `"type"`
(e.g. `"title"`), then the bare index as a string. `placeholderKey(ph)` (and
`LayoutSlotSummary.key` from `summarizeLayouts`) gives you the exact key for a
slot. Only **text** placeholders are fillable — `title`, `ctrTitle`,
`subTitle`, `body`, `obj`, and the untyped default (`LayoutSlotSummary.fillable
=== true`, `category === "text"`). Those become editable text elements
positioned per the layout. Non-text slots (pictures, tables, charts, and footer
chrome like date / slide-number / footer) are skipped — inherit them from the
master, or add real `image` / `table` / `chart` elements to the returned slide.
A placeholder with no matching `fills` entry becomes an empty, editable text
box.

#### Authoring slides directly (no `addSlideFromLayout`)

`addSlideFromLayout` is a convenience; the underlying contract is just a slide
shape, so a host that builds deck JSON in another language (e.g. Python) can
author it directly and let `serializeDeck` paint the chrome:

```jsonc
{
"id": "slide-7",
"background": "transparent", // inherit the layout/master/theme background
"sourceLayoutId": "slideLayout12",
"elements": [ /* any text / image / chart / table / diagram elements */ ]
}
```

Contract:

- **`sourceLayoutId` alone is enough.** No JS call is required — set it on the
slide JSON and `serializeDeck(deck, { source })` points the slide at that
layout's part and paints its background / fonts / theme / footer chrome.
- **Put arbitrary elements on the slide.** It is not limited to elements
`addSlideFromLayout` produced — your filled text, native charts, generated
images, tables, and diagrams all land normally, on top of the layout chrome.
- **Place non-text slots yourself from the layout geometry.** For an image /
chart / table slot, read its geometry from `summarizeLayouts` (every
placeholder is listed with `category` + `x/y/w/h`, fillable or not) and add a
real element at that box:

```ts
const slot = summarizeLayouts(deck)
.find((l) => l.id === "slideLayout12")!
.placeholders.find((p) => p.category === "picture")!;
authoredSlide.elements.push({
id: "img-1", type: "image", src: generatedPhotoDataUrl, fit: "cover",
x: slot.x, y: slot.y, w: slot.w, h: slot.h, rotation: 0, z: 1,
});
```

- **Background:** keep `"transparent"` to inherit the layout/master background;
set an explicit hex to override it.
- **`sourceLayoutId` resolution** is by id, resolved from `deck.layouts` (if you
carried the array) **or** by the `ppt/slideLayouts/<id>.xml` convention
against the `{ source }` archive — so you don't have to ship the `layouts`
array. If neither resolves, the slide falls back to the first source layout
and `serializeDeck` emits a `{ code: "layout-unresolved", slideIndex,
layoutId }` warning through `onWarning` (see below) rather than failing
silently.

### Diagrams

`DiagramElement` models a process / timeline / funnel / matrix / cycle / list
as an ordered set of labelled `nodes`, laid out by `kind`. It renders on the
canvas and serialises to a single grouped, editable `<p:grpSp>` of real shapes
+ connectors (not a flat pile of anonymous shapes). The renderer and writer
share `layoutDiagram`, exported so a host preview / server render stays in sync.

```ts
const slide = {
id: "s1",
background: "transparent",
elements: [
{
id: "d1",
type: "diagram",
kind: "process",
x: 120,
y: 240,
w: 1680,
h: 320,
rotation: 0,
z: 1,
nodes: [
{ id: "n1", text: "Discover" },
{ id: "n2", text: "Design" },
{ id: "n3", text: "Ship" },
],
// optional: palette?: string[], color?, fontFamily?, fontSize?,
// and per-node fill? / color? overrides.
},
],
};
```

`kind` is one of `"process" | "timeline" | "funnel" | "matrix" | "cycle" |
"list"`. The JSON shape (`DiagramElement` / `DiagramNode`) is stable — safe to
emit from another language.

**Server-side rendering.** `layoutDiagram(el)` is **pure and DOM-free** (only
box/arrow arithmetic — no `window` / `document` / `canvas`), the same guarantee
`buildChartOption` gives for charts, and it's committed to staying that way. A
host QA renderer can draw a diagram to SVG/PNG without a browser by walking the
primitives:

```ts
import { layoutDiagram } from "@textcortex/slidewise";

for (const p of layoutDiagram(el)) {
if (p.kind === "box") drawRect(p.x, p.y, p.w, p.h, p.fill, p.text, p.textColor);
else drawArrow(p.x1, p.y1, p.x2, p.y2, p.stroke, p.arrow);
}
```

Coordinates are local to the element box (`0..w` × `0..h`); offset by the
element's `x` / `y` to place them on the slide.

## Theming

Slidewise exposes its surface colors and chrome metrics as CSS custom
Expand Down
Loading
Loading