fika-editor 3.0.44 → 3.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.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Host-mandated watermark for PPTX exports.
3
+ *
4
+ * The resolver runs at the start of every PPTX export, so entitlement changes
5
+ * on the host side (a user upgrading mid-session) take effect on the next
6
+ * download without remounting. Returning null/undefined exports a clean deck.
7
+ * A resolver that throws, or an image that cannot be loaded, fails the export
8
+ * — the engine never silently drops a mark the host asked for.
9
+ */
10
+ export type FikaExportWatermarkPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
11
+ export interface FikaExportWatermark {
12
+ /** PNG or JPEG as a `data:` URL or a URL the browser can fetch. */
13
+ image: string;
14
+ /** Corner of the slide. @default 'bottom-right' */
15
+ position?: FikaExportWatermarkPosition;
16
+ /** Mark width as a fraction of the slide width (0–1]. @default 0.12 */
17
+ widthRatio?: number;
18
+ /** Gap to the slide edges as a fraction of the slide width (0–1]. @default 0.02 */
19
+ marginRatio?: number;
20
+ /** Extra opacity applied on top of the image's own alpha (0–1]. @default 1 */
21
+ opacity?: number;
22
+ /** Shape name shown by presentation apps. @default 'Watermark' */
23
+ name?: string;
24
+ }
25
+ export type FikaExportWatermarkResolver = () => FikaExportWatermark | null | undefined | Promise<FikaExportWatermark | null | undefined>;
26
+ export declare function setFikaExportWatermark(resolver?: FikaExportWatermarkResolver | null): void;
27
+ export declare function getFikaExportWatermark(): FikaExportWatermarkResolver | null;
@@ -3,6 +3,7 @@ export type { FikaController, FikaDocument, FikaHeaderMenuItem, FikaMountOptions
3
3
  export type { FikaDeckAtlasOptions, FikaDeckAtlasSheet, FikaDeckAtlasTile, FikaRenderSlideOptions, } from './render';
4
4
  export type { ExportTabId, FikaExportTabsConfig } from '../configs/exportTabs';
5
5
  export type { FikaExportMediaResolver } from '../configs/exportMediaResolver';
6
+ export type { FikaExportWatermark, FikaExportWatermarkPosition, FikaExportWatermarkResolver, } from '../configs/exportWatermark';
6
7
  export { createFikaMediaUploader, } from '../configs/mediaUpload';
7
8
  export type { FikaMediaConfig, FikaMediaConstraints, FikaMediaKind, FikaMediaSizeLimit, FikaMediaUploadProgress, FikaMediaUploadRequest, FikaMediaUploadResult, FikaMediaUploader, FikaXhrMediaUploaderOptions, } from '../configs/mediaUpload';
8
9
  export type * from './agentic/types';
@@ -1,11 +1,13 @@
1
1
  import type { FikaExportTabsConfig } from '../configs/exportTabs';
2
2
  import type { FikaExportMediaResolver } from '../configs/exportMediaResolver';
3
+ import type { FikaExportWatermark, FikaExportWatermarkPosition, FikaExportWatermarkResolver } from '../configs/exportWatermark';
3
4
  import type { FikaMediaConfig } from '../configs/mediaUpload';
4
5
  import type { Locales } from '../i18n/locale';
5
6
  import type { Slide, SlideTheme, SlideTemplate } from '../types/slides';
6
7
  import type { FikaAgentApi, FikaDeckViewport, FikaSlideReference } from './agentic/types';
7
8
  import type { FikaRevealOptions } from './reveal';
8
9
  export type { FikaExportMediaResolver };
10
+ export type { FikaExportWatermark, FikaExportWatermarkPosition, FikaExportWatermarkResolver };
9
11
  export type { FikaRevealOptions };
10
12
  export type { FikaMediaConfig, FikaMediaConstraints, FikaMediaKind, FikaMediaSizeLimit, FikaMediaUploadProgress, FikaMediaUploadRequest, FikaMediaUploadResult, FikaMediaUploader, FikaXhrMediaUploaderOptions } from '../configs/mediaUpload';
11
13
  export interface FikaTemplatePayload {
@@ -97,6 +99,13 @@ export interface FikaMountOptions {
97
99
  * or null/undefined to give up on that source.
98
100
  */
99
101
  exportMediaResolver?: FikaExportMediaResolver;
102
+ /**
103
+ * Watermark baked into every PPTX download (each slide and slide master, as
104
+ * a locked, unselectable picture). Evaluated at the start of each export so
105
+ * the host can gate it on current entitlement; return null for a clean deck.
106
+ * Applies to regenerated decks and retained source packages alike.
107
+ */
108
+ exportWatermark?: FikaExportWatermarkResolver;
100
109
  /**
101
110
  * Local media picker used by the unified Insert → Media action (images,
102
111
  * video, and audio). Pass `upload` to send files to your backend instead of
@@ -4,17 +4,16 @@ export declare const getChartRaster: (element: PPTChartElement, colors: string[]
4
4
  export declare const getMermaidRaster: (element: PPTMermaidElement, invalidate: () => void) => Raster | undefined;
5
5
  export declare const getCodeRaster: (element: PPTCodeElement, invalidate: () => void) => Raster | undefined;
6
6
  /**
7
- * MathLive typeset of a formula element, mirroring `LatexContent`'s DOM
8
- * (flex-centered box, a 36px stage fitted uniformly into the authored box).
9
- * html-to-image is used instead of a bare foreignObject because SVG-as-image
10
- * cannot load the MathLive web fonts.
7
+ * Tight MathLive snapshot of a formula — same nowrap, max-content stage as
8
+ * `LatexContent`. SnapDOM v3 `width`/`height` are output size only and do not
9
+ * reflow the live tree, but a flex host sized to the slide box *does*: the
10
+ * stage shrinks (`min-width:auto`), `.ML__latex` wraps, and the thumbnail
11
+ * becomes two lines of fallback-looking text. Capture the unconstrained
12
+ * stage, then `paintLatex` scales that bitmap into the authored box.
11
13
  *
12
- * The fit is applied as a font size, not a CSS transform: inside the SVG
13
- * snapshot, box edges (the radical rule, fraction bars) snap to whole CSS
14
- * pixels of the pre-transform layout while glyphs scale exactly, so a scaled
15
- * 36px stage paints a `\sqrt` rule thinner and lower than the surd's tick.
16
- * Laying out at the final size keeps that snap error under half a pixel, and
17
- * the 2x capture halves it again.
14
+ * Font size is the fitted size (not a CSS transform) so radical/fraction
15
+ * rules snap at the painted size. `reconcile: true` pins inline/table-cell
16
+ * boxes SnapDOM otherwise warns may re-wrap under font fallback.
18
17
  */
19
18
  export declare const getLatexRaster: (element: PPTLatexElement, invalidate: () => void) => Raster | undefined;
20
19
  /**
@@ -53,7 +53,7 @@ export type HighlightedTokens = {
53
53
  };
54
54
  export declare function highlightCodeTokens(code: string, language: string, theme: string): Promise<HighlightedTokens>;
55
55
  export declare function highlightCodeBlock(code: string, language: string, theme: string): Promise<HighlightedCode>;
56
- /** Warm Shiki + html-to-image so the first on-slide raster is not a cold start. */
56
+ /** Warm Shiki + SnapDOM so the first on-slide raster is not a cold start. */
57
57
  export declare function prefetchCodeRaster(language?: string, theme?: string): void;
58
58
  /** Load core + lang + theme. Safe to call before CodeMirror/Shiki highlighting. */
59
59
  export declare function prepareHighlighter(language: string, theme: string): Promise<{
@@ -74,7 +74,7 @@ export type CodeRasterInput = {
74
74
  fontSize: number;
75
75
  showLineNumbers: boolean;
76
76
  };
77
- /** Real gutter spans so SnapDOM / html-to-image do not depend on CSS counters. */
77
+ /** Real gutter spans so SnapDOM does not depend on CSS counters. */
78
78
  export declare function injectBoothLineNumbers(shikiHtml: string, showLineNumbers: boolean): string;
79
79
  export declare function codeElementToBoothHtml(el: CodeRasterInput): Promise<string>;
80
80
  export declare function renderCodeElementPng(el: CodeRasterInput & {
@@ -1,17 +1,20 @@
1
1
  /**
2
2
  * `@font-face` CSS with the font files inlined as data URLs, for markup that
3
- * is rasterised as a standalone SVG image (math booths, mermaid diagrams):
4
- * an SVG drawn through `<img>` cannot reach the page's web fonts, so any
5
- * family it names must travel with it.
3
+ * is rasterised as a standalone SVG image (mermaid diagrams): an SVG drawn
4
+ * through `<img>` cannot reach the page's web fonts, so any family it names
5
+ * must travel with it.
6
6
  *
7
- * html-to-image only inlines the families the probed node uses, so the probe
8
- * is a stub naming exactly the requested families. Results are cached per
9
- * family set; a result without inlined data (stylesheet not parsed yet, font
10
- * fetch failed) is not cached so the next capture retries.
7
+ * Walks document stylesheets for matching `@font-face` rules and inlines
8
+ * their `url()` sources. Results are cached per family set; a result without
9
+ * inlined data (stylesheet not parsed yet, font fetch failed) is not cached
10
+ * so the next capture retries.
11
11
  */
12
+ export declare const normalizeFontFamily: (family: string) => string;
12
13
  /** Families the document declares via `@font-face` (loaded or not). */
13
14
  export declare const declaredFontFamilies: () => Set<string>;
14
- export declare const fontEmbedCssFor: (families: readonly string[], host?: HTMLElement) => Promise<string>;
15
+ export declare const fontEmbedCssFor: (families: readonly string[], _host?: HTMLElement) => Promise<string>;
16
+ /** Drop cached CSS so a later capture retries stylesheet reads. Tests only. */
17
+ export declare const resetFontEmbedCssCache: () => void;
15
18
  /**
16
19
  * Inline every declared web font an SVG names into a `<style>` at its top,
17
20
  * so the diagram paints the same through `<img>` as it does in the DOM.
@@ -14,6 +14,13 @@ export declare function latexPropsAfterEdit(_current: Pick<PPTLatexElement, 'wid
14
14
  }): Pick<PPTLatexElement, 'latex' | 'path' | 'viewBox'>;
15
15
  /** Same uniform scale `useLiveBoxFit` writes: authored box / natural viewBox. */
16
16
  export declare function latexPaintScale(el: Pick<PPTLatexElement, 'width' | 'height' | 'viewBox'>): number;
17
+ /** Scale a measured formula so it stays inside the slide with a margin. */
18
+ export declare function fitLatexBoxToSlide(width: number, height: number, slideW: number, slideH: number, pad?: number): {
19
+ width: number;
20
+ height: number;
21
+ left: number;
22
+ top: number;
23
+ };
17
24
  /**
18
25
  * Extract the contents of every equation (or equation*) environment.
19
26
  */
@@ -0,0 +1,24 @@
1
+ import type { FikaExportWatermark } from '../configs/exportWatermark';
2
+ export declare const EXPORT_WATERMARK_MARKER = "fika:export-watermark";
3
+ export interface WatermarkImage {
4
+ bytes: Uint8Array;
5
+ mime: 'image/png' | 'image/jpeg';
6
+ width: number;
7
+ height: number;
8
+ }
9
+ /** Reads dimensions from the PNG/JPEG header — no decoder or DOM needed. */
10
+ export declare function readWatermarkImage(bytes: Uint8Array): WatermarkImage;
11
+ /** Resolves `image` (data: URL or fetchable URL) to raw bytes plus dimensions. */
12
+ export declare function loadWatermarkImage(image: string): Promise<WatermarkImage>;
13
+ /** True when the package already carries the export watermark on any slide. */
14
+ export declare function hasPptxExportWatermark(bytes: ArrayBuffer | Uint8Array): Promise<boolean>;
15
+ /**
16
+ * Returns a new package with the watermark on every slide and slide master.
17
+ * Throws when the input is not a presentation package.
18
+ */
19
+ export declare function applyPptxExportWatermark(bytes: ArrayBuffer | Uint8Array, watermark: FikaExportWatermark, image: WatermarkImage): Promise<Uint8Array>;
20
+ /**
21
+ * Browser download through a real `HTMLAnchorElement.click()` — the same seam
22
+ * pptxgenjs uses, which embedding hosts rely on to capture export bytes.
23
+ */
24
+ export declare function downloadPptxBytes(bytes: Uint8Array, fileName: string): void;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * SnapDOM v3 capture helpers.
3
+ *
4
+ * Width/height are the final CSS size (they win over `scale`). `dpr` defaults
5
+ * to 1 so those dimensions are also the pixel size — pass `dpr: 2` when a
6
+ * booth needs a hidpi backing store. Fonts embed automatically (`'auto'`);
7
+ * pass `false` only to skip webfonts the way the old `fontEmbedCSS: ''` did.
8
+ */
9
+ import { type SnapdomOptions } from '@zumer/snapdom';
10
+ export type SnapdomCaptureOptions = Pick<SnapdomOptions, 'width' | 'height' | 'dpr' | 'quality' | 'embedFonts' | 'backgroundColor' | 'exclude' | 'excludeMode' | 'filter' | 'filterMode' | 'invalidate' | 'canvas' | 'reconcile'>;
11
+ export declare const prefetchSnapdom: () => void;
12
+ export declare const captureToCanvas: (element: Element, options?: SnapdomCaptureOptions) => Promise<HTMLCanvasElement>;
13
+ export declare const captureToPngDataUrl: (element: Element, options?: SnapdomCaptureOptions) => Promise<string>;
14
+ export declare const captureToJpegDataUrl: (element: Element, options?: SnapdomCaptureOptions) => Promise<string>;
@@ -13,6 +13,15 @@
13
13
  import { prepare as pretextPrepare } from '@chenglou/pretext';
14
14
  /** ProseMirror's default text size (assets/styles/prosemirror.scss). */
15
15
  export declare const DEFAULT_TEXT_FONT_SIZE = 16;
16
+ /**
17
+ * Live text boxes use `$textElementFont` when the theme/element family is
18
+ * empty. Canvas wrap must use the same stack — generic `sans-serif` is a
19
+ * different face and packs a different number of words per line.
20
+ */
21
+ export declare const DEFAULT_TEXT_FONT_FAMILY = "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\"";
22
+ /** @deprecated Use DEFAULT_TEXT_FONT_FAMILY */
23
+ export declare const DEFAULT_FIT_FONT_FAMILY = "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\"";
24
+ export declare function resolvePaintFontFamily(family?: string | null): string;
16
25
  /** Matches `ul, ol { padding-inline-start: 1em }` when the list has no inline pad. */
17
26
  export declare const DEFAULT_LIST_PADDING_EM = 1;
18
27
  /** Matches `li { padding-inline-start: 0.4em }` (marker-to-text gap). */
@@ -143,6 +152,15 @@ export declare const lineBoxHeight: (runs: TextFitRun[], sizeScale: number, line
143
152
  * item occupies exactly the chip width.
144
153
  */
145
154
  export declare const MATH_PLACEHOLDER = "\uFFFC";
155
+ /**
156
+ * Live ProseMirror uses `white-space: break-spaces`: the space after the last
157
+ * word on a line still occupies width, so the wrap column is one space
158
+ * narrower than the content box. pretext is `white-space: normal` and would
159
+ * otherwise pack an extra word per line (thumb vs slide).
160
+ */
161
+ export declare const measureSpaceWidth: (font: string) => number;
162
+ export declare const breakSpacesWrapWidth: (width: number, spaceWidth: number) => number;
163
+ export declare const editorWrapWidth: (width: number, font: string) => number;
146
164
  export declare const richInlineFromRun: (run: TextFitRun, font: string, sizeScale: number, letterSpacing?: number) => {
147
165
  text: string;
148
166
  font: string;
package/docs/EMBED.md CHANGED
@@ -283,6 +283,34 @@ await mountFika(host, {
283
283
 
284
284
  Direct fetch is tried first; the resolver runs only on failure.
285
285
 
286
+ ### Export watermark
287
+
288
+ 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.
289
+
290
+ ```ts
291
+ await mountFika(host, {
292
+ exportWatermark: () =>
293
+ isFreeTier()
294
+ ? {
295
+ image: `${location.origin}/images/logo.png`, // PNG or JPEG; data: URLs work too
296
+ position: 'bottom-right', // default
297
+ widthRatio: 0.12, // fraction of slide width, default
298
+ marginRatio: 0.02, // fraction of slide width, default
299
+ name: 'Acme', // shape name, default 'Watermark'
300
+ }
301
+ : null,
302
+ })
303
+ ```
304
+
305
+ The mark is written into the OOXML after generation, so it is identical for regenerated decks and retained source packages:
306
+
307
+ - one shared media part, referenced from **every slide and every slide master**;
308
+ - 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;
309
+ - 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);
310
+ - parts already carrying the mark are skipped, so a re-imported export does not stack marks.
311
+
312
+ 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.
313
+
286
314
  ### Media uploads
287
315
 
288
316
  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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fika-editor",
3
- "version": "3.0.44",
3
+ "version": "3.1.1",
4
4
  "description": "Fika presentation editor embed bundle with a typed agentic bridge.",
5
5
  "type": "module",
6
6
  "main": "dist/embed/fika-embed.js",
@@ -88,6 +88,7 @@
88
88
  "@tanstack/react-virtual": "^3.14.10",
89
89
  "@use-gesture/react": "^10.3.1",
90
90
  "@use-gesture/vanilla": "^10.3.1",
91
+ "@zumer/snapdom": "^3.0.0",
91
92
  "animate.css": "^4.1.1",
92
93
  "buffer": "^6.0.3",
93
94
  "clipboard": "^2.0.11",
@@ -103,7 +104,6 @@
103
104
  "file-saver": "^2.0.5",
104
105
  "flatbush": "^4.6.2",
105
106
  "hfmath": "^0.0.2",
106
- "html-to-image": "^1.11.13",
107
107
  "immer": "^11.1.18",
108
108
  "jszip": "^3.10.1",
109
109
  "lucide-react": "^1.33.0",