@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 +76 -0
- package/dist/Context.d.ts +24 -0
- package/dist/animation.d.ts +6 -12
- package/dist/index.d.ts +33 -2
- package/dist/index.esm.js +190 -111
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +189 -109
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +15 -1
- package/package.json +1 -1
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>;
|
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;
|
|
@@ -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
|
-
|
|
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,76 @@ 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
|
+
}
|
|
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
|
-
|
|
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
|
-
|
|
392
|
-
container.querySelectorAll("em > span").forEach(
|
|
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
|
|
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
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
//
|
|
426
|
-
//
|
|
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
|
-
// ──
|
|
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
|
|
439
|
-
heroHTML =
|
|
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
|
|
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
|