@edwinvakayil/calligraphy 1.3.0 → 1.5.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 +351 -130
- package/dist/animation.d.ts +24 -2
- package/dist/index.d.ts +164 -37
- package/dist/index.esm.js +560 -115
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +560 -114
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +158 -36
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -127,55 +127,250 @@ function preloadFonts(families) {
|
|
|
127
127
|
|
|
128
128
|
const STYLE_ID = "rts-hero-animations";
|
|
129
129
|
const CSS = `
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
@keyframes rts-
|
|
140
|
-
@keyframes rts-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
@keyframes rts-
|
|
145
|
-
@keyframes rts-
|
|
146
|
-
@keyframes rts-
|
|
147
|
-
@keyframes rts-
|
|
148
|
-
@keyframes rts-
|
|
149
|
-
@keyframes rts-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
@keyframes rts-
|
|
153
|
-
@keyframes rts-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
130
|
+
/* ─── Easing tokens (reused across all keyframes) ──────────────────────────
|
|
131
|
+
All durations are tuned for perceptual smoothness:
|
|
132
|
+
- spring = cubic-bezier(0.16,1,0.3,1) — soft, natural overshoot
|
|
133
|
+
- snap = cubic-bezier(0.77,0,0.18,1) — fast-in, crisp-out
|
|
134
|
+
- bounce = cubic-bezier(0.34,1.56,0.64,1)— elastic overshoot
|
|
135
|
+
- cinema = cubic-bezier(0.4,0,0.2,1) — material easing
|
|
136
|
+
──────────────────────────────────────────────────────────────────────── */
|
|
137
|
+
|
|
138
|
+
/* ── Batch 1 — originals ─────────────────────────────────────────────────── */
|
|
139
|
+
@keyframes rts-rise {from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:translateY(0)}}
|
|
140
|
+
@keyframes rts-clip {from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0% 0 0)}}
|
|
141
|
+
@keyframes rts-pop {0%{opacity:0;transform:scale(0.75)}60%{opacity:1;transform:scale(1.04)}100%{transform:scale(1)}}
|
|
142
|
+
@keyframes rts-blur {from{opacity:0;filter:blur(14px);transform:scale(1.04)}to{opacity:1;filter:blur(0);transform:scale(1)}}
|
|
143
|
+
@keyframes rts-flip {from{opacity:0;transform:perspective(600px) rotateX(30deg) translateY(20px)}to{opacity:1;transform:perspective(600px) rotateX(0) translateY(0)}}
|
|
144
|
+
@keyframes rts-swipe {from{opacity:0;transform:translateX(60px)}to{opacity:1;transform:translateX(0)}}
|
|
145
|
+
@keyframes rts-bounce {0%{opacity:0;transform:translateY(-60px)}60%{opacity:1;transform:translateY(10px)}80%{transform:translateY(-5px)}100%{transform:translateY(0)}}
|
|
146
|
+
@keyframes rts-type {from{width:0}to{width:100%}}
|
|
147
|
+
@keyframes rts-blink {50%{border-color:transparent}}
|
|
148
|
+
@keyframes rts-word-rise {from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}}
|
|
149
|
+
@keyframes rts-letter-in {from{opacity:0;transform:translateX(-16px) rotate(-4deg)}to{opacity:1;transform:none}}
|
|
150
|
+
|
|
151
|
+
/* ── Batch 2 — modern ────────────────────────────────────────────────────── */
|
|
152
|
+
@keyframes rts-velvet {from{opacity:0;transform:translate(-12px,20px) skewX(4deg)}to{opacity:1;transform:none}}
|
|
153
|
+
@keyframes rts-curtain {from{clip-path:inset(0 0 100% 0)}to{clip-path:inset(0 0 0% 0)}}
|
|
154
|
+
@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:none}}
|
|
155
|
+
@keyframes rts-ground {from{transform:translateY(110%);opacity:0}to{transform:translateY(0);opacity:1}}
|
|
156
|
+
@keyframes rts-cascade {from{opacity:0;transform:translateY(-28px) translateX(10px) rotate(8deg)}to{opacity:1;transform:none}}
|
|
157
|
+
@keyframes rts-spotlight {0%{opacity:0;letter-spacing:0.3em;transform:scaleX(1.15)}100%{opacity:1;letter-spacing:-0.03em;transform:scaleX(1)}}
|
|
158
|
+
@keyframes rts-ink {0%{opacity:0;transform:translateY(6px) scale(0.96)}100%{opacity:1;transform:none}}
|
|
159
|
+
@keyframes rts-hinge {from{opacity:0;transform:perspective(400px) rotateY(-40deg) translateX(-20px)}to{opacity:1;transform:perspective(400px) rotateY(0) translateX(0)}}
|
|
160
|
+
@keyframes rts-stretch {0%{opacity:0;transform:scaleX(0.05)}60%{transform:scaleX(1.04)}100%{opacity:1;transform:none}}
|
|
161
|
+
@keyframes rts-peel {from{clip-path:inset(100% 0 0 0)}to{clip-path:inset(0% 0 0 0)}}
|
|
162
|
+
@keyframes rts-fold {from{opacity:0;transform:rotateZ(-8deg) translateY(18px)}to{opacity:1;transform:none}}
|
|
163
|
+
@keyframes rts-shear {from{opacity:0;transform:skewY(8deg) translateY(12px)}to{opacity:1;transform:none}}
|
|
164
|
+
/* ripple — elastic scale from compressed point, outward wave delay */
|
|
165
|
+
@keyframes rts-ripple-w {
|
|
166
|
+
0% {opacity:0;transform:scale(0.4) translateY(20px)}
|
|
167
|
+
55% {opacity:1;transform:scale(1.08) translateY(-3px)}
|
|
168
|
+
75% {transform:scale(0.97) translateY(1px)}
|
|
169
|
+
100%{opacity:1;transform:scale(1) translateY(0)}
|
|
170
|
+
}
|
|
171
|
+
/* cinch — character pinches on scaleX then snaps open with skew */
|
|
172
|
+
@keyframes rts-cinch {
|
|
173
|
+
0% {opacity:0;transform:scaleX(0) skewX(20deg)}
|
|
174
|
+
50% {opacity:1;transform:scaleX(1.12) skewX(-4deg)}
|
|
175
|
+
75% {transform:scaleX(0.95) skewX(1deg)}
|
|
176
|
+
100%{opacity:1;transform:scaleX(1) skewX(0)}
|
|
177
|
+
}
|
|
178
|
+
/* tiltrise — words rise while untilting from a sideways lean */
|
|
179
|
+
@keyframes rts-tiltrise {
|
|
180
|
+
0% {opacity:0;transform:translateY(36px) rotate(-6deg) skewX(8deg)}
|
|
181
|
+
65% {opacity:1;transform:translateY(-3px) rotate(0.5deg) skewX(-1deg)}
|
|
182
|
+
100%{opacity:1;transform:translateY(0) rotate(0) skewX(0)}
|
|
183
|
+
}
|
|
184
|
+
@keyframes rts-cardflip {from{opacity:0;transform:perspective(300px) rotateX(-90deg)}to{opacity:1;transform:perspective(300px) rotateX(0)}}
|
|
185
|
+
@keyframes rts-conv-l {from{opacity:0;transform:translateX(-40px)}to{opacity:1;transform:translateX(0)}}
|
|
186
|
+
@keyframes rts-conv-r {from{opacity:0;transform:translateX( 40px)}to{opacity:1;transform:translateX(0)}}
|
|
187
|
+
@keyframes rts-splitrise-t{from{opacity:0;transform:translateY(-24px)}to{opacity:1;transform:translateY(0)}}
|
|
188
|
+
@keyframes rts-splitrise-b{from{opacity:0;transform:translateY( 24px)}to{opacity:1;transform:translateY(0)}}
|
|
189
|
+
|
|
190
|
+
/* ── Batch 3 — new mechanics ─────────────────────────────────────────────── */
|
|
191
|
+
|
|
192
|
+
/* tectonic: words slam from alternating sides with skew — unique X+skew combo */
|
|
193
|
+
@keyframes rts-tec-l{from{opacity:0;transform:translateX(-55px) skewX(-7deg)}to{opacity:1;transform:none}}
|
|
194
|
+
@keyframes rts-tec-r{from{opacity:0;transform:translateX( 55px) skewX( 7deg)}to{opacity:1;transform:none}}
|
|
195
|
+
|
|
196
|
+
/* stratify: each word enters from deep Z with blur — different from blur (which is whole-el) */
|
|
197
|
+
@keyframes rts-strat{
|
|
198
|
+
from{opacity:0;transform:perspective(900px) translateZ(-200px) rotateX(10deg);filter:blur(6px)}
|
|
199
|
+
to {opacity:1;transform:perspective(900px) translateZ(0) rotateX(0); filter:blur(0)}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* unfurl: line expands from center — horizontal center-clip, not edge-clip */
|
|
203
|
+
@keyframes rts-unfurl{from{clip-path:inset(0 50% 0 50%)}to{clip-path:inset(0 0% 0 0%)}}
|
|
204
|
+
|
|
205
|
+
/* gravityWell: chars fall from sky with precise physics micro-rotation */
|
|
206
|
+
@keyframes rts-gwell{
|
|
207
|
+
0% {opacity:0;transform:translateY(-72px) scale(1.18) rotate(-3.5deg)}
|
|
208
|
+
52% {opacity:1;transform:translateY(7px) scale(0.98) rotate(0.4deg)}
|
|
209
|
+
78% {transform:translateY(-3px) scale(1.005) rotate(0)}
|
|
210
|
+
100%{opacity:1;transform:none}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* orbit: words grow from dot, rotating on Z — scale+rotation combo, unique */
|
|
214
|
+
@keyframes rts-orbit{
|
|
215
|
+
from{opacity:0;transform:scale(0.12) rotate(-28deg)}
|
|
216
|
+
58% {opacity:1;transform:scale(1.07) rotate(2.5deg)}
|
|
217
|
+
100%{opacity:1;transform:none}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* liquid: per-word squash then overshoot spring — scaleY+scaleX cross-axis */
|
|
221
|
+
@keyframes rts-liquid{
|
|
222
|
+
0% {opacity:0;transform:scaleY(0.08) scaleX(1.5) translateY(28px)}
|
|
223
|
+
40% {opacity:1;transform:scaleY(1.12) scaleX(0.94) translateY(-4px)}
|
|
224
|
+
65% {transform:scaleY(0.97) scaleX(1.015)}
|
|
225
|
+
100%{opacity:1;transform:none}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* noiseFade: 3 random opacity waveforms per word — signal-lock feel */
|
|
229
|
+
@keyframes rts-nf0{0%{opacity:0}22%{opacity:.7}44%{opacity:.1}66%{opacity:.95}88%{opacity:.4}100%{opacity:1}}
|
|
230
|
+
@keyframes rts-nf1{0%{opacity:0}18%{opacity:.8}38%{opacity:.15}58%{opacity:1}78%{opacity:.5}100%{opacity:1}}
|
|
231
|
+
@keyframes rts-nf2{0%{opacity:0}28%{opacity:.3}50%{opacity:.9}70%{opacity:.05}88%{opacity:.85}100%{opacity:1}}
|
|
232
|
+
|
|
233
|
+
/* slab: scaleX from-left stamp — fastest entrance, print-press energy */
|
|
234
|
+
@keyframes rts-slab{
|
|
235
|
+
0% {opacity:0;transform:scaleX(0) skewX(8deg)}
|
|
236
|
+
52% {opacity:1;transform:scaleX(1.05) skewX(-1deg)}
|
|
237
|
+
100%{opacity:1;transform:none}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* thread: chars ride individual sine-wave Y offsets set via CSS var */
|
|
241
|
+
@keyframes rts-thread{from{opacity:0;transform:translateY(var(--ty,18px))}to{opacity:1;transform:translateY(0)}}
|
|
242
|
+
|
|
243
|
+
/* billboard: whole-line rotateY — different axis from flip (rotateX) */
|
|
244
|
+
@keyframes rts-billboard{
|
|
245
|
+
from{opacity:0;transform:perspective(800px) rotateY(-32deg) translateX(-24px);filter:blur(3px)}
|
|
246
|
+
to {opacity:1;transform:perspective(800px) rotateY(0) translateX(0); filter:blur(0)}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* ═══════════════════════════════════════════════════════════════════════════
|
|
250
|
+
CLASS DECLARATIONS — all animations use animation-fill-mode: both so
|
|
251
|
+
the element stays at its final state without needing JS cleanup.
|
|
252
|
+
═══════════════════════════════════════════════════════════════════════════ */
|
|
253
|
+
|
|
254
|
+
/* ── Batch 1 whole-element ───────────────────────────────────────────────── */
|
|
255
|
+
.rts-rise {animation:rts-rise 0.9s cubic-bezier(0.16,1,0.3,1) both}
|
|
256
|
+
.rts-clip {animation:rts-clip 1.1s cubic-bezier(0.77,0,0.18,1) both}
|
|
257
|
+
.rts-pop {animation:rts-pop 0.7s cubic-bezier(0.34,1.56,0.64,1) both}
|
|
258
|
+
.rts-blur {animation:rts-blur 1s cubic-bezier(0.16,1,0.3,1) both}
|
|
259
|
+
.rts-flip {animation:rts-flip 0.9s cubic-bezier(0.16,1,0.3,1) both;transform-origin:center bottom}
|
|
260
|
+
.rts-swipe {animation:rts-swipe 0.8s cubic-bezier(0.16,1,0.3,1) both}
|
|
261
|
+
.rts-bounce {animation:rts-bounce 0.9s cubic-bezier(0.36,0.07,0.19,0.97) both}
|
|
262
|
+
.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}
|
|
263
|
+
|
|
264
|
+
/* ── Batch 2 whole-element ───────────────────────────────────────────────── */
|
|
265
|
+
.rts-morph {animation:rts-morph 0.8s cubic-bezier(0.34,1.56,0.64,1) both}
|
|
266
|
+
.rts-spotlight {animation:rts-spotlight 1s cubic-bezier(0.16,1,0.3,1) both}
|
|
267
|
+
.rts-stretch {animation:rts-stretch 0.9s cubic-bezier(0.34,1.56,0.64,1) both}
|
|
268
|
+
|
|
269
|
+
/* ── Batch 3 whole-element ───────────────────────────────────────────────── */
|
|
270
|
+
.rts-unfurl {animation:rts-unfurl 0.95s cubic-bezier(0.77,0,0.18,1) both}
|
|
271
|
+
.rts-billboard {animation:rts-billboard 0.95s cubic-bezier(0.16,1,0.3,1) both;transform-origin:left center}
|
|
272
|
+
|
|
273
|
+
/* ── Per-word / per-char spans (batch 1+2) ───────────────────────────────── */
|
|
274
|
+
.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}
|
|
275
|
+
.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}
|
|
276
|
+
.rts-velvet-word {display:inline-block;opacity:0;animation:rts-velvet 0.65s cubic-bezier(0.16,1,0.3,1) both}
|
|
277
|
+
.rts-curtain-word {display:inline-block;overflow:hidden;animation:rts-curtain 0.7s cubic-bezier(0.77,0,0.18,1) both}
|
|
278
|
+
.rts-ground-wrap {display:inline-block;overflow:hidden;vertical-align:bottom}
|
|
279
|
+
.rts-ground-inner {display:inline-block;animation:rts-ground 0.65s cubic-bezier(0.16,1,0.3,1) both}
|
|
280
|
+
.rts-cascade-ch {display:inline-block;opacity:0;animation:rts-cascade 0.45s cubic-bezier(0.34,1.56,0.64,1) both}
|
|
281
|
+
.rts-ink-word {display:inline-block;opacity:0;animation:rts-ink 0.9s cubic-bezier(0.16,1,0.3,1) both}
|
|
282
|
+
.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}
|
|
283
|
+
.rts-peel-word {display:inline-block;overflow:hidden;animation:rts-peel 0.6s cubic-bezier(0.77,0,0.18,1) both}
|
|
284
|
+
.rts-fold-word {display:inline-block;opacity:0;transform-origin:center bottom;animation:rts-fold 0.6s cubic-bezier(0.34,1.4,0.64,1) both}
|
|
285
|
+
.rts-shear-word {display:inline-block;opacity:0;animation:rts-shear 0.65s cubic-bezier(0.16,1,0.3,1) both}
|
|
286
|
+
.rts-ripple-word {display:inline-block;opacity:0;animation:rts-ripple-w 0.75s cubic-bezier(0.34,1.4,0.64,1) both}
|
|
287
|
+
.rts-cinch-ch {display:inline-block;opacity:0;transform-origin:center;animation:rts-cinch 0.55s cubic-bezier(0.34,1.2,0.64,1) both}
|
|
288
|
+
.rts-tiltrise-word{display:inline-block;opacity:0;animation:rts-tiltrise 0.8s cubic-bezier(0.16,1,0.3,1) both}
|
|
289
|
+
.rts-cardflip-ch {display:inline-block;opacity:0;transform-origin:center bottom;animation:rts-cardflip 0.4s cubic-bezier(0.34,1.4,0.64,1) both}
|
|
290
|
+
.rts-conv-l {display:inline-block;opacity:0;animation:rts-conv-l 0.7s cubic-bezier(0.16,1,0.3,1) both}
|
|
291
|
+
.rts-conv-r {display:inline-block;opacity:0;animation:rts-conv-r 0.7s cubic-bezier(0.16,1,0.3,1) both}
|
|
292
|
+
.rts-splitrise-t {display:inline-block;opacity:0;animation:rts-splitrise-t 0.65s cubic-bezier(0.16,1,0.3,1) both}
|
|
293
|
+
.rts-splitrise-b {display:inline-block;opacity:0;animation:rts-splitrise-b 0.65s cubic-bezier(0.16,1,0.3,1) both}
|
|
294
|
+
|
|
295
|
+
/* ── Per-word / per-char spans (batch 3) ────────────────────────────────── */
|
|
296
|
+
.rts-tec-l {display:inline-block;opacity:0;animation:rts-tec-l 0.65s cubic-bezier(0.16,1,0.3,1) both}
|
|
297
|
+
.rts-tec-r {display:inline-block;opacity:0;animation:rts-tec-r 0.65s cubic-bezier(0.16,1,0.3,1) both}
|
|
298
|
+
.rts-strat-word {display:inline-block;opacity:0;animation:rts-strat 0.85s cubic-bezier(0.16,1,0.3,1) both}
|
|
299
|
+
.rts-gwell-ch {display:inline-block;opacity:0;animation:rts-gwell 0.62s cubic-bezier(0.36,0.07,0.19,0.97) both}
|
|
300
|
+
.rts-orbit-word {display:inline-block;opacity:0;transform-origin:center;animation:rts-orbit 0.65s cubic-bezier(0.34,1.4,0.64,1) both}
|
|
301
|
+
.rts-liquid-word{display:inline-block;opacity:0;transform-origin:center bottom;animation:rts-liquid 0.72s cubic-bezier(0.34,1.56,0.64,1) both}
|
|
302
|
+
.rts-nf0 {display:inline-block;opacity:0;animation:rts-nf0 0.9s ease both}
|
|
303
|
+
.rts-nf1 {display:inline-block;opacity:0;animation:rts-nf1 0.9s ease both}
|
|
304
|
+
.rts-nf2 {display:inline-block;opacity:0;animation:rts-nf2 0.9s ease both}
|
|
305
|
+
.rts-slab-word {display:inline-block;opacity:0;transform-origin:left center;animation:rts-slab 0.5s cubic-bezier(0.77,0,0.18,1) both}
|
|
306
|
+
.rts-thread-ch {display:inline-block;opacity:0;animation:rts-thread 0.55s cubic-bezier(0.16,1,0.3,1) both}
|
|
307
|
+
|
|
308
|
+
/* ── Batch 7 — 8 genuinely new mechanics ────────────────────────────────── */
|
|
309
|
+
|
|
310
|
+
/* glassReveal — backdrop-filter blur evaporates as text solidifies */
|
|
311
|
+
@keyframes rts-glass {
|
|
312
|
+
0% { opacity:0; filter:blur(0px); backdrop-filter:blur(20px); transform:scale(1.03) }
|
|
313
|
+
35% { opacity:0.6; filter:blur(2px) }
|
|
314
|
+
100%{ opacity:1; filter:blur(0px); backdrop-filter:blur(0px); transform:scale(1) }
|
|
315
|
+
}
|
|
316
|
+
.rts-glass { animation:rts-glass 1.2s cubic-bezier(0.16,1,0.3,1) both }
|
|
317
|
+
|
|
318
|
+
/* wordPop — per-word springs from 0 at its own center, pure scale */
|
|
319
|
+
@keyframes rts-wpop {
|
|
320
|
+
0% { opacity:0; transform:scale(0) }
|
|
321
|
+
55% { opacity:1; transform:scale(1.08) }
|
|
322
|
+
75% { transform:scale(0.97) }
|
|
323
|
+
100%{ transform:scale(1) }
|
|
324
|
+
}
|
|
325
|
+
.rts-wpop-word { display:inline-block; opacity:0; animation:rts-wpop 0.55s cubic-bezier(0.34,1.56,0.64,1) both }
|
|
326
|
+
|
|
327
|
+
/* charDrop — pure gravity fall, no overshoot, no rotation */
|
|
328
|
+
@keyframes rts-cdrop {
|
|
329
|
+
0% { opacity:0; transform:translateY(-48px) }
|
|
330
|
+
100%{ opacity:1; transform:translateY(0) }
|
|
331
|
+
}
|
|
332
|
+
.rts-cdrop-ch { display:inline-block; opacity:0; animation:rts-cdrop 0.6s cubic-bezier(0.55,0,1,0.45) both }
|
|
333
|
+
|
|
334
|
+
/* scanline — single-pixel horizontal clip expands to full height */
|
|
335
|
+
@keyframes rts-scan {
|
|
336
|
+
0% { clip-path:inset(49% 0 49% 0); opacity:0 }
|
|
337
|
+
15% { opacity:1 }
|
|
338
|
+
100%{ clip-path:inset(0% 0 0% 0) }
|
|
339
|
+
}
|
|
340
|
+
.rts-scan { animation:rts-scan 0.9s cubic-bezier(0.77,0,0.18,1) both }
|
|
341
|
+
|
|
342
|
+
/* chromaShift — RGB channel offsets collapse to zero */
|
|
343
|
+
@keyframes rts-chroma {
|
|
344
|
+
0% { opacity:0; text-shadow: -8px 0 0 rgba(255,0,80,0.7), 8px 0 0 rgba(0,200,255,0.7) }
|
|
345
|
+
40% { opacity:1; text-shadow: -4px 0 0 rgba(255,0,80,0.35), 4px 0 0 rgba(0,200,255,0.35) }
|
|
346
|
+
100%{ text-shadow:none }
|
|
347
|
+
}
|
|
348
|
+
.rts-chroma { animation:rts-chroma 1s cubic-bezier(0.16,1,0.3,1) both }
|
|
349
|
+
|
|
350
|
+
/* wordFade — pure cross-dissolve with warmth scale, no Y movement */
|
|
351
|
+
@keyframes rts-wfade {
|
|
352
|
+
0% { opacity:0; transform:scale(0.97) }
|
|
353
|
+
100%{ opacity:1; transform:scale(1) }
|
|
354
|
+
}
|
|
355
|
+
.rts-wfade-word { display:inline-block; opacity:0; animation:rts-wfade 0.9s cubic-bezier(0.4,0,0.2,1) both }
|
|
356
|
+
|
|
357
|
+
/* rotateIn — full Y-axis card flip per word */
|
|
358
|
+
@keyframes rts-rotatein {
|
|
359
|
+
0% { opacity:0; transform:perspective(500px) rotateY(90deg) }
|
|
360
|
+
55% { opacity:1; transform:perspective(500px) rotateY(-8deg) }
|
|
361
|
+
100%{ transform:perspective(500px) rotateY(0deg) }
|
|
362
|
+
}
|
|
363
|
+
.rts-rotatein-word { display:inline-block; opacity:0; transform-origin:center; animation:rts-rotatein 0.6s cubic-bezier(0.34,1.4,0.64,1) both }
|
|
364
|
+
|
|
365
|
+
/* pressIn — presses down to 0.92 then springs outward past 1 */
|
|
366
|
+
@keyframes rts-pressin {
|
|
367
|
+
0% { opacity:0; transform:scale(0.5) }
|
|
368
|
+
30% { opacity:1; transform:scale(0.92) }
|
|
369
|
+
60% { transform:scale(1.06) }
|
|
370
|
+
80% { transform:scale(0.98) }
|
|
371
|
+
100%{ transform:scale(1) }
|
|
372
|
+
}
|
|
373
|
+
.rts-pressin-word { display:inline-block; opacity:0; animation:rts-pressin 0.65s cubic-bezier(0.34,1.56,0.64,1) both }
|
|
179
374
|
`;
|
|
180
375
|
function injectAnimationStyles() {
|
|
181
376
|
if (typeof document === "undefined")
|
|
@@ -187,7 +382,7 @@ function injectAnimationStyles() {
|
|
|
187
382
|
style.textContent = CSS;
|
|
188
383
|
document.head.appendChild(style);
|
|
189
384
|
}
|
|
190
|
-
// ─── Whole-element
|
|
385
|
+
// ─── Whole-element animation map ─────────────────────────────────────────────
|
|
191
386
|
const WHOLE_CLASS_MAP = {
|
|
192
387
|
rise: "rts-rise",
|
|
193
388
|
clip: "rts-clip",
|
|
@@ -200,35 +395,35 @@ const WHOLE_CLASS_MAP = {
|
|
|
200
395
|
morph: "rts-morph",
|
|
201
396
|
spotlight: "rts-spotlight",
|
|
202
397
|
stretch: "rts-stretch",
|
|
398
|
+
unfurl: "rts-unfurl",
|
|
399
|
+
billboard: "rts-billboard",
|
|
400
|
+
// Batch 7 — whole-element
|
|
401
|
+
glassReveal: "rts-glass",
|
|
402
|
+
scanline: "rts-scan",
|
|
403
|
+
chromaShift: "rts-chroma",
|
|
203
404
|
};
|
|
204
|
-
/** Returns the CSS class for whole-element animations, or "" for split ones. */
|
|
205
405
|
function getAnimationClass(animation) {
|
|
206
406
|
var _a;
|
|
207
407
|
return (_a = WHOLE_CLASS_MAP[animation]) !== null && _a !== void 0 ? _a : "";
|
|
208
408
|
}
|
|
209
|
-
/** True if the animation needs the HTML to be split into word/char spans. */
|
|
210
409
|
function isSplitAnimation(animation) {
|
|
211
410
|
return !(animation in WHOLE_CLASS_MAP);
|
|
212
411
|
}
|
|
213
|
-
// ─── HTML
|
|
214
|
-
function wrapWords(html, cls,
|
|
412
|
+
// ─── HTML split builders ──────────────────────────────────────────────────────
|
|
413
|
+
function wrapWords(html, cls, step) {
|
|
215
414
|
var _a;
|
|
216
415
|
const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
|
|
217
|
-
return tokens
|
|
218
|
-
|
|
219
|
-
const delay = (i * delayStep).toFixed(2);
|
|
416
|
+
return tokens.map((tok, i) => {
|
|
417
|
+
const delay = (i * step).toFixed(2);
|
|
220
418
|
if (tok.startsWith("<em>")) {
|
|
221
419
|
return `<em><span class="${cls}" style="animation-delay:${delay}s">${tok.slice(4, -5)}</span></em>`;
|
|
222
420
|
}
|
|
223
421
|
return `<span class="${cls}" style="animation-delay:${delay}s">${tok}</span>`;
|
|
224
|
-
})
|
|
225
|
-
.join(" ");
|
|
422
|
+
}).join(" ");
|
|
226
423
|
}
|
|
227
|
-
function wrapChars(html, cls,
|
|
424
|
+
function wrapChars(html, cls, step) {
|
|
228
425
|
const result = [];
|
|
229
|
-
let inEm = false;
|
|
230
|
-
let delay = 0;
|
|
231
|
-
let i = 0;
|
|
426
|
+
let inEm = false, delay = 0, i = 0;
|
|
232
427
|
while (i < html.length) {
|
|
233
428
|
if (html.startsWith("<em>", i)) {
|
|
234
429
|
inEm = true;
|
|
@@ -248,28 +443,107 @@ function wrapChars(html, cls, delayStep) {
|
|
|
248
443
|
}
|
|
249
444
|
const span = `<span class="${cls}" style="animation-delay:${delay.toFixed(2)}s">${ch}</span>`;
|
|
250
445
|
result.push(inEm ? `<em>${span}</em>` : span);
|
|
251
|
-
delay +=
|
|
446
|
+
delay += step;
|
|
252
447
|
i++;
|
|
253
448
|
}
|
|
254
449
|
return result.join("");
|
|
255
450
|
}
|
|
256
|
-
function wrapGround(html,
|
|
451
|
+
function wrapGround(html, step) {
|
|
257
452
|
var _a;
|
|
258
453
|
const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
|
|
259
|
-
return tokens
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
const inner = tok.startsWith("<em>")
|
|
263
|
-
? `<em>${tok.slice(4, -5)}</em>`
|
|
264
|
-
: tok;
|
|
454
|
+
return tokens.map((tok, i) => {
|
|
455
|
+
const delay = (i * step).toFixed(2);
|
|
456
|
+
const inner = tok.startsWith("<em>") ? `<em>${tok.slice(4, -5)}</em>` : tok;
|
|
265
457
|
return `<span class="rts-ground-wrap"><span class="rts-ground-inner" style="animation-delay:${delay}s">${inner}</span></span>`;
|
|
266
|
-
})
|
|
267
|
-
.join(" ");
|
|
458
|
+
}).join(" ");
|
|
268
459
|
}
|
|
460
|
+
function wrapTectonic(html) {
|
|
461
|
+
var _a;
|
|
462
|
+
const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
|
|
463
|
+
return tokens.map((tok, i) => {
|
|
464
|
+
const cls = i % 2 === 0 ? "rts-tec-l" : "rts-tec-r";
|
|
465
|
+
const delay = (i * 0.09).toFixed(2);
|
|
466
|
+
if (tok.startsWith("<em>")) {
|
|
467
|
+
return `<em><span class="${cls}" style="animation-delay:${delay}s">${tok.slice(4, -5)}</span></em>`;
|
|
468
|
+
}
|
|
469
|
+
return `<span class="${cls}" style="animation-delay:${delay}s">${tok}</span>`;
|
|
470
|
+
}).join(" ");
|
|
471
|
+
}
|
|
472
|
+
function wrapNoiseFade(html) {
|
|
473
|
+
var _a;
|
|
474
|
+
const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
|
|
475
|
+
return tokens.map((tok, i) => {
|
|
476
|
+
const cls = `rts-nf${i % 3}`;
|
|
477
|
+
const delay = (i * 0.12).toFixed(2);
|
|
478
|
+
if (tok.startsWith("<em>")) {
|
|
479
|
+
return `<em><span class="${cls}" style="animation-delay:${delay}s">${tok.slice(4, -5)}</span></em>`;
|
|
480
|
+
}
|
|
481
|
+
return `<span class="${cls}" style="animation-delay:${delay}s">${tok}</span>`;
|
|
482
|
+
}).join(" ");
|
|
483
|
+
}
|
|
484
|
+
function wrapConverge(html) {
|
|
485
|
+
var _a;
|
|
486
|
+
const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
|
|
487
|
+
const mid = Math.ceil(tokens.length / 2);
|
|
488
|
+
return tokens.map((tok, i) => {
|
|
489
|
+
const isLeft = i < mid;
|
|
490
|
+
const dist = isLeft ? mid - 1 - i : i - mid;
|
|
491
|
+
const delay = (dist * 0.07).toFixed(2);
|
|
492
|
+
const cls = isLeft ? "rts-conv-l" : "rts-conv-r";
|
|
493
|
+
const inner = tok.startsWith("<em>") ? `<em>${tok.slice(4, -5)}</em>` : tok;
|
|
494
|
+
return `<span class="${cls}" style="animation-delay:${delay}s">${inner}</span>`;
|
|
495
|
+
}).join(" ");
|
|
496
|
+
}
|
|
497
|
+
function wrapRipple(html) {
|
|
498
|
+
var _a;
|
|
499
|
+
const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
|
|
500
|
+
const mid = Math.floor(tokens.length / 2);
|
|
501
|
+
return tokens.map((tok, i) => {
|
|
502
|
+
const delay = (Math.abs(i - mid) * 0.1).toFixed(2);
|
|
503
|
+
if (tok.startsWith("<em>")) {
|
|
504
|
+
return `<em><span class="rts-ripple-word" style="animation-delay:${delay}s">${tok.slice(4, -5)}</span></em>`;
|
|
505
|
+
}
|
|
506
|
+
return `<span class="rts-ripple-word" style="animation-delay:${delay}s">${tok}</span>`;
|
|
507
|
+
}).join(" ");
|
|
508
|
+
}
|
|
509
|
+
function wrapSplitRise(html) {
|
|
510
|
+
var _a;
|
|
511
|
+
const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
|
|
512
|
+
return tokens.map((tok, i) => {
|
|
513
|
+
const cls = i % 2 === 0 ? "rts-splitrise-t" : "rts-splitrise-b";
|
|
514
|
+
const delay = (i * 0.08).toFixed(2);
|
|
515
|
+
if (tok.startsWith("<em>")) {
|
|
516
|
+
return `<em><span class="${cls}" style="animation-delay:${delay}s">${tok.slice(4, -5)}</span></em>`;
|
|
517
|
+
}
|
|
518
|
+
return `<span class="${cls}" style="animation-delay:${delay}s">${tok}</span>`;
|
|
519
|
+
}).join(" ");
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Thread: each char gets a sine-wave Y offset injected as a CSS custom
|
|
523
|
+
* property --ty so the keyframe can read it. Must be post-processed by
|
|
524
|
+
* Typography after innerHTML is set — see applyThreadOffsets().
|
|
525
|
+
*/
|
|
526
|
+
function wrapThread(html) {
|
|
527
|
+
return wrapChars(html, "rts-thread-ch", 0.04);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Called by Typography after dangerouslySetInnerHTML for "thread" animation.
|
|
531
|
+
* Stamps the sine-wave Y offset as --ty on each character span.
|
|
532
|
+
*/
|
|
533
|
+
function applyThreadOffsets(container) {
|
|
534
|
+
const spans = container.querySelectorAll(".rts-thread-ch");
|
|
535
|
+
spans.forEach((el, i) => {
|
|
536
|
+
const offset = Math.sin(i * 0.85) * 22;
|
|
537
|
+
el.style.setProperty("--ty", `${offset.toFixed(1)}px`);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
269
541
|
function buildSplitHTML(animation, html) {
|
|
270
542
|
switch (animation) {
|
|
543
|
+
// Batch 1
|
|
271
544
|
case "stagger": return wrapWords(html, "rts-word", 0.07);
|
|
272
545
|
case "letters": return wrapChars(html, "rts-letter", 0.04);
|
|
546
|
+
// Batch 2
|
|
273
547
|
case "velvet": return wrapWords(html, "rts-velvet-word", 0.08);
|
|
274
548
|
case "curtain": return wrapWords(html, "rts-curtain-word", 0.10);
|
|
275
549
|
case "ground": return wrapGround(html, 0.09);
|
|
@@ -277,9 +551,114 @@ function buildSplitHTML(animation, html) {
|
|
|
277
551
|
case "ink": return wrapWords(html, "rts-ink-word", 0.10);
|
|
278
552
|
case "hinge": return wrapWords(html, "rts-hinge-word", 0.09);
|
|
279
553
|
case "peel": return wrapWords(html, "rts-peel-word", 0.10);
|
|
554
|
+
case "fold": return wrapWords(html, "rts-fold-word", 0.09);
|
|
555
|
+
case "shear": return wrapWords(html, "rts-shear-word", 0.08);
|
|
556
|
+
case "ripple": return wrapRipple(html);
|
|
557
|
+
case "cinch": return wrapChars(html, "rts-cinch-ch", 0.048);
|
|
558
|
+
case "tiltrise": return wrapWords(html, "rts-tiltrise-word", 0.09);
|
|
559
|
+
case "cardFlip": return wrapChars(html, "rts-cardflip-ch", 0.045);
|
|
560
|
+
case "converge": return wrapConverge(html);
|
|
561
|
+
case "splitRise": return wrapSplitRise(html);
|
|
562
|
+
// Batch 3
|
|
563
|
+
case "tectonic": return wrapTectonic(html);
|
|
564
|
+
case "stratify": return wrapWords(html, "rts-strat-word", 0.10);
|
|
565
|
+
case "gravityWell": return wrapChars(html, "rts-gwell-ch", 0.05);
|
|
566
|
+
case "orbit": return wrapWords(html, "rts-orbit-word", 0.10);
|
|
567
|
+
case "liquid": return wrapWords(html, "rts-liquid-word", 0.09);
|
|
568
|
+
case "noiseFade": return wrapNoiseFade(html);
|
|
569
|
+
case "slab": return wrapWords(html, "rts-slab-word", 0.11);
|
|
570
|
+
case "thread": return wrapThread(html);
|
|
571
|
+
// Batch 7 — split
|
|
572
|
+
case "wordPop": return wrapWords(html, "rts-wpop-word", 0.07);
|
|
573
|
+
case "charDrop": return wrapChars(html, "rts-cdrop-ch", 0.04);
|
|
574
|
+
case "wordFade": return wrapWords(html, "rts-wfade-word", 0.09);
|
|
575
|
+
case "rotateIn": return wrapWords(html, "rts-rotatein-word", 0.08);
|
|
576
|
+
case "pressIn": return wrapWords(html, "rts-pressin-word", 0.08);
|
|
280
577
|
default: return html;
|
|
281
578
|
}
|
|
282
579
|
}
|
|
580
|
+
// ─── Custom motion builder ────────────────────────────────────────────────────
|
|
581
|
+
/**
|
|
582
|
+
* Injects a one-off @keyframes rule with a generated name and returns that name.
|
|
583
|
+
* Deduped by a hash of the keyframe body so the same keyframe is only injected once.
|
|
584
|
+
*/
|
|
585
|
+
function injectCustomKeyframes(keyframeBody) {
|
|
586
|
+
const hash = keyframeBody
|
|
587
|
+
.split("")
|
|
588
|
+
.reduce((acc, ch) => (Math.imul(31, acc) + ch.charCodeAt(0)) | 0, 0)
|
|
589
|
+
.toString(36)
|
|
590
|
+
.replace("-", "n");
|
|
591
|
+
const name = `rts-custom-${hash}`;
|
|
592
|
+
if (typeof document === "undefined")
|
|
593
|
+
return name;
|
|
594
|
+
if (document.getElementById(name))
|
|
595
|
+
return name;
|
|
596
|
+
const style = document.createElement("style");
|
|
597
|
+
style.id = name;
|
|
598
|
+
style.textContent = `@keyframes ${name} { ${keyframeBody} }`;
|
|
599
|
+
document.head.appendChild(style);
|
|
600
|
+
return name;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Builds the innerHTML for a custom motionConfig.
|
|
604
|
+
* - split "none" → returns the raw html unchanged; caller applies class to element
|
|
605
|
+
* - split "words" → wraps each word in a span with the animation + stagger delay
|
|
606
|
+
* - split "chars" → wraps each character in a span with the animation + stagger delay
|
|
607
|
+
*
|
|
608
|
+
* Returns { html, animationValue } where animationValue is the full CSS animation
|
|
609
|
+
* shorthand to set on each span (or on the element itself when split is "none").
|
|
610
|
+
*/
|
|
611
|
+
function buildCustomHTML(html, keyframeName, duration, easing, delay, fillMode, split, staggerDelay) {
|
|
612
|
+
var _a;
|
|
613
|
+
const baseAnimation = `${keyframeName} ${duration} ${easing} ${delay} ${fillMode}`;
|
|
614
|
+
if (split === "none") {
|
|
615
|
+
return { html, baseAnimation };
|
|
616
|
+
}
|
|
617
|
+
if (split === "words") {
|
|
618
|
+
const tokens = (_a = html.match(/(<em>[\s\S]*?<\/em>|[^\s]+)/g)) !== null && _a !== void 0 ? _a : [];
|
|
619
|
+
const result = tokens.map((tok, i) => {
|
|
620
|
+
const totalDelay = i === 0
|
|
621
|
+
? delay
|
|
622
|
+
: `${(parseFloat(delay) + i * staggerDelay).toFixed(3)}s`;
|
|
623
|
+
const anim = `${keyframeName} ${duration} ${easing} ${totalDelay} ${fillMode}`;
|
|
624
|
+
if (tok.startsWith("<em>")) {
|
|
625
|
+
return `<em><span style="display:inline-block;animation:${anim}">${tok.slice(4, -5)}</span></em>`;
|
|
626
|
+
}
|
|
627
|
+
return `<span style="display:inline-block;animation:${anim}">${tok}</span>`;
|
|
628
|
+
});
|
|
629
|
+
return { html: result.join(" "), baseAnimation };
|
|
630
|
+
}
|
|
631
|
+
// chars
|
|
632
|
+
const result = [];
|
|
633
|
+
let inEm = false, charIndex = 0, i = 0;
|
|
634
|
+
while (i < html.length) {
|
|
635
|
+
if (html.startsWith("<em>", i)) {
|
|
636
|
+
inEm = true;
|
|
637
|
+
i += 4;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (html.startsWith("</em>", i)) {
|
|
641
|
+
inEm = false;
|
|
642
|
+
i += 5;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const ch = html[i];
|
|
646
|
+
if (ch === " ") {
|
|
647
|
+
result.push(" ");
|
|
648
|
+
i++;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
const totalDelay = charIndex === 0
|
|
652
|
+
? delay
|
|
653
|
+
: `${(parseFloat(delay) + charIndex * staggerDelay).toFixed(3)}s`;
|
|
654
|
+
const anim = `${keyframeName} ${duration} ${easing} ${totalDelay} ${fillMode}`;
|
|
655
|
+
const span = `<span style="display:inline-block;animation:${anim}">${ch}</span>`;
|
|
656
|
+
result.push(inEm ? `<em>${span}</em>` : span);
|
|
657
|
+
charIndex++;
|
|
658
|
+
i++;
|
|
659
|
+
}
|
|
660
|
+
return { html: result.join(""), baseAnimation };
|
|
661
|
+
}
|
|
283
662
|
|
|
284
663
|
// ─── Defaults ─────────────────────────────────────────────────────────────────
|
|
285
664
|
const DEFAULT_THEME = {
|
|
@@ -317,7 +696,7 @@ function useTypographyTheme() {
|
|
|
317
696
|
return useContext(TypographyContext);
|
|
318
697
|
}
|
|
319
698
|
|
|
320
|
-
// ───
|
|
699
|
+
// ─── Variant → HTML tag map ───────────────────────────────────────────────────
|
|
321
700
|
const variantTagMap = {
|
|
322
701
|
Display: "h1",
|
|
323
702
|
H1: "h1",
|
|
@@ -332,6 +711,7 @@ const variantTagMap = {
|
|
|
332
711
|
Label: "label",
|
|
333
712
|
Caption: "span",
|
|
334
713
|
};
|
|
714
|
+
// ─── Variant → base CSS styles ────────────────────────────────────────────────
|
|
335
715
|
const variantStyleMap = {
|
|
336
716
|
Display: {
|
|
337
717
|
fontSize: "clamp(2.5rem, 6vw, 5rem)",
|
|
@@ -407,9 +787,15 @@ const variantStyleMap = {
|
|
|
407
787
|
letterSpacing: "0.03em",
|
|
408
788
|
},
|
|
409
789
|
};
|
|
410
|
-
// ─── Constants
|
|
790
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
791
|
+
// Always pre-loaded for hero variants so toggling italic is instant with no FOUC
|
|
411
792
|
const INSTRUMENT_SERIF_URL = "https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&display=swap";
|
|
412
|
-
// ─── Helpers
|
|
793
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
794
|
+
/**
|
|
795
|
+
* Serialise React children to a raw HTML string.
|
|
796
|
+
* Handles plain strings and <em>text</em> elements.
|
|
797
|
+
* Used to feed text into the animation split-builders.
|
|
798
|
+
*/
|
|
413
799
|
function childrenToHTML(children) {
|
|
414
800
|
var _a, _b;
|
|
415
801
|
return ((_b = (_a = Children.map(children, (child) => {
|
|
@@ -417,12 +803,19 @@ function childrenToHTML(children) {
|
|
|
417
803
|
return String(child);
|
|
418
804
|
}
|
|
419
805
|
if (isValidElement(child) && child.type === "em") {
|
|
420
|
-
const inner = typeof child.props.children === "string"
|
|
806
|
+
const inner = typeof child.props.children === "string"
|
|
807
|
+
? child.props.children
|
|
808
|
+
: "";
|
|
421
809
|
return `<em>${inner}</em>`;
|
|
422
810
|
}
|
|
423
811
|
return "";
|
|
424
812
|
})) === null || _a === void 0 ? void 0 : _a.join("")) !== null && _b !== void 0 ? _b : "");
|
|
425
813
|
}
|
|
814
|
+
/**
|
|
815
|
+
* Standard (no-animation) render path.
|
|
816
|
+
* Clones <em> children with explicit inline styles so the font switch is
|
|
817
|
+
* guaranteed — parent fontFamily cannot override a child's own inline style.
|
|
818
|
+
*/
|
|
426
819
|
function renderChildrenWithEmStyles(children, italic, accentColor, headingFont) {
|
|
427
820
|
const italicStyle = {
|
|
428
821
|
fontFamily: "'Instrument Serif', serif",
|
|
@@ -443,6 +836,11 @@ function renderChildrenWithEmStyles(children, italic, accentColor, headingFont)
|
|
|
443
836
|
return child;
|
|
444
837
|
});
|
|
445
838
|
}
|
|
839
|
+
/**
|
|
840
|
+
* Animation (dangerouslySetInnerHTML) render path.
|
|
841
|
+
* After the DOM is written we walk it and stamp inline styles onto every
|
|
842
|
+
* <em> and <em > span — guaranteed to beat any inherited fontFamily.
|
|
843
|
+
*/
|
|
446
844
|
function applyEmStylesDOM(container, italic, accentColor, headingFont) {
|
|
447
845
|
const apply = (el) => {
|
|
448
846
|
if (italic) {
|
|
@@ -452,7 +850,9 @@ function applyEmStylesDOM(container, italic, accentColor, headingFont) {
|
|
|
452
850
|
el.style.color = accentColor;
|
|
453
851
|
}
|
|
454
852
|
else {
|
|
455
|
-
el.style.fontFamily = headingFont
|
|
853
|
+
el.style.fontFamily = headingFont
|
|
854
|
+
? `'${headingFont}', sans-serif`
|
|
855
|
+
: "inherit";
|
|
456
856
|
el.style.fontStyle = "normal";
|
|
457
857
|
el.style.fontWeight = "inherit";
|
|
458
858
|
el.style.color = "inherit";
|
|
@@ -461,61 +861,98 @@ function applyEmStylesDOM(container, italic, accentColor, headingFont) {
|
|
|
461
861
|
container.querySelectorAll("em").forEach(apply);
|
|
462
862
|
container.querySelectorAll("em > span").forEach(apply);
|
|
463
863
|
}
|
|
464
|
-
// ─── Component
|
|
864
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
465
865
|
const Typography = (_a) => {
|
|
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"]);
|
|
866
|
+
var _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
867
|
+
var { variant = "Body", font: fontProp, color: colorProp, animation: animationProp, motionConfig, motionRef, italic: italicProp, accentColor: accentColorProp, align, className, style, children, as, truncate, maxLines } = _a, rest = __rest(_a, ["variant", "font", "color", "animation", "motionConfig", "motionRef", "italic", "accentColor", "align", "className", "style", "children", "as", "truncate", "maxLines"]);
|
|
468
868
|
const theme = useTypographyTheme();
|
|
469
869
|
const isHero = variant === "Display" || variant === "H1";
|
|
470
870
|
const ref = useRef(null);
|
|
471
|
-
// Prop wins
|
|
871
|
+
// Prop wins → theme → built-in default
|
|
472
872
|
const font = fontProp !== null && fontProp !== void 0 ? fontProp : (theme.font || undefined);
|
|
473
873
|
const color = colorProp !== null && colorProp !== void 0 ? colorProp : (theme.color || undefined);
|
|
474
|
-
const animation = isHero
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
//
|
|
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.
|
|
874
|
+
const animation = isHero
|
|
875
|
+
? ((_b = animationProp !== null && animationProp !== void 0 ? animationProp : theme.animation) !== null && _b !== void 0 ? _b : undefined)
|
|
876
|
+
: undefined;
|
|
877
|
+
const italic = (_c = italicProp !== null && italicProp !== void 0 ? italicProp : theme.italic) !== null && _c !== void 0 ? _c : false;
|
|
878
|
+
const accentColor = (_d = accentColorProp !== null && accentColorProp !== void 0 ? accentColorProp : theme.accentColor) !== null && _d !== void 0 ? _d : "#c8b89a";
|
|
879
|
+
// ── Font & style injection ─────────────────────────────────────────────────
|
|
494
880
|
useInsertionEffect(() => {
|
|
495
|
-
// Instrument Serif — always pre-load for hero so toggling italic is instant
|
|
496
881
|
if (isHero) {
|
|
497
882
|
injectFont(INSTRUMENT_SERIF_URL);
|
|
498
883
|
}
|
|
499
|
-
// Heading Google Font
|
|
500
884
|
if (font && GOOGLE_FONTS.includes(font)) {
|
|
501
885
|
injectFont(buildFontUrl(font));
|
|
502
886
|
}
|
|
503
|
-
// Animation keyframe stylesheet
|
|
504
887
|
if (animation && isHero) {
|
|
505
888
|
injectAnimationStyles();
|
|
506
889
|
}
|
|
507
|
-
|
|
508
|
-
|
|
890
|
+
// Inject custom keyframes as soon as the prop arrives
|
|
891
|
+
if (motionConfig === null || motionConfig === void 0 ? void 0 : motionConfig.keyframes) {
|
|
892
|
+
injectCustomKeyframes(motionConfig.keyframes);
|
|
893
|
+
}
|
|
894
|
+
}, [isHero, font, animation, motionConfig === null || motionConfig === void 0 ? void 0 : motionConfig.keyframes]);
|
|
895
|
+
// ── Re-stamp <em> inline styles after every relevant change ───────────────
|
|
509
896
|
useEffect(() => {
|
|
510
897
|
if (!isHero || !animation || !ref.current)
|
|
511
898
|
return;
|
|
512
899
|
applyEmStylesDOM(ref.current, italic, accentColor, font);
|
|
900
|
+
if (animation === "thread")
|
|
901
|
+
applyThreadOffsets(ref.current);
|
|
513
902
|
}, [italic, accentColor, font, animation, isHero]);
|
|
903
|
+
// ── motionRef callback — fires after mount and on every re-render ──────────
|
|
904
|
+
// motionRef wins over animation and motionConfig — the user drives the DOM.
|
|
905
|
+
useEffect(() => {
|
|
906
|
+
if (!motionRef)
|
|
907
|
+
return;
|
|
908
|
+
motionRef(ref.current);
|
|
909
|
+
});
|
|
910
|
+
// ── Strip lingering CSS properties after animation ends ───────────────────
|
|
911
|
+
useEffect(() => {
|
|
912
|
+
const el = ref.current;
|
|
913
|
+
if (!el || !animation)
|
|
914
|
+
return;
|
|
915
|
+
const cleanup = () => {
|
|
916
|
+
if (animation === "maskSweep") {
|
|
917
|
+
el.style.setProperty("mask-image", "none");
|
|
918
|
+
el.style.setProperty("-webkit-mask-image", "none");
|
|
919
|
+
}
|
|
920
|
+
if (animation === "gradSweep") {
|
|
921
|
+
el.style.animation = "none";
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
el.addEventListener("animationend", cleanup, { once: true });
|
|
925
|
+
return () => el.removeEventListener("animationend", cleanup);
|
|
926
|
+
}, [animation]);
|
|
514
927
|
const Tag = (as !== null && as !== void 0 ? as : variantTagMap[variant]);
|
|
515
|
-
// ──
|
|
928
|
+
// ── Build inner HTML — priority: motionRef > motionConfig > animation ──────
|
|
516
929
|
let animClass = "";
|
|
517
930
|
let heroHTML = null;
|
|
518
|
-
|
|
931
|
+
let customAnimStyle;
|
|
932
|
+
// motionRef — no HTML manipulation needed, user handles everything via ref
|
|
933
|
+
if (motionRef) ;
|
|
934
|
+
// motionConfig — works on any variant, not just heroes
|
|
935
|
+
else if (motionConfig === null || motionConfig === void 0 ? void 0 : motionConfig.keyframes) {
|
|
936
|
+
const keyframeName = injectCustomKeyframes(motionConfig.keyframes);
|
|
937
|
+
const duration = (_e = motionConfig.duration) !== null && _e !== void 0 ? _e : "0.8s";
|
|
938
|
+
const easing = (_f = motionConfig.easing) !== null && _f !== void 0 ? _f : "cubic-bezier(0.16,1,0.3,1)";
|
|
939
|
+
const delay = (_g = motionConfig.delay) !== null && _g !== void 0 ? _g : "0s";
|
|
940
|
+
const fillMode = (_h = motionConfig.fillMode) !== null && _h !== void 0 ? _h : "both";
|
|
941
|
+
const split = (_j = motionConfig.split) !== null && _j !== void 0 ? _j : "none";
|
|
942
|
+
const staggerDelay = (_k = motionConfig.staggerDelay) !== null && _k !== void 0 ? _k : (split === "chars" ? 0.04 : 0.07);
|
|
943
|
+
const rawHTML = childrenToHTML(children);
|
|
944
|
+
const { html, baseAnimation } = buildCustomHTML(rawHTML, keyframeName, duration, easing, delay, fillMode, split, staggerDelay);
|
|
945
|
+
if (split === "none") {
|
|
946
|
+
// Apply animation directly on the element via inline style
|
|
947
|
+
customAnimStyle = baseAnimation;
|
|
948
|
+
heroHTML = rawHTML;
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
heroHTML = html;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
// Built-in animation preset
|
|
955
|
+
else if (animation && isHero) {
|
|
519
956
|
const rawHTML = childrenToHTML(children);
|
|
520
957
|
if (isSplitAnimation(animation)) {
|
|
521
958
|
heroHTML = buildSplitHTML(animation, rawHTML);
|
|
@@ -526,8 +963,12 @@ const Typography = (_a) => {
|
|
|
526
963
|
}
|
|
527
964
|
}
|
|
528
965
|
// ── Computed container styles ─────────────────────────────────────────────
|
|
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
|
|
530
|
-
? {
|
|
966
|
+
const computedStyle = Object.assign(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
|
|
967
|
+
? {
|
|
968
|
+
overflow: "hidden",
|
|
969
|
+
textOverflow: "ellipsis",
|
|
970
|
+
whiteSpace: "nowrap",
|
|
971
|
+
}
|
|
531
972
|
: {})), (maxLines && !truncate
|
|
532
973
|
? {
|
|
533
974
|
display: "-webkit-box",
|
|
@@ -535,17 +976,21 @@ const Typography = (_a) => {
|
|
|
535
976
|
WebkitBoxOrient: "vertical",
|
|
536
977
|
overflow: "hidden",
|
|
537
978
|
}
|
|
538
|
-
: {})), { margin: 0, padding: 0 }), style);
|
|
539
|
-
// ── Render: animation path
|
|
979
|
+
: {})), (customAnimStyle ? { animation: customAnimStyle } : {})), { margin: 0, padding: 0 }), style);
|
|
980
|
+
// ── Render: animation path ────────────────────────────────────────────────
|
|
981
|
+
//
|
|
982
|
+
// key={animation} forces React to unmount + remount the element when the
|
|
983
|
+
// animation value changes, guaranteeing the CSS keyframe fires from frame 0
|
|
984
|
+
// on every switch.
|
|
540
985
|
if (heroHTML !== null) {
|
|
541
986
|
return (jsx(Tag, Object.assign({ ref: ref, className: [animClass, className].filter(Boolean).join(" "), style: computedStyle, dangerouslySetInnerHTML: { __html: heroHTML } }, rest), animation));
|
|
542
987
|
}
|
|
543
|
-
// ── Render: standard path
|
|
988
|
+
// ── Render: standard path ─────────────────────────────────────────────────
|
|
544
989
|
const processedChildren = isHero
|
|
545
990
|
? renderChildrenWithEmStyles(children, italic, accentColor, font)
|
|
546
991
|
: children;
|
|
547
992
|
return (jsx(Tag, Object.assign({ ref: ref, className: className, style: computedStyle }, rest, { children: processedChildren })));
|
|
548
993
|
};
|
|
549
994
|
|
|
550
|
-
export { GOOGLE_FONTS, Typography, TypographyProvider, buildFontUrl, Typography as default, injectFont, preloadFonts };
|
|
995
|
+
export { GOOGLE_FONTS, Typography, TypographyProvider, buildFontUrl, Typography as default, injectFont, preloadFonts, useTypographyTheme };
|
|
551
996
|
//# sourceMappingURL=index.esm.js.map
|