@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/README.md CHANGED
@@ -222,6 +222,82 @@ preloadFonts(["Bricolage Grotesque", "Instrument Serif", "DM Sans"]);
222
222
 
223
223
  All are on Google Fonts and auto-injected when passed to the `font` prop.
224
224
 
225
+ ## TypographyProvider
226
+
227
+ Wrap your app (or a section of it) with `TypographyProvider` to set defaults once. Any prop passed directly to `<Typography>` still wins — the provider is just the fallback.
228
+
229
+ ```tsx
230
+ import { TypographyProvider, Typography } from "react-type-scale";
231
+
232
+ export default function App() {
233
+ return (
234
+ <TypographyProvider
235
+ theme={{
236
+ font: "Bricolage Grotesque",
237
+ accentColor: "#6366f1",
238
+ italic: true,
239
+ animation: "rise",
240
+ color: "#1a1a1a",
241
+ }}
242
+ >
243
+ {/* Inherits font, accentColor, italic, animation from theme */}
244
+ <Typography variant="Display">
245
+ Build with <em>intention</em>
246
+ </Typography>
247
+
248
+ {/* Overrides just the animation — everything else from theme */}
249
+ <Typography variant="H1" animation="clip">
250
+ Another hero heading
251
+ </Typography>
252
+
253
+ {/* Overrides font only */}
254
+ <Typography variant="Body" font="Lora">
255
+ Body copy in a different font.
256
+ </Typography>
257
+
258
+ {/* italic=false wins over theme's italic=true */}
259
+ <Typography variant="Display" italic={false}>
260
+ No serif accent here
261
+ </Typography>
262
+ </TypographyProvider>
263
+ );
264
+ }
265
+ ```
266
+
267
+ ### Theme shape
268
+
269
+ ```ts
270
+ interface TypographyTheme {
271
+ font?: string // Google Font name applied to all variants
272
+ accentColor?: string // <em> accent color for Display / H1
273
+ italic?: boolean // italic accent on/off for Display / H1
274
+ animation?: HeroAnimation // entrance animation for Display / H1
275
+ color?: string // default text color for all variants
276
+ }
277
+ ```
278
+
279
+ ### Priority order
280
+
281
+ ```
282
+ Explicit prop > TypographyProvider theme > built-in default
283
+ ```
284
+
285
+ ### Nesting providers
286
+
287
+ Providers can be nested. The nearest one wins:
288
+
289
+ ```tsx
290
+ <TypographyProvider theme={{ font: "Bricolage Grotesque", color: "#111" }}>
291
+ <Typography variant="H1">Uses Bricolage Grotesque</Typography>
292
+
293
+ <TypographyProvider theme={{ font: "Playfair Display", accentColor: "#e11d48" }}>
294
+ <Typography variant="Display">
295
+ Uses Playfair Display with red accent
296
+ </Typography>
297
+ </TypographyProvider>
298
+ </TypographyProvider>
299
+ ```
300
+
225
301
  ## License
226
302
 
227
303
  Copyright (c) 2025 Edwin Vakayil. All rights reserved.
@@ -0,0 +1,24 @@
1
+ import React from "react";
2
+ import { HeroAnimation } from "./types";
3
+ export 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
+ export interface TypographyProviderProps {
16
+ theme: TypographyTheme;
17
+ children: React.ReactNode;
18
+ }
19
+ export declare const TypographyProvider: React.FC<TypographyProviderProps>;
20
+ /**
21
+ * Returns the resolved theme from the nearest TypographyProvider.
22
+ * Falls back to DEFAULT_THEME if used outside a provider.
23
+ */
24
+ export declare function useTypographyTheme(): Required<TypographyTheme>;
@@ -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;
@@ -81,4 +112,4 @@ declare function injectFont(url: string): void;
81
112
  */
82
113
  declare function preloadFonts(families: string[]): void;
83
114
 
84
- export { GOOGLE_FONTS, HeroAnimation, TextAlign, Typography, TypographyProps, TypographyVariant, buildFontUrl, Typography as default, injectFont, preloadFonts };
115
+ export { GOOGLE_FONTS, HeroAnimation, TextAlign, Typography, TypographyProps, TypographyProvider, TypographyProviderProps, TypographyTheme, TypographyVariant, buildFontUrl, Typography as default, injectFont, preloadFonts };
package/dist/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import { 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,76 @@ 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
+ }
283
+
284
+ // ─── Defaults ─────────────────────────────────────────────────────────────────
285
+ const DEFAULT_THEME = {
286
+ font: "",
287
+ accentColor: "#c8b89a",
288
+ italic: false,
289
+ animation: "rise",
290
+ color: "",
291
+ };
292
+ // ─── Context ──────────────────────────────────────────────────────────────────
293
+ const TypographyContext = createContext(DEFAULT_THEME);
294
+ const TypographyProvider = ({ theme, children, }) => {
295
+ const resolved = useMemo(() => {
296
+ var _a, _b, _c, _d, _e;
297
+ return ({
298
+ font: (_a = theme.font) !== null && _a !== void 0 ? _a : DEFAULT_THEME.font,
299
+ accentColor: (_b = theme.accentColor) !== null && _b !== void 0 ? _b : DEFAULT_THEME.accentColor,
300
+ italic: (_c = theme.italic) !== null && _c !== void 0 ? _c : DEFAULT_THEME.italic,
301
+ animation: (_d = theme.animation) !== null && _d !== void 0 ? _d : DEFAULT_THEME.animation,
302
+ color: (_e = theme.color) !== null && _e !== void 0 ? _e : DEFAULT_THEME.color,
303
+ });
304
+ }, [theme.font, theme.accentColor, theme.italic, theme.animation, theme.color]);
305
+ // Pre-load the theme font as soon as the provider mounts
306
+ if (resolved.font && GOOGLE_FONTS.includes(resolved.font)) {
307
+ injectFont(buildFontUrl(resolved.font));
308
+ }
309
+ return (jsx(TypographyContext.Provider, { value: resolved, children: children }));
310
+ };
311
+ // ─── Hook ─────────────────────────────────────────────────────────────────────
312
+ /**
313
+ * Returns the resolved theme from the nearest TypographyProvider.
314
+ * Falls back to DEFAULT_THEME if used outside a provider.
315
+ */
316
+ function useTypographyTheme() {
317
+ return useContext(TypographyContext);
318
+ }
235
319
 
236
320
  // ─── Static maps ─────────────────────────────────────────────────────────────
237
321
  const variantTagMap = {
@@ -326,10 +410,6 @@ const variantStyleMap = {
326
410
  // ─── Constants ───────────────────────────────────────────────────────────────
327
411
  const INSTRUMENT_SERIF_URL = "https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&display=swap";
328
412
  // ─── Helpers ─────────────────────────────────────────────────────────────────
329
- /**
330
- * Serialise React children → raw HTML string.
331
- * Preserves <em>text</em> nodes for animation builders.
332
- */
333
413
  function childrenToHTML(children) {
334
414
  var _a, _b;
335
415
  return ((_b = (_a = Children.map(children, (child) => {
@@ -343,11 +423,6 @@ function childrenToHTML(children) {
343
423
  return "";
344
424
  })) === null || _a === void 0 ? void 0 : _a.join("")) !== null && _b !== void 0 ? _b : "");
345
425
  }
346
- /**
347
- * Re-map React children so that <em> elements get explicit inline styles.
348
- * This is used for the no-animation render path where we keep real React nodes.
349
- * Inline styles on the <em> itself beat any inherited font-family from the parent.
350
- */
351
426
  function renderChildrenWithEmStyles(children, italic, accentColor, headingFont) {
352
427
  const italicStyle = {
353
428
  fontFamily: "'Instrument Serif', serif",
@@ -368,13 +443,8 @@ function renderChildrenWithEmStyles(children, italic, accentColor, headingFont)
368
443
  return child;
369
444
  });
370
445
  }
371
- /**
372
- * After dangerouslySetInnerHTML renders, walk the DOM and apply inline styles
373
- * to every <em> and <em> > span so the font switch is guaranteed.
374
- */
375
446
  function applyEmStylesDOM(container, italic, accentColor, headingFont) {
376
- // Select both <em> and any animated letter spans nested inside <em>
377
- container.querySelectorAll("em").forEach((el) => {
447
+ const apply = (el) => {
378
448
  if (italic) {
379
449
  el.style.fontFamily = "'Instrument Serif', serif";
380
450
  el.style.fontStyle = "italic";
@@ -387,59 +457,68 @@ function applyEmStylesDOM(container, italic, accentColor, headingFont) {
387
457
  el.style.fontWeight = "inherit";
388
458
  el.style.color = "inherit";
389
459
  }
390
- });
391
- // Also style the animated letter spans inside <em> (used by "letters" animation)
392
- container.querySelectorAll("em > span").forEach((el) => {
393
- if (italic) {
394
- el.style.fontFamily = "'Instrument Serif', serif";
395
- el.style.fontStyle = "italic";
396
- el.style.fontWeight = "400";
397
- el.style.color = accentColor;
398
- }
399
- else {
400
- el.style.fontFamily = headingFont ? `'${headingFont}', sans-serif` : "inherit";
401
- el.style.fontStyle = "normal";
402
- el.style.fontWeight = "inherit";
403
- el.style.color = "inherit";
404
- }
405
- });
460
+ };
461
+ container.querySelectorAll("em").forEach(apply);
462
+ container.querySelectorAll("em > span").forEach(apply);
406
463
  }
407
464
  // ─── Component ───────────────────────────────────────────────────────────────
408
465
  const Typography = (_a) => {
409
- 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"]);
466
+ var _b;
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"]);
468
+ const theme = useTypographyTheme();
410
469
  const isHero = variant === "Display" || variant === "H1";
411
470
  const ref = useRef(null);
412
- // Always inject Instrument Serif for hero variants so it's pre-loaded
413
- // and ready the moment italic is toggled on no flash of wrong font.
414
- if (isHero) {
415
- injectFont(INSTRUMENT_SERIF_URL);
416
- }
417
- // Inject heading Google Font
418
- if (font && GOOGLE_FONTS.includes(font)) {
419
- injectFont(buildFontUrl(font));
420
- }
421
- // Inject animation keyframes (once, global)
422
- if (animation && isHero) {
423
- injectAnimationStyles();
424
- }
425
- // For animation paths (dangerouslySetInnerHTML), walk the DOM after render
426
- // and stamp inline styles onto every <em> — guaranteed to beat inheritance.
471
+ // Prop wins; fall back to theme; fall back to built-in default
472
+ const font = fontProp !== null && fontProp !== void 0 ? fontProp : (theme.font || undefined);
473
+ const color = colorProp !== null && colorProp !== void 0 ? colorProp : (theme.color || undefined);
474
+ const animation = isHero ? ((_b = animationProp !== null && animationProp !== void 0 ? animationProp : theme.animation) !== null && _b !== void 0 ? _b : undefined) : undefined;
475
+ const italic = italicProp !== null && italicProp !== void 0 ? italicProp : theme.italic;
476
+ const accentColor = accentColorProp !== null && accentColorProp !== void 0 ? accentColorProp : theme.accentColor;
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 ───────────
427
509
  useEffect(() => {
428
510
  if (!isHero || !animation || !ref.current)
429
511
  return;
430
512
  applyEmStylesDOM(ref.current, italic, accentColor, font);
431
513
  }, [italic, accentColor, font, animation, isHero]);
432
514
  const Tag = (as !== null && as !== void 0 ? as : variantTagMap[variant]);
433
- // ── Compute animation class + inner HTML ──────────────────────────────────
515
+ // ── Animation path: build inner HTML ─────────────────────────────────────
434
516
  let animClass = "";
435
517
  let heroHTML = null;
436
518
  if (animation && isHero) {
437
519
  const rawHTML = childrenToHTML(children);
438
- if (animation === "stagger") {
439
- heroHTML = buildStaggerHTML(rawHTML);
440
- }
441
- else if (animation === "letters") {
442
- heroHTML = buildLettersHTML(rawHTML);
520
+ if (isSplitAnimation(animation)) {
521
+ heroHTML = buildSplitHTML(animation, rawHTML);
443
522
  }
444
523
  else {
445
524
  heroHTML = rawHTML;
@@ -461,12 +540,12 @@ const Typography = (_a) => {
461
540
  if (heroHTML !== null) {
462
541
  return (jsx(Tag, Object.assign({ ref: ref, className: [animClass, className].filter(Boolean).join(" "), style: computedStyle, dangerouslySetInnerHTML: { __html: heroHTML } }, rest), animation));
463
542
  }
464
- // ── Render: standard path (real React children with em styles) ────────────
543
+ // ── Render: standard path ────────────────────────────────────────────────
465
544
  const processedChildren = isHero
466
545
  ? renderChildrenWithEmStyles(children, italic, accentColor, font)
467
546
  : children;
468
547
  return (jsx(Tag, Object.assign({ ref: ref, className: className, style: computedStyle }, rest, { children: processedChildren })));
469
548
  };
470
549
 
471
- export { GOOGLE_FONTS, Typography, buildFontUrl, Typography as default, injectFont, preloadFonts };
550
+ export { GOOGLE_FONTS, Typography, TypographyProvider, buildFontUrl, Typography as default, injectFont, preloadFonts };
472
551
  //# sourceMappingURL=index.esm.js.map