# Embedding Fika in sciobot-next (React)

Fika ships as an **ESM library** (`fika-editor/embed`), not an iframe. React and React DOM are **peer dependencies** (`>19.2`) — the host must install them and the embed imports the host copy. The standalone demo app bundles its own React.

## Build the embed bundle

```bash
cd Fika
npm install
npm run build:embed
```

Outputs:

| File | Purpose |
|------|---------|
| `dist/embed/fika-embed.js` | ESM entry — `mountFika`, `unmountFika` |
| `dist/embed/fika-embed.css` | All styles (load via `<link>` from `assetBaseUrl`, not bundled in React) |
| `dist/embed/mocks/`, `fonts/` | Optional demo mocks and runtime fonts |

## React usage

```tsx
import { useEffect, useRef } from 'react'
import { mountFika, type FikaController } from 'fika-editor/embed'

const assetBase = import.meta.env.VITE_FIKA_ASSET_BASE ?? '/fika-assets'

export function FikaEditor({ locale }: { locale: 'cs' | 'en' | 'sk' | 'pl' }) {
  const hostRef = useRef<HTMLDivElement>(null)
  const controllerRef = useRef<FikaController | null>(null)

  useEffect(() => {
    const link = document.createElement('link')
    link.rel = 'stylesheet'
    link.href = `${assetBase}/fika-embed.css`
    document.head.appendChild(link)
    return () => link.remove()
  }, [])

  useEffect(() => {
    const el = hostRef.current
    if (!el) return

    let cancelled = false

    void mountFika(el, {
      locale,
      loadMockOnEmpty: true,
      assetBaseUrl: import.meta.env.VITE_FIKA_ASSET_BASE ?? '/fika-assets',
      onChange: (doc) => console.log('deck changed', doc.title),
      onPresentationModeChange: (active) => console.log('presentation mode', active),
    }).then(({ controller }) => {
      if (cancelled) {
        controller.destroy()
        return
      }
      controllerRef.current = controller
    })

    return () => {
      cancelled = true
      controllerRef.current?.destroy()
      controllerRef.current = null
    }
  }, [locale])

  return <div ref={hostRef} className="h-full min-h-0 w-full" />
}
```

### Overlays stay inside the host

Dialogs, loaders, toasts, context menus and the MathLive virtual keyboard render into a portal that spans the **host element**, not the viewport. The host becomes their containing block: if it is `position: static`, `mountFika` sets `position: relative` on it for the lifetime of the mount. Size the host to the area Fika may cover. Only presentation mode takes the whole viewport.

## Imperative API (`FikaController`)

| Method | Description |
|--------|-------------|
| `getDocument()` | `{ title, slides, theme, viewport }` JSON snapshot |
| `setDocument(doc)` | Replace deck (applies `viewport` when present) |
| `setTitle(title)` | Update title only |
| `setLocale(locale)` | `cs` / `en` / `sk` / `pl` + reload i18n namespaces |
| `enterPresentation()` | Full-screen slide show |
| `exitPresentation()` | Back to editor |
| `export.json()` | Serializable `{ title, slides, theme }` snapshot for agent/host persistence |
| `importPptx(data, options?)` | Parse a `.pptx` file into the editor. Default: replace, no confirm. `{ mode: 'append' }` inserts; `{ confirm: true }` asks before replacing a multi-slide deck. |
| `destroy()` | Unmount the editor and clear the host |

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.

### Agent command execution

`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:

```ts
const result = await controller.execute({
  id: crypto.randomUUID(),
  type: 'slides.create',
  payload: {
    select: true,
    slide: {
      elements: [],
      background: { type: 'solid', color: '#fff' },
    },
  },
  meta: { source: 'agent', label: 'Create generated slide' },
})

if (!result.ok) {
  throw new Error(result.errors?.map(error => error.message).join('\n') || 'Fika command failed')
}
```

### Domain API helpers

For React code that is already coupled to `FikaController`, prefer the domain helpers. They keep command payloads typed while returning the same `FikaCommandResult` shape:

```ts
const createSlide = await controller.slides.create({
  select: true,
  slide: {
    elements: [],
    background: { type: 'solid', color: '#fff' },
  },
}, { source: 'agent', label: 'Create lesson slide' })

if (!createSlide.ok || !createSlide.data) return

await controller.deck.setTitle('Generated sciobot presentation')

const createTitle = await controller.elements.create({
  slideId: createSlide.data.id,
  element: {
    type: 'text',
    left: 80,
    top: 80,
    width: 640,
    height: 96,
    rotate: 0,
    content: '<p>Generated by sciobot</p>',
    defaultFontName: '',
    defaultColor: '#111',
  },
})

if (createTitle.ok && createTitle.data) {
  await controller.links.set(createTitle.data.id, { type: 'web', target: 'https://sciobot.app' })
  await controller.elements.select(createTitle.data.id)
}

await controller.slides.setRemark(createSlide.data.id, '<p>Teacher notes from the sciobot agent</p>')
```

### Skeleton slides (streaming a deck in)

A slide with `skeleton: true` is a placeholder for content that is still being produced. The rail thumbnail and the live canvas paint shimmering grey blocks over its background, the canvas refuses edits on it, and `renderSlide` paints the same blocks statically. Seed the whole deck up front with the ids you will later fill, then swap each one in place — a `slides.update` patch that carries `elements` (or `skeleton: false`) clears the flag:

```ts
await controller.setDocument({
  title,
  slides: outline.map(item => ({ id: item.id, elements: [], skeleton: true, background })),
})

// …as each slide finishes generating:
await controller.slides.update(item.id, { elements, background, remark })
controller.goToSlide(item.id)
```

### Bridge subscriptions

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:

```tsx
useEffect(() => {
  const controller = controllerRef.current
  if (!controller) return

  return controller.subscribe(event => {
    if (event.type === 'documentChanged') {
      void savePresentationDraft(event.data)
    }

    if (event.type === 'commandFailed') {
      console.warn('Fika command failed', event.command, event.result?.errors)
    }
  })
}, [controllerRef.current])
```

### Batch edits

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:

```ts
const slideId = controller.slides.get()?.id
if (!slideId) return

const results = await controller.executeBatch([
  { type: 'slides.select', payload: { slideIdOrIndex: slideId } },
  {
    type: 'elements.create',
    payload: {
      slideId,
      element: {
        type: 'text',
        left: 80,
        top: 80,
        width: 520,
        height: 96,
        rotate: 0,
        content: '<p>Agent summary</p>',
        defaultFontName: '',
        defaultColor: '#111',
      },
      select: true,
    },
  },
  { type: 'slides.setRemark', payload: { slideId, remark: '<p>Notes</p>' } },
], { atomic: true })

const failed = results.find(result => !result.ok)
if (failed) console.error(failed.errors)
```

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`.

### Export Boundary

The embed controller exposes JSON export only:

```ts
const document = controller.export.json()
const result = await controller.execute<FikaDocument>({ type: 'export.json' })
```

`export.json()` and the `export.json` command return the serializable `FikaDocument` model and do not require the editor DOM. PPTX export depends on pptxgenjs and in-browser media inlining, so it stays on the editor export dialog rather than the agentic bridge. Hosts that need a PPTX file should drive Fika's export UI in a browser context or add a dedicated DOM-aware integration boundary.

### Export dialog formats

The export dialog offers native PPTX and JSON only. Hosts can hide either format via `exportTabs`; omitted keys stay enabled.

```ts
await mountFika(host, {
  exportTabs: {
    pptx: true,
    json: false,
  },
})
```

When only one format is enabled, the dialog shows a single download card for that format.

### Locale switcher

The editor header's right cluster can show a locale button (`EN` / `CS` / `SK` / `PL`). It is **off by default** — embedding hosts usually drive locale via `mountFika({ locale })` or `controller.setLocale()`. Pass `showLocaleSwitcher: true` to enable it. The standalone demo turns it on from `App.tsx`.

```ts
await mountFika(host, {
  locale: 'cs',
  showLocaleSwitcher: true,
})
```

### Export media resolver (CORS)

Third-party image hosts often allow `<img>` display but block browser `fetch`/XHR (no `Access-Control-Allow-Origin`). PPTX export needs the bytes, so hosts can pass `exportMediaResolver` — typically a same-origin proxy that returns a `data:` URL:

```ts
await mountFika(host, {
  exportMediaResolver: async (url) => {
    const res = await fetch('/functions/v1/media-fetch', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', /* auth */ },
      body: JSON.stringify({ url }),
    })
    if (!res.ok) return null
    // … blob → data URL
    return dataUrl
  },
})
```

Direct fetch is tried first; the resolver runs only on failure.

### Export watermark

Hosts that gate clean downloads behind a plan can have every PPTX export carry a watermark. Pass `exportWatermark`, a resolver evaluated at the start of each export — return the mark for accounts that need it, `null` for a clean deck. Because it runs per export, an entitlement change takes effect on the next download without remounting.

```ts
await mountFika(host, {
  exportWatermark: () =>
    isFreeTier()
      ? {
          image: `${location.origin}/images/logo.png`,      // light slides (dark artwork)
          imageOnDark: `${location.origin}/images/logo-on-dark.png`, // optional; dark / photo slides
          position: 'bottom-right',                    // default
          widthRatio: 0.12,                            // fraction of slide width, default
          marginRatio: 0.02,                           // fraction of slide width, default
          name: 'Acme',                                // shape name, default 'Watermark'
        }
      : null,
})
```

The mark is written into the OOXML after generation, so it is identical for regenerated decks and retained source packages:

- one media part per polarity (`image` on light fills, `imageOnDark` on dark / photo fills). The engine picks the variant with the same `preferredInk` query as editor chrome; image backgrounds follow the invert mark;
- the slide copy sits last in the shape tree (visible over full-bleed content) and is fully locked — `noSelect` keeps PowerPoint from selecting it by click, marquee, Ctrl+A or the selection pane, so there is no in-app way to delete it;
- the master copy is `userDrawn` and locked too; it survives slide-level deletions in editors that ignore locks and only goes away through the slide master view (or by hiding background graphics on a slide);
- parts already carrying the mark are skipped, so a re-imported export does not stack marks.

OOXML is plain XML in a ZIP — a determined user can still strip the picture with a text editor. The mark is designed to defeat casual removal, not to be cryptographically enforced. If the resolver throws or the image cannot be loaded, the export fails with the standard export error instead of silently downloading a clean deck.

### Media uploads

The toolbar **Media** action (images, video, and audio) opens a local-file picker. Pexels / remote galleries are not used. Without a host `upload` callback, images become data URLs and audio/video become blob URLs — fine for demos, not for persistence.

Pass `media` to `mountFika()` for production:

```ts
import { mountFika, createFikaMediaUploader } from 'fika-editor/embed'

await mountFika(host, {
  media: {
    constraints: {
      maxFiles: 12,
      maxFileSize: {
        image: 20 * 1024 * 1024,
        audio: 50 * 1024 * 1024,
        video: 200 * 1024 * 1024,
      },
      kinds: ['image', 'video', 'audio'],
      // Optional extra allowlists:
      // mimeTypes: ['image/png', 'image/jpeg', 'video/mp4'],
      // extensions: ['png', 'jpg', 'jpeg', 'mp4', 'mp3'],
      // accept: 'image/*,video/*,audio/*',
    },
    concurrency: 3,
    upload: createFikaMediaUploader({
      url: '/api/media',
      fieldName: 'file',
      headers: () => ({ Authorization: `Bearer ${token}` }),
      // Default parser accepts `{ url }`, `{ src }`, or `{ data: { url } }`.
    }),
  },
})
```

`upload` can also be a custom function if you need signed URLs or a non-FormData protocol:

```ts
media: {
  upload: async ({ file, kind, signal, onProgress }) => {
    // … PUT to a signed URL, call onProgress({ loaded, total })
    return { src: 'https://cdn.example.com/slide-asset.mp4', ext: 'mp4' }
  },
}
```

## sciobot-next wiring

1. **package.json** — `"fika-editor": "^2.0.0"` after the package is published. During local development, sciobot's Vite config can fall back to the sibling `../Fika/dist/embed` build.
2. After changing Fika locally, run `npm run build:embed` in Fika, then restart or refresh sciobot.
3. **VITE_FIKA_ASSET_BASE** — URL prefix for `fika-embed.css`, `mocks/`, and `imgs/`:
   - Dev: proxy `/fika-assets` → Fika `dist/embed` or `public/`
   - Prod: copy `dist/embed/{fika-embed.css,mocks,imgs}` into sciobot `public/fika-assets/`
4. **Locale** — pass sciobot `Locales`; same union as typesafe-i18n in both apps.
5. **Vite** — do not pre-bundle `fika-editor/embed` into the host app (keep the embed as its own chunk):

```ts
// vite.config.ts (sciobot-next)
resolve: {
  alias: {
    'fika-editor/embed': path.resolve(__dirname, '../Fika/dist/embed/fika-embed.js'),
  },
},
optimizeDeps: {
  exclude: ['fika-editor/embed'],
},
```

### CSS asset loading constraints

Load `fika-embed.css` as a plain asset from the same `assetBaseUrl` used for mocks and images:

```tsx
useEffect(() => {
  const href = `${assetBase}/fika-embed.css`
  if (document.querySelector(`link[data-fika-embed-css][href="${href}"]`)) return

  const link = document.createElement('link')
  link.rel = 'stylesheet'
  link.href = href
  link.dataset.fikaEmbedCss = 'true'
  document.head.appendChild(link)

  return () => {
    document.querySelector(`link[data-fika-embed-css][href="${href}"]`)?.remove()
  }
}, [])
```

Do not `import 'fika-editor/embed.css'` in React. The CSS is large and should not enter the host PostCSS/Tailwind pipeline.

### Migrating legacy document calls

Existing save/load code can keep using `getDocument()` and `setDocument()` at persistence boundaries:

```ts
await savePresentation(controller.getDocument())

const document = await loadPresentation(id)
controller.setDocument(document)
```

Agent edits should migrate to bridge commands so sciobot can observe command results, failures, and undo snapshots:

```ts
// Legacy: replace the whole document to add a slide.
const document = controller.getDocument()
controller.setDocument({
  ...document,
  slides: [...document.slides, generatedSlide],
})

// Agentic: create the slide through the bridge.
await controller.slides.create({
  slide: {
    ...generatedSlide,
    id: undefined,
  },
  select: true,
})
```

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. Pass `{ mode: 'append' }` on import meta/payload to insert slides instead of replacing; `{ confirm: false }` skips the replace dialog (the default on the controller).

## Host CSS

Load `fika-embed.css` as a standalone `<link>` (do not bundle it through the host toolchain). The embed root resets inherited host preflight — especially Tailwind's `html { line-height: 1.5 }` and `img { max-width: 100% }` — so slide text and media match the standalone demo.

Persist `document.viewport` from `onChange` / `getDocument()` and pass it back on `mount` / `setDocument`. Without it, imported decks fall back to 1000×16:9 and elements overflow the slide box.

## Why not iframe?

- Shared imperative API for save/load with Supabase
- Locale driven by sciobot zustand without postMessage
- Single CSP / auth surface when both apps are same origin
- Heavier initial JS, but one integrated surface for teachers

## Future

- `postMessage` optional bridge for cross-origin CDN hosting
- Persist `FikaDocument` in `linked_materials` + `presentation` kind in workspace tabs
- Split chunk / lazy `import('fika-editor/embed')` on first open of presentation tab

### Generated Markdown outside the editor

`fika-editor/text` is a small ESM entry without React or DOM dependencies. It
uses CommonMark and math syntax trees with source positions, so servers and
browsers can recover escaped line breaks without changing code, formulas,
link destinations, or HTML source.

```ts
import { normalizeGeneratedMarkdown, normalizeGeneratedNewlines } from 'fika-editor/text'

normalizeGeneratedMarkdown(String.raw`**Title**\r\nSecond line`)
// "**Title**\nSecond line" (a real newline)

// Preserve entities if only newline recovery is wanted:
normalizeGeneratedNewlines(String.raw`# Heading\n- **Point**`)
```

`controller.markdownToHtml()` and Markdown layout slots use this normalization
before rendering formatted runs. `getDocument()` returns the complete target
slide during `revealSlide()`, so a host can safely snapshot it during animation.
The ProseMirror schema preserves hard line breaks and Markdown strikethrough
through storage, static rendering, and editing.
Pass `assetBaseUrl` to `mountFika()` to resolve lazy editor CSS under the same
asset directory as the embed stylesheet.

### Host-configured slide designs

Pass `designThemes` to replace the built-in design cards (not editor chrome).
The supplied order and host-localized `name` appear in the picker. Custom designs
also drive layout previews, new layout slides, background looks and chart colors.
Omit the option for Fika defaults; pass `[]` to hide the preset section.
Configuring the catalog does not apply a design to the existing document.

```ts
import { mountFika, getFikaDefaultDesignThemes, type FikaDesignTheme } from 'fika-editor/embed'

const paper: FikaDesignTheme = {
  id: 'my-paper', name: 'Paper',
  background: '#FAF8F4', fontColor: '#2B3445', fontname: 'Inter',
  colors: ['#D25F2B', '#2A7FA3', '#5F8A2C'],
  borderColor: '#E3DFD6',
}
const { controller } = await mountFika('#editor', { designThemes: [paper] })
controller.setDesignThemes([{ ...paper, name: 'Papír' }]) // reactive label/catalog update
controller.setDesignThemes(getFikaDefaultDesignThemes()) // independent copies of defaults
```

Design IDs must be nonempty and unique; names and accent palettes are required.
Use distinct accent palettes so the active design can be recognized after a
saved document is loaded. Optional `chartColors`, `featureFontColor`,
`contentBackground`, `featureBackground`, `altBackground` and `backgrounds`
provide the same appearance controls as built-in designs. Catalogs are runtime
host configuration, not part of the persisted document. Like Fika's other embed
configuration and stores, the catalog is shared by the active editor runtime;
use separate runtimes for independently configured simultaneous editors.

Agents can use the same active catalog via `controller.deck.listDesignThemes()`
(read-only), then apply one by ID with
`controller.deck.applyDesignTheme('scio-blue')`. The command-bus equivalent is
`{ type: 'deck.applyDesignTheme', payload: { themeId: 'scio-blue' } }`.
Application uses the picker implementation and creates one undo step for the
whole deck. Unknown IDs fail without changing the presentation. Hosts should
include the small catalog in agent context so applying a named theme needs only
one call, without a preceding discovery request.

### Content-aware canvas resizing

`controller.deck.setViewport({ ratio: 0.75 })` fits the entire deck to 4:3 in
one undo step, using the same operation as the size menu and custom size dialog.
It identifies full-width headings and independent body columns, distributes
rows through their available height, scales type with its boxes, preserves media
proportions and groups, remaps connectors, and anchors vector edge decorations.
It consumes spare vertical gutters before shrinking type when returning to a
shorter canvas. It preserves the authored composition rather than inventing a
new reading order or cropping photographs. Imported objects already outside the
canvas may still need manual correction.

Hosts authoring slides into a differently sized deck can use the pure,
DOM-free `resizeDeckSlides(slides, from, to)` export from `fika-editor/layout`;
`from` and `to` are `{ width, height }`. Raw document loading remains available
through `deck.set` / `deck.patch` with `slides` and `viewport` supplied together.

Host design themes can opt into `preserveColorRoles: true` to keep pale panels,
neutral surfaces and saturated accents separate during theme application.
Text is retinted against the actual panel beneath it, including badge and table
cell fills. Named `Decor` shapes become soft accents on the selected background.
Reapplying a selected theme is supported, including for older generated decks.


## Myna graphic design mode

Pass `viewMode: 'canva'` (or its `'myna'` alias) to open the Myna design
workspace. Omit `document` for its square editable starter, or provide any
ordinary Fika document and viewport. `createMynaDocument()` and `MYNA_FORMATS`
are exported for hosts that need the starter or format catalog. The same
controller, history, media uploader, read-only options, and change callbacks
apply. See [Myna integration and capabilities](./MYNA.md).


### Export without a mounted editor

`exportPresentationPptx(document, options)` and `exportPresentationPdf(document, options)`
return `Promise<Blob>`. They snapshot the supplied `FikaDocument`, do not open
an export dialog, and do not trigger a browser download. A saved deck can be
exported while another deck remains open and editable.

```ts
import { exportPresentationPptx, exportPresentationPdf } from 'fika-editor/embed'

const options = { assetBaseUrl: '/fika-assets' }
const pptx = await exportPresentationPptx(document, options)
const pdf = await exportPresentationPdf(document, options)
```

PPTX retains editable elements. PDF uses the existing slide painter with one
high-resolution JPEG per page (2560 pixels wide by default), preserving slide
order and aspect ratio. PDF text is rasterized. No additional document IR is
required. Load `fika-embed.css` before PDF export so bundled fonts are available.

Options include `mediaResolver`, `watermark`, and `onProgress(completed, total, detail)`.
The optional `detail` (`FikaExportProgress`) includes `phase` (`preparing`, `rendering`,
`packaging`, or `complete`) and normalized `progress` (0–1). Counted slides are
completed slides; packaging continues after all slides render, and `progress: 1`
is emitted only when the returned Blob is ready. Errors reject without a completion event.

PPTX generation, PDF packaging, and font decoding run in module
workers. Serve the complete `dist/embed` directory, including worker and chunk
files, under `assetBaseUrl`; the host's CSP must permit same-origin workers.
DOM-dependent preparation still runs on the main thread with cooperative yields.
Omitted resolvers use the current embed configuration. Pass `null` explicitly
to disable a resolver. PDF also accepts `width` (64 to 8192 pixels) and
`timeoutMs` (default 30000 per slide). `imageFormat` defaults to `image/jpeg` with
`quality: 0.95`; use `imageFormat: 'image/png'` for lossless output. JPEG bytes are
embedded directly without decoding and recompressing each page. Rendering uses
bounded concurrency and skips idle waits when all resources are ready. Export
time still depends on slide count, image downloads, raster size, and hardware.
Required watermark and PDF image failures
reject the promise. PPTX retains its existing partial-media warning behavior.

The native export dialog now offers `pptx`, `pdf`, and `json`; control their
visibility with `exportTabs`. Host exports should call these APIs directly,
without simulating export button clicks or mounting an offscreen editor.

### Multiple presentation tabs

The embed uses shared module-level stores and permits one active editor per
module. Destroy the previous controller before mounting a different deck;
mounting a second host concurrently throws before changing any document.
Cache each deck separately in the host and pass its latest snapshot on remount.
`destroy()` flushes pending rich-text changes and the final `onChange` before
releasing the editor. Hosts should flush their own autosave queue on teardown.

### Portable PPTX fonts

PPTX exports automatically embed bundled fonts used by the exported text runs,
tables, shapes, charts and theme, including fonts that are installed on the
exporting computer. Inline-only font choices are included. CSS fallback stacks
are converted to a single OOXML typeface, and bundled family aliases use their
canonical names. No host option is required.

Serve the package's font assets alongside its workers and chunks. If a required
bundled font cannot be fetched or decoded, export rejects instead of silently
omitting it. Fonts outside the bundled catalog still depend on the receiving
application's font availability. Embedded fonts improve portability in viewers
that support them; PDF is available for a fixed visual result.

New presentations and theme presets default to Calibri for text. Browsers use
installed Calibri where available, otherwise bundled Carlito (OFL, metrically
compatible) in regular, bold, italic, and bold italic. This fallback needs no
Google Fonts request. PPTX retains the Calibri family name for native Office
rendering; it does not embed Carlito under the Calibri name. Explicit document
font choices are preserved.
