@office-kit/pptx-editor 0.0.0-stage → 0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yuichiro Yamashita
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,386 @@
1
- # Temporary Holding Version
1
+ # @office-kit/pptx-editor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A browser editor for `.pptx` presentations that you embed in your own web
4
+ application. It opens a `.pptx`, lets people edit it with a ribbon, menus,
5
+ slide thumbnails and direct manipulation, and hands the edited `.pptx` back.
6
+ Everything it does goes through the public API of
7
+ [`@office-kit/pptx`](https://github.com/office-kit/pptx), so the saved file is
8
+ an ordinary, editable presentation.
9
+
10
+ > **Experimental (0.x).** The editor's behaviour is still growing and a minor
11
+ > release may change the API. Pin a version.
12
+
13
+ ```sh
14
+ npm install @office-kit/pptx-editor
15
+ ```
16
+
17
+ ## Embedding
18
+
19
+ ```ts
20
+ import { mountEditor } from '@office-kit/pptx-editor';
21
+
22
+ const editor = mountEditor(document.getElementById('editor')!, {
23
+ source: new Uint8Array(await (await fetch('/decks/q3.pptx')).arrayBuffer()),
24
+ locale: 'en',
25
+ onSave: async (pptx) => {
26
+ await fetch('/decks/q3.pptx', { method: 'PUT', body: pptx });
27
+ },
28
+ });
29
+
30
+ await editor.ready; // the deck is open
31
+ const pptx = await editor.snapshot(); // the deck as it is now
32
+ editor.destroy(); // remove the editor again
33
+ ```
34
+
35
+ The editor fills the element you mount it in, so give that element a size
36
+ (for example `height: 100vh`).
37
+
38
+ ### `mountEditor(target, options?)`
39
+
40
+ | Option | Type | Default |
41
+ | ---------- | ------------------------------------------------------------------- | ------------------------------------------------------------ |
42
+ | `source` | `Uint8Array` | A new presentation with one title slide, as File ▸ New. |
43
+ | `fileName` | `string` | `Untitled.pptx`: shown in the title bar, used for downloads. |
44
+ | `locale` | `'en' \| 'ja'` | The language last picked in the editor, else the browser's. |
45
+ | `onSave` | `(pptx: Uint8Array) => Promise<boolean \| void> \| boolean \| void` | Without it, saving downloads the file. |
46
+ | `autoSave` | `boolean` | `false`. Shows the AutoSave switch; needs `onSave`. |
47
+ | `compact` | `boolean` | `false`. A slimmer title bar without the product name. |
48
+ | `status` | `HTMLElement` | None. Your own element in the title bar (a save status). |
49
+ | `isolate` | `boolean` | `true`. `false` renders into the page, without shadow root. |
50
+
51
+ `onSave` runs when the user saves: the Save button, File ▸ Save, or
52
+ Ctrl/Cmd+S; when you call `save()`; and, with `autoSave` while its switch is
53
+ on, shortly after each edit. It receives the complete `.pptx`. If it throws or
54
+ rejects, the editor tells the user the save failed. Resolve `false` when you
55
+ did not save and have told the user why yourself (you are offline, say): the
56
+ deck stays marked as changed, the editor shows nothing and does not retry, so
57
+ call `save()` once you can. A call never starts before the previous one has
58
+ settled.
59
+
60
+ `status` stays in your page's DOM, slotted into the editor's title bar, so your
61
+ own styles apply to it. `isolate: false` is only for a page that belongs to the
62
+ editor alone, such as its own `<iframe>`: the page's styles then reach the
63
+ editor and the editor's reach the page.
64
+
65
+ It returns an `EditorHandle`:
66
+
67
+ | Member | Description |
68
+ | -------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
69
+ | `ready` | Resolves once `source` is open and the editor is shown; rejects if `source` is not a readable `.pptx`. |
70
+ | `dirty` | Whether the deck has changes `onSave` has not saved. |
71
+ | `locale` | The interface language in use. |
72
+ | `snapshot()` | The presentation as it is now, as `.pptx` bytes. It does not call `onSave` or mark the deck saved. |
73
+ | `selection()` | The shapes the user has selected, as `ShapeRef`s (see [Agents in the browser](#agents-in-the-browser)). |
74
+ | `apply(label, edit)` | Runs `edit(presentation)` with the `@office-kit/pptx` API as one undo step named "Agent: `label`". |
75
+ | `propose(base, edited, options)` | Merges a version made elsewhere into the deck (see [Long-running agents](#long-running-agents)). |
76
+ | `open(pptx, options?)` | Opens another deck in place of this one, as File ▸ Open does; options `{ fileName, unsaved }`. |
77
+ | `save()` | Saves as the Save button does: through `onSave`, or as a download without it. |
78
+ | `tools()` | Every editing capability as tool definitions for a language model (see [Tools for a model](#tools-for-a-model)). |
79
+ | `run(name, input)` | Runs one of those tools with the model's JSON input, as one undo step named "Agent: `name`". |
80
+ | `on(type, listener)` | Calls `listener` for each event below until the returned `off()` is called. |
81
+ | `destroy()` | Removes the editor and every listener it added, leaving `target` as it was, ready to mount again. |
82
+
83
+ | Event | Payload | Fires when |
84
+ | ------------------- | ------------------------------------------- | ------------------------------------------------- |
85
+ | `'change'` | `{ source: 'user' \| 'agent' \| 'source' }` | The presentation keeps an edit (see below). |
86
+ | `'selectionchange'` | `{ selection: ShapeRef[] }` | The user selects other shapes. |
87
+ | `'dirtychange'` | `{ dirty: boolean }` | The deck gets unsaved changes, or they are saved. |
88
+ | `'localechange'` | `{ locale: 'en' \| 'ja' }` | The user picks another language in the header. |
89
+
90
+ `'change'` fires after every edit the presentation keeps: the user's, an
91
+ `apply` or `propose`, Undo, Redo, File ▸ New / Open and `open()`. It does not
92
+ fire for the source you passed, nor while a shape is still being dragged.
93
+
94
+ ### React
95
+
96
+ ```tsx
97
+ import { useEffect, useRef } from 'react';
98
+ import { mountEditor } from '@office-kit/pptx-editor';
99
+
100
+ export function DeckEditor({
101
+ source,
102
+ onSave,
103
+ }: {
104
+ source: Uint8Array;
105
+ onSave: (pptx: Uint8Array) => Promise<void>;
106
+ }) {
107
+ const target = useRef<HTMLDivElement>(null);
108
+ useEffect(() => {
109
+ const editor = mountEditor(target.current!, { source, onSave });
110
+ return () => editor.destroy();
111
+ }, [source, onSave]);
112
+ return <div ref={target} style={{ height: '100vh' }} />;
113
+ }
114
+ ```
115
+
116
+ ### Vue
117
+
118
+ ```vue
119
+ <script setup lang="ts">
120
+ import { onBeforeUnmount, onMounted, ref } from 'vue';
121
+ import { mountEditor, type EditorHandle } from '@office-kit/pptx-editor';
122
+
123
+ const props = defineProps<{ source: Uint8Array; onSave: (pptx: Uint8Array) => Promise<void> }>();
124
+ const target = ref<HTMLElement>();
125
+ let editor: EditorHandle | undefined;
126
+ onMounted(() => {
127
+ editor = mountEditor(target.value!, { source: props.source, onSave: props.onSave });
128
+ });
129
+ onBeforeUnmount(() => editor?.destroy());
130
+ </script>
131
+
132
+ <template>
133
+ <div ref="target" style="height: 100vh" />
134
+ </template>
135
+ ```
136
+
137
+ ## Agents in the browser
138
+
139
+ An agent running in your page — an LLM with tools, a script, a chat panel next
140
+ to the editor — works on the same document the user sees, through the handle:
141
+
142
+ - **Read**: `snapshot()` gives the `.pptx`; open it with `loadPresentation`
143
+ from `@office-kit/pptx` to read text, layout and anything else. To _look_ at
144
+ a slide, render it with `renderSlideToSvg` from `@office-kit/pptx-preview`,
145
+ the renderer the editor itself uses.
146
+ - **"This shape"**: `selection()` returns what the user selected as
147
+ `ShapeRef`s — `{ slideIndex, slide, shapeId, name }`. A ref names the slide
148
+ by its part name and the shape by its id, so it stays valid while slides
149
+ move and across Undo. `resolveShape(presentation, ref)` finds the shape again
150
+ and throws if it was deleted, so an agent never edits the wrong shape.
151
+ - **Write**: `apply(label, edit)` runs `edit` against the live presentation
152
+ with the `@office-kit/pptx` API. The user sees the change at once, and it is
153
+ one undo step named "Agent: `label`" (「エージェント: `label`」 in Japanese).
154
+ It marks the deck as changed exactly as a user edit does; `onSave` still runs
155
+ only when the user saves. If `edit` throws, nothing of it is kept and
156
+ `apply` rejects with the error. `edit` must be synchronous.
157
+ - **Follow along**: `on('change', …)` reports every kept edit with its
158
+ `source` (`'user'` or `'agent'`), including Undo and Redo;
159
+ `on('selectionchange', …)` reports the user's new selection.
160
+
161
+ `apply` is for code you write. When a model decides the edits, use
162
+ [`tools()` and `run`](#tools-for-a-model) instead: they check the model's
163
+ input for you.
164
+
165
+ ```ts
166
+ import { mountEditor, resolveShape } from '@office-kit/pptx-editor';
167
+ import { setShapeFill } from '@office-kit/pptx';
168
+
169
+ const editor = mountEditor(target, { source });
170
+ await editor.ready;
171
+
172
+ const [selected] = editor.selection();
173
+ if (selected)
174
+ await editor.apply('Highlight', (presentation) => {
175
+ setShapeFill(resolveShape(presentation, selected), '#FFF2CC');
176
+ });
177
+
178
+ editor.on('change', ({ source }) => {
179
+ if (source === 'user') {
180
+ // The user edited (or undid an agent's edit): refresh what the agent knows.
181
+ }
182
+ });
183
+ ```
184
+
185
+ There is no `renderSlide` on the handle: `snapshot()` plus
186
+ `@office-kit/pptx-preview` already does it, with the preview's own options.
187
+
188
+ ### Tools for a model
189
+
190
+ `tools()` describes every mutating `@office-kit/pptx` export as a tool a
191
+ language model can call, and `run(name, input)` runs the call the model makes.
192
+ There is no per-function glue to write, and a new library function becomes a
193
+ tool with the next release.
194
+
195
+ - **Names** are the export names (`setShapeFill`, `addSlideShape`, …), sorted.
196
+ Four tools are the editor's own, for finding what the others take:
197
+ `listSlides`, `listShapes` (refs, kind, bounds and text of each shape),
198
+ `listLayouts` and `listComments`.
199
+ - **Descriptions** are the exports' TSDoc. **Input schemas** are JSON Schema
200
+ (draft 2020-12) generated from the functions' TypeScript signatures, one
201
+ property per parameter. The open presentation is never an input; a slide is
202
+ its part name (`"/ppt/slides/slide1.xml"`), a shape a `ShapeRef`, a table
203
+ cell a `ShapeRef` plus `row` and `col`, a layout its part name. Lengths are
204
+ integers in EMU (914400 per inch, 12700 per point). Bytes (pictures, media)
205
+ are base64. Each schema is self-contained (`$defs`, `$ref`).
206
+ - **`run`** checks `input` against the schema before anything happens and
207
+ never coerces: `"5"` is not a number. On a mismatch it rejects with every
208
+ problem by its JSON Pointer, for example
209
+ `Invalid input for setShapePosition:\n- /x: expected integer, got string "1in"`;
210
+ send that back to the model as the tool's error and it can correct the call.
211
+ A ref to a slide or shape that is gone also rejects, editing nothing. An
212
+ editing tool is one undo step, named "Agent: `setShapeFill`"; a read tool
213
+ edits nothing. It resolves with the result as JSON: `null` for most tools,
214
+ the ref of the slide or shape a tool added, the count a `clear…` tool reports.
215
+ - **Not tools**: `createPresentation`, `importSlide`, `mergePresentations` and
216
+ `sortSlides`, which take another presentation or a function.
217
+
218
+ Each element of `tools()` is `{ name, description, input_schema }`, which the
219
+ Anthropic Messages API takes as it is. For OpenAI's function calling, pass
220
+ `{ type: 'function', function: { name, description, parameters: input_schema } }`.
221
+
222
+ All the tools together are large (on the order of 150k tokens; the chart tools
223
+ alone are about 30k). Give the model the ones its job needs:
224
+
225
+ ```ts
226
+ import Anthropic from '@anthropic-ai/sdk';
227
+ import { mountEditor } from '@office-kit/pptx-editor';
228
+
229
+ const editor = mountEditor(target, { source });
230
+ await editor.ready;
231
+
232
+ const wanted = new Set([
233
+ 'listSlides',
234
+ 'listShapes',
235
+ 'setShapeText',
236
+ 'setShapeFill',
237
+ 'addSlideShape',
238
+ ]);
239
+ const tools = (await editor.tools()).filter((tool) => wanted.has(tool.name));
240
+
241
+ // Keep your API key on your server: point the SDK at a proxy that adds it.
242
+ const client = new Anthropic({
243
+ baseURL: '/api/anthropic',
244
+ apiKey: 'unused',
245
+ dangerouslyAllowBrowser: true,
246
+ });
247
+
248
+ async function instruct(instruction: string): Promise<void> {
249
+ const messages: Anthropic.MessageParam[] = [
250
+ {
251
+ role: 'user',
252
+ content: `${instruction}\n\nSelected shapes: ${JSON.stringify(editor.selection())}`,
253
+ },
254
+ ];
255
+ for (;;) {
256
+ const response = await client.messages.create({
257
+ model: 'claude-opus-5-5',
258
+ max_tokens: 16000,
259
+ tools,
260
+ messages,
261
+ });
262
+ messages.push({ role: 'assistant', content: response.content });
263
+ if (response.stop_reason !== 'tool_use') return;
264
+ const results: Anthropic.ToolResultBlockParam[] = [];
265
+ for (const block of response.content) {
266
+ if (block.type !== 'tool_use') continue;
267
+ try {
268
+ const result = await editor.run(block.name, block.input);
269
+ results.push({
270
+ type: 'tool_result',
271
+ tool_use_id: block.id,
272
+ content: JSON.stringify(result),
273
+ });
274
+ } catch (error) {
275
+ results.push({
276
+ type: 'tool_result',
277
+ tool_use_id: block.id,
278
+ content: error instanceof Error ? error.message : String(error),
279
+ is_error: true,
280
+ });
281
+ }
282
+ }
283
+ messages.push({ role: 'user', content: results });
284
+ }
285
+ }
286
+ ```
287
+
288
+ ## Long-running agents
289
+
290
+ `apply` edits the live deck at once. An agent that works for longer — on a
291
+ server, in a worker, over many tool calls — takes a copy instead, works on
292
+ that, and hands the result back with `propose`. The user keeps editing
293
+ meanwhile:
294
+
295
+ ```ts
296
+ const base = await editor.snapshot();
297
+ const edited = await agent.revise(base); // minutes later, perhaps
298
+ const result = await editor.propose(base, edited, { label: 'Tighten the wording' });
299
+ ```
300
+
301
+ The editor merges `base`, the deck as it is now (unsaved edits included) and
302
+ `edited`. Changes to different slides, shapes, media and links combine into one
303
+ undo step named "Agent: `label`", and `result` is `{ status: 'applied' }`.
304
+ When the same item changed on both sides, nothing is applied until the user
305
+ chooses in the title bar, which names each collision ("Slide 2: Title 1 was
306
+ changed both here and by the agent."):
307
+
308
+ - **Keep my edits** resolves `{ status: 'kept-mine', conflicts }`; the proposal
309
+ is dropped.
310
+ - **Use the agent's version** resolves `{ status: 'took-theirs', conflicts }`;
311
+ the deck becomes `edited` as one undo step, so Undo brings the user's
312
+ version back.
313
+
314
+ The user can download the agent's version before choosing. `conflicts` lists
315
+ `{ part, slide, shape, reason }` for each collision. Pass `base: null` when
316
+ there is no common version: any difference then asks the user. One proposal
317
+ runs at a time.
318
+
319
+ `from: 'source'` is for a newer version of the file itself (it changed on
320
+ disk, or was rebuilt): the undo step is named `label`, `'change'` reports
321
+ `'source'`, the prompt speaks of "the source", and because `edited` is what you
322
+ have saved, a deck that ends up equal to it has no unsaved changes.
323
+ [`@office-kit/pptx-dev`](../dev) applies its TSX rebuilds this way.
324
+
325
+ The merge itself is `mergeDecks(base, ours, theirs)` from
326
+ `@office-kit/pptx-editor/merge`. It has no DOM dependency, for hosts that merge
327
+ on a server, and returns `{ ok: true, bytes }` or `{ ok: false, conflicts }`.
328
+
329
+ ## Behaviour on your page
330
+
331
+ - **Styles are isolated.** The editor renders inside a shadow root on an
332
+ element it adds to `target`. Your page's stylesheets do not reach it, and
333
+ inherited text styles (font, line height, letter spacing …) stop at its
334
+ boundary; its styles do not reach your page.
335
+ - **Keyboard shortcuts stay in the editor.** Keys typed into your page's own
336
+ inputs and buttons are left alone. Keys typed while nothing on the page has
337
+ focus go to the editor used last, as in a desktop application.
338
+ - **Several editors can share a page.** Each has its own presentation, undo
339
+ history and selection. The interface language is page-wide: every editor on
340
+ a page uses the same one.
341
+ - **Preferences are remembered in `localStorage`** (the language picked in the
342
+ editor, view options such as rulers and gridlines). A `locale` you pass is not
343
+ stored.
344
+ - **Copy and paste use the system clipboard.** Copying slides or objects
345
+ writes one clipboard item with HTML, a PNG picture, an SVG drawing (in
346
+ browsers that write SVG) and plain text, so other apps paste something
347
+ useful: slides and drawn objects as pictures at their size on the slide,
348
+ text boxes and tables on their own as text and tables. It also holds a
349
+ private copy of the slides that another editor
350
+ (another tab, page or browser) pastes as editable slides and objects. The
351
+ browser asks the user before the editor may read the clipboard; without that
352
+ permission, ⌘V / Ctrl+V still pastes text and pictures, and copies made in
353
+ the same editor still paste. To embed in a cross-origin frame, allow
354
+ `clipboard-read; clipboard-write` on it.
355
+ - **Moving slides into other presentation apps.** A web page cannot put
356
+ slides on the clipboard in a form other presentation apps import as slides:
357
+ pasting there gives a picture or text. To keep slides editable, right-click
358
+ the selected thumbnails, choose **Download Selected Slides...**, and import
359
+ the downloaded `.pptx` with the other app's reuse- or import-slides command.
360
+ - Slide Show and the Agents pane are only available in the
361
+ [`@office-kit/pptx-dev`](../dev) preview, which hosts the editor in a frame.
362
+
363
+ ## Why the editor ships compiled
364
+
365
+ The editor is written in [Svelte](https://svelte.dev). The package contains
366
+ the compiled JavaScript with the small Svelte runtime bundled in, so your
367
+ application needs no Svelte, no build plugin and no particular framework, and a
368
+ Svelte version of your own never conflicts with the editor's. `@office-kit/pptx`
369
+ and `@office-kit/pptx-preview` stay ordinary dependencies.
370
+
371
+ ## Contributing
372
+
373
+ How the editor is put together, and how its command registry follows the
374
+ library's API, is described in [`src/README.md`](src/README.md).
375
+
376
+ `@office-kit/pptx-editor/internal` and
377
+ `@office-kit/pptx-editor/internal/animation-player` are **not public API**.
378
+ They exist only for `@office-kit/pptx-dev` and change or disappear in any
379
+ release: the first is `mountEditor` for the dev tool's editor page, which in
380
+ the preview's frame presents slide shows and hosts the Agents pane over
381
+ `postMessage`; the second is the slide-show animation player that page loads on
382
+ its own.
383
+
384
+ ## Trademarks
385
+
386
+ Not affiliated with or endorsed by Microsoft. PowerPoint is a trademark of the Microsoft group of companies.