tegaki 0.5.0 → 0.6.0

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,3 @@
1
+ import { A as TegakiSingletonEffectName, C as Stroke, D as TegakiEffects, E as TegakiEffectName, O as TegakiGlyphData, S as Point, T as TegakiEffectConfigs, _ as CSSLength, a as TimeControlProp, b as LineCap, c as TimelineEntry, d as computeTextLayout, f as ensureFontFace, g as BBox, h as resolveEffects, i as TimeControlMode, j as TimedPoint, k as TegakiMultiEffectName, l as computeTimeline, m as ResolvedEffect, n as TegakiEngine, o as Timeline, p as drawGlyph, r as TegakiEngineOptions, s as TimelineConfig, t as CreateElementFn, u as TextLayout, v as FontOutput, w as TegakiBundle, x as PathCommand, y as GlyphData } from "../index-Bzxy01Zq.mjs";
2
+ import { n as TegakiRendererHandle, r as TegakiRendererProps, t as TegakiRenderer } from "../index-BUNd9bnS.mjs";
3
+ export { BBox, CSSLength, CreateElementFn, FontOutput, GlyphData, LineCap, PathCommand, Point, ResolvedEffect, Stroke, TegakiBundle, TegakiEffectConfigs, TegakiEffectName, TegakiEffects, TegakiEngine, TegakiEngineOptions, TegakiGlyphData, TegakiMultiEffectName, TegakiRenderer, TegakiRendererHandle, TegakiRendererProps, TegakiSingletonEffectName, TextLayout, TimeControlMode, TimeControlProp, TimedPoint, Timeline, TimelineConfig, TimelineEntry, computeTextLayout, computeTimeline, drawGlyph, ensureFontFace, resolveEffects };
@@ -0,0 +1,3 @@
1
+ import { a as drawGlyph, i as ensureFontFace, n as computeTimeline, r as computeTextLayout, s as resolveEffects, t as TegakiEngine } from "../core-aZknK_0L.mjs";
2
+ import { t as TegakiRenderer } from "../react-Ddx6VfJc.mjs";
3
+ export { TegakiEngine, TegakiRenderer, computeTextLayout, computeTimeline, drawGlyph, ensureFontFace, resolveEffects };
@@ -0,0 +1,53 @@
1
+ import { o as coerceToString, t as TegakiEngine } from "./core-aZknK_0L.mjs";
2
+ import { createElement, useEffect, useImperativeHandle, useRef } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/react/TegakiRenderer.tsx
5
+ function reactCreateElement(tag, props, ...children) {
6
+ return createElement(tag, {
7
+ ...props,
8
+ key: props["data-tegaki"]
9
+ }, ...children);
10
+ }
11
+ function TegakiRenderer({ ref, font, text, children, time: timeProp, onComplete, effects, segmentSize, timing, showOverlay, ...divProps }) {
12
+ const containerRef = useRef(null);
13
+ const engineRef = useRef(null);
14
+ const engineOptions = {
15
+ text: text ?? coerceToString(children),
16
+ font,
17
+ time: timeProp,
18
+ effects,
19
+ segmentSize,
20
+ timing,
21
+ showOverlay,
22
+ onComplete
23
+ };
24
+ const elements = TegakiEngine.renderElements(engineOptions, reactCreateElement);
25
+ useEffect(() => {
26
+ const engine = new TegakiEngine(containerRef.current, { adopt: true });
27
+ engineRef.current = engine;
28
+ return () => {
29
+ engine.destroy();
30
+ engineRef.current = null;
31
+ };
32
+ }, []);
33
+ useEffect(() => {
34
+ engineRef.current?.update(engineOptions);
35
+ });
36
+ useImperativeHandle(ref, () => ({
37
+ get engine() {
38
+ return engineRef.current;
39
+ },
40
+ get element() {
41
+ return containerRef.current;
42
+ }
43
+ }), []);
44
+ return /* @__PURE__ */ jsx("div", {
45
+ ref: containerRef,
46
+ ...divProps,
47
+ children: elements
48
+ });
49
+ }
50
+ //#endregion
51
+ export { TegakiRenderer as t };
52
+
53
+ //# sourceMappingURL=react-Ddx6VfJc.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-Ddx6VfJc.mjs","names":[],"sources":["../src/react/TegakiRenderer.tsx"],"sourcesContent":["'use client';\n\nimport { type ComponentProps, createElement, type ReactNode, type Ref, useEffect, useImperativeHandle, useRef } from 'react';\nimport { TegakiEngine, type TegakiEngineOptions } from '../core/engine.ts';\nimport type { Coercible } from '../lib/utils.ts';\nimport { coerceToString } from '../lib/utils.ts';\nimport type { TegakiEffects } from '../types.ts';\n\n/** Imperative handle exposed via the `ref` prop. */\nexport interface TegakiRendererHandle {\n /** The underlying engine instance. `null` before mount and after unmount. */\n readonly engine: TegakiEngine | null;\n /** The container DOM element. */\n readonly element: HTMLDivElement | null;\n}\n\nexport interface TegakiRendererProps<E extends TegakiEffects<E> = Record<string, never>>\n extends Omit<TegakiEngineOptions, 'effects'>,\n Omit<ComponentProps<'div'>, 'children' | 'ref'> {\n /** Imperative handle ref for playback controls and DOM access. */\n ref?: Ref<TegakiRendererHandle>;\n\n /** Children coerced to string. Strings and numbers are kept; everything else is ignored. */\n children?: Coercible;\n\n /** Visual effects applied during canvas rendering. */\n effects?: E;\n}\n\nfunction reactCreateElement(tag: string, props: Record<string, any>, ...children: (ReactNode | string)[]): ReactNode {\n return createElement(tag, { ...props, key: props['data-tegaki'] }, ...children);\n}\n\nexport function TegakiRenderer<const E extends TegakiEffects<E> = Record<string, never>>({\n ref,\n font,\n text,\n children,\n time: timeProp,\n onComplete,\n effects,\n segmentSize,\n timing,\n showOverlay,\n ...divProps\n}: TegakiRendererProps<E>) {\n const containerRef = useRef<HTMLDivElement>(null);\n const engineRef = useRef<TegakiEngine | null>(null);\n const resolvedText = text ?? coerceToString(children);\n\n // Render the element tree via the engine's static method (SSR-safe)\n const engineOptions: TegakiEngineOptions = {\n text: resolvedText,\n font,\n time: timeProp,\n effects: effects as Record<string, any>,\n segmentSize,\n timing,\n showOverlay,\n onComplete,\n };\n const elements = TegakiEngine.renderElements(engineOptions, reactCreateElement);\n\n // Create engine on mount, adopting the pre-rendered elements\n useEffect(() => {\n const engine = new TegakiEngine(containerRef.current!, { adopt: true });\n engineRef.current = engine;\n return () => {\n engine.destroy();\n engineRef.current = null;\n };\n }, []);\n\n // Update engine with current props every render\n useEffect(() => {\n engineRef.current?.update(engineOptions);\n });\n\n // Imperative handle\n useImperativeHandle(\n ref,\n () => ({\n get engine() {\n return engineRef.current;\n },\n get element() {\n return containerRef.current;\n },\n }),\n [],\n );\n\n return (\n <div ref={containerRef} {...divProps}>\n {elements}\n </div>\n );\n}\n"],"mappings":";;;;AA6BA,SAAS,mBAAmB,KAAa,OAA4B,GAAG,UAA6C;AACnH,QAAO,cAAc,KAAK;EAAE,GAAG;EAAO,KAAK,MAAM;EAAgB,EAAE,GAAG,SAAS;;AAGjF,SAAgB,eAAyE,EACvF,KACA,MACA,MACA,UACA,MAAM,UACN,YACA,SACA,aACA,QACA,aACA,GAAG,YACsB;CACzB,MAAM,eAAe,OAAuB,KAAK;CACjD,MAAM,YAAY,OAA4B,KAAK;CAInD,MAAM,gBAAqC;EACzC,MAJmB,QAAQ,eAAe,SAAS;EAKnD;EACA,MAAM;EACG;EACT;EACA;EACA;EACA;EACD;CACD,MAAM,WAAW,aAAa,eAAe,eAAe,mBAAmB;AAG/E,iBAAgB;EACd,MAAM,SAAS,IAAI,aAAa,aAAa,SAAU,EAAE,OAAO,MAAM,CAAC;AACvE,YAAU,UAAU;AACpB,eAAa;AACX,UAAO,SAAS;AAChB,aAAU,UAAU;;IAErB,EAAE,CAAC;AAGN,iBAAgB;AACd,YAAU,SAAS,OAAO,cAAc;GACxC;AAGF,qBACE,YACO;EACL,IAAI,SAAS;AACX,UAAO,UAAU;;EAEnB,IAAI,UAAU;AACZ,UAAO,aAAa;;EAEvB,GACD,EAAE,CACH;AAED,QACE,oBAAC,OAAD;EAAK,KAAK;EAAc,GAAI;YACzB;EACG,CAAA"}
@@ -0,0 +1,19 @@
1
+ import { A as TegakiSingletonEffectName, C as Stroke, D as TegakiEffects, E as TegakiEffectName, O as TegakiGlyphData, S as Point, T as TegakiEffectConfigs, _ as CSSLength, a as TimeControlProp, b as LineCap, c as TimelineEntry, d as computeTextLayout, f as ensureFontFace, g as BBox, h as resolveEffects, i as TimeControlMode, j as TimedPoint, k as TegakiMultiEffectName, l as computeTimeline, m as ResolvedEffect, n as TegakiEngine, o as Timeline, p as drawGlyph, r as TegakiEngineOptions, s as TimelineConfig, t as CreateElementFn, u as TextLayout, v as FontOutput, w as TegakiBundle, x as PathCommand, y as GlyphData } from "../index-Bzxy01Zq.mjs";
2
+ import { JSX } from "solid-js";
3
+
4
+ //#region src/solid/TegakiRenderer.d.ts
5
+ interface TegakiRendererProps extends Omit<TegakiEngineOptions, 'effects'> {
6
+ /** Visual effects applied during canvas rendering. */
7
+ effects?: TegakiEffects<Record<string, any>>;
8
+ class?: string;
9
+ ref?: (handle: TegakiRendererHandle) => void;
10
+ [key: string]: any;
11
+ }
12
+ interface TegakiRendererHandle {
13
+ readonly engine: TegakiEngine | null;
14
+ readonly element: HTMLDivElement | null;
15
+ }
16
+ declare function TegakiRenderer(props: TegakiRendererProps): JSX.Element;
17
+ //#endregion
18
+ export { BBox, CSSLength, CreateElementFn, FontOutput, GlyphData, LineCap, PathCommand, Point, ResolvedEffect, Stroke, TegakiBundle, TegakiEffectConfigs, TegakiEffectName, TegakiEffects, TegakiEngine, TegakiEngineOptions, TegakiGlyphData, TegakiMultiEffectName, TegakiRenderer, type TegakiRendererHandle, type TegakiRendererProps, TegakiSingletonEffectName, TextLayout, TimeControlMode, TimeControlProp, TimedPoint, Timeline, TimelineConfig, TimelineEntry, computeTextLayout, computeTimeline, drawGlyph, ensureFontFace, resolveEffects };
19
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1,77 @@
1
+ import { a as drawGlyph, i as ensureFontFace, n as computeTimeline, r as computeTextLayout, s as resolveEffects, t as TegakiEngine } from "../core-aZknK_0L.mjs";
2
+ import { createEffect, createMemo, on, onCleanup, onMount, splitProps } from "solid-js";
3
+ import { jsx } from "solid-js/jsx-runtime";
4
+ //#region src/solid/TegakiRenderer.tsx
5
+ /** @jsxImportSource solid-js */
6
+ function solidCreateElement(tag, props, ...children) {
7
+ const parts = [];
8
+ for (const [key, value] of Object.entries(props)) {
9
+ if (value == null || value === false) continue;
10
+ if (key === "style" && typeof value === "object") {
11
+ const css = Object.entries(value).filter(([, v]) => v != null).map(([k, v]) => {
12
+ return `${k.startsWith("--") ? k : k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}:${typeof v === "number" && !k.startsWith("--") ? `${v}px` : String(v)}`;
13
+ }).join(";");
14
+ if (css) parts.push(`style="${escapeAttr(css)}"`);
15
+ } else if (typeof value === "boolean") parts.push(key);
16
+ else parts.push(`${key}="${escapeAttr(String(value))}"`);
17
+ }
18
+ return `${parts.length > 0 ? `<${tag} ${parts.join(" ")}>` : `<${tag}>`}${children.map((c) => typeof c === "string" && !c.startsWith("<") ? escapeHtml(c) : c).join("")}</${tag}>`;
19
+ }
20
+ function escapeAttr(s) {
21
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
22
+ }
23
+ function escapeHtml(s) {
24
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
25
+ }
26
+ function TegakiRenderer(props) {
27
+ const [local, divProps] = splitProps(props, [
28
+ "text",
29
+ "font",
30
+ "time",
31
+ "onComplete",
32
+ "effects",
33
+ "segmentSize",
34
+ "timing",
35
+ "showOverlay",
36
+ "ref"
37
+ ]);
38
+ let container;
39
+ let engine = null;
40
+ const engineOptions = createMemo(() => ({
41
+ text: local.text,
42
+ font: local.font,
43
+ time: local.time,
44
+ effects: local.effects,
45
+ segmentSize: local.segmentSize,
46
+ timing: local.timing,
47
+ showOverlay: local.showOverlay,
48
+ onComplete: local.onComplete
49
+ }));
50
+ const innerHTML = TegakiEngine.renderElements(engineOptions(), solidCreateElement);
51
+ onMount(() => {
52
+ engine = new TegakiEngine(container, {
53
+ ...engineOptions(),
54
+ adopt: true
55
+ });
56
+ local.ref?.({
57
+ engine,
58
+ element: container
59
+ });
60
+ });
61
+ onCleanup(() => {
62
+ engine?.destroy();
63
+ engine = null;
64
+ });
65
+ createEffect(on(engineOptions, (options) => {
66
+ engine?.update(options);
67
+ }));
68
+ return /* @__PURE__ */ jsx("div", {
69
+ ref: container,
70
+ ...divProps,
71
+ innerHTML
72
+ });
73
+ }
74
+ //#endregion
75
+ export { TegakiEngine, TegakiRenderer, computeTextLayout, computeTimeline, drawGlyph, ensureFontFace, resolveEffects };
76
+
77
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/solid/TegakiRenderer.tsx"],"sourcesContent":["/** @jsxImportSource solid-js */\nimport { createEffect, createMemo, type JSX, on, onCleanup, onMount, splitProps } from 'solid-js';\nimport { TegakiEngine, type TegakiEngineOptions } from '../core/engine.ts';\nimport type { TegakiEffects } from '../types.ts';\n\nexport interface TegakiRendererProps extends Omit<TegakiEngineOptions, 'effects'> {\n /** Visual effects applied during canvas rendering. */\n effects?: TegakiEffects<Record<string, any>>;\n class?: string;\n ref?: (handle: TegakiRendererHandle) => void;\n [key: string]: any;\n}\n\nexport interface TegakiRendererHandle {\n readonly engine: TegakiEngine | null;\n readonly element: HTMLDivElement | null;\n}\n\nfunction solidCreateElement(tag: string, props: Record<string, any>, ...children: (JSX.Element | string)[]): JSX.Element {\n const parts: string[] = [];\n for (const [key, value] of Object.entries(props)) {\n if (value == null || value === false) continue;\n if (key === 'style' && typeof value === 'object') {\n const css = Object.entries(value)\n .filter(([, v]) => v != null)\n .map(([k, v]) => {\n const prop = k.startsWith('--') ? k : k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);\n const val = typeof v === 'number' && !k.startsWith('--') ? `${v}px` : String(v);\n return `${prop}:${val}`;\n })\n .join(';');\n if (css) parts.push(`style=\"${escapeAttr(css)}\"`);\n } else if (typeof value === 'boolean') {\n parts.push(key);\n } else {\n parts.push(`${key}=\"${escapeAttr(String(value))}\"`);\n }\n }\n const open = parts.length > 0 ? `<${tag} ${parts.join(' ')}>` : `<${tag}>`;\n const content = children.map((c) => (typeof c === 'string' && !c.startsWith('<') ? escapeHtml(c) : (c as string))).join('');\n return `${open}${content}</${tag}>` as unknown as JSX.Element;\n}\n\nfunction escapeAttr(s: string): string {\n return s.replace(/&/g, '&amp;').replace(/\"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n}\n\nexport function TegakiRenderer(props: TegakiRendererProps) {\n const [local, divProps] = splitProps(props, [\n 'text',\n 'font',\n 'time',\n 'onComplete',\n 'effects',\n 'segmentSize',\n 'timing',\n 'showOverlay',\n 'ref',\n ]);\n\n let container!: HTMLDivElement;\n let engine: TegakiEngine | null = null;\n\n const engineOptions = createMemo<TegakiEngineOptions>(() => ({\n text: local.text,\n font: local.font,\n time: local.time,\n effects: local.effects as Record<string, any>,\n segmentSize: local.segmentSize,\n timing: local.timing,\n showOverlay: local.showOverlay,\n onComplete: local.onComplete,\n }));\n\n // Compute initial HTML once — after the engine adopts, all updates go through engine.update().\n const innerHTML = TegakiEngine.renderElements(engineOptions(), solidCreateElement) as unknown as string;\n\n onMount(() => {\n engine = new TegakiEngine(container, { ...engineOptions(), adopt: true });\n local.ref?.({ engine, element: container });\n });\n\n onCleanup(() => {\n engine?.destroy();\n engine = null;\n });\n\n createEffect(\n on(engineOptions, (options) => {\n engine?.update(options);\n }),\n );\n\n return <div ref={container!} {...divProps} innerHTML={innerHTML} />;\n}\n"],"mappings":";;;;;AAkBA,SAAS,mBAAmB,KAAa,OAA4B,GAAG,UAAiD;CACvH,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,EAAE;AAChD,MAAI,SAAS,QAAQ,UAAU,MAAO;AACtC,MAAI,QAAQ,WAAW,OAAO,UAAU,UAAU;GAChD,MAAM,MAAM,OAAO,QAAQ,MAAM,CAC9B,QAAQ,GAAG,OAAO,KAAK,KAAK,CAC5B,KAAK,CAAC,GAAG,OAAO;AAGf,WAAO,GAFM,EAAE,WAAW,KAAK,GAAG,IAAI,EAAE,QAAQ,WAAW,MAAM,IAAI,EAAE,aAAa,GAAG,CAExE,GADH,OAAO,MAAM,YAAY,CAAC,EAAE,WAAW,KAAK,GAAG,GAAG,EAAE,MAAM,OAAO,EAAE;KAE/E,CACD,KAAK,IAAI;AACZ,OAAI,IAAK,OAAM,KAAK,UAAU,WAAW,IAAI,CAAC,GAAG;aACxC,OAAO,UAAU,UAC1B,OAAM,KAAK,IAAI;MAEf,OAAM,KAAK,GAAG,IAAI,IAAI,WAAW,OAAO,MAAM,CAAC,CAAC,GAAG;;AAKvD,QAAO,GAFM,MAAM,SAAS,IAAI,IAAI,IAAI,GAAG,MAAM,KAAK,IAAI,CAAC,KAAK,IAAI,IAAI,KACxD,SAAS,KAAK,MAAO,OAAO,MAAM,YAAY,CAAC,EAAE,WAAW,IAAI,GAAG,WAAW,EAAE,GAAI,EAAc,CAAC,KAAK,GAAG,CAClG,IAAI,IAAI;;AAGnC,SAAS,WAAW,GAAmB;AACrC,QAAO,EAAE,QAAQ,MAAM,QAAQ,CAAC,QAAQ,MAAM,SAAS,CAAC,QAAQ,MAAM,OAAO,CAAC,QAAQ,MAAM,OAAO;;AAGrG,SAAS,WAAW,GAAmB;AACrC,QAAO,EAAE,QAAQ,MAAM,QAAQ,CAAC,QAAQ,MAAM,OAAO,CAAC,QAAQ,MAAM,OAAO;;AAG7E,SAAgB,eAAe,OAA4B;CACzD,MAAM,CAAC,OAAO,YAAY,WAAW,OAAO;EAC1C;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC;CAEF,IAAI;CACJ,IAAI,SAA8B;CAElC,MAAM,gBAAgB,kBAAuC;EAC3D,MAAM,MAAM;EACZ,MAAM,MAAM;EACZ,MAAM,MAAM;EACZ,SAAS,MAAM;EACf,aAAa,MAAM;EACnB,QAAQ,MAAM;EACd,aAAa,MAAM;EACnB,YAAY,MAAM;EACnB,EAAE;CAGH,MAAM,YAAY,aAAa,eAAe,eAAe,EAAE,mBAAmB;AAElF,eAAc;AACZ,WAAS,IAAI,aAAa,WAAW;GAAE,GAAG,eAAe;GAAE,OAAO;GAAM,CAAC;AACzE,QAAM,MAAM;GAAE;GAAQ,SAAS;GAAW,CAAC;GAC3C;AAEF,iBAAgB;AACd,UAAQ,SAAS;AACjB,WAAS;GACT;AAEF,cACE,GAAG,gBAAgB,YAAY;AAC7B,UAAQ,OAAO,QAAQ;GACvB,CACH;AAED,QAAO,oBAAC,OAAD;EAAK,KAAK;EAAY,GAAI;EAAqB;EAAa,CAAA"}
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "tegaki",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
- "description": "React component for rendering animated handwriting from any font",
5
+ "description": "Framework-agnostic animated handwriting renderer with React, Svelte, Vue, SolidJS, and Astro adapters",
6
6
  "keywords": [
7
7
  "handwriting",
8
8
  "animation",
9
9
  "svg",
10
10
  "react",
11
+ "svelte",
12
+ "vue",
13
+ "solid",
14
+ "astro",
11
15
  "font",
12
16
  "calligraphy",
13
17
  "stroke-order",
@@ -36,6 +40,36 @@
36
40
  "types": "./dist/index.d.mts",
37
41
  "import": "./dist/index.mjs"
38
42
  },
43
+ "./core": {
44
+ "tegaki@dev": "./src/core/index.ts",
45
+ "source": "./src/core/index.ts",
46
+ "types": "./dist/core/index.d.mts",
47
+ "import": "./dist/core/index.mjs"
48
+ },
49
+ "./react": {
50
+ "tegaki@dev": "./src/react/index.ts",
51
+ "source": "./src/react/index.ts",
52
+ "types": "./dist/react/index.d.mts",
53
+ "import": "./dist/react/index.mjs"
54
+ },
55
+ "./svelte": {
56
+ "tegaki@dev": "./src/svelte/index.ts",
57
+ "source": "./src/svelte/index.ts",
58
+ "svelte": "./src/svelte/index.ts",
59
+ "default": "./src/svelte/index.ts"
60
+ },
61
+ "./vue": {
62
+ "tegaki@dev": "./src/vue/index.ts",
63
+ "source": "./src/vue/index.ts",
64
+ "default": "./src/vue/index.ts"
65
+ },
66
+ "./solid": {
67
+ "tegaki@dev": "./src/solid/index.ts",
68
+ "source": "./src/solid/index.ts",
69
+ "types": "./dist/solid/index.d.mts",
70
+ "import": "./dist/solid/index.mjs"
71
+ },
72
+ "./astro": "./src/astro/TegakiRenderer.astro",
39
73
  "./fonts/caveat": "./fonts/caveat/bundle.ts",
40
74
  "./fonts/italianno": "./fonts/italianno/bundle.ts",
41
75
  "./fonts/tangerine": "./fonts/tangerine/bundle.ts",
@@ -52,7 +86,7 @@
52
86
  ],
53
87
  "scripts": {
54
88
  "typecheck": "tsc --noEmit",
55
- "build": "tsdown src/index.ts --dts --sourcemap",
89
+ "build": "tsdown src/index.ts src/core/index.ts src/react/index.ts src/solid/index.ts --dts --sourcemap",
56
90
  "prepack": "bun run build && bun ../../scripts/prepareForRelease.ts",
57
91
  "generate-fonts": "bun --filter tegaki-generator start generate Caveat --output ../renderer/fonts/caveat && bun --filter tegaki-generator start generate Italianno --output ../renderer/fonts/italianno && bun --filter tegaki-generator start generate Tangerine --output ../renderer/fonts/tangerine && bun --filter tegaki-generator start generate Parisienne --output ../renderer/fonts/parisienne"
58
92
  },
@@ -60,7 +94,24 @@
60
94
  "@chenglou/pretext": "^0.0.4"
61
95
  },
62
96
  "peerDependencies": {
63
- "react": ">=18"
97
+ "react": ">=18",
98
+ "svelte": ">=5",
99
+ "vue": ">=3",
100
+ "solid-js": ">=1"
101
+ },
102
+ "peerDependenciesMeta": {
103
+ "react": {
104
+ "optional": true
105
+ },
106
+ "svelte": {
107
+ "optional": true
108
+ },
109
+ "vue": {
110
+ "optional": true
111
+ },
112
+ "solid-js": {
113
+ "optional": true
114
+ }
64
115
  },
65
116
  "devDependencies": {
66
117
  "@types/react": "^19.2.14",
@@ -0,0 +1,126 @@
1
+ ---
2
+ import { TegakiEngine, type TegakiEngineOptions } from '../core/index.ts';
3
+ import type { TegakiBundle } from '../types.ts';
4
+
5
+ interface Props extends Omit<TegakiEngineOptions, 'onComplete'> {
6
+ /** When true, registers the font bundle for client-side lookup by name. If no text is provided, renders nothing else. */
7
+ bundle?: boolean;
8
+ /** When true, injects an @font-face rule so the browser starts loading the font immediately. */
9
+ loadFont?: boolean;
10
+ class?: string;
11
+ [key: string]: any;
12
+ }
13
+
14
+ const { text, font, bundle: registerBundle, loadFont, time, effects, timing, segmentSize, showOverlay, class: className, ...attrs } = Astro.props;
15
+
16
+ // Resolve the font bundle for SSR
17
+ let resolvedFont: TegakiBundle | undefined;
18
+ if (typeof font === 'string') {
19
+ resolvedFont = TegakiEngine.getBundle(font);
20
+ } else if (font) {
21
+ resolvedFont = font;
22
+ TegakiEngine.registerBundle(font);
23
+ }
24
+
25
+ const shouldRender = !!text || !(registerBundle || loadFont);
26
+
27
+ const engineOptions: Omit<TegakiEngineOptions, 'onComplete'> = {
28
+ text,
29
+ font: resolvedFont,
30
+ time,
31
+ effects: effects as Record<string, any>,
32
+ timing,
33
+ segmentSize,
34
+ showOverlay,
35
+ };
36
+
37
+ function styleToString(style: Record<string, any>): string {
38
+ return Object.entries(style)
39
+ .filter(([, v]) => v != null)
40
+ .map(([k, v]) => {
41
+ const prop = k.startsWith('--') ? k : k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
42
+ const val = typeof v === 'number' && !k.startsWith('--') ? `${v}px` : String(v);
43
+ return `${prop}:${val}`;
44
+ })
45
+ .join(';');
46
+ }
47
+
48
+ function escapeAttr(s: string): string {
49
+ return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
50
+ }
51
+
52
+ function escapeHtml(s: string): string {
53
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
54
+ }
55
+
56
+ function htmlCreateElement(tag: string, props: Record<string, any>, ...children: string[]): string {
57
+ const parts: string[] = [];
58
+ for (const [key, value] of Object.entries(props)) {
59
+ if (value == null || value === false) continue;
60
+ if (key === 'style' && typeof value === 'object') {
61
+ const css = styleToString(value);
62
+ if (css) parts.push(`style="${escapeAttr(css)}"`);
63
+ } else if (typeof value === 'boolean') {
64
+ parts.push(key);
65
+ } else {
66
+ parts.push(`${key}="${escapeAttr(String(value))}"`);
67
+ }
68
+ }
69
+ const open = parts.length > 0 ? `<${tag} ${parts.join(' ')}>` : `<${tag}>`;
70
+ const content = children.map((c) => (typeof c === 'string' && !c.startsWith('<') ? escapeHtml(c) : c)).join('');
71
+ return `${open}${content}</${tag}>`;
72
+ }
73
+
74
+ const innerHtml = shouldRender ? TegakiEngine.renderElements(engineOptions, htmlCreateElement) : '';
75
+
76
+ // For serialization, replace the full bundle with just the family name.
77
+ const clientOptions = {
78
+ ...engineOptions,
79
+ font: resolvedFont?.family,
80
+ };
81
+ ---
82
+
83
+ {loadFont && resolvedFont?.fontFaceCSS && (
84
+ <style is:inline set:html={resolvedFont.fontFaceCSS} />
85
+ )}
86
+
87
+ {registerBundle && resolvedFont && (
88
+ <script is:inline type="application/json" data-tegaki-bundle set:html={JSON.stringify(resolvedFont)} />
89
+ )}
90
+
91
+ {shouldRender && (
92
+ <div class={className} {...attrs} data-tegaki-options={JSON.stringify(clientOptions)} set:html={innerHtml} />
93
+ )}
94
+
95
+ <script>
96
+ import { TegakiEngine } from 'tegaki/core';
97
+
98
+ function hydrateBundle(el: Element) {
99
+ TegakiEngine.registerBundle(JSON.parse(el.textContent || '{}'));
100
+ }
101
+
102
+ function hydrateContainer(container: HTMLElement) {
103
+ if (container.dataset.tegakiHydrated) return;
104
+ container.dataset.tegakiHydrated = '';
105
+ const options = JSON.parse(container.dataset.tegakiOptions || '{}');
106
+ const engine = new TegakiEngine(container, { adopt: true, ...options });
107
+ container.dispatchEvent(new CustomEvent('tegaki:ready', { detail: { engine }, bubbles: true }));
108
+ }
109
+
110
+ // Hydrate existing elements
111
+ document.querySelectorAll<HTMLScriptElement>('[data-tegaki-bundle]').forEach(hydrateBundle);
112
+ document.querySelectorAll<HTMLElement>('[data-tegaki-options]').forEach(hydrateContainer);
113
+
114
+ // Watch for dynamically added elements (View Transitions, client-side routing, etc.)
115
+ new MutationObserver((mutations) => {
116
+ for (const mutation of mutations) {
117
+ for (const node of mutation.addedNodes) {
118
+ if (!(node instanceof HTMLElement)) continue;
119
+ if (node.matches('[data-tegaki-bundle]')) hydrateBundle(node);
120
+ if (node.matches('[data-tegaki-options]')) hydrateContainer(node);
121
+ node.querySelectorAll<HTMLScriptElement>('[data-tegaki-bundle]').forEach(hydrateBundle);
122
+ node.querySelectorAll<HTMLElement>('[data-tegaki-options]').forEach(hydrateContainer);
123
+ }
124
+ }
125
+ }).observe(document.body, { childList: true, subtree: true });
126
+ </script>