@edwinvakayil/calligraphy 1.2.6 → 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.
@@ -1,15 +1,9 @@
1
1
  import { HeroAnimation } from "./types";
2
2
  export declare function injectAnimationStyles(): void;
3
+ /** Returns the CSS class for whole-element animations, or "" for split ones. */
3
4
  export declare function getAnimationClass(animation: HeroAnimation): string;
4
- /**
5
- * Wraps each word in an animated span.
6
- * <em> tokens are preserved as-is in the HTML — Typography's useEffect
7
- * will apply inline styles to them after mount.
8
- */
9
- export declare function buildStaggerHTML(html: string): string;
10
- /**
11
- * Wraps each character in an animated span.
12
- * <em> tags are preserved in the output — Typography's useEffect applies
13
- * the actual italic/non-italic inline styles after the DOM is ready.
14
- */
15
- export declare function buildLettersHTML(html: string): string;
5
+ /** True if the animation needs the HTML to be split into word/char spans. */
6
+ export declare function isSplitAnimation(animation: HeroAnimation): boolean;
7
+ export declare function buildSplitHTML(animation: HeroAnimation, html: string): string;
8
+ export declare const buildStaggerHTML: (html: string) => string;
9
+ export declare const buildLettersHTML: (html: string) => string;
package/dist/index.d.ts CHANGED
@@ -1,11 +1,30 @@
1
1
  import React, { HTMLAttributes, ElementType, CSSProperties } from 'react';
2
2
 
3
+ interface TypographyTheme {
4
+ /** Default Google Font for all Typography components */
5
+ font?: string;
6
+ /** Default accent color for <em> italic spans in Display / H1 */
7
+ accentColor?: string;
8
+ /** Default italic setting for Display / H1 heroes */
9
+ italic?: boolean;
10
+ /** Default entrance animation for Display / H1 heroes */
11
+ animation?: HeroAnimation;
12
+ /** Default text color applied to all variants */
13
+ color?: string;
14
+ }
15
+ interface TypographyProviderProps {
16
+ theme: TypographyTheme;
17
+ children: React.ReactNode;
18
+ }
19
+ declare const TypographyProvider: React.FC<TypographyProviderProps>;
20
+
3
21
  type TypographyVariant = "Display" | "H1" | "H2" | "H3" | "H4" | "H5" | "H6" | "Subheading" | "Overline" | "Body" | "Label" | "Caption";
4
22
  type TextAlign = "left" | "center" | "right" | "justify";
5
23
  /**
6
24
  * Built-in hero text entrance animations.
7
25
  * Applied via CSS keyframes — GPU-composited, 60fps safe.
8
26
  *
27
+ * — Original —
9
28
  * rise — smooth upward fade-in (universal default)
10
29
  * stagger — each word rises in sequence
11
30
  * clip — text unmasked left-to-right (editorial)
@@ -16,8 +35,20 @@ type TextAlign = "left" | "center" | "right" | "justify";
16
35
  * swipe — slides in from the right
17
36
  * typewriter — character-by-character reveal
18
37
  * bounce — drops from above with a soft bounce
38
+ *
39
+ * — Modern —
40
+ * velvet — words drift in with a soft skew (buttery & modern)
41
+ * curtain — each word clips upward like a rising curtain
42
+ * morph — squash-and-stretch spring (expressive & bold)
43
+ * ground — words emerge from behind the baseline (editorial)
44
+ * cascade — diagonal character waterfall (dynamic & layered)
45
+ * spotlight — expands from compressed letterspace (cinematic)
46
+ * ink — words fade in with a gentle scale (calm & precise)
47
+ * hinge — words rotate in from their left edge (mechanical)
48
+ * stretch — horizontal rubber-band expand (playful & punchy)
49
+ * peel — bottom-to-top clip reveal per word (sharp)
19
50
  */
20
- type HeroAnimation = "rise" | "stagger" | "clip" | "pop" | "letters" | "blur" | "flip" | "swipe" | "typewriter" | "bounce";
51
+ type HeroAnimation = "rise" | "stagger" | "clip" | "pop" | "letters" | "blur" | "flip" | "swipe" | "typewriter" | "bounce" | "velvet" | "curtain" | "morph" | "ground" | "cascade" | "spotlight" | "ink" | "hinge" | "stretch" | "peel";
21
52
  interface TypographyProps extends HTMLAttributes<HTMLElement> {
22
53
  /** Typography scale variant */
23
54
  variant?: TypographyVariant;
@@ -58,24 +89,6 @@ interface TypographyProps extends HTMLAttributes<HTMLElement> {
58
89
 
59
90
  declare const Typography: React.FC<TypographyProps>;
60
91
 
61
- interface TypographyTheme {
62
- /** Default Google Font for all Typography components */
63
- font?: string;
64
- /** Default accent color for <em> italic spans in Display / H1 */
65
- accentColor?: string;
66
- /** Default italic setting for Display / H1 heroes */
67
- italic?: boolean;
68
- /** Default entrance animation for Display / H1 heroes */
69
- animation?: HeroAnimation;
70
- /** Default text color applied to all variants */
71
- color?: string;
72
- }
73
- interface TypographyProviderProps {
74
- theme: TypographyTheme;
75
- children: React.ReactNode;
76
- }
77
- declare const TypographyProvider: React.FC<TypographyProviderProps>;
78
-
79
92
  /**
80
93
  * A curated list of popular Google Fonts.
81
94
  * Pass any valid Google Font name to the `font` prop — if it's in this list,
package/dist/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import { createContext, useMemo, useContext, useRef, useEffect, Children, isValidElement } from 'react';
2
+ import { createContext, useMemo, useContext, useRef, useInsertionEffect, useEffect, Children, isValidElement } from 'react';
3
3
 
4
4
  /******************************************************************************
5
5
  Copyright (c) Microsoft Corporation.
@@ -127,28 +127,55 @@ function preloadFonts(families) {
127
127
 
128
128
  const STYLE_ID = "rts-hero-animations";
129
129
  const CSS = `
130
- @keyframes rts-rise{from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:translateY(0)}}
131
- @keyframes rts-clip{from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0% 0 0)}}
132
- @keyframes rts-pop{0%{opacity:0;transform:scale(0.75)}60%{opacity:1;transform:scale(1.04)}100%{transform:scale(1)}}
133
- @keyframes rts-blur{from{opacity:0;filter:blur(14px);transform:scale(1.04)}to{opacity:1;filter:blur(0);transform:scale(1)}}
134
- @keyframes rts-flip{from{opacity:0;transform:perspective(600px) rotateX(30deg) translateY(20px)}to{opacity:1;transform:perspective(600px) rotateX(0) translateY(0)}}
135
- @keyframes rts-swipe{from{opacity:0;transform:translateX(60px)}to{opacity:1;transform:translateX(0)}}
136
- @keyframes rts-bounce{0%{opacity:0;transform:translateY(-60px)}60%{opacity:1;transform:translateY(10px)}80%{transform:translateY(-5px)}100%{transform:translateY(0)}}
137
- @keyframes rts-type{from{width:0}to{width:100%}}
138
- @keyframes rts-blink{50%{border-color:transparent}}
139
- @keyframes rts-word-rise{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}}
140
- @keyframes rts-letter-in{from{opacity:0;transform:translateX(-16px) rotate(-4deg)}to{opacity:1;transform:none}}
130
+ @keyframes rts-rise { from{opacity:0;transform:translateY(32px)} to{opacity:1;transform:translateY(0)} }
131
+ @keyframes rts-clip { from{clip-path:inset(0 100% 0 0)} to{clip-path:inset(0 0% 0 0)} }
132
+ @keyframes rts-pop { 0%{opacity:0;transform:scale(0.75)} 60%{opacity:1;transform:scale(1.04)} 100%{transform:scale(1)} }
133
+ @keyframes rts-blur { from{opacity:0;filter:blur(14px);transform:scale(1.04)} to{opacity:1;filter:blur(0);transform:scale(1)} }
134
+ @keyframes rts-flip { from{opacity:0;transform:perspective(600px) rotateX(30deg) translateY(20px)} to{opacity:1;transform:perspective(600px) rotateX(0) translateY(0)} }
135
+ @keyframes rts-swipe { from{opacity:0;transform:translateX(60px)} to{opacity:1;transform:translateX(0)} }
136
+ @keyframes rts-bounce { 0%{opacity:0;transform:translateY(-60px)} 60%{opacity:1;transform:translateY(10px)} 80%{transform:translateY(-5px)} 100%{transform:translateY(0)} }
137
+ @keyframes rts-type { from{width:0} to{width:100%} }
138
+ @keyframes rts-blink { 50%{border-color:transparent} }
139
+ @keyframes rts-word-rise { from{opacity:0;transform:translateY(24px)} to{opacity:1;transform:translateY(0)} }
140
+ @keyframes rts-letter-in { from{opacity:0;transform:translateX(-16px) rotate(-4deg)} to{opacity:1;transform:none} }
141
141
 
142
- .rts-rise { animation: rts-rise 0.9s cubic-bezier(0.16,1,0.3,1) both }
143
- .rts-clip { animation: rts-clip 1.1s cubic-bezier(0.77,0,0.18,1) both }
144
- .rts-pop { animation: rts-pop 0.7s cubic-bezier(0.34,1.56,0.64,1) both }
145
- .rts-blur { animation: rts-blur 1s cubic-bezier(0.16,1,0.3,1) both }
146
- .rts-flip { animation: rts-flip 0.9s cubic-bezier(0.16,1,0.3,1) both; transform-origin: center bottom }
147
- .rts-swipe { animation: rts-swipe 0.8s cubic-bezier(0.16,1,0.3,1) both }
148
- .rts-bounce { animation: rts-bounce 0.9s cubic-bezier(0.36,0.07,0.19,0.97) both }
149
- .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 }
150
- .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 }
151
- .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 }
142
+ /* ── New modern animations ─────────────────────────────────────────────── */
143
+
144
+ @keyframes rts-velvet { from{opacity:0;transform:translate(-12px,20px) skewX(4deg)} to{opacity:1;transform:translate(0,0) skewX(0deg)} }
145
+ @keyframes rts-curtain { from{clip-path:inset(0 0 100% 0)} to{clip-path:inset(0 0 0% 0)} }
146
+ @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)} }
147
+ @keyframes rts-ground { from{transform:translateY(110%);opacity:0} to{transform:translateY(0);opacity:1} }
148
+ @keyframes rts-cascade { from{opacity:0;transform:translateY(-28px) translateX(10px) rotate(8deg)} to{opacity:1;transform:none} }
149
+ @keyframes rts-spotlight { 0%{opacity:0;letter-spacing:0.3em;transform:scaleX(1.15)} 100%{opacity:1;letter-spacing:-0.03em;transform:scaleX(1)} }
150
+ @keyframes rts-ink { 0%{opacity:0;transform:translateY(6px) scale(0.96)} 100%{opacity:1;transform:translateY(0) scale(1)} }
151
+ @keyframes rts-hinge { from{opacity:0;transform:perspective(400px) rotateY(-40deg) translateX(-20px)} to{opacity:1;transform:perspective(400px) rotateY(0) translateX(0)} }
152
+ @keyframes rts-stretch { 0%{opacity:0;transform:scaleX(0.05)} 60%{transform:scaleX(1.04)} 100%{opacity:1;transform:scaleX(1)} }
153
+ @keyframes rts-peel { from{clip-path:inset(100% 0 0 0)} to{clip-path:inset(0% 0 0 0)} }
154
+
155
+ /* ── Whole-element classes (no splitting needed) ───────────────────────── */
156
+ .rts-rise { animation: rts-rise 0.9s cubic-bezier(0.16,1,0.3,1) both }
157
+ .rts-clip { animation: rts-clip 1.1s cubic-bezier(0.77,0,0.18,1) both }
158
+ .rts-pop { animation: rts-pop 0.7s cubic-bezier(0.34,1.56,0.64,1) both }
159
+ .rts-blur { animation: rts-blur 1s cubic-bezier(0.16,1,0.3,1) both }
160
+ .rts-flip { animation: rts-flip 0.9s cubic-bezier(0.16,1,0.3,1) both; transform-origin: center bottom }
161
+ .rts-swipe { animation: rts-swipe 0.8s cubic-bezier(0.16,1,0.3,1) both }
162
+ .rts-bounce { animation: rts-bounce 0.9s cubic-bezier(0.36,0.07,0.19,0.97) both }
163
+ .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 }
164
+ .rts-morph { animation: rts-morph 0.8s cubic-bezier(0.34,1.56,0.64,1) both }
165
+ .rts-spotlight { animation: rts-spotlight 1s cubic-bezier(0.16,1,0.3,1) both }
166
+ .rts-stretch { animation: rts-stretch 0.9s cubic-bezier(0.34,1.56,0.64,1) both }
167
+
168
+ /* ── Per-word / per-character span classes ─────────────────────────────── */
169
+ .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 }
170
+ .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 }
171
+ .rts-velvet-word { display:inline-block;opacity:0;animation:rts-velvet 0.65s cubic-bezier(0.16,1,0.3,1) both }
172
+ .rts-curtain-word { display:inline-block;overflow:hidden;animation:rts-curtain 0.7s cubic-bezier(0.77,0,0.18,1) both }
173
+ .rts-ground-wrap { display:inline-block;overflow:hidden;vertical-align:bottom }
174
+ .rts-ground-inner { display:inline-block;animation:rts-ground 0.65s cubic-bezier(0.16,1,0.3,1) both }
175
+ .rts-cascade-ch { display:inline-block;opacity:0;animation:rts-cascade 0.45s cubic-bezier(0.34,1.56,0.64,1) both }
176
+ .rts-ink-word { display:inline-block;opacity:0;animation:rts-ink 0.9s cubic-bezier(0.16,1,0.3,1) both }
177
+ .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 }
178
+ .rts-peel-word { display:inline-block;overflow:hidden;animation:rts-peel 0.6s cubic-bezier(0.77,0,0.18,1) both }
152
179
  `;
153
180
  function injectAnimationStyles() {
154
181
  if (typeof document === "undefined")
@@ -160,52 +187,47 @@ function injectAnimationStyles() {
160
187
  style.textContent = CSS;
161
188
  document.head.appendChild(style);
162
189
  }
190
+ // ─── Whole-element class map ──────────────────────────────────────────────────
191
+ const WHOLE_CLASS_MAP = {
192
+ rise: "rts-rise",
193
+ clip: "rts-clip",
194
+ pop: "rts-pop",
195
+ blur: "rts-blur",
196
+ flip: "rts-flip",
197
+ swipe: "rts-swipe",
198
+ bounce: "rts-bounce",
199
+ typewriter: "rts-typewriter",
200
+ morph: "rts-morph",
201
+ spotlight: "rts-spotlight",
202
+ stretch: "rts-stretch",
203
+ };
204
+ /** Returns the CSS class for whole-element animations, or "" for split ones. */
163
205
  function getAnimationClass(animation) {
164
206
  var _a;
165
- const map = {
166
- rise: "rts-rise",
167
- clip: "rts-clip",
168
- pop: "rts-pop",
169
- blur: "rts-blur",
170
- flip: "rts-flip",
171
- swipe: "rts-swipe",
172
- bounce: "rts-bounce",
173
- typewriter: "rts-typewriter",
174
- stagger: "",
175
- letters: "",
176
- };
177
- return (_a = map[animation]) !== null && _a !== void 0 ? _a : "";
207
+ return (_a = WHOLE_CLASS_MAP[animation]) !== null && _a !== void 0 ? _a : "";
178
208
  }
179
- /**
180
- * Wraps each word in an animated span.
181
- * <em> tokens are preserved as-is in the HTML — Typography's useEffect
182
- * will apply inline styles to them after mount.
183
- */
184
- function buildStaggerHTML(html) {
209
+ /** True if the animation needs the HTML to be split into word/char spans. */
210
+ function isSplitAnimation(animation) {
211
+ return !(animation in WHOLE_CLASS_MAP);
212
+ }
213
+ // ─── HTML builders for split animations ──────────────────────────────────────
214
+ function wrapWords(html, cls, delayStep) {
185
215
  var _a;
186
216
  const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
187
217
  return tokens
188
218
  .map((tok, i) => {
189
- const delay = (i * 0.07).toFixed(2);
219
+ const delay = (i * delayStep).toFixed(2);
190
220
  if (tok.startsWith("<em>")) {
191
- // Wrap the inner text in the animated span, keep <em> outside
192
- const inner = tok.slice(4, -5);
193
- return `<em><span class="rts-word" style="animation-delay:${delay}s">${inner}</span></em>`;
221
+ return `<em><span class="${cls}" style="animation-delay:${delay}s">${tok.slice(4, -5)}</span></em>`;
194
222
  }
195
- return `<span class="rts-word" style="animation-delay:${delay}s">${tok}</span>`;
223
+ return `<span class="${cls}" style="animation-delay:${delay}s">${tok}</span>`;
196
224
  })
197
225
  .join(" ");
198
226
  }
199
- /**
200
- * Wraps each character in an animated span.
201
- * <em> tags are preserved in the output — Typography's useEffect applies
202
- * the actual italic/non-italic inline styles after the DOM is ready.
203
- */
204
- function buildLettersHTML(html) {
227
+ function wrapChars(html, cls, delayStep) {
205
228
  const result = [];
206
229
  let inEm = false;
207
230
  let delay = 0;
208
- const step = 0.04;
209
231
  let i = 0;
210
232
  while (i < html.length) {
211
233
  if (html.startsWith("<em>", i)) {
@@ -224,14 +246,40 @@ function buildLettersHTML(html) {
224
246
  i++;
225
247
  continue;
226
248
  }
227
- const span = `<span class="rts-letter" style="animation-delay:${delay.toFixed(2)}s">${ch}</span>`;
228
- // Preserve <em> wrapper in DOM — styles applied by useEffect
249
+ const span = `<span class="${cls}" style="animation-delay:${delay.toFixed(2)}s">${ch}</span>`;
229
250
  result.push(inEm ? `<em>${span}</em>` : span);
230
- delay += step;
251
+ delay += delayStep;
231
252
  i++;
232
253
  }
233
254
  return result.join("");
234
255
  }
256
+ function wrapGround(html, delayStep) {
257
+ var _a;
258
+ const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
259
+ return tokens
260
+ .map((tok, i) => {
261
+ const delay = (i * delayStep).toFixed(2);
262
+ const inner = tok.startsWith("<em>")
263
+ ? `<em>${tok.slice(4, -5)}</em>`
264
+ : tok;
265
+ return `<span class="rts-ground-wrap"><span class="rts-ground-inner" style="animation-delay:${delay}s">${inner}</span></span>`;
266
+ })
267
+ .join(" ");
268
+ }
269
+ function buildSplitHTML(animation, html) {
270
+ switch (animation) {
271
+ case "stagger": return wrapWords(html, "rts-word", 0.07);
272
+ case "letters": return wrapChars(html, "rts-letter", 0.04);
273
+ case "velvet": return wrapWords(html, "rts-velvet-word", 0.08);
274
+ case "curtain": return wrapWords(html, "rts-curtain-word", 0.10);
275
+ case "ground": return wrapGround(html, 0.09);
276
+ case "cascade": return wrapChars(html, "rts-cascade-ch", 0.05);
277
+ case "ink": return wrapWords(html, "rts-ink-word", 0.10);
278
+ case "hinge": return wrapWords(html, "rts-hinge-word", 0.09);
279
+ case "peel": return wrapWords(html, "rts-peel-word", 0.10);
280
+ default: return html;
281
+ }
282
+ }
235
283
 
236
284
  // ─── Defaults ─────────────────────────────────────────────────────────────────
237
285
  const DEFAULT_THEME = {
@@ -396,7 +444,7 @@ function renderChildrenWithEmStyles(children, italic, accentColor, headingFont)
396
444
  });
397
445
  }
398
446
  function applyEmStylesDOM(container, italic, accentColor, headingFont) {
399
- const applyTo = (el) => {
447
+ const apply = (el) => {
400
448
  if (italic) {
401
449
  el.style.fontFamily = "'Instrument Serif', serif";
402
450
  el.style.fontStyle = "italic";
@@ -410,63 +458,74 @@ function applyEmStylesDOM(container, italic, accentColor, headingFont) {
410
458
  el.style.color = "inherit";
411
459
  }
412
460
  };
413
- container.querySelectorAll("em").forEach(applyTo);
414
- container.querySelectorAll("em > span").forEach(applyTo);
461
+ container.querySelectorAll("em").forEach(apply);
462
+ container.querySelectorAll("em > span").forEach(apply);
415
463
  }
416
464
  // ─── Component ───────────────────────────────────────────────────────────────
417
465
  const Typography = (_a) => {
418
466
  var _b;
419
- var { variant = "Body",
420
- // Explicit undefined = "not set by caller" → fall through to context
421
- 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"]);
467
+ 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"]);
422
468
  const theme = useTypographyTheme();
423
469
  const isHero = variant === "Display" || variant === "H1";
424
470
  const ref = useRef(null);
425
- // Prop wins if explicitly provided; otherwise fall back to theme value.
426
- // We use `?? ` (nullish coalescing) so that false / 0 / "" from props still win.
471
+ // Prop wins; fall back to theme; fall back to built-in default
427
472
  const font = fontProp !== null && fontProp !== void 0 ? fontProp : (theme.font || undefined);
428
473
  const color = colorProp !== null && colorProp !== void 0 ? colorProp : (theme.color || undefined);
429
- const animation = isHero
430
- ? ((_b = animationProp !== null && animationProp !== void 0 ? animationProp : theme.animation) !== null && _b !== void 0 ? _b : undefined)
431
- : undefined;
474
+ const animation = isHero ? ((_b = animationProp !== null && animationProp !== void 0 ? animationProp : theme.animation) !== null && _b !== void 0 ? _b : undefined) : undefined;
432
475
  const italic = italicProp !== null && italicProp !== void 0 ? italicProp : theme.italic;
433
476
  const accentColor = accentColorProp !== null && accentColorProp !== void 0 ? accentColorProp : theme.accentColor;
434
- // Always pre-load Instrument Serif for hero elements
435
- if (isHero) {
436
- injectFont(INSTRUMENT_SERIF_URL);
437
- }
438
- // Inject heading Google Font
439
- if (font && GOOGLE_FONTS.includes(font)) {
440
- injectFont(buildFontUrl(font));
441
- }
442
- // Inject animation keyframes once
443
- if (animation && isHero) {
444
- injectAnimationStyles();
445
- }
446
- // Re-stamp em styles after DOM updates (animation / dangerouslySetInnerHTML path)
477
+ // ── useInsertionEffect: inject <link> and <style> tags ────────────────────
478
+ //
479
+ // WHY useInsertionEffect instead of plain render-phase calls:
480
+ //
481
+ // 1. Server safety — useInsertionEffect (like all effects) is never called
482
+ // on the server, so document.createElement / document.head never run
483
+ // during SSR. The isBrowser guard in ssr.ts is a belt-and-suspenders
484
+ // backup, but the effect boundary is the real guarantee.
485
+ //
486
+ // 2. Correctness React 18 concurrent mode can call the render function
487
+ // multiple times before committing. Doing DOM work in render can fire
488
+ // those side-effects redundantly or out of order. useInsertionEffect
489
+ // fires synchronously before the browser paints, once per commit.
490
+ //
491
+ // 3. No FOUC — because it fires before paint (earlier than useLayoutEffect),
492
+ // the <style> tag is in the DOM before any text is visible, so there is
493
+ // no flash of unstyled / wrong-font text.
494
+ useInsertionEffect(() => {
495
+ // Instrument Serif — always pre-load for hero so toggling italic is instant
496
+ if (isHero) {
497
+ injectFont(INSTRUMENT_SERIF_URL);
498
+ }
499
+ // Heading Google Font
500
+ if (font && GOOGLE_FONTS.includes(font)) {
501
+ injectFont(buildFontUrl(font));
502
+ }
503
+ // Animation keyframe stylesheet
504
+ if (animation && isHero) {
505
+ injectAnimationStyles();
506
+ }
507
+ }, [isHero, font, animation]);
508
+ // ── useEffect: re-stamp inline styles on <em> after DOM updates ───────────
447
509
  useEffect(() => {
448
510
  if (!isHero || !animation || !ref.current)
449
511
  return;
450
512
  applyEmStylesDOM(ref.current, italic, accentColor, font);
451
513
  }, [italic, accentColor, font, animation, isHero]);
452
514
  const Tag = (as !== null && as !== void 0 ? as : variantTagMap[variant]);
453
- // ── Animation path ────────────────────────────────────────────────────────
515
+ // ── Animation path: build inner HTML ─────────────────────────────────────
454
516
  let animClass = "";
455
517
  let heroHTML = null;
456
518
  if (animation && isHero) {
457
519
  const rawHTML = childrenToHTML(children);
458
- if (animation === "stagger") {
459
- heroHTML = buildStaggerHTML(rawHTML);
460
- }
461
- else if (animation === "letters") {
462
- heroHTML = buildLettersHTML(rawHTML);
520
+ if (isSplitAnimation(animation)) {
521
+ heroHTML = buildSplitHTML(animation, rawHTML);
463
522
  }
464
523
  else {
465
524
  heroHTML = rawHTML;
466
525
  animClass = getAnimationClass(animation);
467
526
  }
468
527
  }
469
- // ── Computed styles ───────────────────────────────────────────────────────
528
+ // ── Computed container styles ─────────────────────────────────────────────
470
529
  const computedStyle = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, variantStyleMap[variant]), (font ? { fontFamily: `'${font}', sans-serif` } : {})), (color ? { color } : {})), (align ? { textAlign: align } : {})), (truncate
471
530
  ? { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }
472
531
  : {})), (maxLines && !truncate
@@ -481,7 +540,7 @@ const Typography = (_a) => {
481
540
  if (heroHTML !== null) {
482
541
  return (jsx(Tag, Object.assign({ ref: ref, className: [animClass, className].filter(Boolean).join(" "), style: computedStyle, dangerouslySetInnerHTML: { __html: heroHTML } }, rest), animation));
483
542
  }
484
- // ── Render: standard path (real React children) ───────────────────────────
543
+ // ── Render: standard path ────────────────────────────────────────────────
485
544
  const processedChildren = isHero
486
545
  ? renderChildrenWithEmStyles(children, italic, accentColor, font)
487
546
  : children;