@lofcz/pptist 2.0.0 → 2.0.2

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/docs/EMBED.md CHANGED
@@ -1,307 +1,307 @@
1
- # Embedding PPTist in sciobot-next (React)
2
-
3
- PPTist ships as a **Vite library bundle** (Vue + Pinia + UI inside one ESM chunk), not an iframe. The React host loads the module and calls an **imperative controller API**.
4
-
5
- ## Build the embed bundle
6
-
7
- ```bash
8
- cd PPTist
9
- npm install
10
- npm run build:embed
11
- ```
12
-
13
- Outputs:
14
-
15
- | File | Purpose |
16
- |------|---------|
17
- | `dist/embed/pptist-embed.js` | ESM entry — `mountPptist`, `unmountPptist` |
18
- | `dist/embed/pptist-embed.css` | All styles (load via `<link>` from `assetBaseUrl`, not bundled in React) |
19
- | `dist/embed/mocks/`, `imgs/` | Runtime assets for templates / demo deck |
20
-
21
- ## React usage
22
-
23
- ```tsx
24
- import { useEffect, useRef } from 'react'
25
- import { mountPptist, type PptistController } from '@lofcz/pptist/embed'
26
-
27
- const assetBase = import.meta.env.VITE_PPTIST_ASSET_BASE ?? '/pptist-assets'
28
-
29
- export function PptistEditor({ locale }: { locale: 'cs' | 'en' | 'sk' | 'pl' }) {
30
- const hostRef = useRef<HTMLDivElement>(null)
31
- const controllerRef = useRef<PptistController | null>(null)
32
-
33
- useEffect(() => {
34
- const link = document.createElement('link')
35
- link.rel = 'stylesheet'
36
- link.href = `${assetBase}/pptist-embed.css`
37
- document.head.appendChild(link)
38
- return () => link.remove()
39
- }, [])
40
-
41
- useEffect(() => {
42
- const el = hostRef.current
43
- if (!el) return
44
-
45
- let cancelled = false
46
-
47
- void mountPptist(el, {
48
- locale,
49
- loadMockOnEmpty: true,
50
- assetBaseUrl: import.meta.env.VITE_PPTIST_ASSET_BASE ?? '/pptist-assets',
51
- onChange: (doc) => console.log('deck changed', doc.title),
52
- }).then(({ controller }) => {
53
- if (cancelled) {
54
- controller.destroy()
55
- return
56
- }
57
- controllerRef.current = controller
58
- })
59
-
60
- return () => {
61
- cancelled = true
62
- controllerRef.current?.destroy()
63
- controllerRef.current = null
64
- }
65
- }, [locale])
66
-
67
- return <div ref={hostRef} className="h-full min-h-0 w-full" />
68
- }
69
- ```
70
-
71
- ## Imperative API (`PptistController`)
72
-
73
- | Method | Description |
74
- |--------|-------------|
75
- | `getDocument()` | `{ title, slides, theme }` JSON snapshot |
76
- | `setDocument(doc)` | Replace deck |
77
- | `setTitle(title)` | Update title only |
78
- | `setLocale(locale)` | `cs` / `en` / `sk` / `pl` + reload i18n namespaces |
79
- | `enterPresentation()` | Full-screen slide show |
80
- | `exitPresentation()` | Back to editor |
81
- | `export.json()` | Serializable `{ title, slides, theme }` snapshot for agent/host persistence |
82
- | `destroy()` | `app.unmount()` and clear host |
83
-
84
- The controller also exposes the agentic bridge documented in [`AGENTIC_BRIDGE.md`](./AGENTIC_BRIDGE.md). Use the legacy document methods for whole-deck load/save boundaries, and use the bridge for sciobot agent edits inside an already-mounted editor.
85
-
86
- ### Agent command execution
87
-
88
- `controller.execute()` accepts a typed `domain.action` command. This is useful when the agent runtime stores commands as JSON or streams tool calls from sciobot:
89
-
90
- ```ts
91
- const result = await controller.execute({
92
- id: crypto.randomUUID(),
93
- type: 'slides.create',
94
- payload: {
95
- select: true,
96
- slide: {
97
- elements: [],
98
- background: { type: 'solid', color: '#fff' },
99
- },
100
- },
101
- meta: { source: 'agent', label: 'Create generated slide' },
102
- })
103
-
104
- if (!result.ok) {
105
- throw new Error(result.errors?.map(error => error.message).join('\n') || 'PPTist command failed')
106
- }
107
- ```
108
-
109
- ### Domain API helpers
110
-
111
- For React code that is already coupled to `PptistController`, prefer the domain helpers. They keep command payloads typed while returning the same `PptistCommandResult` shape:
112
-
113
- ```ts
114
- const createSlide = await controller.slides.create({
115
- select: true,
116
- slide: {
117
- elements: [],
118
- background: { type: 'solid', color: '#fff' },
119
- },
120
- }, { source: 'agent', label: 'Create lesson slide' })
121
-
122
- if (!createSlide.ok || !createSlide.data) return
123
-
124
- await controller.deck.setTitle('Generated sciobot presentation')
125
-
126
- const createTitle = await controller.elements.create({
127
- slideId: createSlide.data.id,
128
- element: {
129
- type: 'text',
130
- left: 80,
131
- top: 80,
132
- width: 640,
133
- height: 96,
134
- rotate: 0,
135
- content: '<p>Generated by sciobot</p>',
136
- defaultFontName: '',
137
- defaultColor: '#111',
138
- },
139
- })
140
-
141
- if (createTitle.ok && createTitle.data) {
142
- await controller.links.set(createTitle.data.id, { type: 'web', target: 'https://sciobot.app' })
143
- await controller.elements.select(createTitle.data.id)
144
- }
145
-
146
- await controller.slides.setRemark(createSlide.data.id, '<p>Teacher notes from the sciobot agent</p>')
147
- ```
148
-
149
- ### Bridge subscriptions
150
-
151
- Subscribe once when the controller is mounted, then unsubscribe before replacing or destroying it. `documentChanged` is the best signal for persistence, while `commandFailed` is the best signal for agent telemetry:
152
-
153
- ```tsx
154
- useEffect(() => {
155
- const controller = controllerRef.current
156
- if (!controller) return
157
-
158
- return controller.subscribe(event => {
159
- if (event.type === 'documentChanged') {
160
- void savePresentationDraft(event.data)
161
- }
162
-
163
- if (event.type === 'commandFailed') {
164
- console.warn('PPTist command failed', event.command, event.result?.errors)
165
- }
166
- })
167
- }, [controllerRef.current])
168
- ```
169
-
170
- ### Batch edits
171
-
172
- For agent workflows, prefer `executeBatch()` when multiple edits should be one undo step. Batch commands run with intermediate `commit: false` and commit once at the end unless `{ commit: false }` is passed:
173
-
174
- ```ts
175
- const slideId = controller.slides.get()?.id
176
- if (!slideId) return
177
-
178
- const results = await controller.executeBatch([
179
- { type: 'slides.select', payload: { slideIdOrIndex: slideId } },
180
- {
181
- type: 'elements.create',
182
- payload: {
183
- slideId,
184
- element: {
185
- type: 'text',
186
- left: 80,
187
- top: 80,
188
- width: 520,
189
- height: 96,
190
- rotate: 0,
191
- content: '<p>Agent summary</p>',
192
- defaultFontName: '',
193
- defaultColor: '#111',
194
- },
195
- select: true,
196
- },
197
- },
198
- { type: 'slides.setRemark', payload: { slideId, remark: '<p>Notes</p>' } },
199
- ], { atomic: true })
200
-
201
- const failed = results.find(result => !result.ok)
202
- if (failed) console.error(failed.errors)
203
- ```
204
-
205
- Speaker remarks are stored as HTML strings. Use `controller.slides.getRemark(slideId)` or the `slides.getRemark` command to read them, and `controller.slides.setRemark(slideId, html)` or `slides.setRemark` to write them; successful writes return the updated slide in `result.data`.
206
-
207
- ### Export Boundary
208
-
209
- The embed controller exposes JSON export only:
210
-
211
- ```ts
212
- const document = controller.export.json()
213
- const result = await controller.execute<PptistDocument>({ type: 'export.json' })
214
- ```
215
-
216
- `export.json()` and the `export.json` command return the serializable `PptistDocument` model and do not require the editor DOM. PDF, PPTX, and image exports depend on rendered slide DOM, browser canvas/image loading, and the existing export dialogs/hooks, so they are intentionally outside the agentic bridge. Hosts that need those formats should drive PPTist's UI/export workflow in a browser context or add a dedicated DOM-aware integration boundary.
217
-
218
- ## sciobot-next wiring
219
-
220
- 1. **package.json** — `"@lofcz/pptist": "^2.0.0"` after the package is published. During local development, sciobot's Vite config can fall back to the sibling `../PPTist/dist/embed` build.
221
- 2. After changing PPTist locally, run `npm run build:embed` in PPTist, then restart or refresh sciobot.
222
- 3. **VITE_PPTIST_ASSET_BASE** — URL prefix for `pptist-embed.css`, `mocks/`, and `imgs/`:
223
- - Dev: proxy `/pptist-assets` → PPTist `dist/embed` or `public/`
224
- - Prod: copy `dist/embed/{pptist-embed.css,mocks,imgs}` into sciobot `public/pptist-assets/`
225
- 4. **Locale** — pass sciobot `Locales`; same union as typesafe-i18n in both apps.
226
- 5. **Vite** — do not pre-bundle `@lofcz/pptist/embed` into React (keep Vue inside the embed chunk):
227
-
228
- ```ts
229
- // vite.config.ts (sciobot-next)
230
- resolve: {
231
- alias: {
232
- '@lofcz/pptist/embed': path.resolve(__dirname, '../PPTist/dist/embed/pptist-embed.js'),
233
- },
234
- },
235
- optimizeDeps: {
236
- exclude: ['@lofcz/pptist/embed'],
237
- },
238
- ```
239
-
240
- ### CSS asset loading constraints
241
-
242
- Load `pptist-embed.css` as a plain asset from the same `assetBaseUrl` used for mocks and images:
243
-
244
- ```tsx
245
- useEffect(() => {
246
- const href = `${assetBase}/pptist-embed.css`
247
- if (document.querySelector(`link[data-pptist-embed-css][href="${href}"]`)) return
248
-
249
- const link = document.createElement('link')
250
- link.rel = 'stylesheet'
251
- link.href = href
252
- link.dataset.pptistEmbedCss = 'true'
253
- document.head.appendChild(link)
254
-
255
- return () => {
256
- document.querySelector(`link[data-pptist-embed-css][href="${href}"]`)?.remove()
257
- }
258
- }, [])
259
- ```
260
-
261
- Do not `import '@lofcz/pptist/embed.css'` in React. The CSS is large and should not enter the host PostCSS/Tailwind pipeline.
262
-
263
- ### Migrating legacy document calls
264
-
265
- Existing save/load code can keep using `getDocument()` and `setDocument()` at persistence boundaries:
266
-
267
- ```ts
268
- await savePresentation(controller.getDocument())
269
-
270
- const document = await loadPresentation(id)
271
- controller.setDocument(document)
272
- ```
273
-
274
- Agent edits should migrate to bridge commands so sciobot can observe command results, failures, and undo snapshots:
275
-
276
- ```ts
277
- // Legacy: replace the whole document to add a slide.
278
- const document = controller.getDocument()
279
- controller.setDocument({
280
- ...document,
281
- slides: [...document.slides, generatedSlide],
282
- })
283
-
284
- // Agentic: create the slide through the bridge.
285
- await controller.slides.create({
286
- slide: {
287
- ...generatedSlide,
288
- id: undefined,
289
- },
290
- select: true,
291
- })
292
- ```
293
-
294
- Use `controller.export.json()`, `controller.import.json(document)`, or `controller.execute({ type: 'import.json', payload: { document } })` when the persistence layer wants command results for whole-document operations.
295
-
296
- ## Why not iframe?
297
-
298
- - Shared imperative API for save/load with Supabase
299
- - Locale driven by sciobot zustand without postMessage
300
- - Single CSP / auth surface when both apps are same origin
301
- - Heavier initial JS, but one integrated surface for teachers
302
-
303
- ## Future
304
-
305
- - `postMessage` optional bridge for cross-origin CDN hosting
306
- - Persist `PptistDocument` in `linked_materials` + `presentation` kind in workspace tabs
307
- - Split chunk / lazy `import('@lofcz/pptist/embed')` on first open of presentation tab
1
+ # Embedding PPTist in sciobot-next (React)
2
+
3
+ PPTist ships as a **Vite library bundle** (Vue + Pinia + UI inside one ESM chunk), not an iframe. The React host loads the module and calls an **imperative controller API**.
4
+
5
+ ## Build the embed bundle
6
+
7
+ ```bash
8
+ cd PPTist
9
+ npm install
10
+ npm run build:embed
11
+ ```
12
+
13
+ Outputs:
14
+
15
+ | File | Purpose |
16
+ |------|---------|
17
+ | `dist/embed/pptist-embed.js` | ESM entry — `mountPptist`, `unmountPptist` |
18
+ | `dist/embed/pptist-embed.css` | All styles (load via `<link>` from `assetBaseUrl`, not bundled in React) |
19
+ | `dist/embed/mocks/`, `imgs/` | Runtime assets for templates / demo deck |
20
+
21
+ ## React usage
22
+
23
+ ```tsx
24
+ import { useEffect, useRef } from 'react'
25
+ import { mountPptist, type PptistController } from '@lofcz/pptist/embed'
26
+
27
+ const assetBase = import.meta.env.VITE_PPTIST_ASSET_BASE ?? '/pptist-assets'
28
+
29
+ export function PptistEditor({ locale }: { locale: 'cs' | 'en' | 'sk' | 'pl' }) {
30
+ const hostRef = useRef<HTMLDivElement>(null)
31
+ const controllerRef = useRef<PptistController | null>(null)
32
+
33
+ useEffect(() => {
34
+ const link = document.createElement('link')
35
+ link.rel = 'stylesheet'
36
+ link.href = `${assetBase}/pptist-embed.css`
37
+ document.head.appendChild(link)
38
+ return () => link.remove()
39
+ }, [])
40
+
41
+ useEffect(() => {
42
+ const el = hostRef.current
43
+ if (!el) return
44
+
45
+ let cancelled = false
46
+
47
+ void mountPptist(el, {
48
+ locale,
49
+ loadMockOnEmpty: true,
50
+ assetBaseUrl: import.meta.env.VITE_PPTIST_ASSET_BASE ?? '/pptist-assets',
51
+ onChange: (doc) => console.log('deck changed', doc.title),
52
+ }).then(({ controller }) => {
53
+ if (cancelled) {
54
+ controller.destroy()
55
+ return
56
+ }
57
+ controllerRef.current = controller
58
+ })
59
+
60
+ return () => {
61
+ cancelled = true
62
+ controllerRef.current?.destroy()
63
+ controllerRef.current = null
64
+ }
65
+ }, [locale])
66
+
67
+ return <div ref={hostRef} className="h-full min-h-0 w-full" />
68
+ }
69
+ ```
70
+
71
+ ## Imperative API (`PptistController`)
72
+
73
+ | Method | Description |
74
+ |--------|-------------|
75
+ | `getDocument()` | `{ title, slides, theme }` JSON snapshot |
76
+ | `setDocument(doc)` | Replace deck |
77
+ | `setTitle(title)` | Update title only |
78
+ | `setLocale(locale)` | `cs` / `en` / `sk` / `pl` + reload i18n namespaces |
79
+ | `enterPresentation()` | Full-screen slide show |
80
+ | `exitPresentation()` | Back to editor |
81
+ | `export.json()` | Serializable `{ title, slides, theme }` snapshot for agent/host persistence |
82
+ | `destroy()` | `app.unmount()` and clear host |
83
+
84
+ The controller also exposes the agentic bridge documented in [`AGENTIC_BRIDGE.md`](./AGENTIC_BRIDGE.md). Use the legacy document methods for whole-deck load/save boundaries, and use the bridge for sciobot agent edits inside an already-mounted editor.
85
+
86
+ ### Agent command execution
87
+
88
+ `controller.execute()` accepts a typed `domain.action` command. This is useful when the agent runtime stores commands as JSON or streams tool calls from sciobot:
89
+
90
+ ```ts
91
+ const result = await controller.execute({
92
+ id: crypto.randomUUID(),
93
+ type: 'slides.create',
94
+ payload: {
95
+ select: true,
96
+ slide: {
97
+ elements: [],
98
+ background: { type: 'solid', color: '#fff' },
99
+ },
100
+ },
101
+ meta: { source: 'agent', label: 'Create generated slide' },
102
+ })
103
+
104
+ if (!result.ok) {
105
+ throw new Error(result.errors?.map(error => error.message).join('\n') || 'PPTist command failed')
106
+ }
107
+ ```
108
+
109
+ ### Domain API helpers
110
+
111
+ For React code that is already coupled to `PptistController`, prefer the domain helpers. They keep command payloads typed while returning the same `PptistCommandResult` shape:
112
+
113
+ ```ts
114
+ const createSlide = await controller.slides.create({
115
+ select: true,
116
+ slide: {
117
+ elements: [],
118
+ background: { type: 'solid', color: '#fff' },
119
+ },
120
+ }, { source: 'agent', label: 'Create lesson slide' })
121
+
122
+ if (!createSlide.ok || !createSlide.data) return
123
+
124
+ await controller.deck.setTitle('Generated sciobot presentation')
125
+
126
+ const createTitle = await controller.elements.create({
127
+ slideId: createSlide.data.id,
128
+ element: {
129
+ type: 'text',
130
+ left: 80,
131
+ top: 80,
132
+ width: 640,
133
+ height: 96,
134
+ rotate: 0,
135
+ content: '<p>Generated by sciobot</p>',
136
+ defaultFontName: '',
137
+ defaultColor: '#111',
138
+ },
139
+ })
140
+
141
+ if (createTitle.ok && createTitle.data) {
142
+ await controller.links.set(createTitle.data.id, { type: 'web', target: 'https://sciobot.app' })
143
+ await controller.elements.select(createTitle.data.id)
144
+ }
145
+
146
+ await controller.slides.setRemark(createSlide.data.id, '<p>Teacher notes from the sciobot agent</p>')
147
+ ```
148
+
149
+ ### Bridge subscriptions
150
+
151
+ Subscribe once when the controller is mounted, then unsubscribe before replacing or destroying it. `documentChanged` is the best signal for persistence, while `commandFailed` is the best signal for agent telemetry:
152
+
153
+ ```tsx
154
+ useEffect(() => {
155
+ const controller = controllerRef.current
156
+ if (!controller) return
157
+
158
+ return controller.subscribe(event => {
159
+ if (event.type === 'documentChanged') {
160
+ void savePresentationDraft(event.data)
161
+ }
162
+
163
+ if (event.type === 'commandFailed') {
164
+ console.warn('PPTist command failed', event.command, event.result?.errors)
165
+ }
166
+ })
167
+ }, [controllerRef.current])
168
+ ```
169
+
170
+ ### Batch edits
171
+
172
+ For agent workflows, prefer `executeBatch()` when multiple edits should be one undo step. Batch commands run with intermediate `commit: false` and commit once at the end unless `{ commit: false }` is passed:
173
+
174
+ ```ts
175
+ const slideId = controller.slides.get()?.id
176
+ if (!slideId) return
177
+
178
+ const results = await controller.executeBatch([
179
+ { type: 'slides.select', payload: { slideIdOrIndex: slideId } },
180
+ {
181
+ type: 'elements.create',
182
+ payload: {
183
+ slideId,
184
+ element: {
185
+ type: 'text',
186
+ left: 80,
187
+ top: 80,
188
+ width: 520,
189
+ height: 96,
190
+ rotate: 0,
191
+ content: '<p>Agent summary</p>',
192
+ defaultFontName: '',
193
+ defaultColor: '#111',
194
+ },
195
+ select: true,
196
+ },
197
+ },
198
+ { type: 'slides.setRemark', payload: { slideId, remark: '<p>Notes</p>' } },
199
+ ], { atomic: true })
200
+
201
+ const failed = results.find(result => !result.ok)
202
+ if (failed) console.error(failed.errors)
203
+ ```
204
+
205
+ Speaker remarks are stored as HTML strings. Use `controller.slides.getRemark(slideId)` or the `slides.getRemark` command to read them, and `controller.slides.setRemark(slideId, html)` or `slides.setRemark` to write them; successful writes return the updated slide in `result.data`.
206
+
207
+ ### Export Boundary
208
+
209
+ The embed controller exposes JSON export only:
210
+
211
+ ```ts
212
+ const document = controller.export.json()
213
+ const result = await controller.execute<PptistDocument>({ type: 'export.json' })
214
+ ```
215
+
216
+ `export.json()` and the `export.json` command return the serializable `PptistDocument` model and do not require the editor DOM. PDF, PPTX, and image exports depend on rendered slide DOM, browser canvas/image loading, and the existing export dialogs/hooks, so they are intentionally outside the agentic bridge. Hosts that need those formats should drive PPTist's UI/export workflow in a browser context or add a dedicated DOM-aware integration boundary.
217
+
218
+ ## sciobot-next wiring
219
+
220
+ 1. **package.json** — `"@lofcz/pptist": "^2.0.0"` after the package is published. During local development, sciobot's Vite config can fall back to the sibling `../PPTist/dist/embed` build.
221
+ 2. After changing PPTist locally, run `npm run build:embed` in PPTist, then restart or refresh sciobot.
222
+ 3. **VITE_PPTIST_ASSET_BASE** — URL prefix for `pptist-embed.css`, `mocks/`, and `imgs/`:
223
+ - Dev: proxy `/pptist-assets` → PPTist `dist/embed` or `public/`
224
+ - Prod: copy `dist/embed/{pptist-embed.css,mocks,imgs}` into sciobot `public/pptist-assets/`
225
+ 4. **Locale** — pass sciobot `Locales`; same union as typesafe-i18n in both apps.
226
+ 5. **Vite** — do not pre-bundle `@lofcz/pptist/embed` into React (keep Vue inside the embed chunk):
227
+
228
+ ```ts
229
+ // vite.config.ts (sciobot-next)
230
+ resolve: {
231
+ alias: {
232
+ '@lofcz/pptist/embed': path.resolve(__dirname, '../PPTist/dist/embed/pptist-embed.js'),
233
+ },
234
+ },
235
+ optimizeDeps: {
236
+ exclude: ['@lofcz/pptist/embed'],
237
+ },
238
+ ```
239
+
240
+ ### CSS asset loading constraints
241
+
242
+ Load `pptist-embed.css` as a plain asset from the same `assetBaseUrl` used for mocks and images:
243
+
244
+ ```tsx
245
+ useEffect(() => {
246
+ const href = `${assetBase}/pptist-embed.css`
247
+ if (document.querySelector(`link[data-pptist-embed-css][href="${href}"]`)) return
248
+
249
+ const link = document.createElement('link')
250
+ link.rel = 'stylesheet'
251
+ link.href = href
252
+ link.dataset.pptistEmbedCss = 'true'
253
+ document.head.appendChild(link)
254
+
255
+ return () => {
256
+ document.querySelector(`link[data-pptist-embed-css][href="${href}"]`)?.remove()
257
+ }
258
+ }, [])
259
+ ```
260
+
261
+ Do not `import '@lofcz/pptist/embed.css'` in React. The CSS is large and should not enter the host PostCSS/Tailwind pipeline.
262
+
263
+ ### Migrating legacy document calls
264
+
265
+ Existing save/load code can keep using `getDocument()` and `setDocument()` at persistence boundaries:
266
+
267
+ ```ts
268
+ await savePresentation(controller.getDocument())
269
+
270
+ const document = await loadPresentation(id)
271
+ controller.setDocument(document)
272
+ ```
273
+
274
+ Agent edits should migrate to bridge commands so sciobot can observe command results, failures, and undo snapshots:
275
+
276
+ ```ts
277
+ // Legacy: replace the whole document to add a slide.
278
+ const document = controller.getDocument()
279
+ controller.setDocument({
280
+ ...document,
281
+ slides: [...document.slides, generatedSlide],
282
+ })
283
+
284
+ // Agentic: create the slide through the bridge.
285
+ await controller.slides.create({
286
+ slide: {
287
+ ...generatedSlide,
288
+ id: undefined,
289
+ },
290
+ select: true,
291
+ })
292
+ ```
293
+
294
+ Use `controller.export.json()`, `controller.import.json(document)`, or `controller.execute({ type: 'import.json', payload: { document } })` when the persistence layer wants command results for whole-document operations.
295
+
296
+ ## Why not iframe?
297
+
298
+ - Shared imperative API for save/load with Supabase
299
+ - Locale driven by sciobot zustand without postMessage
300
+ - Single CSP / auth surface when both apps are same origin
301
+ - Heavier initial JS, but one integrated surface for teachers
302
+
303
+ ## Future
304
+
305
+ - `postMessage` optional bridge for cross-origin CDN hosting
306
+ - Persist `PptistDocument` in `linked_materials` + `presentation` kind in workspace tabs
307
+ - Split chunk / lazy `import('@lofcz/pptist/embed')` on first open of presentation tab