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