@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.
- package/dist/animation.d.ts +6 -12
- package/dist/index.d.ts +32 -19
- package/dist/index.esm.js +147 -88
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +146 -87
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +15 -1
- package/package.json +1 -1
package/dist/animation.d.ts
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
function
|
|
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 *
|
|
219
|
+
const delay = (i * delayStep).toFixed(2);
|
|
190
220
|
if (tok.startsWith("<em>")) {
|
|
191
|
-
|
|
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="
|
|
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="
|
|
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 +=
|
|
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
|
|
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(
|
|
414
|
-
container.querySelectorAll("em > span").forEach(
|
|
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
|
|
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
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
//
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
//
|
|
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
|
|
459
|
-
heroHTML =
|
|
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
|
|
543
|
+
// ── Render: standard path ────────────────────────────────────────────────
|
|
485
544
|
const processedChildren = isHero
|
|
486
545
|
? renderChildrenWithEmStyles(children, italic, accentColor, font)
|
|
487
546
|
: children;
|