fetta 1.5.4 → 1.6.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 +112 -260
- package/dist/{chunk-OUXSJF3P.js → chunk-44MU2I5B.js} +18 -1
- package/dist/chunk-HSIMGGOI.js +1 -0
- package/dist/{chunk-PA22PLRB.js → chunk-TEDMJHF7.js} +1 -13
- package/dist/chunk-V23SUR2S.js +13 -0
- package/dist/helpers.d.ts +1 -1
- package/dist/{index-c1UKfWWK.d.ts → index-Box92Sue.d.ts} +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/morph.d.ts +38 -0
- package/dist/morph.js +556 -0
- package/dist/motion.d.ts +4 -2
- package/dist/motion.js +38 -3
- package/dist/react.d.ts +4 -2
- package/dist/react.js +3 -2
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -2,54 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
Text splitting that keeps kerning intact.
|
|
4
4
|
|
|
5
|
-
Split text into
|
|
5
|
+
Split text into characters, words, and lines for animation — without breaking your typography. Most splitting libraries wrap each character in a `<span>` and call it done, but that destroys the kerning between character pairs. Fetta compensates for this automatically.
|
|
6
6
|
|
|
7
7
|
Docs: https://fetta.dimi.me/
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Install
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npm install fetta
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
## Choose Your Entry Point
|
|
16
|
-
|
|
17
|
-
| Import | Use when | Size (min + brotli) |
|
|
18
|
-
|------|------|------|
|
|
19
|
-
| `fetta` | You want the framework-agnostic core API (`splitText`) | ~7.04 kB |
|
|
20
|
-
| `fetta/react` | You use React and want callback/lifecycle control | ~8.18 kB |
|
|
21
|
-
| `fetta/motion` | You use Motion variants and built-in triggers | ~13.71 kB |
|
|
22
|
-
| `fetta/helpers` | You need optional utilities for layered split effects | ~742 B |
|
|
23
|
-
|
|
24
|
-
## Features
|
|
25
|
-
|
|
26
|
-
- **Kerning Compensation** — Maintains original character spacing when splitting by chars
|
|
27
|
-
- **Nested Elements** — Preserves `<a>`, `<em>`, `<strong>` and other inline elements with all attributes
|
|
28
|
-
- **Line Detection** — Groups words into rendered lines
|
|
29
|
-
- **Dash Handling** — Allows text to wrap naturally after em-dashes, en-dashes, hyphens, and slashes
|
|
30
|
-
- **Auto Re-split** — Re-splits on container resize
|
|
31
|
-
- **Auto Revert** — Restores original HTML after animations
|
|
32
|
-
- **Masking** — Wrap elements in clip containers for reveal animations
|
|
33
|
-
- **Emoji Support** — Properly handles compound emojis and complex Unicode characters
|
|
34
|
-
- **Accessible** — Automatic screen reader support, even with nested links or emphasis
|
|
35
|
-
- **TypeScript** — Full type definitions included
|
|
36
|
-
- **Library Agnostic** — Works with Motion, GSAP, CSS, WAAPI, or custom animation code
|
|
37
|
-
|
|
38
15
|
## Quick Start
|
|
39
16
|
|
|
40
|
-
### Vanilla
|
|
17
|
+
### Vanilla
|
|
41
18
|
|
|
42
19
|
```ts
|
|
43
20
|
import { splitText } from "fetta";
|
|
44
21
|
import { animate, stagger } from "motion";
|
|
45
22
|
|
|
46
|
-
const
|
|
47
|
-
const { chars } = splitText(element, { type: "chars" });
|
|
48
|
-
|
|
23
|
+
const { chars } = splitText(document.querySelector("h1"), { type: "chars" });
|
|
49
24
|
animate(chars, { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.02) });
|
|
50
25
|
```
|
|
51
26
|
|
|
52
|
-
### React
|
|
27
|
+
### React
|
|
53
28
|
|
|
54
29
|
```tsx
|
|
55
30
|
import { SplitText } from "fetta/react";
|
|
@@ -62,194 +37,149 @@ import { animate, stagger } from "motion";
|
|
|
62
37
|
}}
|
|
63
38
|
>
|
|
64
39
|
<h1>Hello World</h1>
|
|
65
|
-
</SplitText
|
|
40
|
+
</SplitText>
|
|
66
41
|
```
|
|
67
42
|
|
|
68
|
-
### Motion
|
|
43
|
+
### Motion
|
|
69
44
|
|
|
70
45
|
```tsx
|
|
71
46
|
import { SplitText } from "fetta/motion";
|
|
72
47
|
import { stagger } from "motion";
|
|
73
48
|
|
|
74
49
|
<SplitText
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
visible: { words: { opacity: 1, y: 0 } },
|
|
78
|
-
}}
|
|
79
|
-
initial="hidden"
|
|
80
|
-
animate="visible"
|
|
50
|
+
initial={{ opacity: 0, y: 20 }}
|
|
51
|
+
animate={{ opacity: 1, y: 0 }}
|
|
81
52
|
transition={{ duration: 0.65, delay: stagger(0.04) }}
|
|
82
53
|
options={{ type: "words" }}
|
|
83
54
|
>
|
|
84
55
|
<h1>Hello World</h1>
|
|
85
|
-
</SplitText
|
|
56
|
+
</SplitText>
|
|
86
57
|
```
|
|
87
58
|
|
|
88
|
-
|
|
59
|
+
### Morph
|
|
89
60
|
|
|
90
|
-
|
|
61
|
+
```tsx
|
|
62
|
+
import { MorphText } from "fetta/morph";
|
|
91
63
|
|
|
92
|
-
|
|
64
|
+
<MorphText>{text}</MorphText>
|
|
65
|
+
```
|
|
93
66
|
|
|
94
|
-
|
|
67
|
+
## Features
|
|
95
68
|
|
|
96
|
-
|
|
97
|
-
|
|
69
|
+
- **Kerning Compensation** — Maintains original character spacing when splitting by chars
|
|
70
|
+
- **Nested Elements** — Preserves `<a>`, `<em>`, `<strong>` and other inline elements with all attributes
|
|
71
|
+
- **Line Detection** — Groups words into rendered lines
|
|
72
|
+
- **Dash Handling** — Wraps naturally after em-dashes, en-dashes, hyphens, and slashes
|
|
73
|
+
- **Auto Re-split** — Re-splits on container resize
|
|
74
|
+
- **Auto Revert** — Restores original HTML after animations
|
|
75
|
+
- **Masking** — Clip containers for reveal animations
|
|
76
|
+
- **Emoji Support** — Handles compound emojis and complex Unicode characters
|
|
77
|
+
- **Accessible** — Automatic screen reader support, even with nested links or emphasis
|
|
78
|
+
- **TypeScript** — Full type definitions included
|
|
79
|
+
- **Zero Dependencies** — 7 kB core with no external packages
|
|
80
|
+
- **Library Agnostic** — Works with Motion, GSAP, CSS, WAAPI, or any animation code
|
|
98
81
|
|
|
99
|
-
|
|
100
|
-
|
|
82
|
+
## Entry Points
|
|
83
|
+
|
|
84
|
+
| Import | Use for | Size |
|
|
85
|
+
|------|------|------|
|
|
86
|
+
| `fetta` | Vanilla JS or any framework | 7.11 kB |
|
|
87
|
+
| `fetta/react` | React with callback/lifecycle control | 8.23 kB |
|
|
88
|
+
| `fetta/motion` | Declarative Motion animations | 13.78 kB |
|
|
89
|
+
| `fetta/morph` | Standalone MorphText component | 7.95 kB |
|
|
101
90
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
| Option | Type | Default | Description |
|
|
105
|
-
|------|------|------|------|
|
|
106
|
-
| `type` | `SplitType` | `"chars,words,lines"` | What to split: `"chars"`, `"words"`, `"lines"`, or combinations |
|
|
107
|
-
| `charClass` | `string` | `"split-char"` | CSS class for character elements |
|
|
108
|
-
| `wordClass` | `string` | `"split-word"` | CSS class for word elements |
|
|
109
|
-
| `lineClass` | `string` | `"split-line"` | CSS class for line elements |
|
|
110
|
-
| `mask` | `"chars" \| "words" \| "lines"` | — | Wrap elements in `overflow: clip` container |
|
|
111
|
-
| `autoSplit` | `boolean` | `false` | Re-split on container resize |
|
|
112
|
-
| `onResplit` | `(result) => void` | — | Callback after autoSplit/full-resplit replaces split output elements |
|
|
113
|
-
| `onSplit` | `(result) => CallbackReturn` | — | Callback after initial split. Return animation/promise for `revertOnComplete` |
|
|
114
|
-
| `revertOnComplete` | `boolean` | `false` | Auto-revert when returned animation completes |
|
|
115
|
-
| `propIndex` | `boolean` | `false` | Add CSS custom properties: `--char-index`, `--word-index`, `--line-index` |
|
|
116
|
-
| `disableKerning` | `boolean` | `false` | Skip kerning compensation (no margin adjustments) |
|
|
117
|
-
| `initialStyles` | `object` | — | Apply initial inline styles to chars/words/lines after split. Values can be objects or `(el, index) => object` functions |
|
|
118
|
-
| `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines. Values are strings |
|
|
91
|
+
Sizes are minified + brotli.
|
|
119
92
|
|
|
120
|
-
|
|
121
|
-
type SplitType =
|
|
122
|
-
| "chars"
|
|
123
|
-
| "words"
|
|
124
|
-
| "lines"
|
|
125
|
-
| "chars,words"
|
|
126
|
-
| "words,lines"
|
|
127
|
-
| "chars,lines"
|
|
128
|
-
| "chars,words,lines";
|
|
129
|
-
```
|
|
93
|
+
## API Overview
|
|
130
94
|
|
|
131
|
-
|
|
95
|
+
Full API reference at [fetta.dimi.me](https://fetta.dimi.me/). Summary below.
|
|
96
|
+
|
|
97
|
+
### `splitText(element, options?)` — [Core docs](https://fetta.dimi.me/api/core)
|
|
98
|
+
|
|
99
|
+
Returns `{ chars, words, lines, revert }`. Key options: `type`, `mask`, `autoSplit`, `onSplit`, `revertOnComplete`, `initialStyles`, `propIndex`.
|
|
132
100
|
|
|
133
101
|
```ts
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
102
|
+
import { splitText } from "fetta";
|
|
103
|
+
import { animate, stagger } from "motion";
|
|
104
|
+
|
|
105
|
+
document.fonts.ready.then(() => {
|
|
106
|
+
const { words } = splitText(element, { type: "words", mask: "words" });
|
|
107
|
+
animate(words, { y: ["100%", "0%"] }, { delay: stagger(0.1) });
|
|
108
|
+
});
|
|
140
109
|
```
|
|
141
110
|
|
|
142
|
-
### `<SplitText>` (
|
|
111
|
+
### `<SplitText>` — [React docs](https://fetta.dimi.me/api/react)
|
|
112
|
+
|
|
113
|
+
Wraps `splitText()` with React lifecycle, viewport callbacks, and automatic cleanup. Key props: `onSplit`, `onResplit`, `options`, `autoSplit`, `waitForFonts`, `revertOnComplete`, `viewport`, `onViewportEnter`, `initialStyles`.
|
|
143
114
|
|
|
144
115
|
```tsx
|
|
145
116
|
import { SplitText } from "fetta/react";
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
`fetta/react` wraps `splitText()` for React with lifecycle hooks, viewport callbacks, and automatic cleanup.
|
|
149
|
-
|
|
150
|
-
#### React Props
|
|
151
|
-
|
|
152
|
-
| Prop | Type | Default | Description |
|
|
153
|
-
|------|------|------|------|
|
|
154
|
-
| `children` | `ReactElement` | — | Single React element to split |
|
|
155
|
-
| `as` | `keyof JSX.IntrinsicElements` | `"div"` | Wrapper element type |
|
|
156
|
-
| `className` | `string` | — | Wrapper class name |
|
|
157
|
-
| `style` | `CSSProperties` | — | Wrapper styles |
|
|
158
|
-
| `ref` | `Ref<HTMLElement>` | — | Ref to wrapper element |
|
|
159
|
-
| `onSplit` | `(result) => CallbackReturn` | — | Called after initial split |
|
|
160
|
-
| `onResplit` | `(result) => void` | — | Called when autoSplit/full-resplit replaces split output elements |
|
|
161
|
-
| `options` | `SplitTextOptions` | — | Split options (`type`, classes, mask, etc.) |
|
|
162
|
-
| `autoSplit` | `boolean` | `false` | Re-split on container resize |
|
|
163
|
-
| `waitForFonts` | `boolean` | `true` | Wait for `document.fonts.ready` before splitting |
|
|
164
|
-
| `revertOnComplete` | `boolean` | `false` | Revert after animation completes |
|
|
165
|
-
| `onRevert` | `() => void` | — | Called when split text reverts |
|
|
166
|
-
| `viewport` | `ViewportOptions` | — | Configure viewport detection |
|
|
167
|
-
| `onViewportEnter` | `(result) => CallbackReturn` | — | Called when entering viewport |
|
|
168
|
-
| `onViewportLeave` | `(result) => CallbackReturn` | — | Called when leaving viewport |
|
|
169
|
-
| `initialStyles` | `object` | — | Apply initial inline styles to chars/words/lines |
|
|
170
|
-
| `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines |
|
|
171
|
-
| `resetOnViewportLeave` | `boolean` | `false` | Re-apply initial styles/classes when leaving viewport |
|
|
172
|
-
|
|
173
|
-
All callbacks receive:
|
|
174
|
-
|
|
175
|
-
```ts
|
|
176
|
-
{
|
|
177
|
-
chars: HTMLSpanElement[];
|
|
178
|
-
words: HTMLSpanElement[];
|
|
179
|
-
lines: HTMLSpanElement[];
|
|
180
|
-
revert: () => void;
|
|
181
|
-
}
|
|
182
|
-
```
|
|
117
|
+
import { animate, stagger } from "motion";
|
|
183
118
|
|
|
184
|
-
|
|
185
|
-
type
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
119
|
+
<SplitText
|
|
120
|
+
options={{ type: "words" }}
|
|
121
|
+
initialStyles={{ words: { opacity: 0, transform: "translateY(20px)" } }}
|
|
122
|
+
viewport={{ amount: 0.5 }}
|
|
123
|
+
onViewportEnter={({ words }) =>
|
|
124
|
+
animate(words, { opacity: 1, y: 0 }, { delay: stagger(0.05) })
|
|
125
|
+
}
|
|
126
|
+
resetOnViewportLeave
|
|
127
|
+
>
|
|
128
|
+
<p>Animates when scrolled into view</p>
|
|
129
|
+
</SplitText>
|
|
191
130
|
```
|
|
192
131
|
|
|
193
|
-
|
|
132
|
+
### `<SplitText>` — [Motion docs](https://fetta.dimi.me/api/motion)
|
|
194
133
|
|
|
195
|
-
|
|
134
|
+
Includes all React props plus Motion animation: `variants`, `initial`, `animate`, `exit`, `whileInView`, `whileScroll`, `whileHover`, `whileTap`, `whileFocus`, `transition`, `delayScope`, `custom`. Supports flat targets, per-type targets (`chars`/`words`/`lines`/`wrapper`), and function variants.
|
|
196
135
|
|
|
197
136
|
```tsx
|
|
198
137
|
import { SplitText } from "fetta/motion";
|
|
138
|
+
import { stagger } from "motion";
|
|
139
|
+
|
|
140
|
+
<SplitText
|
|
141
|
+
variants={{
|
|
142
|
+
hidden: { chars: { opacity: 0, y: 10 } },
|
|
143
|
+
visible: {
|
|
144
|
+
chars: ({ lineIndex }) => ({
|
|
145
|
+
opacity: 1,
|
|
146
|
+
y: 0,
|
|
147
|
+
transition: {
|
|
148
|
+
delay: stagger(0.02, { startDelay: lineIndex * 0.15 }),
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
},
|
|
152
|
+
}}
|
|
153
|
+
initial="hidden"
|
|
154
|
+
animate="visible"
|
|
155
|
+
options={{ type: "chars,lines", mask: "lines" }}
|
|
156
|
+
>
|
|
157
|
+
<p>Per-line staggered reveal</p>
|
|
158
|
+
</SplitText>
|
|
199
159
|
```
|
|
200
160
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
#### Motion-only Props
|
|
204
|
-
|
|
205
|
-
| Prop | Type | Default | Description |
|
|
206
|
-
|------|------|------|------|
|
|
207
|
-
| `variants` | `Record<string, VariantDefinition<TCustom>>` | — | Named variant definitions |
|
|
208
|
-
| `initial` | `string \| VariantDefinition<TCustom> \| false` | — | Initial variant applied instantly after split |
|
|
209
|
-
| `animate` | `string \| VariantDefinition<TCustom>` | — | Base variant |
|
|
210
|
-
| `exit` | `string \| VariantDefinition<TCustom> \| false` | — | Exit variant (`AnimatePresence`) |
|
|
211
|
-
| `whileInView` | `string \| VariantDefinition<TCustom>` | — | Variant while element is in view |
|
|
212
|
-
| `whileOutOfView` | `string \| VariantDefinition<TCustom>` | — | Variant after element leaves view |
|
|
213
|
-
| `whileScroll` | `string \| VariantDefinition<TCustom>` | — | Scroll-driven variant (highest trigger priority) |
|
|
214
|
-
| `whileHover` | `string \| VariantDefinition<TCustom>` | — | Variant on hover |
|
|
215
|
-
| `whileTap` | `string \| VariantDefinition<TCustom>` | — | Variant on tap/press |
|
|
216
|
-
| `whileFocus` | `string \| VariantDefinition<TCustom>` | — | Variant on focus |
|
|
217
|
-
| `animateOnResplit` | `boolean` | `false` | Replay `initial -> animate` on autoSplit/full-resplit |
|
|
218
|
-
| `scroll` | `{ offset?, axis?, container? }` | — | Scroll tracking options for `whileScroll` |
|
|
219
|
-
| `transition` | `AnimationOptions` | — | Global/default transition for variants |
|
|
220
|
-
| `custom` | `TCustom` | — | Custom data forwarded to function variants |
|
|
221
|
-
| `delayScope` | `"global" \| "local"` | `"global"` | Delay-function index scope (`globalIndex` vs local `index`) |
|
|
222
|
-
| `reducedMotion` | `"user" \| "always" \| "never"` | `"never"` | Reduced-motion behavior for this component |
|
|
223
|
-
| `onHoverStart` | `() => void` | — | Called when hover starts |
|
|
224
|
-
| `onHoverEnd` | `() => void` | — | Called when hover ends |
|
|
225
|
-
|
|
226
|
-
Exit animations use standard Motion behavior (`SplitText` must be a direct child of `AnimatePresence`):
|
|
161
|
+
### `<MorphText>` — [Morph docs](https://fetta.dimi.me/api/morph)
|
|
227
162
|
|
|
228
|
-
|
|
229
|
-
import { AnimatePresence } from "motion/react";
|
|
230
|
-
import { SplitText } from "fetta/motion";
|
|
163
|
+
Text morphing with stable token identity. Matching tokens interpolate position, new tokens enter, removed tokens exit. Supports `splitBy="chars"` (default) and `splitBy="words"`. The `initial`, `animate`, and `exit` props accept static targets or `({ index, count }) => Target` callbacks.
|
|
231
164
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
</SplitText>
|
|
246
|
-
)}
|
|
247
|
-
</AnimatePresence>;
|
|
165
|
+
```tsx
|
|
166
|
+
import { MorphText } from "fetta/morph";
|
|
167
|
+
|
|
168
|
+
<MorphText
|
|
169
|
+
splitBy="words"
|
|
170
|
+
initial={({ index, count }) => ({
|
|
171
|
+
opacity: 0,
|
|
172
|
+
x: index <= count / 2 ? -75 : 75,
|
|
173
|
+
})}
|
|
174
|
+
animate={{ opacity: 1, x: 0 }}
|
|
175
|
+
>
|
|
176
|
+
{statusText}
|
|
177
|
+
</MorphText>
|
|
248
178
|
```
|
|
249
179
|
|
|
250
|
-
### `createSplitClones(
|
|
180
|
+
### `createSplitClones()` — [Helpers docs](https://fetta.dimi.me/api/helpers)
|
|
251
181
|
|
|
252
|
-
|
|
182
|
+
Creates clone layers from split output for reveal/swap effects. Pass an existing `splitText()` result.
|
|
253
183
|
|
|
254
184
|
```ts
|
|
255
185
|
import { splitText } from "fetta";
|
|
@@ -257,97 +187,19 @@ import { createSplitClones } from "fetta/helpers";
|
|
|
257
187
|
|
|
258
188
|
const split = splitText(element, { type: "chars", mask: "chars" });
|
|
259
189
|
const layers = createSplitClones(split, { unit: "chars", wrap: true });
|
|
190
|
+
// animate layers.originals + layers.clones...
|
|
191
|
+
layers.cleanup();
|
|
260
192
|
```
|
|
261
193
|
|
|
262
|
-
#### Helper Options
|
|
263
|
-
|
|
264
|
-
| Option | Type | Default | Description |
|
|
265
|
-
|------|------|------|------|
|
|
266
|
-
| `unit` | `"chars" \| "words" \| "lines"` | — | Which split nodes to layer |
|
|
267
|
-
| `wrap` | `boolean` | `false` | Wrap each original in a track wrapper (`position: relative`) |
|
|
268
|
-
| `display` | `"auto" \| "inline-block" \| "block"` | `"auto"` | Track display when `wrap: true` (`lines` => `block`, others => `inline-block`) |
|
|
269
|
-
| `cloneOffset.axis` | `"x" \| "y"` | `"y"` | Axis used for initial clone offset |
|
|
270
|
-
| `cloneOffset.direction` | `"start" \| "end"` | `"start"` | Offset direction (`start` => negative) |
|
|
271
|
-
| `cloneOffset.distance` | `string` | `"100%"` | Offset distance |
|
|
272
|
-
| `trackClassName` / `cloneClassName` | `string \| (ctx) => string \| undefined` | — | Class names (static or per-item) |
|
|
273
|
-
| `trackStyle` / `cloneStyle` | `object \| (ctx) => object` | — | Inline styles (static or per-item) |
|
|
274
|
-
|
|
275
|
-
#### Helper Behavior
|
|
276
|
-
|
|
277
|
-
- Helper does not call `splitText()`; pass an existing split result.
|
|
278
|
-
- Clone is appended to the current parent of each original split node.
|
|
279
|
-
- `wrap: false` appends clone to existing parent.
|
|
280
|
-
- `wrap: true` moves original into a generated track, then appends clone there.
|
|
281
|
-
- `cleanup()` removes helper-created tracks/clones and is idempotent.
|
|
282
|
-
- `cleanup({ revertSplit: true })` also calls `split.revert()`.
|
|
283
|
-
|
|
284
|
-
For reveal/swap effects, match helper `unit` with `splitText` `mask` (`"chars"`, `"words"`, `"lines"`).
|
|
285
|
-
|
|
286
|
-
## CSS Classes
|
|
287
|
-
|
|
288
|
-
Default classes applied to split output:
|
|
289
|
-
|
|
290
|
-
| Class | Element |
|
|
291
|
-
|------|------|
|
|
292
|
-
| `.split-char` | Character span |
|
|
293
|
-
| `.split-word` | Word span |
|
|
294
|
-
| `.split-line` | Line span |
|
|
295
|
-
|
|
296
|
-
Split elements receive index attributes:
|
|
297
|
-
|
|
298
|
-
- `data-char-index`
|
|
299
|
-
- `data-word-index`
|
|
300
|
-
- `data-line-index`
|
|
301
|
-
|
|
302
|
-
## Font Loading
|
|
303
|
-
|
|
304
|
-
For stable kerning measurements in vanilla usage, wait for fonts before splitting:
|
|
305
|
-
|
|
306
|
-
```ts
|
|
307
|
-
document.fonts.ready.then(() => {
|
|
308
|
-
const { words } = splitText(element);
|
|
309
|
-
animate(words, { opacity: [0, 1] });
|
|
310
|
-
});
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
React and Motion components wait for fonts by default (`waitForFonts={true}`).
|
|
314
|
-
|
|
315
|
-
## Accessibility
|
|
316
|
-
|
|
317
|
-
Fetta keeps split text readable by screen readers:
|
|
318
|
-
|
|
319
|
-
- Headings/landmarks: split nodes are hidden from assistive tech and parent gets `aria-label`.
|
|
320
|
-
- Generic/nested content: visual split output is hidden and a screen-reader copy preserves semantics.
|
|
321
|
-
- Existing `aria-label` values are preserved.
|
|
322
|
-
|
|
323
194
|
## Notes
|
|
324
195
|
|
|
325
|
-
- Ligatures are disabled (`font-variant-ligatures: none`) because ligatures
|
|
326
|
-
-
|
|
327
|
-
|
|
328
|
-
## Browser Support
|
|
329
|
-
|
|
330
|
-
All modern browsers: Chrome, Firefox, Safari, Edge.
|
|
331
|
-
|
|
332
|
-
Requires:
|
|
333
|
-
|
|
334
|
-
- `ResizeObserver`
|
|
335
|
-
- `IntersectionObserver`
|
|
336
|
-
- `Intl.Segmenter`
|
|
337
|
-
|
|
338
|
-
Safari kerning compensation works, but font rendering precision can vary slightly by font. If you notice subtle shifts around `revert()`, use `disableKerning: true`.
|
|
196
|
+
- Ligatures are disabled (`font-variant-ligatures: none`) because ligatures can't span multiple elements.
|
|
197
|
+
- React and Motion components wait for fonts by default (`waitForFonts`). In vanilla, wrap calls in `document.fonts.ready`.
|
|
198
|
+
- Accessibility is automatic: headings get `aria-label`, generic elements get a screen-reader-only copy.
|
|
339
199
|
|
|
340
|
-
##
|
|
200
|
+
## Sponsors
|
|
341
201
|
|
|
342
|
-
|
|
343
|
-
- https://fetta.dimi.me/installation
|
|
344
|
-
- https://fetta.dimi.me/api/core
|
|
345
|
-
- https://fetta.dimi.me/api/react
|
|
346
|
-
- https://fetta.dimi.me/api/motion
|
|
347
|
-
- https://fetta.dimi.me/api/helpers
|
|
348
|
-
- https://fetta.dimi.me/examples/vanilla
|
|
349
|
-
- https://fetta.dimi.me/examples/react
|
|
350
|
-
- https://fetta.dimi.me/examples/motion
|
|
202
|
+
If you find Fetta useful, consider [sponsoring the project](https://github.com/sponsors/dimicx) to support continued development.
|
|
351
203
|
|
|
352
204
|
## License
|
|
353
205
|
|
|
@@ -470,6 +470,13 @@ var ARIA_LABEL_ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
|
470
470
|
"footer",
|
|
471
471
|
"main"
|
|
472
472
|
]);
|
|
473
|
+
var DEFAULT_RESPLIT_DEBOUNCE_MS = 100;
|
|
474
|
+
function resolveResplitDebounceMs(value) {
|
|
475
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
476
|
+
return DEFAULT_RESPLIT_DEBOUNCE_MS;
|
|
477
|
+
}
|
|
478
|
+
return value;
|
|
479
|
+
}
|
|
473
480
|
function splitTextData(element, rawOptions = {}) {
|
|
474
481
|
const options = rawOptions;
|
|
475
482
|
const {
|
|
@@ -1378,6 +1385,7 @@ function splitText(element, rawOptions = {}) {
|
|
|
1378
1385
|
lineClass = "split-line",
|
|
1379
1386
|
mask,
|
|
1380
1387
|
autoSplit = false,
|
|
1388
|
+
resplitDebounceMs,
|
|
1381
1389
|
onResplit,
|
|
1382
1390
|
onSplit,
|
|
1383
1391
|
revertOnComplete = false,
|
|
@@ -1387,6 +1395,7 @@ function splitText(element, rawOptions = {}) {
|
|
|
1387
1395
|
initialClasses
|
|
1388
1396
|
} = options;
|
|
1389
1397
|
const isolateKerningMeasurement = options.isolateKerningMeasurement !== false;
|
|
1398
|
+
const resolvedResplitDebounceMs = resolveResplitDebounceMs(resplitDebounceMs);
|
|
1390
1399
|
if (!(element instanceof HTMLElement)) {
|
|
1391
1400
|
throw new Error("splitText: element must be an HTMLElement");
|
|
1392
1401
|
}
|
|
@@ -1717,8 +1726,16 @@ function splitText(element, rawOptions = {}) {
|
|
|
1717
1726
|
lastChangedTarget = changedTarget;
|
|
1718
1727
|
if (autoSplitDebounceTimer) {
|
|
1719
1728
|
clearTimeout(autoSplitDebounceTimer);
|
|
1729
|
+
autoSplitDebounceTimer = null;
|
|
1720
1730
|
}
|
|
1721
|
-
|
|
1731
|
+
if (resolvedResplitDebounceMs <= 0) {
|
|
1732
|
+
handleResize();
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
autoSplitDebounceTimer = setTimeout(
|
|
1736
|
+
handleResize,
|
|
1737
|
+
resolvedResplitDebounceMs
|
|
1738
|
+
);
|
|
1722
1739
|
});
|
|
1723
1740
|
targets.forEach((target) => {
|
|
1724
1741
|
autoSplitObserver.observe(target);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -28,18 +28,6 @@ function reapplyInitialClasses(elements, className) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// src/internal/waitForFontsReady.ts
|
|
32
|
-
async function waitForFontsReady(waitForFonts) {
|
|
33
|
-
if (!waitForFonts) return;
|
|
34
|
-
const fonts = document.fonts;
|
|
35
|
-
const ready = fonts?.ready;
|
|
36
|
-
if (!ready || typeof ready.then !== "function") return;
|
|
37
|
-
try {
|
|
38
|
-
await ready;
|
|
39
|
-
} catch {
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
31
|
// src/internal/viewportObserver.ts
|
|
44
32
|
function createViewportObserver(options, hasTriggeredOnceRef, onEnter, onLeave) {
|
|
45
33
|
const vpOptions = options ?? {};
|
|
@@ -70,4 +58,4 @@ function createViewportObserver(options, hasTriggeredOnceRef, onEnter, onLeave)
|
|
|
70
58
|
);
|
|
71
59
|
}
|
|
72
60
|
|
|
73
|
-
export { createViewportObserver, reapplyInitialClasses, reapplyInitialStyles
|
|
61
|
+
export { createViewportObserver, reapplyInitialClasses, reapplyInitialStyles };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/internal/waitForFontsReady.ts
|
|
2
|
+
async function waitForFontsReady(waitForFonts) {
|
|
3
|
+
if (!waitForFonts) return;
|
|
4
|
+
const fonts = document.fonts;
|
|
5
|
+
const ready = fonts?.ready;
|
|
6
|
+
if (!ready || typeof ready.then !== "function") return;
|
|
7
|
+
try {
|
|
8
|
+
await ready;
|
|
9
|
+
} catch {
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { waitForFontsReady };
|
package/dist/helpers.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ interface SplitTextOptions {
|
|
|
32
32
|
mask?: "lines" | "words" | "chars";
|
|
33
33
|
/** Auto-split on resize (observes parent element) */
|
|
34
34
|
autoSplit?: boolean;
|
|
35
|
+
/** Debounce delay for autoSplit/full-resplit width updates in milliseconds (`0` disables debounce). */
|
|
36
|
+
resplitDebounceMs?: number;
|
|
35
37
|
/** Callback when autoSplit/full-resplit replaces split output elements */
|
|
36
38
|
onResplit?: (result: Omit<SplitTextResult, "revert" | "dispose">) => void;
|
|
37
39
|
/** Callback fired after text is split, receives split elements. Return animation for revertOnComplete. */
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { a as SplitTextOptions, S as SplitTextResult, s as splitText } from './index-
|
|
1
|
+
export { a as SplitTextOptions, S as SplitTextResult, s as splitText } from './index-Box92Sue.js';
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { splitText } from './chunk-
|
|
1
|
+
export { splitText } from './chunk-44MU2I5B.js';
|
package/dist/morph.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { AnimationOptions } from 'motion';
|
|
2
|
+
import { HTMLMotionProps } from 'motion/react';
|
|
3
|
+
import { CSSProperties, RefAttributes } from 'react';
|
|
4
|
+
|
|
5
|
+
interface MorphVariantInfo {
|
|
6
|
+
/** Index of this element among all animated elements */
|
|
7
|
+
index: number;
|
|
8
|
+
/** Total number of animated elements */
|
|
9
|
+
count: number;
|
|
10
|
+
}
|
|
11
|
+
type StaticMotionInitialProp = HTMLMotionProps<"div">["initial"];
|
|
12
|
+
type StaticMotionAnimateProp = HTMLMotionProps<"div">["animate"];
|
|
13
|
+
type StaticMotionExitProp = HTMLMotionProps<"div">["exit"];
|
|
14
|
+
type MotionInitialProp = StaticMotionInitialProp | ((info: MorphVariantInfo) => StaticMotionInitialProp);
|
|
15
|
+
type MotionAnimateProp = StaticMotionAnimateProp | ((info: MorphVariantInfo) => StaticMotionAnimateProp);
|
|
16
|
+
type MotionExitProp = StaticMotionExitProp | ((info: MorphVariantInfo) => StaticMotionExitProp);
|
|
17
|
+
type ControlledWrapperMotionKeys = "children" | "className" | "style" | "as" | "ref" | "transition" | "reducedMotion" | "initial" | "animate" | "exit";
|
|
18
|
+
type WrapperMotionProps = Omit<HTMLMotionProps<"div">, ControlledWrapperMotionKeys>;
|
|
19
|
+
interface MorphTextProps extends WrapperMotionProps {
|
|
20
|
+
children: string;
|
|
21
|
+
as?: keyof HTMLElementTagNameMap;
|
|
22
|
+
className?: string;
|
|
23
|
+
style?: CSSProperties;
|
|
24
|
+
transition?: AnimationOptions;
|
|
25
|
+
waitForFonts?: boolean;
|
|
26
|
+
reducedMotion?: "user" | "always" | "never";
|
|
27
|
+
splitBy?: "chars" | "words";
|
|
28
|
+
animateInitial?: boolean;
|
|
29
|
+
initial?: MotionInitialProp;
|
|
30
|
+
animate?: MotionAnimateProp;
|
|
31
|
+
exit?: MotionExitProp;
|
|
32
|
+
stagger?: number;
|
|
33
|
+
onMorphComplete?: () => void;
|
|
34
|
+
}
|
|
35
|
+
type MorphTextComponent = (props: MorphTextProps & RefAttributes<HTMLElement>) => React.ReactElement | null;
|
|
36
|
+
declare const MorphText: MorphTextComponent;
|
|
37
|
+
|
|
38
|
+
export { MorphText, type MorphTextProps, type MorphVariantInfo };
|
package/dist/morph.js
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import './chunk-HSIMGGOI.js';
|
|
2
|
+
import { waitForFontsReady } from './chunk-V23SUR2S.js';
|
|
3
|
+
import { splitTextData } from './chunk-44MU2I5B.js';
|
|
4
|
+
import { useReducedMotion, motion, AnimatePresence, MotionConfig } from 'motion/react';
|
|
5
|
+
import { forwardRef, useRef, useCallback, useState, useMemo, useEffect, createElement } from 'react';
|
|
6
|
+
|
|
7
|
+
// src/internal/splitIdentity.ts
|
|
8
|
+
var DEFAULT_ID_PREFIX = "c";
|
|
9
|
+
function nextIdentityId(prefix, counter) {
|
|
10
|
+
return `${prefix}${counter}`;
|
|
11
|
+
}
|
|
12
|
+
function findMatchesByLcs(prevValues, nextValues) {
|
|
13
|
+
const n = prevValues.length;
|
|
14
|
+
const m = nextValues.length;
|
|
15
|
+
const dp = Array.from(
|
|
16
|
+
{ length: n + 1 },
|
|
17
|
+
() => new Array(m + 1).fill(0)
|
|
18
|
+
);
|
|
19
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
20
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
21
|
+
if (prevValues[i2] === nextValues[j2]) {
|
|
22
|
+
dp[i2][j2] = dp[i2 + 1][j2 + 1] + 1;
|
|
23
|
+
} else {
|
|
24
|
+
dp[i2][j2] = Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const matches = [];
|
|
29
|
+
let i = 0;
|
|
30
|
+
let j = 0;
|
|
31
|
+
while (i < n && j < m) {
|
|
32
|
+
if (prevValues[i] === nextValues[j]) {
|
|
33
|
+
matches.push([i, j]);
|
|
34
|
+
i += 1;
|
|
35
|
+
j += 1;
|
|
36
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
37
|
+
i += 1;
|
|
38
|
+
} else {
|
|
39
|
+
j += 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return matches;
|
|
43
|
+
}
|
|
44
|
+
function reconcileSplitIdentity(prevSnapshot, nextValuesInput, options = {}) {
|
|
45
|
+
const unit = options.unit ?? "chars";
|
|
46
|
+
const idPrefix = typeof options.idPrefix === "string" && options.idPrefix.length > 0 ? options.idPrefix : unit === "words" ? "w" : DEFAULT_ID_PREFIX;
|
|
47
|
+
const prevIds = prevSnapshot?.ids ?? [];
|
|
48
|
+
const prevValues = prevSnapshot?.values ?? [];
|
|
49
|
+
const nextValues = Array.from(nextValuesInput);
|
|
50
|
+
let nextIdCounter = prevSnapshot?.nextId ?? 0;
|
|
51
|
+
const matches = findMatchesByLcs(prevValues, nextValues);
|
|
52
|
+
const prevIndexByNextIndex = new Array(nextValues.length).fill(-1);
|
|
53
|
+
const nextIndexByPrevIndex = new Array(prevValues.length).fill(-1);
|
|
54
|
+
matches.forEach(([prevIndex, nextIndex]) => {
|
|
55
|
+
prevIndexByNextIndex[nextIndex] = prevIndex;
|
|
56
|
+
nextIndexByPrevIndex[prevIndex] = nextIndex;
|
|
57
|
+
});
|
|
58
|
+
const nextIds = new Array(nextValues.length);
|
|
59
|
+
for (let nextIndex = 0; nextIndex < nextValues.length; nextIndex++) {
|
|
60
|
+
const prevIndex = prevIndexByNextIndex[nextIndex];
|
|
61
|
+
if (prevIndex >= 0 && prevIds[prevIndex]) {
|
|
62
|
+
nextIds[nextIndex] = prevIds[prevIndex];
|
|
63
|
+
} else {
|
|
64
|
+
nextIds[nextIndex] = nextIdentityId(idPrefix, nextIdCounter);
|
|
65
|
+
nextIdCounter += 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const changes = [];
|
|
69
|
+
for (let nextIndex = 0; nextIndex < nextValues.length; nextIndex++) {
|
|
70
|
+
const prevIndex = prevIndexByNextIndex[nextIndex];
|
|
71
|
+
const id = nextIds[nextIndex];
|
|
72
|
+
if (prevIndex >= 0) {
|
|
73
|
+
changes.push({
|
|
74
|
+
id,
|
|
75
|
+
status: "persist",
|
|
76
|
+
value: nextValues[nextIndex],
|
|
77
|
+
prevIndex,
|
|
78
|
+
nextIndex
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
changes.push({
|
|
82
|
+
id,
|
|
83
|
+
status: "enter",
|
|
84
|
+
value: nextValues[nextIndex],
|
|
85
|
+
nextIndex
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (let prevIndex = 0; prevIndex < prevValues.length; prevIndex++) {
|
|
90
|
+
if (nextIndexByPrevIndex[prevIndex] >= 0) continue;
|
|
91
|
+
const id = prevIds[prevIndex] ?? nextIdentityId(idPrefix, nextIdCounter);
|
|
92
|
+
if (!prevIds[prevIndex]) {
|
|
93
|
+
nextIdCounter += 1;
|
|
94
|
+
}
|
|
95
|
+
changes.push({
|
|
96
|
+
id,
|
|
97
|
+
status: "exit",
|
|
98
|
+
value: prevValues[prevIndex] ?? "",
|
|
99
|
+
prevIndex
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
snapshot: {
|
|
104
|
+
unit,
|
|
105
|
+
ids: nextIds,
|
|
106
|
+
values: nextValues,
|
|
107
|
+
nextId: nextIdCounter
|
|
108
|
+
},
|
|
109
|
+
changes
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
var SPLIT_ID_ATTR = "data-fetta-id";
|
|
113
|
+
var DEFAULT_ENTER_STATE = { opacity: 0 };
|
|
114
|
+
var DEFAULT_ANIMATE_STATE = { opacity: 1 };
|
|
115
|
+
var DEFAULT_EXIT_STATE = { opacity: 0 };
|
|
116
|
+
var DEFAULT_TRANSITION = { type: "spring", bounce: 0, duration: 0.4 };
|
|
117
|
+
function parseStyleValue(styleText) {
|
|
118
|
+
const style = {};
|
|
119
|
+
const parts = styleText.split(";").map((part) => part.trim());
|
|
120
|
+
for (const part of parts) {
|
|
121
|
+
if (!part) continue;
|
|
122
|
+
const [rawKey, ...rawValueParts] = part.split(":");
|
|
123
|
+
if (!rawKey || rawValueParts.length === 0) continue;
|
|
124
|
+
const rawValue = rawValueParts.join(":").trim();
|
|
125
|
+
const key = rawKey.trim();
|
|
126
|
+
if (key.startsWith("--")) {
|
|
127
|
+
style[key] = rawValue;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const camelKey = key.replace(
|
|
131
|
+
/-([a-z])/g,
|
|
132
|
+
(_, char) => char.toUpperCase()
|
|
133
|
+
);
|
|
134
|
+
style[camelKey] = rawValue;
|
|
135
|
+
}
|
|
136
|
+
return style;
|
|
137
|
+
}
|
|
138
|
+
function attrsToProps(attrs) {
|
|
139
|
+
const props = {};
|
|
140
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
141
|
+
if (name === "class") {
|
|
142
|
+
props.className = value;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (name === "style") {
|
|
146
|
+
props.style = parseStyleValue(value);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
props[name] = value;
|
|
150
|
+
}
|
|
151
|
+
return props;
|
|
152
|
+
}
|
|
153
|
+
var motionComponentCache = /* @__PURE__ */ new Map();
|
|
154
|
+
function getMotionComponent(tag) {
|
|
155
|
+
let component = motionComponentCache.get(tag);
|
|
156
|
+
if (!component) {
|
|
157
|
+
const registry = motion;
|
|
158
|
+
component = registry[tag] ?? motion.span;
|
|
159
|
+
motionComponentCache.set(tag, component);
|
|
160
|
+
}
|
|
161
|
+
return component;
|
|
162
|
+
}
|
|
163
|
+
function readSerializedNodeText(node) {
|
|
164
|
+
if (node.type === "text") {
|
|
165
|
+
return node.text;
|
|
166
|
+
}
|
|
167
|
+
return node.children.map(readSerializedNodeText).join("");
|
|
168
|
+
}
|
|
169
|
+
function collectRawTokens(nodes, splitBy = "chars") {
|
|
170
|
+
const rawTokens = [];
|
|
171
|
+
let nonCharIndex = 0;
|
|
172
|
+
const targetSplit = splitBy === "words" ? "word" : "char";
|
|
173
|
+
const walk = (list) => {
|
|
174
|
+
for (const node of list) {
|
|
175
|
+
if (node.type === "text") {
|
|
176
|
+
if (node.text.length > 0) {
|
|
177
|
+
rawTokens.push({
|
|
178
|
+
tokenType: "text",
|
|
179
|
+
key: `t-${nonCharIndex++}`,
|
|
180
|
+
value: node.text
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (node.attrs["data-fetta-sr-copy"] === "true") {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (node.tag === "br") {
|
|
189
|
+
rawTokens.push({
|
|
190
|
+
tokenType: "br",
|
|
191
|
+
key: `br-${nonCharIndex++}`,
|
|
192
|
+
attrs: node.attrs
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (node.split === targetSplit) {
|
|
197
|
+
rawTokens.push({
|
|
198
|
+
tokenType: splitBy === "words" ? "word" : "char",
|
|
199
|
+
node,
|
|
200
|
+
value: readSerializedNodeText(node)
|
|
201
|
+
});
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
walk(node.children);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
walk(nodes);
|
|
208
|
+
return rawTokens;
|
|
209
|
+
}
|
|
210
|
+
function buildRenderTokens(nodes, previousSnapshot, splitBy = "chars") {
|
|
211
|
+
const rawTokens = collectRawTokens(nodes, splitBy);
|
|
212
|
+
const unitTokenType = splitBy === "words" ? "word" : "char";
|
|
213
|
+
const rawUnits = rawTokens.filter(
|
|
214
|
+
(token) => token.tokenType === unitTokenType
|
|
215
|
+
);
|
|
216
|
+
const diff = reconcileSplitIdentity(
|
|
217
|
+
previousSnapshot,
|
|
218
|
+
rawUnits.map((token) => token.value),
|
|
219
|
+
{ unit: splitBy }
|
|
220
|
+
);
|
|
221
|
+
const statusByNextIndex = /* @__PURE__ */ new Map();
|
|
222
|
+
let hasExits = false;
|
|
223
|
+
for (const change of diff.changes) {
|
|
224
|
+
if (typeof change.nextIndex === "number") {
|
|
225
|
+
statusByNextIndex.set(change.nextIndex, change.status);
|
|
226
|
+
}
|
|
227
|
+
if (change.status === "exit") {
|
|
228
|
+
hasExits = true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const fallbackPrefix = splitBy === "words" ? "w" : "c";
|
|
232
|
+
let unitIndex = 0;
|
|
233
|
+
const intermediateTokens = rawTokens.map((token) => {
|
|
234
|
+
if (token.tokenType === "text") {
|
|
235
|
+
return token;
|
|
236
|
+
}
|
|
237
|
+
if (token.tokenType === "br") {
|
|
238
|
+
return {
|
|
239
|
+
tokenType: "br",
|
|
240
|
+
key: token.key,
|
|
241
|
+
props: attrsToProps(token.attrs)
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const id = diff.snapshot.ids[unitIndex] ?? `${fallbackPrefix}-fallback-${unitIndex}`;
|
|
245
|
+
const status = statusByNextIndex.get(unitIndex) ?? "persist";
|
|
246
|
+
unitIndex += 1;
|
|
247
|
+
return {
|
|
248
|
+
tokenType: unitTokenType,
|
|
249
|
+
id,
|
|
250
|
+
status,
|
|
251
|
+
tag: token.node.tag,
|
|
252
|
+
props: attrsToProps(token.node.attrs),
|
|
253
|
+
value: token.value
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
let tokens;
|
|
257
|
+
if (splitBy === "words") {
|
|
258
|
+
tokens = [];
|
|
259
|
+
let prevWasWord = false;
|
|
260
|
+
for (const token of intermediateTokens) {
|
|
261
|
+
if (token.tokenType === "word" && prevWasWord) {
|
|
262
|
+
tokens.push({
|
|
263
|
+
tokenType: "space",
|
|
264
|
+
id: `sp-${token.id}`,
|
|
265
|
+
status: token.status
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
tokens.push(token);
|
|
269
|
+
if (token.tokenType === "word" || token.tokenType === "br") {
|
|
270
|
+
prevWasWord = token.tokenType === "word";
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
tokens = [];
|
|
275
|
+
for (let i = 0; i < intermediateTokens.length; i++) {
|
|
276
|
+
const token = intermediateTokens[i];
|
|
277
|
+
if (token.tokenType === "text" && /^\s+$/.test(token.value)) {
|
|
278
|
+
const next = intermediateTokens[i + 1];
|
|
279
|
+
const prev = intermediateTokens[i - 1];
|
|
280
|
+
const neighbor = next?.tokenType === "char" ? next : prev?.tokenType === "char" ? prev : null;
|
|
281
|
+
const status = neighbor ? neighbor.status : "persist";
|
|
282
|
+
const prevId = prev?.tokenType === "char" ? prev.id : "start";
|
|
283
|
+
const nextId = next?.tokenType === "char" ? next.id : "end";
|
|
284
|
+
tokens.push({
|
|
285
|
+
tokenType: "space",
|
|
286
|
+
id: `sp-${prevId}-${nextId}`,
|
|
287
|
+
status
|
|
288
|
+
});
|
|
289
|
+
} else {
|
|
290
|
+
tokens.push(token);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
let unitCount = 0;
|
|
295
|
+
let enterCount = 0;
|
|
296
|
+
for (const token of tokens) {
|
|
297
|
+
if (token.tokenType === "char" || token.tokenType === "word" || token.tokenType === "space") {
|
|
298
|
+
token.index = unitCount;
|
|
299
|
+
unitCount += 1;
|
|
300
|
+
if (token.status === "enter") {
|
|
301
|
+
token.enterIndex = enterCount;
|
|
302
|
+
enterCount += 1;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
snapshot: diff.snapshot,
|
|
308
|
+
tokens,
|
|
309
|
+
enterCount,
|
|
310
|
+
unitCount,
|
|
311
|
+
hasExits
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
var MorphText = forwardRef(function MorphText2({
|
|
315
|
+
children,
|
|
316
|
+
as: Component = "span",
|
|
317
|
+
className,
|
|
318
|
+
style: userStyle,
|
|
319
|
+
transition,
|
|
320
|
+
waitForFonts = true,
|
|
321
|
+
reducedMotion = "user",
|
|
322
|
+
splitBy = "chars",
|
|
323
|
+
animateInitial = false,
|
|
324
|
+
initial: initialProp,
|
|
325
|
+
animate: animateProp,
|
|
326
|
+
exit: exitProp,
|
|
327
|
+
stagger: staggerProp,
|
|
328
|
+
onMorphComplete,
|
|
329
|
+
...wrapperProps
|
|
330
|
+
}, forwardedRef) {
|
|
331
|
+
const wrapperRef = useRef(null);
|
|
332
|
+
const mergedRef = useCallback(
|
|
333
|
+
(node) => {
|
|
334
|
+
wrapperRef.current = node;
|
|
335
|
+
if (typeof forwardedRef === "function") {
|
|
336
|
+
forwardedRef(node);
|
|
337
|
+
} else if (forwardedRef) {
|
|
338
|
+
forwardedRef.current = node;
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
[forwardedRef]
|
|
342
|
+
);
|
|
343
|
+
const enterState = initialProp === false ? false : initialProp ?? DEFAULT_ENTER_STATE;
|
|
344
|
+
const animateState = animateProp ?? DEFAULT_ANIMATE_STATE;
|
|
345
|
+
const exitState = exitProp ?? DEFAULT_EXIT_STATE;
|
|
346
|
+
const [tokens, setTokens] = useState(null);
|
|
347
|
+
const [unitCount, setUnitCount] = useState(0);
|
|
348
|
+
const [isReady, setIsReady] = useState(false);
|
|
349
|
+
const snapshotRef = useRef(null);
|
|
350
|
+
const prefersReducedMotion = useReducedMotion();
|
|
351
|
+
const reduceMotionActive = reducedMotion === "always" || reducedMotion === "user" && !!prefersReducedMotion;
|
|
352
|
+
const reducedTransition = useMemo(
|
|
353
|
+
() => ({ duration: 0, delay: 0, layout: { duration: 0 } }),
|
|
354
|
+
[]
|
|
355
|
+
);
|
|
356
|
+
const resolvedTransition = reduceMotionActive ? reducedTransition : transition ?? DEFAULT_TRANSITION;
|
|
357
|
+
const transitionRef = useRef(resolvedTransition);
|
|
358
|
+
transitionRef.current = resolvedTransition;
|
|
359
|
+
const onMorphCompleteRef = useRef(onMorphComplete);
|
|
360
|
+
onMorphCompleteRef.current = onMorphComplete;
|
|
361
|
+
const enterRemainingRef = useRef(0);
|
|
362
|
+
const exitsActiveRef = useRef(false);
|
|
363
|
+
const isFirstRenderRef = useRef(true);
|
|
364
|
+
const staggerRef = useRef(staggerProp);
|
|
365
|
+
staggerRef.current = staggerProp;
|
|
366
|
+
const enterStateRef = useRef(enterState);
|
|
367
|
+
enterStateRef.current = enterState;
|
|
368
|
+
const animateStateRef = useRef(animateState);
|
|
369
|
+
animateStateRef.current = animateState;
|
|
370
|
+
const exitStateRef = useRef(exitState);
|
|
371
|
+
exitStateRef.current = exitState;
|
|
372
|
+
const tryFireMorphComplete = useCallback(() => {
|
|
373
|
+
if (enterRemainingRef.current <= 0 && !exitsActiveRef.current) {
|
|
374
|
+
onMorphCompleteRef.current?.();
|
|
375
|
+
}
|
|
376
|
+
}, []);
|
|
377
|
+
const buildSplitDataFromProbe = useCallback((nextText, unit) => {
|
|
378
|
+
const wrapperElement = wrapperRef.current;
|
|
379
|
+
if (!wrapperElement) return null;
|
|
380
|
+
const parentElement = wrapperElement.parentElement;
|
|
381
|
+
if (!parentElement) return null;
|
|
382
|
+
const probeHost = wrapperElement.ownerDocument.createElement("div");
|
|
383
|
+
probeHost.style.position = "fixed";
|
|
384
|
+
probeHost.style.left = "-99999px";
|
|
385
|
+
probeHost.style.top = "0";
|
|
386
|
+
probeHost.style.visibility = "hidden";
|
|
387
|
+
probeHost.style.pointerEvents = "none";
|
|
388
|
+
probeHost.style.contain = "layout style paint";
|
|
389
|
+
probeHost.style.width = `${Math.max(1, parentElement.getBoundingClientRect().width)}px`;
|
|
390
|
+
const probeElement = wrapperElement.cloneNode(false);
|
|
391
|
+
probeElement.textContent = nextText;
|
|
392
|
+
probeHost.appendChild(probeElement);
|
|
393
|
+
parentElement.appendChild(probeHost);
|
|
394
|
+
try {
|
|
395
|
+
return splitTextData(probeElement, { type: unit === "words" ? "words" : "chars" });
|
|
396
|
+
} finally {
|
|
397
|
+
probeHost.remove();
|
|
398
|
+
}
|
|
399
|
+
}, []);
|
|
400
|
+
const measureAndSetTokens = useCallback(
|
|
401
|
+
(nextText, unit) => {
|
|
402
|
+
const nextData = buildSplitDataFromProbe(nextText, unit);
|
|
403
|
+
if (!nextData) return;
|
|
404
|
+
const { snapshot, tokens: nextTokens, enterCount, unitCount: nextUnitCount, hasExits } = buildRenderTokens(
|
|
405
|
+
nextData.nodes,
|
|
406
|
+
snapshotRef.current,
|
|
407
|
+
unit
|
|
408
|
+
);
|
|
409
|
+
snapshotRef.current = snapshot;
|
|
410
|
+
const isFirst = isFirstRenderRef.current;
|
|
411
|
+
isFirstRenderRef.current = false;
|
|
412
|
+
const skipCallback = isFirst && !animateInitial;
|
|
413
|
+
const hasAnimation = enterCount > 0 || hasExits;
|
|
414
|
+
if (!skipCallback && hasAnimation && onMorphCompleteRef.current) {
|
|
415
|
+
enterRemainingRef.current = enterCount;
|
|
416
|
+
exitsActiveRef.current = hasExits;
|
|
417
|
+
} else {
|
|
418
|
+
enterRemainingRef.current = 0;
|
|
419
|
+
exitsActiveRef.current = false;
|
|
420
|
+
}
|
|
421
|
+
setTokens(nextTokens);
|
|
422
|
+
setUnitCount(nextUnitCount);
|
|
423
|
+
setIsReady(true);
|
|
424
|
+
},
|
|
425
|
+
[buildSplitDataFromProbe, animateInitial]
|
|
426
|
+
);
|
|
427
|
+
useEffect(() => {
|
|
428
|
+
if (typeof children !== "string") return;
|
|
429
|
+
let cancelled = false;
|
|
430
|
+
waitForFontsReady(waitForFonts).then(() => {
|
|
431
|
+
if (cancelled) return;
|
|
432
|
+
measureAndSetTokens(children, splitBy);
|
|
433
|
+
});
|
|
434
|
+
return () => {
|
|
435
|
+
cancelled = true;
|
|
436
|
+
};
|
|
437
|
+
}, [children, waitForFonts, splitBy, measureAndSetTokens]);
|
|
438
|
+
const handleEnterAnimationComplete = useCallback(() => {
|
|
439
|
+
enterRemainingRef.current = Math.max(0, enterRemainingRef.current - 1);
|
|
440
|
+
tryFireMorphComplete();
|
|
441
|
+
}, [tryFireMorphComplete]);
|
|
442
|
+
const handleExitComplete = useCallback(() => {
|
|
443
|
+
exitsActiveRef.current = false;
|
|
444
|
+
tryFireMorphComplete();
|
|
445
|
+
}, [tryFireMorphComplete]);
|
|
446
|
+
const renderToken = useCallback(
|
|
447
|
+
(token) => {
|
|
448
|
+
if (token.tokenType === "text") {
|
|
449
|
+
return token.value;
|
|
450
|
+
}
|
|
451
|
+
if (token.tokenType === "br") {
|
|
452
|
+
return createElement("br", {
|
|
453
|
+
key: token.key,
|
|
454
|
+
...token.props,
|
|
455
|
+
"aria-hidden": "true"
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
const currentEnterState = enterStateRef.current;
|
|
459
|
+
const currentAnimateState = animateStateRef.current;
|
|
460
|
+
const currentExitState = exitStateRef.current;
|
|
461
|
+
const currentStagger = staggerRef.current;
|
|
462
|
+
const info = { index: token.index, count: unitCount };
|
|
463
|
+
const resolvedEnterState = typeof currentEnterState === "function" ? currentEnterState(info) : currentEnterState;
|
|
464
|
+
const resolvedAnimateState = typeof currentAnimateState === "function" ? currentAnimateState(info) : currentAnimateState;
|
|
465
|
+
const resolvedExitState = typeof currentExitState === "function" ? currentExitState(info) : currentExitState;
|
|
466
|
+
let tokenTransition = transitionRef.current;
|
|
467
|
+
if (currentStagger && typeof token.enterIndex === "number") {
|
|
468
|
+
const existingDelay = tokenTransition?.delay;
|
|
469
|
+
const baseDelay = typeof existingDelay === "number" ? existingDelay : 0;
|
|
470
|
+
tokenTransition = {
|
|
471
|
+
...tokenTransition,
|
|
472
|
+
delay: baseDelay + token.enterIndex * currentStagger
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
if (token.tokenType === "space") {
|
|
476
|
+
return createElement(
|
|
477
|
+
motion.span,
|
|
478
|
+
{
|
|
479
|
+
key: `space-${token.id}`,
|
|
480
|
+
"aria-hidden": "true",
|
|
481
|
+
layout: "position",
|
|
482
|
+
initial: token.status === "enter" ? resolvedEnterState : false,
|
|
483
|
+
animate: resolvedAnimateState,
|
|
484
|
+
exit: resolvedExitState,
|
|
485
|
+
transition: tokenTransition,
|
|
486
|
+
style: { display: "inline", whiteSpace: "pre" },
|
|
487
|
+
onAnimationComplete: token.status === "enter" ? handleEnterAnimationComplete : void 0
|
|
488
|
+
},
|
|
489
|
+
" "
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
const propsStyle = token.props.style ?? void 0;
|
|
493
|
+
const MotionTag = getMotionComponent(token.tag);
|
|
494
|
+
const keyPrefix = token.tokenType === "word" ? "word" : "char";
|
|
495
|
+
return createElement(
|
|
496
|
+
MotionTag,
|
|
497
|
+
{
|
|
498
|
+
key: `${keyPrefix}-${token.id}`,
|
|
499
|
+
...token.props,
|
|
500
|
+
"aria-hidden": "true",
|
|
501
|
+
[SPLIT_ID_ATTR]: token.id,
|
|
502
|
+
layout: "position",
|
|
503
|
+
initial: token.status === "enter" ? resolvedEnterState : false,
|
|
504
|
+
animate: resolvedAnimateState,
|
|
505
|
+
exit: resolvedExitState,
|
|
506
|
+
transition: tokenTransition,
|
|
507
|
+
style: {
|
|
508
|
+
display: "inline-block",
|
|
509
|
+
whiteSpace: "pre",
|
|
510
|
+
...propsStyle
|
|
511
|
+
},
|
|
512
|
+
onAnimationComplete: token.status === "enter" ? handleEnterAnimationComplete : void 0
|
|
513
|
+
},
|
|
514
|
+
token.value
|
|
515
|
+
);
|
|
516
|
+
},
|
|
517
|
+
[handleEnterAnimationComplete, unitCount]
|
|
518
|
+
);
|
|
519
|
+
if (typeof children !== "string") {
|
|
520
|
+
console.error("MorphText: children must be a string.");
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
const Wrapper = getMotionComponent(Component);
|
|
524
|
+
const wrapperStyle = {
|
|
525
|
+
...userStyle,
|
|
526
|
+
visibility: isReady || !waitForFonts ? "visible" : "hidden"
|
|
527
|
+
};
|
|
528
|
+
const wrapperAttrs = {
|
|
529
|
+
ref: mergedRef,
|
|
530
|
+
...wrapperProps,
|
|
531
|
+
className,
|
|
532
|
+
style: wrapperStyle
|
|
533
|
+
};
|
|
534
|
+
if (wrapperAttrs["aria-label"] === void 0) {
|
|
535
|
+
wrapperAttrs["aria-label"] = children;
|
|
536
|
+
}
|
|
537
|
+
const content = createElement(
|
|
538
|
+
Wrapper,
|
|
539
|
+
wrapperAttrs,
|
|
540
|
+
tokens ? createElement(
|
|
541
|
+
AnimatePresence,
|
|
542
|
+
{
|
|
543
|
+
mode: "popLayout",
|
|
544
|
+
initial: animateInitial,
|
|
545
|
+
onExitComplete: handleExitComplete
|
|
546
|
+
},
|
|
547
|
+
tokens.map(renderToken)
|
|
548
|
+
) : children
|
|
549
|
+
);
|
|
550
|
+
if (reducedMotion !== "never") {
|
|
551
|
+
return createElement(MotionConfig, { reducedMotion }, content);
|
|
552
|
+
}
|
|
553
|
+
return content;
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
export { MorphText };
|
package/dist/motion.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { I as InitialStyles, a as InitialClasses } from './initialStyles-BGuPp5C
|
|
|
2
2
|
import { DOMKeyframesDefinition, AnimationOptions, scroll } from 'motion';
|
|
3
3
|
import { HTMLMotionProps } from 'motion/react';
|
|
4
4
|
import { ReactElement, RefAttributes } from 'react';
|
|
5
|
-
import { A as AnimationCallbackReturn } from './index-
|
|
6
|
-
export { a as CoreSplitTextOptions, S as SplitTextResult } from './index-
|
|
5
|
+
import { A as AnimationCallbackReturn } from './index-Box92Sue.js';
|
|
6
|
+
export { a as CoreSplitTextOptions, S as SplitTextResult } from './index-Box92Sue.js';
|
|
7
7
|
|
|
8
8
|
interface SplitTextOptions {
|
|
9
9
|
type?: "chars" | "words" | "lines" | "chars,words" | "words,lines" | "chars,lines" | "chars,words,lines";
|
|
@@ -12,6 +12,8 @@ interface SplitTextOptions {
|
|
|
12
12
|
lineClass?: string;
|
|
13
13
|
/** Apply overflow mask wrapper to elements for reveal animations */
|
|
14
14
|
mask?: "lines" | "words" | "chars";
|
|
15
|
+
/** Debounce delay for autoSplit/full-resplit width updates in milliseconds (`0` disables debounce). */
|
|
16
|
+
resplitDebounceMs?: number;
|
|
15
17
|
propIndex?: boolean;
|
|
16
18
|
/** Skip kerning compensation (no margin adjustments applied).
|
|
17
19
|
* Kerning is naturally lost when splitting into inline-block spans.
|
package/dist/motion.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { createViewportObserver, reapplyInitialStyles, reapplyInitialClasses } from './chunk-TEDMJHF7.js';
|
|
2
|
+
import './chunk-HSIMGGOI.js';
|
|
3
|
+
import { waitForFontsReady } from './chunk-V23SUR2S.js';
|
|
4
|
+
import { splitTextData, buildLineFingerprintFromData, normalizeToPromise, buildKerningStyleKey, resolveAutoSplitTargets, getObservedWidth, recordWidthChange, resolveAutoSplitWidth, querySplitWords, clearKerningCompensation, applyKerningCompensation } from './chunk-44MU2I5B.js';
|
|
3
5
|
import { scroll, animate } from 'motion';
|
|
4
6
|
import { usePresence, useReducedMotion, MotionConfig, motion } from 'motion/react';
|
|
5
7
|
import { forwardRef, useRef, useState, useCallback, useMemo, useLayoutEffect, isValidElement, useEffect, createElement, cloneElement } from 'react';
|
|
6
8
|
|
|
9
|
+
var DEFAULT_RESPLIT_DEBOUNCE_MS = 100;
|
|
10
|
+
function resolveResplitDebounceMs(value) {
|
|
11
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
12
|
+
return DEFAULT_RESPLIT_DEBOUNCE_MS;
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
7
16
|
var ELEMENT_TYPE_KEYS = ["chars", "words", "lines"];
|
|
8
17
|
var VOID_HTML_TAGS = /* @__PURE__ */ new Set(["br", "hr", "img", "input", "meta", "link"]);
|
|
9
18
|
function isPerTypeVariant(v) {
|
|
@@ -1097,6 +1106,15 @@ var SplitText = forwardRef(function SplitText2({
|
|
|
1097
1106
|
}
|
|
1098
1107
|
return safeFallbackWidth;
|
|
1099
1108
|
}, []);
|
|
1109
|
+
const lockCurrentRenderedLines = useCallback((root) => {
|
|
1110
|
+
const lineClass = optionsRef.current?.lineClass ?? "split-line";
|
|
1111
|
+
const classTokens = lineClass.split(/\s+/).filter(Boolean);
|
|
1112
|
+
if (classTokens.length === 0) return;
|
|
1113
|
+
const selector = `.${classTokens.join(".")}`;
|
|
1114
|
+
root.querySelectorAll(selector).forEach((line) => {
|
|
1115
|
+
line.style.whiteSpace = "nowrap";
|
|
1116
|
+
});
|
|
1117
|
+
}, []);
|
|
1100
1118
|
useEffect(() => {
|
|
1101
1119
|
if (!childElement) return;
|
|
1102
1120
|
if (hasSplitRef.current) return;
|
|
@@ -1526,6 +1544,7 @@ var SplitText = forwardRef(function SplitText2({
|
|
|
1526
1544
|
clearTimeout(resizeTimerRef.current);
|
|
1527
1545
|
resizeTimerRef.current = null;
|
|
1528
1546
|
}
|
|
1547
|
+
lockCurrentRenderedLines(currentElement);
|
|
1529
1548
|
pendingFullResplitRef.current = true;
|
|
1530
1549
|
let resplitWidth;
|
|
1531
1550
|
const targets = resolveAutoSplitTargets(currentElement);
|
|
@@ -1607,6 +1626,7 @@ var SplitText = forwardRef(function SplitText2({
|
|
|
1607
1626
|
autoSplit,
|
|
1608
1627
|
childTreeVersion,
|
|
1609
1628
|
data,
|
|
1629
|
+
lockCurrentRenderedLines,
|
|
1610
1630
|
measureAndSetData,
|
|
1611
1631
|
resolveLineMeasureWidth
|
|
1612
1632
|
]);
|
|
@@ -1707,14 +1727,28 @@ var SplitText = forwardRef(function SplitText2({
|
|
|
1707
1727
|
}
|
|
1708
1728
|
if (resizeTimerRef.current) {
|
|
1709
1729
|
clearTimeout(resizeTimerRef.current);
|
|
1730
|
+
resizeTimerRef.current = null;
|
|
1731
|
+
}
|
|
1732
|
+
const debounceMs = resolveResplitDebounceMs(
|
|
1733
|
+
optionsRef.current?.resplitDebounceMs
|
|
1734
|
+
);
|
|
1735
|
+
if (debounceMs <= 0) {
|
|
1736
|
+
lockCurrentRenderedLines(child2);
|
|
1737
|
+
pendingFullResplitRef.current = true;
|
|
1738
|
+
measureAndSetData(
|
|
1739
|
+
true,
|
|
1740
|
+
splitLines ? lineMeasureWidth : currentWidth
|
|
1741
|
+
);
|
|
1742
|
+
return;
|
|
1710
1743
|
}
|
|
1711
1744
|
resizeTimerRef.current = setTimeout(() => {
|
|
1745
|
+
lockCurrentRenderedLines(child2);
|
|
1712
1746
|
pendingFullResplitRef.current = true;
|
|
1713
1747
|
measureAndSetData(
|
|
1714
1748
|
true,
|
|
1715
1749
|
splitLines ? lineMeasureWidth : currentWidth
|
|
1716
1750
|
);
|
|
1717
|
-
},
|
|
1751
|
+
}, debounceMs);
|
|
1718
1752
|
};
|
|
1719
1753
|
resizeObserverRef.current = new ResizeObserver((entries) => {
|
|
1720
1754
|
let changed = false;
|
|
@@ -1755,6 +1789,7 @@ var SplitText = forwardRef(function SplitText2({
|
|
|
1755
1789
|
}, [
|
|
1756
1790
|
autoSplit,
|
|
1757
1791
|
data,
|
|
1792
|
+
lockCurrentRenderedLines,
|
|
1758
1793
|
measureAndSetData,
|
|
1759
1794
|
measureLineFingerprintForWidth,
|
|
1760
1795
|
resolveLineMeasureWidth
|
package/dist/react.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactElement } from 'react';
|
|
3
|
-
import { A as AnimationCallbackReturn } from './index-
|
|
4
|
-
export { a as CoreSplitTextOptions, S as SplitTextResult } from './index-
|
|
3
|
+
import { A as AnimationCallbackReturn } from './index-Box92Sue.js';
|
|
4
|
+
export { a as CoreSplitTextOptions, S as SplitTextResult } from './index-Box92Sue.js';
|
|
5
5
|
import { I as InitialStyles, a as InitialClasses } from './initialStyles-BGuPp5CS.js';
|
|
6
6
|
|
|
7
7
|
interface SplitTextOptions {
|
|
@@ -11,6 +11,8 @@ interface SplitTextOptions {
|
|
|
11
11
|
lineClass?: string;
|
|
12
12
|
/** Apply overflow mask wrapper to elements for reveal animations */
|
|
13
13
|
mask?: "lines" | "words" | "chars";
|
|
14
|
+
/** Debounce delay for autoSplit/full-resplit width updates in milliseconds (`0` disables debounce). */
|
|
15
|
+
resplitDebounceMs?: number;
|
|
14
16
|
propIndex?: boolean;
|
|
15
17
|
/** Skip kerning compensation (no margin adjustments applied).
|
|
16
18
|
* Kerning is naturally lost when splitting into inline-block spans.
|
package/dist/react.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createViewportObserver, reapplyInitialStyles, reapplyInitialClasses } from './chunk-TEDMJHF7.js';
|
|
2
|
+
import { waitForFontsReady } from './chunk-V23SUR2S.js';
|
|
3
|
+
import { splitText, normalizeToPromise } from './chunk-44MU2I5B.js';
|
|
3
4
|
import { forwardRef, useRef, useCallback, useState, useLayoutEffect, useEffect, isValidElement, createElement } from 'react';
|
|
4
5
|
|
|
5
6
|
var SplitText = forwardRef(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fetta",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Text splitting library with kerning compensation for animations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"./motion": {
|
|
25
25
|
"types": "./dist/motion.d.ts",
|
|
26
26
|
"import": "./dist/motion.js"
|
|
27
|
+
},
|
|
28
|
+
"./morph": {
|
|
29
|
+
"types": "./dist/morph.d.ts",
|
|
30
|
+
"import": "./dist/morph.js"
|
|
27
31
|
}
|
|
28
32
|
},
|
|
29
33
|
"main": "./dist/index.js",
|
|
@@ -65,6 +69,12 @@
|
|
|
65
69
|
"import": "*",
|
|
66
70
|
"ignore": ["react", "motion", "motion/react"]
|
|
67
71
|
},
|
|
72
|
+
{
|
|
73
|
+
"name": "Morph",
|
|
74
|
+
"path": "dist/morph.js",
|
|
75
|
+
"import": "*",
|
|
76
|
+
"ignore": ["react", "motion", "motion/react"]
|
|
77
|
+
},
|
|
68
78
|
{
|
|
69
79
|
"name": "Helpers",
|
|
70
80
|
"path": "dist/helpers.js",
|