@edwinvakayil/calligraphy 1.2.5 → 1.3.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.
package/dist/index.js CHANGED
@@ -131,28 +131,55 @@ function preloadFonts(families) {
131
131
 
132
132
  const STYLE_ID = "rts-hero-animations";
133
133
  const CSS = `
134
- @keyframes rts-rise{from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:translateY(0)}}
135
- @keyframes rts-clip{from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0% 0 0)}}
136
- @keyframes rts-pop{0%{opacity:0;transform:scale(0.75)}60%{opacity:1;transform:scale(1.04)}100%{transform:scale(1)}}
137
- @keyframes rts-blur{from{opacity:0;filter:blur(14px);transform:scale(1.04)}to{opacity:1;filter:blur(0);transform:scale(1)}}
138
- @keyframes rts-flip{from{opacity:0;transform:perspective(600px) rotateX(30deg) translateY(20px)}to{opacity:1;transform:perspective(600px) rotateX(0) translateY(0)}}
139
- @keyframes rts-swipe{from{opacity:0;transform:translateX(60px)}to{opacity:1;transform:translateX(0)}}
140
- @keyframes rts-bounce{0%{opacity:0;transform:translateY(-60px)}60%{opacity:1;transform:translateY(10px)}80%{transform:translateY(-5px)}100%{transform:translateY(0)}}
141
- @keyframes rts-type{from{width:0}to{width:100%}}
142
- @keyframes rts-blink{50%{border-color:transparent}}
143
- @keyframes rts-word-rise{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}}
144
- @keyframes rts-letter-in{from{opacity:0;transform:translateX(-16px) rotate(-4deg)}to{opacity:1;transform:none}}
134
+ @keyframes rts-rise { from{opacity:0;transform:translateY(32px)} to{opacity:1;transform:translateY(0)} }
135
+ @keyframes rts-clip { from{clip-path:inset(0 100% 0 0)} to{clip-path:inset(0 0% 0 0)} }
136
+ @keyframes rts-pop { 0%{opacity:0;transform:scale(0.75)} 60%{opacity:1;transform:scale(1.04)} 100%{transform:scale(1)} }
137
+ @keyframes rts-blur { from{opacity:0;filter:blur(14px);transform:scale(1.04)} to{opacity:1;filter:blur(0);transform:scale(1)} }
138
+ @keyframes rts-flip { from{opacity:0;transform:perspective(600px) rotateX(30deg) translateY(20px)} to{opacity:1;transform:perspective(600px) rotateX(0) translateY(0)} }
139
+ @keyframes rts-swipe { from{opacity:0;transform:translateX(60px)} to{opacity:1;transform:translateX(0)} }
140
+ @keyframes rts-bounce { 0%{opacity:0;transform:translateY(-60px)} 60%{opacity:1;transform:translateY(10px)} 80%{transform:translateY(-5px)} 100%{transform:translateY(0)} }
141
+ @keyframes rts-type { from{width:0} to{width:100%} }
142
+ @keyframes rts-blink { 50%{border-color:transparent} }
143
+ @keyframes rts-word-rise { from{opacity:0;transform:translateY(24px)} to{opacity:1;transform:translateY(0)} }
144
+ @keyframes rts-letter-in { from{opacity:0;transform:translateX(-16px) rotate(-4deg)} to{opacity:1;transform:none} }
145
145
 
146
- .rts-rise { animation: rts-rise 0.9s cubic-bezier(0.16,1,0.3,1) both }
147
- .rts-clip { animation: rts-clip 1.1s cubic-bezier(0.77,0,0.18,1) both }
148
- .rts-pop { animation: rts-pop 0.7s cubic-bezier(0.34,1.56,0.64,1) both }
149
- .rts-blur { animation: rts-blur 1s cubic-bezier(0.16,1,0.3,1) both }
150
- .rts-flip { animation: rts-flip 0.9s cubic-bezier(0.16,1,0.3,1) both; transform-origin: center bottom }
151
- .rts-swipe { animation: rts-swipe 0.8s cubic-bezier(0.16,1,0.3,1) both }
152
- .rts-bounce { animation: rts-bounce 0.9s cubic-bezier(0.36,0.07,0.19,0.97) both }
153
- .rts-typewriter{ overflow: hidden; white-space: nowrap; border-right: 2px solid currentColor; width: 0; animation: rts-type 1.6s steps(22,end) both, rts-blink 0.7s step-end 1.6s 3 }
154
- .rts-word { display: inline-block; opacity: 0; transform: translateY(24px); animation: rts-word-rise 0.7s cubic-bezier(0.16,1,0.3,1) both }
155
- .rts-letter { display: inline-block; opacity: 0; transform: translateX(-16px) rotate(-4deg); animation: rts-letter-in 0.5s cubic-bezier(0.16,1,0.3,1) both }
146
+ /* ── New modern animations ─────────────────────────────────────────────── */
147
+
148
+ @keyframes rts-velvet { from{opacity:0;transform:translate(-12px,20px) skewX(4deg)} to{opacity:1;transform:translate(0,0) skewX(0deg)} }
149
+ @keyframes rts-curtain { from{clip-path:inset(0 0 100% 0)} to{clip-path:inset(0 0 0% 0)} }
150
+ @keyframes rts-morph { 0%{opacity:0;transform:scaleY(0.3) scaleX(1.3) translateY(10px)} 60%{opacity:1;transform:scaleY(1.08) scaleX(0.97)} 100%{transform:scaleY(1) scaleX(1)} }
151
+ @keyframes rts-ground { from{transform:translateY(110%);opacity:0} to{transform:translateY(0);opacity:1} }
152
+ @keyframes rts-cascade { from{opacity:0;transform:translateY(-28px) translateX(10px) rotate(8deg)} to{opacity:1;transform:none} }
153
+ @keyframes rts-spotlight { 0%{opacity:0;letter-spacing:0.3em;transform:scaleX(1.15)} 100%{opacity:1;letter-spacing:-0.03em;transform:scaleX(1)} }
154
+ @keyframes rts-ink { 0%{opacity:0;transform:translateY(6px) scale(0.96)} 100%{opacity:1;transform:translateY(0) scale(1)} }
155
+ @keyframes rts-hinge { from{opacity:0;transform:perspective(400px) rotateY(-40deg) translateX(-20px)} to{opacity:1;transform:perspective(400px) rotateY(0) translateX(0)} }
156
+ @keyframes rts-stretch { 0%{opacity:0;transform:scaleX(0.05)} 60%{transform:scaleX(1.04)} 100%{opacity:1;transform:scaleX(1)} }
157
+ @keyframes rts-peel { from{clip-path:inset(100% 0 0 0)} to{clip-path:inset(0% 0 0 0)} }
158
+
159
+ /* ── Whole-element classes (no splitting needed) ───────────────────────── */
160
+ .rts-rise { animation: rts-rise 0.9s cubic-bezier(0.16,1,0.3,1) both }
161
+ .rts-clip { animation: rts-clip 1.1s cubic-bezier(0.77,0,0.18,1) both }
162
+ .rts-pop { animation: rts-pop 0.7s cubic-bezier(0.34,1.56,0.64,1) both }
163
+ .rts-blur { animation: rts-blur 1s cubic-bezier(0.16,1,0.3,1) both }
164
+ .rts-flip { animation: rts-flip 0.9s cubic-bezier(0.16,1,0.3,1) both; transform-origin: center bottom }
165
+ .rts-swipe { animation: rts-swipe 0.8s cubic-bezier(0.16,1,0.3,1) both }
166
+ .rts-bounce { animation: rts-bounce 0.9s cubic-bezier(0.36,0.07,0.19,0.97) both }
167
+ .rts-typewriter { overflow: hidden; white-space: nowrap; border-right: 2px solid currentColor; width: 0; animation: rts-type 1.6s steps(22,end) both, rts-blink 0.7s step-end 1.6s 3 }
168
+ .rts-morph { animation: rts-morph 0.8s cubic-bezier(0.34,1.56,0.64,1) both }
169
+ .rts-spotlight { animation: rts-spotlight 1s cubic-bezier(0.16,1,0.3,1) both }
170
+ .rts-stretch { animation: rts-stretch 0.9s cubic-bezier(0.34,1.56,0.64,1) both }
171
+
172
+ /* ── Per-word / per-character span classes ─────────────────────────────── */
173
+ .rts-word { display:inline-block;opacity:0;transform:translateY(24px);animation:rts-word-rise 0.7s cubic-bezier(0.16,1,0.3,1) both }
174
+ .rts-letter { display:inline-block;opacity:0;transform:translateX(-16px) rotate(-4deg);animation:rts-letter-in 0.5s cubic-bezier(0.16,1,0.3,1) both }
175
+ .rts-velvet-word { display:inline-block;opacity:0;animation:rts-velvet 0.65s cubic-bezier(0.16,1,0.3,1) both }
176
+ .rts-curtain-word { display:inline-block;overflow:hidden;animation:rts-curtain 0.7s cubic-bezier(0.77,0,0.18,1) both }
177
+ .rts-ground-wrap { display:inline-block;overflow:hidden;vertical-align:bottom }
178
+ .rts-ground-inner { display:inline-block;animation:rts-ground 0.65s cubic-bezier(0.16,1,0.3,1) both }
179
+ .rts-cascade-ch { display:inline-block;opacity:0;animation:rts-cascade 0.45s cubic-bezier(0.34,1.56,0.64,1) both }
180
+ .rts-ink-word { display:inline-block;opacity:0;animation:rts-ink 0.9s cubic-bezier(0.16,1,0.3,1) both }
181
+ .rts-hinge-word { display:inline-block;opacity:0;transform-origin:left center;animation:rts-hinge 0.6s cubic-bezier(0.16,1,0.3,1) both }
182
+ .rts-peel-word { display:inline-block;overflow:hidden;animation:rts-peel 0.6s cubic-bezier(0.77,0,0.18,1) both }
156
183
  `;
157
184
  function injectAnimationStyles() {
158
185
  if (typeof document === "undefined")
@@ -164,52 +191,47 @@ function injectAnimationStyles() {
164
191
  style.textContent = CSS;
165
192
  document.head.appendChild(style);
166
193
  }
194
+ // ─── Whole-element class map ──────────────────────────────────────────────────
195
+ const WHOLE_CLASS_MAP = {
196
+ rise: "rts-rise",
197
+ clip: "rts-clip",
198
+ pop: "rts-pop",
199
+ blur: "rts-blur",
200
+ flip: "rts-flip",
201
+ swipe: "rts-swipe",
202
+ bounce: "rts-bounce",
203
+ typewriter: "rts-typewriter",
204
+ morph: "rts-morph",
205
+ spotlight: "rts-spotlight",
206
+ stretch: "rts-stretch",
207
+ };
208
+ /** Returns the CSS class for whole-element animations, or "" for split ones. */
167
209
  function getAnimationClass(animation) {
168
210
  var _a;
169
- const map = {
170
- rise: "rts-rise",
171
- clip: "rts-clip",
172
- pop: "rts-pop",
173
- blur: "rts-blur",
174
- flip: "rts-flip",
175
- swipe: "rts-swipe",
176
- bounce: "rts-bounce",
177
- typewriter: "rts-typewriter",
178
- stagger: "",
179
- letters: "",
180
- };
181
- return (_a = map[animation]) !== null && _a !== void 0 ? _a : "";
211
+ return (_a = WHOLE_CLASS_MAP[animation]) !== null && _a !== void 0 ? _a : "";
182
212
  }
183
- /**
184
- * Wraps each word in an animated span.
185
- * <em> tokens are preserved as-is in the HTML — Typography's useEffect
186
- * will apply inline styles to them after mount.
187
- */
188
- function buildStaggerHTML(html) {
213
+ /** True if the animation needs the HTML to be split into word/char spans. */
214
+ function isSplitAnimation(animation) {
215
+ return !(animation in WHOLE_CLASS_MAP);
216
+ }
217
+ // ─── HTML builders for split animations ──────────────────────────────────────
218
+ function wrapWords(html, cls, delayStep) {
189
219
  var _a;
190
220
  const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
191
221
  return tokens
192
222
  .map((tok, i) => {
193
- const delay = (i * 0.07).toFixed(2);
223
+ const delay = (i * delayStep).toFixed(2);
194
224
  if (tok.startsWith("<em>")) {
195
- // Wrap the inner text in the animated span, keep <em> outside
196
- const inner = tok.slice(4, -5);
197
- return `<em><span class="rts-word" style="animation-delay:${delay}s">${inner}</span></em>`;
225
+ return `<em><span class="${cls}" style="animation-delay:${delay}s">${tok.slice(4, -5)}</span></em>`;
198
226
  }
199
- return `<span class="rts-word" style="animation-delay:${delay}s">${tok}</span>`;
227
+ return `<span class="${cls}" style="animation-delay:${delay}s">${tok}</span>`;
200
228
  })
201
229
  .join(" ");
202
230
  }
203
- /**
204
- * Wraps each character in an animated span.
205
- * <em> tags are preserved in the output — Typography's useEffect applies
206
- * the actual italic/non-italic inline styles after the DOM is ready.
207
- */
208
- function buildLettersHTML(html) {
231
+ function wrapChars(html, cls, delayStep) {
209
232
  const result = [];
210
233
  let inEm = false;
211
234
  let delay = 0;
212
- const step = 0.04;
213
235
  let i = 0;
214
236
  while (i < html.length) {
215
237
  if (html.startsWith("<em>", i)) {
@@ -228,14 +250,76 @@ function buildLettersHTML(html) {
228
250
  i++;
229
251
  continue;
230
252
  }
231
- const span = `<span class="rts-letter" style="animation-delay:${delay.toFixed(2)}s">${ch}</span>`;
232
- // Preserve <em> wrapper in DOM — styles applied by useEffect
253
+ const span = `<span class="${cls}" style="animation-delay:${delay.toFixed(2)}s">${ch}</span>`;
233
254
  result.push(inEm ? `<em>${span}</em>` : span);
234
- delay += step;
255
+ delay += delayStep;
235
256
  i++;
236
257
  }
237
258
  return result.join("");
238
259
  }
260
+ function wrapGround(html, delayStep) {
261
+ var _a;
262
+ const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
263
+ return tokens
264
+ .map((tok, i) => {
265
+ const delay = (i * delayStep).toFixed(2);
266
+ const inner = tok.startsWith("<em>")
267
+ ? `<em>${tok.slice(4, -5)}</em>`
268
+ : tok;
269
+ return `<span class="rts-ground-wrap"><span class="rts-ground-inner" style="animation-delay:${delay}s">${inner}</span></span>`;
270
+ })
271
+ .join(" ");
272
+ }
273
+ function buildSplitHTML(animation, html) {
274
+ switch (animation) {
275
+ case "stagger": return wrapWords(html, "rts-word", 0.07);
276
+ case "letters": return wrapChars(html, "rts-letter", 0.04);
277
+ case "velvet": return wrapWords(html, "rts-velvet-word", 0.08);
278
+ case "curtain": return wrapWords(html, "rts-curtain-word", 0.10);
279
+ case "ground": return wrapGround(html, 0.09);
280
+ case "cascade": return wrapChars(html, "rts-cascade-ch", 0.05);
281
+ case "ink": return wrapWords(html, "rts-ink-word", 0.10);
282
+ case "hinge": return wrapWords(html, "rts-hinge-word", 0.09);
283
+ case "peel": return wrapWords(html, "rts-peel-word", 0.10);
284
+ default: return html;
285
+ }
286
+ }
287
+
288
+ // ─── Defaults ─────────────────────────────────────────────────────────────────
289
+ const DEFAULT_THEME = {
290
+ font: "",
291
+ accentColor: "#c8b89a",
292
+ italic: false,
293
+ animation: "rise",
294
+ color: "",
295
+ };
296
+ // ─── Context ──────────────────────────────────────────────────────────────────
297
+ const TypographyContext = react.createContext(DEFAULT_THEME);
298
+ const TypographyProvider = ({ theme, children, }) => {
299
+ const resolved = react.useMemo(() => {
300
+ var _a, _b, _c, _d, _e;
301
+ return ({
302
+ font: (_a = theme.font) !== null && _a !== void 0 ? _a : DEFAULT_THEME.font,
303
+ accentColor: (_b = theme.accentColor) !== null && _b !== void 0 ? _b : DEFAULT_THEME.accentColor,
304
+ italic: (_c = theme.italic) !== null && _c !== void 0 ? _c : DEFAULT_THEME.italic,
305
+ animation: (_d = theme.animation) !== null && _d !== void 0 ? _d : DEFAULT_THEME.animation,
306
+ color: (_e = theme.color) !== null && _e !== void 0 ? _e : DEFAULT_THEME.color,
307
+ });
308
+ }, [theme.font, theme.accentColor, theme.italic, theme.animation, theme.color]);
309
+ // Pre-load the theme font as soon as the provider mounts
310
+ if (resolved.font && GOOGLE_FONTS.includes(resolved.font)) {
311
+ injectFont(buildFontUrl(resolved.font));
312
+ }
313
+ return (jsxRuntime.jsx(TypographyContext.Provider, { value: resolved, children: children }));
314
+ };
315
+ // ─── Hook ─────────────────────────────────────────────────────────────────────
316
+ /**
317
+ * Returns the resolved theme from the nearest TypographyProvider.
318
+ * Falls back to DEFAULT_THEME if used outside a provider.
319
+ */
320
+ function useTypographyTheme() {
321
+ return react.useContext(TypographyContext);
322
+ }
239
323
 
240
324
  // ─── Static maps ─────────────────────────────────────────────────────────────
241
325
  const variantTagMap = {
@@ -330,10 +414,6 @@ const variantStyleMap = {
330
414
  // ─── Constants ───────────────────────────────────────────────────────────────
331
415
  const INSTRUMENT_SERIF_URL = "https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&display=swap";
332
416
  // ─── Helpers ─────────────────────────────────────────────────────────────────
333
- /**
334
- * Serialise React children → raw HTML string.
335
- * Preserves <em>text</em> nodes for animation builders.
336
- */
337
417
  function childrenToHTML(children) {
338
418
  var _a, _b;
339
419
  return ((_b = (_a = react.Children.map(children, (child) => {
@@ -347,11 +427,6 @@ function childrenToHTML(children) {
347
427
  return "";
348
428
  })) === null || _a === void 0 ? void 0 : _a.join("")) !== null && _b !== void 0 ? _b : "");
349
429
  }
350
- /**
351
- * Re-map React children so that <em> elements get explicit inline styles.
352
- * This is used for the no-animation render path where we keep real React nodes.
353
- * Inline styles on the <em> itself beat any inherited font-family from the parent.
354
- */
355
430
  function renderChildrenWithEmStyles(children, italic, accentColor, headingFont) {
356
431
  const italicStyle = {
357
432
  fontFamily: "'Instrument Serif', serif",
@@ -372,13 +447,8 @@ function renderChildrenWithEmStyles(children, italic, accentColor, headingFont)
372
447
  return child;
373
448
  });
374
449
  }
375
- /**
376
- * After dangerouslySetInnerHTML renders, walk the DOM and apply inline styles
377
- * to every <em> and <em> > span so the font switch is guaranteed.
378
- */
379
450
  function applyEmStylesDOM(container, italic, accentColor, headingFont) {
380
- // Select both <em> and any animated letter spans nested inside <em>
381
- container.querySelectorAll("em").forEach((el) => {
451
+ const apply = (el) => {
382
452
  if (italic) {
383
453
  el.style.fontFamily = "'Instrument Serif', serif";
384
454
  el.style.fontStyle = "italic";
@@ -391,59 +461,68 @@ function applyEmStylesDOM(container, italic, accentColor, headingFont) {
391
461
  el.style.fontWeight = "inherit";
392
462
  el.style.color = "inherit";
393
463
  }
394
- });
395
- // Also style the animated letter spans inside <em> (used by "letters" animation)
396
- container.querySelectorAll("em > span").forEach((el) => {
397
- if (italic) {
398
- el.style.fontFamily = "'Instrument Serif', serif";
399
- el.style.fontStyle = "italic";
400
- el.style.fontWeight = "400";
401
- el.style.color = accentColor;
402
- }
403
- else {
404
- el.style.fontFamily = headingFont ? `'${headingFont}', sans-serif` : "inherit";
405
- el.style.fontStyle = "normal";
406
- el.style.fontWeight = "inherit";
407
- el.style.color = "inherit";
408
- }
409
- });
464
+ };
465
+ container.querySelectorAll("em").forEach(apply);
466
+ container.querySelectorAll("em > span").forEach(apply);
410
467
  }
411
468
  // ─── Component ───────────────────────────────────────────────────────────────
412
469
  const Typography = (_a) => {
413
- var { variant = "Body", font, color, align, className, style, children, as, truncate, maxLines, animation, italic = false, accentColor = "#c8b89a" } = _a, rest = __rest(_a, ["variant", "font", "color", "align", "className", "style", "children", "as", "truncate", "maxLines", "animation", "italic", "accentColor"]);
470
+ var _b;
471
+ var { variant = "Body", font: fontProp, color: colorProp, animation: animationProp, italic: italicProp, accentColor: accentColorProp, align, className, style, children, as, truncate, maxLines } = _a, rest = __rest(_a, ["variant", "font", "color", "animation", "italic", "accentColor", "align", "className", "style", "children", "as", "truncate", "maxLines"]);
472
+ const theme = useTypographyTheme();
414
473
  const isHero = variant === "Display" || variant === "H1";
415
474
  const ref = react.useRef(null);
416
- // Always inject Instrument Serif for hero variants so it's pre-loaded
417
- // and ready the moment italic is toggled on no flash of wrong font.
418
- if (isHero) {
419
- injectFont(INSTRUMENT_SERIF_URL);
420
- }
421
- // Inject heading Google Font
422
- if (font && GOOGLE_FONTS.includes(font)) {
423
- injectFont(buildFontUrl(font));
424
- }
425
- // Inject animation keyframes (once, global)
426
- if (animation && isHero) {
427
- injectAnimationStyles();
428
- }
429
- // For animation paths (dangerouslySetInnerHTML), walk the DOM after render
430
- // and stamp inline styles onto every <em> — guaranteed to beat inheritance.
475
+ // Prop wins; fall back to theme; fall back to built-in default
476
+ const font = fontProp !== null && fontProp !== void 0 ? fontProp : (theme.font || undefined);
477
+ const color = colorProp !== null && colorProp !== void 0 ? colorProp : (theme.color || undefined);
478
+ const animation = isHero ? ((_b = animationProp !== null && animationProp !== void 0 ? animationProp : theme.animation) !== null && _b !== void 0 ? _b : undefined) : undefined;
479
+ const italic = italicProp !== null && italicProp !== void 0 ? italicProp : theme.italic;
480
+ const accentColor = accentColorProp !== null && accentColorProp !== void 0 ? accentColorProp : theme.accentColor;
481
+ // ── useInsertionEffect: inject <link> and <style> tags ────────────────────
482
+ //
483
+ // WHY useInsertionEffect instead of plain render-phase calls:
484
+ //
485
+ // 1. Server safety — useInsertionEffect (like all effects) is never called
486
+ // on the server, so document.createElement / document.head never run
487
+ // during SSR. The isBrowser guard in ssr.ts is a belt-and-suspenders
488
+ // backup, but the effect boundary is the real guarantee.
489
+ //
490
+ // 2. Correctness — React 18 concurrent mode can call the render function
491
+ // multiple times before committing. Doing DOM work in render can fire
492
+ // those side-effects redundantly or out of order. useInsertionEffect
493
+ // fires synchronously before the browser paints, once per commit.
494
+ //
495
+ // 3. No FOUC — because it fires before paint (earlier than useLayoutEffect),
496
+ // the <style> tag is in the DOM before any text is visible, so there is
497
+ // no flash of unstyled / wrong-font text.
498
+ react.useInsertionEffect(() => {
499
+ // Instrument Serif — always pre-load for hero so toggling italic is instant
500
+ if (isHero) {
501
+ injectFont(INSTRUMENT_SERIF_URL);
502
+ }
503
+ // Heading Google Font
504
+ if (font && GOOGLE_FONTS.includes(font)) {
505
+ injectFont(buildFontUrl(font));
506
+ }
507
+ // Animation keyframe stylesheet
508
+ if (animation && isHero) {
509
+ injectAnimationStyles();
510
+ }
511
+ }, [isHero, font, animation]);
512
+ // ── useEffect: re-stamp inline styles on <em> after DOM updates ───────────
431
513
  react.useEffect(() => {
432
514
  if (!isHero || !animation || !ref.current)
433
515
  return;
434
516
  applyEmStylesDOM(ref.current, italic, accentColor, font);
435
517
  }, [italic, accentColor, font, animation, isHero]);
436
518
  const Tag = (as !== null && as !== void 0 ? as : variantTagMap[variant]);
437
- // ── Compute animation class + inner HTML ──────────────────────────────────
519
+ // ── Animation path: build inner HTML ─────────────────────────────────────
438
520
  let animClass = "";
439
521
  let heroHTML = null;
440
522
  if (animation && isHero) {
441
523
  const rawHTML = childrenToHTML(children);
442
- if (animation === "stagger") {
443
- heroHTML = buildStaggerHTML(rawHTML);
444
- }
445
- else if (animation === "letters") {
446
- heroHTML = buildLettersHTML(rawHTML);
524
+ if (isSplitAnimation(animation)) {
525
+ heroHTML = buildSplitHTML(animation, rawHTML);
447
526
  }
448
527
  else {
449
528
  heroHTML = rawHTML;
@@ -465,7 +544,7 @@ const Typography = (_a) => {
465
544
  if (heroHTML !== null) {
466
545
  return (jsxRuntime.jsx(Tag, Object.assign({ ref: ref, className: [animClass, className].filter(Boolean).join(" "), style: computedStyle, dangerouslySetInnerHTML: { __html: heroHTML } }, rest), animation));
467
546
  }
468
- // ── Render: standard path (real React children with em styles) ────────────
547
+ // ── Render: standard path ────────────────────────────────────────────────
469
548
  const processedChildren = isHero
470
549
  ? renderChildrenWithEmStyles(children, italic, accentColor, font)
471
550
  : children;
@@ -474,6 +553,7 @@ const Typography = (_a) => {
474
553
 
475
554
  exports.GOOGLE_FONTS = GOOGLE_FONTS;
476
555
  exports.Typography = Typography;
556
+ exports.TypographyProvider = TypographyProvider;
477
557
  exports.buildFontUrl = buildFontUrl;
478
558
  exports.default = Typography;
479
559
  exports.injectFont = injectFont;