fetta 1.5.3 → 1.5.5
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 +187 -423
- package/dist/{chunk-OUXSJF3P.js → chunk-44MU2I5B.js} +18 -1
- 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/motion.d.ts +4 -2
- package/dist/motion.js +35 -2
- package/dist/react.d.ts +4 -2
- package/dist/react.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
Text splitting that keeps kerning intact.
|
|
4
4
|
|
|
5
|
-
Split text into
|
|
5
|
+
Split text into chars, words, and lines while preserving original typography. `fetta` provides the framework-agnostic `splitText()` API for vanilla and imperative workflows, `fetta/react` is for React lifecycle and callback-based control, and `fetta/motion` is for Motion-first variants and triggers. `fetta/helpers` adds optional utilities for advanced layered effects.
|
|
6
|
+
|
|
7
|
+
Docs: https://fetta.dimi.me/
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install fetta
|
|
13
|
+
```
|
|
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 |
|
|
6
23
|
|
|
7
24
|
## Features
|
|
8
25
|
|
|
@@ -14,186 +31,147 @@ Split text into characters, words, and lines while preserving the original typog
|
|
|
14
31
|
- **Auto Revert** — Restores original HTML after animations
|
|
15
32
|
- **Masking** — Wrap elements in clip containers for reveal animations
|
|
16
33
|
- **Emoji Support** — Properly handles compound emojis and complex Unicode characters
|
|
17
|
-
- **Accessible** — Automatic screen reader support, even
|
|
34
|
+
- **Accessible** — Automatic screen reader support, even with nested links or emphasis
|
|
18
35
|
- **TypeScript** — Full type definitions included
|
|
19
|
-
- **
|
|
20
|
-
- **Viewport Triggers** — Scroll enter/leave callbacks with configurable thresholds in React
|
|
21
|
-
- **Library Agnostic** — Works with Motion, GSAP, CSS, or any animation library
|
|
36
|
+
- **Library Agnostic** — Works with Motion, GSAP, CSS, WAAPI, or custom animation code
|
|
22
37
|
|
|
23
|
-
##
|
|
38
|
+
## Quick Start
|
|
24
39
|
|
|
25
|
-
|
|
26
|
-
npm install fetta
|
|
27
|
-
```
|
|
40
|
+
### Vanilla JavaScript (`fetta`)
|
|
28
41
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- `fetta/motion`: ~13.68 kB
|
|
33
|
-
- `fetta/helpers`: ~742 B
|
|
42
|
+
```ts
|
|
43
|
+
import { splitText } from "fetta";
|
|
44
|
+
import { animate, stagger } from "motion";
|
|
34
45
|
|
|
35
|
-
|
|
46
|
+
const element = document.querySelector("h1");
|
|
47
|
+
const { chars } = splitText(element, { type: "chars" });
|
|
36
48
|
|
|
37
|
-
|
|
49
|
+
animate(chars, { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.02) });
|
|
50
|
+
```
|
|
38
51
|
|
|
39
|
-
|
|
40
|
-
import { splitText } from 'fetta';
|
|
41
|
-
import { animate, stagger } from 'motion';
|
|
52
|
+
### React Callbacks (`fetta/react`)
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
);
|
|
54
|
+
```tsx
|
|
55
|
+
import { SplitText } from "fetta/react";
|
|
56
|
+
import { animate, stagger } from "motion";
|
|
47
57
|
|
|
48
|
-
|
|
58
|
+
<SplitText
|
|
59
|
+
options={{ type: "words" }}
|
|
60
|
+
onSplit={({ words }) => {
|
|
61
|
+
animate(words, { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.05) });
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<h1>Hello World</h1>
|
|
65
|
+
</SplitText>;
|
|
49
66
|
```
|
|
50
67
|
|
|
51
|
-
###
|
|
68
|
+
### Motion Variants (`fetta/motion`)
|
|
52
69
|
|
|
53
70
|
```tsx
|
|
54
|
-
import { SplitText } from
|
|
55
|
-
import {
|
|
71
|
+
import { SplitText } from "fetta/motion";
|
|
72
|
+
import { stagger } from "motion";
|
|
56
73
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
<SplitText
|
|
75
|
+
variants={{
|
|
76
|
+
hidden: { words: { opacity: 0, y: 20 } },
|
|
77
|
+
visible: { words: { opacity: 1, y: 0 } },
|
|
78
|
+
}}
|
|
79
|
+
initial="hidden"
|
|
80
|
+
animate="visible"
|
|
81
|
+
transition={{ duration: 0.65, delay: stagger(0.04) }}
|
|
82
|
+
options={{ type: "words" }}
|
|
83
|
+
>
|
|
84
|
+
<h1>Hello World</h1>
|
|
85
|
+
</SplitText>;
|
|
68
86
|
```
|
|
69
87
|
|
|
88
|
+
`fetta/motion` supports standard Motion targets, per-type targets (`chars` / `words` / `lines` / `wrapper`), and split-aware function variants.
|
|
89
|
+
|
|
70
90
|
## API
|
|
71
91
|
|
|
72
|
-
### `splitText(element, options?)`
|
|
92
|
+
### `splitText(element, options?)` (`fetta`)
|
|
73
93
|
|
|
74
94
|
Splits text content into characters, words, and/or lines.
|
|
75
95
|
|
|
76
96
|
```ts
|
|
97
|
+
import { splitText } from "fetta";
|
|
98
|
+
|
|
77
99
|
const result = splitText(element, options);
|
|
78
100
|
```
|
|
79
101
|
|
|
80
102
|
#### Options
|
|
81
103
|
|
|
82
104
|
| Option | Type | Default | Description |
|
|
83
|
-
|
|
84
|
-
| `type` | `
|
|
105
|
+
|------|------|------|------|
|
|
106
|
+
| `type` | `SplitType` | `"chars,words,lines"` | What to split: `"chars"`, `"words"`, `"lines"`, or combinations |
|
|
85
107
|
| `charClass` | `string` | `"split-char"` | CSS class for character elements |
|
|
86
108
|
| `wordClass` | `string` | `"split-word"` | CSS class for word elements |
|
|
87
109
|
| `lineClass` | `string` | `"split-line"` | CSS class for line elements |
|
|
88
|
-
| `mask` | `
|
|
110
|
+
| `mask` | `"chars" \| "words" \| "lines"` | — | Wrap elements in `overflow: clip` container |
|
|
89
111
|
| `autoSplit` | `boolean` | `false` | Re-split on container resize |
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
112
|
+
| `resplitDebounceMs` | `number` | `100` | Debounce delay for autoSplit/full-resplit width updates (`0` disables debounce) |
|
|
113
|
+
| `onResplit` | `(result) => void` | — | Callback after autoSplit/full-resplit replaces split output elements |
|
|
114
|
+
| `onSplit` | `(result) => CallbackReturn` | — | Callback after initial split. Return animation/promise for `revertOnComplete` |
|
|
115
|
+
| `revertOnComplete` | `boolean` | `false` | Auto-revert when returned animation completes |
|
|
93
116
|
| `propIndex` | `boolean` | `false` | Add CSS custom properties: `--char-index`, `--word-index`, `--line-index` |
|
|
94
117
|
| `disableKerning` | `boolean` | `false` | Skip kerning compensation (no margin adjustments) |
|
|
95
118
|
| `initialStyles` | `object` | — | Apply initial inline styles to chars/words/lines after split. Values can be objects or `(el, index) => object` functions |
|
|
96
119
|
| `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines. Values are strings |
|
|
97
120
|
|
|
98
|
-
#### Return Value
|
|
99
|
-
|
|
100
121
|
```ts
|
|
101
|
-
|
|
102
|
-
chars
|
|
103
|
-
words
|
|
104
|
-
lines
|
|
105
|
-
|
|
106
|
-
|
|
122
|
+
type SplitType =
|
|
123
|
+
| "chars"
|
|
124
|
+
| "words"
|
|
125
|
+
| "lines"
|
|
126
|
+
| "chars,words"
|
|
127
|
+
| "words,lines"
|
|
128
|
+
| "chars,lines"
|
|
129
|
+
| "chars,words,lines";
|
|
107
130
|
```
|
|
108
131
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
Builds swap/reveal DOM layers (clones + optional wrappers) without coupling to any animation library.
|
|
132
|
+
#### Return Value
|
|
112
133
|
|
|
113
134
|
```ts
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// Animate with Motion, GSAP, WAAPI, or CSS
|
|
121
|
-
// ...
|
|
122
|
-
|
|
123
|
-
layers.cleanup(); // removes clones/wrappers, keeps split DOM
|
|
124
|
-
// layers.cleanup({ revertSplit: true }) // also calls split.revert()
|
|
135
|
+
interface SplitTextResult {
|
|
136
|
+
chars: HTMLSpanElement[];
|
|
137
|
+
words: HTMLSpanElement[];
|
|
138
|
+
lines: HTMLSpanElement[];
|
|
139
|
+
revert: () => void;
|
|
140
|
+
}
|
|
125
141
|
```
|
|
126
142
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
- Clone is always appended to the **current parent** of the original split node.
|
|
130
|
-
- `wrap: false` (default): clone is appended to existing parent (often the mask wrapper).
|
|
131
|
-
- `wrap: true`: original is first moved into a track wrapper, then clone is appended there.
|
|
132
|
-
- Helper never calls `splitText` and never performs animation.
|
|
133
|
-
|
|
134
|
-
#### Options
|
|
135
|
-
|
|
136
|
-
| Option | Type | Default | Description |
|
|
137
|
-
|--------|------|---------|-------------|
|
|
138
|
-
| `unit` | `"chars" \| "words" \| "lines"` | — | Which split nodes to layer |
|
|
139
|
-
| `wrap` | `boolean` | `false` | Wrap each original in a track wrapper (`position: relative`) |
|
|
140
|
-
| `display` | `"auto" \| "inline-block" \| "block"` | `"auto"` | Track display when `wrap: true` (`lines` => `block`, others => `inline-block`) |
|
|
141
|
-
| `cloneOffset.axis` | `"x" \| "y"` | `"y"` | Axis used for initial clone offset |
|
|
142
|
-
| `cloneOffset.direction` | `"start" \| "end"` | `"start"` | Offset direction (`start` => negative) |
|
|
143
|
-
| `cloneOffset.distance` | `string` | `"100%"` | Offset distance |
|
|
144
|
-
| `trackClassName` / `cloneClassName` | `string \| (ctx) => string \| undefined` | — | Class names (static or per-item) |
|
|
145
|
-
| `trackStyle` / `cloneStyle` | `object \| (ctx) => object` | — | Inline styles (static or per-item) |
|
|
146
|
-
|
|
147
|
-
For reveal/swap effects, use matching `mask` in `splitText` (`"chars"`, `"words"`, or `"lines"`).
|
|
148
|
-
|
|
149
|
-
### `<SplitText>` (React)
|
|
143
|
+
### `<SplitText>` (`fetta/react`)
|
|
150
144
|
|
|
151
145
|
```tsx
|
|
152
|
-
import { SplitText } from
|
|
146
|
+
import { SplitText } from "fetta/react";
|
|
153
147
|
```
|
|
154
148
|
|
|
155
|
-
`fetta/react`
|
|
156
|
-
|
|
157
|
-
`fetta/react` props:
|
|
149
|
+
`fetta/react` wraps `splitText()` for React with lifecycle hooks, viewport callbacks, and automatic cleanup.
|
|
158
150
|
|
|
159
151
|
#### React Props
|
|
160
152
|
|
|
161
153
|
| Prop | Type | Default | Description |
|
|
162
|
-
|
|
154
|
+
|------|------|------|------|
|
|
163
155
|
| `children` | `ReactElement` | — | Single React element to split |
|
|
164
156
|
| `as` | `keyof JSX.IntrinsicElements` | `"div"` | Wrapper element type |
|
|
165
|
-
| `className` | `string` | — |
|
|
166
|
-
| `style` | `CSSProperties` | — |
|
|
167
|
-
| `ref` | `Ref<HTMLElement>` | — | Ref to
|
|
168
|
-
| `onSplit` | `(result) => CallbackReturn` | — | Called after
|
|
157
|
+
| `className` | `string` | — | Wrapper class name |
|
|
158
|
+
| `style` | `CSSProperties` | — | Wrapper styles |
|
|
159
|
+
| `ref` | `Ref<HTMLElement>` | — | Ref to wrapper element |
|
|
160
|
+
| `onSplit` | `(result) => CallbackReturn` | — | Called after initial split |
|
|
169
161
|
| `onResplit` | `(result) => void` | — | Called when autoSplit/full-resplit replaces split output elements |
|
|
170
|
-
| `options` | `SplitTextOptions` | — | Split options (type
|
|
162
|
+
| `options` | `SplitTextOptions` | — | Split options (`type`, classes, mask, debounce, etc.) |
|
|
171
163
|
| `autoSplit` | `boolean` | `false` | Re-split on container resize |
|
|
172
|
-
| `waitForFonts` | `boolean` | `true` | Wait for `document.fonts.ready` before splitting
|
|
164
|
+
| `waitForFonts` | `boolean` | `true` | Wait for `document.fonts.ready` before splitting |
|
|
173
165
|
| `revertOnComplete` | `boolean` | `false` | Revert after animation completes |
|
|
174
|
-
| `onRevert` | `() => void` | — | Called when split text
|
|
166
|
+
| `onRevert` | `() => void` | — | Called when split text reverts |
|
|
175
167
|
| `viewport` | `ViewportOptions` | — | Configure viewport detection |
|
|
176
|
-
| `onViewportEnter` | `(result) => CallbackReturn` | — | Called when
|
|
177
|
-
| `onViewportLeave` | `(result) => CallbackReturn` | — | Called when
|
|
178
|
-
| `initialStyles` | `object` | — | Apply initial inline styles to chars/words/lines
|
|
179
|
-
| `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines
|
|
180
|
-
| `resetOnViewportLeave` | `boolean` | `false` | Re-apply
|
|
168
|
+
| `onViewportEnter` | `(result) => CallbackReturn` | — | Called when entering viewport |
|
|
169
|
+
| `onViewportLeave` | `(result) => CallbackReturn` | — | Called when leaving viewport |
|
|
170
|
+
| `initialStyles` | `object` | — | Apply initial inline styles to chars/words/lines |
|
|
171
|
+
| `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines |
|
|
172
|
+
| `resetOnViewportLeave` | `boolean` | `false` | Re-apply initial styles/classes when leaving viewport |
|
|
181
173
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
| Option | Type | Default | Description |
|
|
185
|
-
|------|------|---------|-------------|
|
|
186
|
-
| `type` | `SplitType` | `"chars,words,lines"` | What to split: `"chars"`, `"words"`, `"lines"`, or combinations |
|
|
187
|
-
| `charClass` | `string` | `"split-char"` | CSS class for character spans |
|
|
188
|
-
| `wordClass` | `string` | `"split-word"` | CSS class for word spans |
|
|
189
|
-
| `lineClass` | `string` | `"split-line"` | CSS class for line spans |
|
|
190
|
-
| `mask` | `"lines" \| "words" \| "chars"` | — | Wrap elements in `overflow: clip` mask containers |
|
|
191
|
-
| `propIndex` | `boolean` | `false` | Add CSS index variables (`--char-index`, `--word-index`, `--line-index`) |
|
|
192
|
-
| `disableKerning` | `boolean` | `false` | Skip kerning compensation (no margin adjustments) |
|
|
193
|
-
|
|
194
|
-
#### Callback Signature
|
|
195
|
-
|
|
196
|
-
All callbacks (`onSplit`, `onResplit`, `onViewportEnter`, `onViewportLeave`) receive:
|
|
174
|
+
All callbacks receive:
|
|
197
175
|
|
|
198
176
|
```ts
|
|
199
177
|
{
|
|
@@ -213,314 +191,118 @@ type CallbackReturn =
|
|
|
213
191
|
| CallbackReturn[];
|
|
214
192
|
```
|
|
215
193
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
`onRevert` is a separate zero-argument callback that fires when a split cycle actually reverts.
|
|
219
|
-
|
|
220
|
-
#### Viewport Options
|
|
221
|
-
|
|
222
|
-
```ts
|
|
223
|
-
{
|
|
224
|
-
amount?: number | "some" | "all"; // Enter threshold, default: 0
|
|
225
|
-
leave?: number | "some" | "all"; // Leave threshold, default: 0
|
|
226
|
-
margin?: string; // Root margin, default: "0px"
|
|
227
|
-
once?: boolean; // Only trigger once, default: false
|
|
228
|
-
root?: RefObject<Element>; // Optional root element
|
|
229
|
-
}
|
|
230
|
-
```
|
|
194
|
+
`fetta/react` and `fetta/motion` both forward common wrapper DOM props (`id`, `role`, `tabIndex`, `aria-*`, `data-*`, event handlers) to the wrapper.
|
|
231
195
|
|
|
232
|
-
### `<SplitText>` (
|
|
196
|
+
### `<SplitText>` (`fetta/motion`)
|
|
233
197
|
|
|
234
198
|
```tsx
|
|
235
199
|
import { SplitText } from "fetta/motion";
|
|
236
200
|
```
|
|
237
201
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
Animate on exit with Motion's `AnimatePresence` (make `SplitText` the direct child):
|
|
241
|
-
|
|
242
|
-
```tsx
|
|
243
|
-
import { AnimatePresence } from "motion/react";
|
|
244
|
-
|
|
245
|
-
<AnimatePresence>
|
|
246
|
-
{isVisible && (
|
|
247
|
-
<SplitText
|
|
248
|
-
variants={{
|
|
249
|
-
enter: { opacity: 1, y: 0 },
|
|
250
|
-
exit: { opacity: 0, y: 12 },
|
|
251
|
-
}}
|
|
252
|
-
initial="enter"
|
|
253
|
-
animate="enter"
|
|
254
|
-
exit="exit"
|
|
255
|
-
options={{ type: "words" }}
|
|
256
|
-
>
|
|
257
|
-
<h1>Goodbye</h1>
|
|
258
|
-
</SplitText>
|
|
259
|
-
)}
|
|
260
|
-
</AnimatePresence>
|
|
261
|
-
```
|
|
202
|
+
Variant-driven component built on Motion. Includes every `fetta/react` prop plus Motion animation/triggers (`initial`, `animate`, `exit`, `whileInView`, `whileScroll`, `whileHover`, etc.).
|
|
262
203
|
|
|
263
204
|
#### Motion-only Props
|
|
264
205
|
|
|
265
206
|
| Prop | Type | Default | Description |
|
|
266
|
-
|
|
207
|
+
|------|------|------|------|
|
|
267
208
|
| `variants` | `Record<string, VariantDefinition<TCustom>>` | — | Named variant definitions |
|
|
268
209
|
| `initial` | `string \| VariantDefinition<TCustom> \| false` | — | Initial variant applied instantly after split |
|
|
269
210
|
| `animate` | `string \| VariantDefinition<TCustom>` | — | Base variant |
|
|
270
|
-
| `exit` | `string \| VariantDefinition<TCustom> \| false` | — | Exit variant (AnimatePresence) |
|
|
211
|
+
| `exit` | `string \| VariantDefinition<TCustom> \| false` | — | Exit variant (`AnimatePresence`) |
|
|
271
212
|
| `whileInView` | `string \| VariantDefinition<TCustom>` | — | Variant while element is in view |
|
|
272
213
|
| `whileOutOfView` | `string \| VariantDefinition<TCustom>` | — | Variant after element leaves view |
|
|
273
214
|
| `whileScroll` | `string \| VariantDefinition<TCustom>` | — | Scroll-driven variant (highest trigger priority) |
|
|
274
215
|
| `whileHover` | `string \| VariantDefinition<TCustom>` | — | Variant on hover |
|
|
275
216
|
| `whileTap` | `string \| VariantDefinition<TCustom>` | — | Variant on tap/press |
|
|
276
217
|
| `whileFocus` | `string \| VariantDefinition<TCustom>` | — | Variant on focus |
|
|
277
|
-
| `animateOnResplit` | `boolean` | `false` | Replay initial->animate on autoSplit/full-resplit |
|
|
218
|
+
| `animateOnResplit` | `boolean` | `false` | Replay `initial -> animate` on autoSplit/full-resplit |
|
|
278
219
|
| `scroll` | `{ offset?, axis?, container? }` | — | Scroll tracking options for `whileScroll` |
|
|
279
220
|
| `transition` | `AnimationOptions` | — | Global/default transition for variants |
|
|
280
221
|
| `custom` | `TCustom` | — | Custom data forwarded to function variants |
|
|
281
|
-
| `delayScope` | `"global" \| "local"` | `"global"` | Delay-function index scope (`globalIndex` vs
|
|
282
|
-
| `reducedMotion` | `"user" \| "always" \| "never"` |
|
|
222
|
+
| `delayScope` | `"global" \| "local"` | `"global"` | Delay-function index scope (`globalIndex` vs local `index`) |
|
|
223
|
+
| `reducedMotion` | `"user" \| "always" \| "never"` | `"never"` | Reduced-motion behavior for this component |
|
|
283
224
|
| `onHoverStart` | `() => void` | — | Called when hover starts |
|
|
284
225
|
| `onHoverEnd` | `() => void` | — | Called when hover ends |
|
|
285
226
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
### Vanilla JavaScript
|
|
289
|
-
|
|
290
|
-
#### Basic
|
|
291
|
-
|
|
292
|
-
```js
|
|
293
|
-
import { splitText } from 'fetta';
|
|
294
|
-
import { animate, stagger } from 'motion';
|
|
295
|
-
|
|
296
|
-
const { words } = splitText(document.querySelector('h1'));
|
|
297
|
-
|
|
298
|
-
animate(words, { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.05) });
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
#### Masked Line Reveal
|
|
302
|
-
|
|
303
|
-
```js
|
|
304
|
-
splitText(element, {
|
|
305
|
-
type: 'lines',
|
|
306
|
-
mask: 'lines',
|
|
307
|
-
onSplit: ({ lines }) => {
|
|
308
|
-
animate(lines, { y: ['100%', '0%'] }, { delay: stagger(0.1) });
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
#### With GSAP
|
|
314
|
-
|
|
315
|
-
```js
|
|
316
|
-
import { splitText } from 'fetta';
|
|
317
|
-
import gsap from 'gsap';
|
|
318
|
-
|
|
319
|
-
splitText(element, {
|
|
320
|
-
revertOnComplete: true,
|
|
321
|
-
onSplit: ({ words }) => {
|
|
322
|
-
return gsap.from(words, {
|
|
323
|
-
opacity: 0,
|
|
324
|
-
y: 20,
|
|
325
|
-
stagger: 0.05,
|
|
326
|
-
duration: 0.6,
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
#### CSS-Only with Index Props
|
|
333
|
-
|
|
334
|
-
```js
|
|
335
|
-
splitText(element, { type: 'chars', propIndex: true });
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
```css
|
|
339
|
-
.split-char {
|
|
340
|
-
opacity: 0;
|
|
341
|
-
animation: fade-in 0.5s forwards;
|
|
342
|
-
animation-delay: calc(var(--char-index) * 0.03s);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
@keyframes fade-in {
|
|
346
|
-
to { opacity: 1; }
|
|
347
|
-
}
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
### React
|
|
351
|
-
|
|
352
|
-
#### Basic
|
|
353
|
-
|
|
354
|
-
```tsx
|
|
355
|
-
<SplitText
|
|
356
|
-
onSplit={({ words }) => {
|
|
357
|
-
animate(words, { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.05) });
|
|
358
|
-
}}
|
|
359
|
-
>
|
|
360
|
-
<h1>Hello World</h1>
|
|
361
|
-
</SplitText>
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
#### Masked Line Reveal
|
|
365
|
-
|
|
366
|
-
```tsx
|
|
367
|
-
<SplitText
|
|
368
|
-
options={{ type: 'lines', mask: 'lines' }}
|
|
369
|
-
onSplit={({ lines }) => {
|
|
370
|
-
animate(lines, { y: ['100%', '0%'] }, { delay: stagger(0.1) });
|
|
371
|
-
}}
|
|
372
|
-
>
|
|
373
|
-
<p>Each line reveals from below</p>
|
|
374
|
-
</SplitText>
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
#### Scroll-Triggered with Viewport
|
|
227
|
+
Exit animations use standard Motion behavior (`SplitText` must be a direct child of `AnimatePresence`):
|
|
378
228
|
|
|
379
229
|
```tsx
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
initialStyles={{
|
|
383
|
-
words: { opacity: '0', transform: 'translateY(20px)' }
|
|
384
|
-
}}
|
|
385
|
-
viewport={{ amount: 0.5 }}
|
|
386
|
-
onViewportEnter={({ words }) => {
|
|
387
|
-
animate(words, { opacity: 1, y: 0 }, { delay: stagger(0.03) });
|
|
388
|
-
}}
|
|
389
|
-
resetOnViewportLeave
|
|
390
|
-
>
|
|
391
|
-
<p>Animates when scrolled into view</p>
|
|
392
|
-
</SplitText>
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
#### Auto-Revert After Animation
|
|
230
|
+
import { AnimatePresence } from "motion/react";
|
|
231
|
+
import { SplitText } from "fetta/motion";
|
|
396
232
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
233
|
+
<AnimatePresence>
|
|
234
|
+
{isVisible && (
|
|
235
|
+
<SplitText
|
|
236
|
+
variants={{
|
|
237
|
+
enter: { opacity: 1, y: 0 },
|
|
238
|
+
exit: { opacity: 0, y: 12 },
|
|
239
|
+
}}
|
|
240
|
+
initial="enter"
|
|
241
|
+
animate="enter"
|
|
242
|
+
exit="exit"
|
|
243
|
+
options={{ type: "words" }}
|
|
244
|
+
>
|
|
245
|
+
<h1>Goodbye</h1>
|
|
246
|
+
</SplitText>
|
|
247
|
+
)}
|
|
248
|
+
</AnimatePresence>;
|
|
406
249
|
```
|
|
407
250
|
|
|
408
|
-
###
|
|
251
|
+
### `createSplitClones(splitResult, options)` (`fetta/helpers`, optional)
|
|
409
252
|
|
|
410
|
-
|
|
253
|
+
Helpers are optional utilities for advanced layered reveal/swap effects.
|
|
411
254
|
|
|
412
|
-
```
|
|
413
|
-
import {
|
|
414
|
-
import {
|
|
415
|
-
|
|
416
|
-
<SplitText
|
|
417
|
-
variants={{
|
|
418
|
-
hidden: { opacity: 0, y: 20, filter: 'blur(6px)' },
|
|
419
|
-
visible: { opacity: 1, y: 0, filter: 'blur(0px)' },
|
|
420
|
-
}}
|
|
421
|
-
initial="hidden"
|
|
422
|
-
animate="visible"
|
|
423
|
-
transition={{ duration: 0.6, delay: stagger(0.04) }}
|
|
424
|
-
options={{ type: 'words' }}
|
|
425
|
-
>
|
|
426
|
-
<h1>Hello World</h1>
|
|
427
|
-
</SplitText>
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
#### Line-Aware Stagger
|
|
431
|
-
|
|
432
|
-
```tsx
|
|
433
|
-
import { SplitText } from 'fetta/motion';
|
|
434
|
-
import { stagger } from 'motion';
|
|
255
|
+
```ts
|
|
256
|
+
import { splitText } from "fetta";
|
|
257
|
+
import { createSplitClones } from "fetta/helpers";
|
|
435
258
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
variants={{
|
|
439
|
-
hidden: { chars: { opacity: 0 } },
|
|
440
|
-
visible: {
|
|
441
|
-
chars: ({ lineIndex }) => ({
|
|
442
|
-
opacity: 1,
|
|
443
|
-
transition: {
|
|
444
|
-
duration: 0.3,
|
|
445
|
-
delay: stagger(0.015, {
|
|
446
|
-
startDelay: lineIndex * 0.2,
|
|
447
|
-
from: lineIndex % 2 === 0 ? "first" : "last",
|
|
448
|
-
}),
|
|
449
|
-
},
|
|
450
|
-
}),
|
|
451
|
-
},
|
|
452
|
-
}}
|
|
453
|
-
initial="hidden"
|
|
454
|
-
animate="visible"
|
|
455
|
-
options={{ type: "chars,lines" }}
|
|
456
|
-
>
|
|
457
|
-
<p>Line-aware per-character animation</p>
|
|
458
|
-
</SplitText>
|
|
259
|
+
const split = splitText(element, { type: "chars", mask: "chars" });
|
|
260
|
+
const layers = createSplitClones(split, { unit: "chars", wrap: true });
|
|
459
261
|
```
|
|
460
262
|
|
|
461
|
-
####
|
|
462
|
-
|
|
463
|
-
```tsx
|
|
464
|
-
import { SplitText } from 'fetta/motion';
|
|
263
|
+
#### Helper Options
|
|
465
264
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}),
|
|
477
|
-
}}
|
|
478
|
-
scroll={{ offset: ["start 90%", "start 10%"] }}
|
|
479
|
-
options={{ type: "chars" }}
|
|
480
|
-
>
|
|
481
|
-
<p>Characters fade in with scroll progress</p>
|
|
482
|
-
</SplitText>
|
|
483
|
-
```
|
|
265
|
+
| Option | Type | Default | Description |
|
|
266
|
+
|------|------|------|------|
|
|
267
|
+
| `unit` | `"chars" \| "words" \| "lines"` | — | Which split nodes to layer |
|
|
268
|
+
| `wrap` | `boolean` | `false` | Wrap each original in a track wrapper (`position: relative`) |
|
|
269
|
+
| `display` | `"auto" \| "inline-block" \| "block"` | `"auto"` | Track display when `wrap: true` (`lines` => `block`, others => `inline-block`) |
|
|
270
|
+
| `cloneOffset.axis` | `"x" \| "y"` | `"y"` | Axis used for initial clone offset |
|
|
271
|
+
| `cloneOffset.direction` | `"start" \| "end"` | `"start"` | Offset direction (`start` => negative) |
|
|
272
|
+
| `cloneOffset.distance` | `string` | `"100%"` | Offset distance |
|
|
273
|
+
| `trackClassName` / `cloneClassName` | `string \| (ctx) => string \| undefined` | — | Class names (static or per-item) |
|
|
274
|
+
| `trackStyle` / `cloneStyle` | `object \| (ctx) => object` | — | Inline styles (static or per-item) |
|
|
484
275
|
|
|
485
|
-
####
|
|
276
|
+
#### Helper Behavior
|
|
486
277
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
278
|
+
- Helper does not call `splitText()`; pass an existing split result.
|
|
279
|
+
- Clone is appended to the current parent of each original split node.
|
|
280
|
+
- `wrap: false` appends clone to existing parent.
|
|
281
|
+
- `wrap: true` moves original into a generated track, then appends clone there.
|
|
282
|
+
- `cleanup()` removes helper-created tracks/clones and is idempotent.
|
|
283
|
+
- `cleanup({ revertSplit: true })` also calls `split.revert()`.
|
|
490
284
|
|
|
491
|
-
|
|
492
|
-
variants={{
|
|
493
|
-
rest: { chars: { opacity: 0.85, y: 0 } },
|
|
494
|
-
hover: { chars: { opacity: 1, y: -6 } },
|
|
495
|
-
}}
|
|
496
|
-
initial="rest"
|
|
497
|
-
animate="rest"
|
|
498
|
-
whileHover="hover"
|
|
499
|
-
transition={{ duration: 0.25, delay: stagger(0.01) }}
|
|
500
|
-
options={{ type: 'chars' }}
|
|
501
|
-
>
|
|
502
|
-
<p>Hover this text</p>
|
|
503
|
-
</SplitText>
|
|
504
|
-
```
|
|
285
|
+
For reveal/swap effects, match helper `unit` with `splitText` `mask` (`"chars"`, `"words"`, `"lines"`).
|
|
505
286
|
|
|
506
287
|
## CSS Classes
|
|
507
288
|
|
|
508
|
-
Default classes applied to split
|
|
289
|
+
Default classes applied to split output:
|
|
509
290
|
|
|
510
|
-
| Class | Element |
|
|
511
|
-
|
|
512
|
-
| `.split-char` |
|
|
513
|
-
| `.split-word` |
|
|
514
|
-
| `.split-line` |
|
|
291
|
+
| Class | Element |
|
|
292
|
+
|------|------|
|
|
293
|
+
| `.split-char` | Character span |
|
|
294
|
+
| `.split-word` | Word span |
|
|
295
|
+
| `.split-line` | Line span |
|
|
515
296
|
|
|
516
|
-
Split elements receive
|
|
517
|
-
|
|
518
|
-
-
|
|
519
|
-
-
|
|
297
|
+
Split elements receive index attributes:
|
|
298
|
+
|
|
299
|
+
- `data-char-index`
|
|
300
|
+
- `data-word-index`
|
|
301
|
+
- `data-line-index`
|
|
520
302
|
|
|
521
303
|
## Font Loading
|
|
522
304
|
|
|
523
|
-
For
|
|
305
|
+
For stable kerning measurements in vanilla usage, wait for fonts before splitting:
|
|
524
306
|
|
|
525
307
|
```ts
|
|
526
308
|
document.fonts.ready.then(() => {
|
|
@@ -529,63 +311,45 @@ document.fonts.ready.then(() => {
|
|
|
529
311
|
});
|
|
530
312
|
```
|
|
531
313
|
|
|
532
|
-
React and Motion components wait for fonts by default (`waitForFonts={true}`)
|
|
533
|
-
|
|
534
|
-
If you notice a visual shift after splitting, keep the default waiting behavior enabled.
|
|
535
|
-
|
|
536
|
-
If you need immediate splitting (for example, responsiveness-first UI), you can opt out with `waitForFonts={false}`:
|
|
537
|
-
|
|
538
|
-
```tsx
|
|
539
|
-
<SplitText waitForFonts={false}>
|
|
540
|
-
<h1>Split Immediately</h1>
|
|
541
|
-
</SplitText>
|
|
542
|
-
```
|
|
314
|
+
React and Motion components wait for fonts by default (`waitForFonts={true}`).
|
|
543
315
|
|
|
544
316
|
## Accessibility
|
|
545
317
|
|
|
546
|
-
Fetta
|
|
547
|
-
|
|
548
|
-
**Headings and landmarks** — For elements that support `aria-label` natively (headings, `<section>`, `<nav>`, etc.), Fetta adds `aria-hidden="true"` to each split span and an `aria-label` on the parent:
|
|
318
|
+
Fetta keeps split text readable by screen readers:
|
|
549
319
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
<span class="split-word" aria-hidden="true">Hello</span>
|
|
554
|
-
<span class="split-word" aria-hidden="true">World</span>
|
|
555
|
-
</h1>
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
**Generic elements and nested content** — For `<span>`, `<div>`, `<p>`, or text containing inline elements like links, Fetta wraps the visual content with `aria-hidden="true"` and adds a screen-reader-only copy that preserves the semantic structure:
|
|
559
|
-
|
|
560
|
-
```html
|
|
561
|
-
<!-- After splitting <p>Click <a href="/signup">here</a> to start</p> -->
|
|
562
|
-
<p>
|
|
563
|
-
<span aria-hidden="true" data-fetta-visual="true">
|
|
564
|
-
<!-- Split visual content -->
|
|
565
|
-
</span>
|
|
566
|
-
<span class="fetta-sr-only" data-fetta-sr-copy="true">
|
|
567
|
-
Click <a href="/signup">here</a> to start
|
|
568
|
-
</span>
|
|
569
|
-
</p>
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
Pre-existing `aria-label` attributes are always preserved.
|
|
320
|
+
- Headings/landmarks: split nodes are hidden from assistive tech and parent gets `aria-label`.
|
|
321
|
+
- Generic/nested content: visual split output is hidden and a screen-reader copy preserves semantics.
|
|
322
|
+
- Existing `aria-label` values are preserved.
|
|
573
323
|
|
|
574
324
|
## Notes
|
|
575
325
|
|
|
576
|
-
-
|
|
577
|
-
-
|
|
326
|
+
- Ligatures are disabled (`font-variant-ligatures: none`) because ligatures cannot span multiple elements.
|
|
327
|
+
- Authored hard breaks are preserved (`<br>` and block boundaries are kept as hard split boundaries).
|
|
328
|
+
- `autoSplit` width resplits are debounced by default (`100ms`), configurable via `resplitDebounceMs`.
|
|
578
329
|
|
|
579
330
|
## Browser Support
|
|
580
331
|
|
|
581
332
|
All modern browsers: Chrome, Firefox, Safari, Edge.
|
|
582
333
|
|
|
583
334
|
Requires:
|
|
335
|
+
|
|
584
336
|
- `ResizeObserver`
|
|
585
337
|
- `IntersectionObserver`
|
|
586
338
|
- `Intl.Segmenter`
|
|
587
339
|
|
|
588
|
-
|
|
340
|
+
Safari kerning compensation works, but font rendering precision can vary slightly by font. If you notice subtle shifts around `revert()`, use `disableKerning: true`.
|
|
341
|
+
|
|
342
|
+
## Docs
|
|
343
|
+
|
|
344
|
+
- https://fetta.dimi.me/
|
|
345
|
+
- https://fetta.dimi.me/installation
|
|
346
|
+
- https://fetta.dimi.me/api/core
|
|
347
|
+
- https://fetta.dimi.me/api/react
|
|
348
|
+
- https://fetta.dimi.me/api/motion
|
|
349
|
+
- https://fetta.dimi.me/api/helpers
|
|
350
|
+
- https://fetta.dimi.me/examples/vanilla
|
|
351
|
+
- https://fetta.dimi.me/examples/react
|
|
352
|
+
- https://fetta.dimi.me/examples/motion
|
|
589
353
|
|
|
590
354
|
## License
|
|
591
355
|
|
|
@@ -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);
|
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/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,16 @@
|
|
|
1
1
|
import { waitForFontsReady, createViewportObserver, reapplyInitialStyles, reapplyInitialClasses } from './chunk-PA22PLRB.js';
|
|
2
|
-
import { splitTextData, buildLineFingerprintFromData, normalizeToPromise, buildKerningStyleKey, resolveAutoSplitTargets, getObservedWidth, recordWidthChange, resolveAutoSplitWidth, querySplitWords, clearKerningCompensation, applyKerningCompensation } from './chunk-
|
|
2
|
+
import { splitTextData, buildLineFingerprintFromData, normalizeToPromise, buildKerningStyleKey, resolveAutoSplitTargets, getObservedWidth, recordWidthChange, resolveAutoSplitWidth, querySplitWords, clearKerningCompensation, applyKerningCompensation } from './chunk-44MU2I5B.js';
|
|
3
3
|
import { scroll, animate } from 'motion';
|
|
4
4
|
import { usePresence, useReducedMotion, MotionConfig, motion } from 'motion/react';
|
|
5
5
|
import { forwardRef, useRef, useState, useCallback, useMemo, useLayoutEffect, isValidElement, useEffect, createElement, cloneElement } from 'react';
|
|
6
6
|
|
|
7
|
+
var DEFAULT_RESPLIT_DEBOUNCE_MS = 100;
|
|
8
|
+
function resolveResplitDebounceMs(value) {
|
|
9
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
10
|
+
return DEFAULT_RESPLIT_DEBOUNCE_MS;
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
7
14
|
var ELEMENT_TYPE_KEYS = ["chars", "words", "lines"];
|
|
8
15
|
var VOID_HTML_TAGS = /* @__PURE__ */ new Set(["br", "hr", "img", "input", "meta", "link"]);
|
|
9
16
|
function isPerTypeVariant(v) {
|
|
@@ -1097,6 +1104,15 @@ var SplitText = forwardRef(function SplitText2({
|
|
|
1097
1104
|
}
|
|
1098
1105
|
return safeFallbackWidth;
|
|
1099
1106
|
}, []);
|
|
1107
|
+
const lockCurrentRenderedLines = useCallback((root) => {
|
|
1108
|
+
const lineClass = optionsRef.current?.lineClass ?? "split-line";
|
|
1109
|
+
const classTokens = lineClass.split(/\s+/).filter(Boolean);
|
|
1110
|
+
if (classTokens.length === 0) return;
|
|
1111
|
+
const selector = `.${classTokens.join(".")}`;
|
|
1112
|
+
root.querySelectorAll(selector).forEach((line) => {
|
|
1113
|
+
line.style.whiteSpace = "nowrap";
|
|
1114
|
+
});
|
|
1115
|
+
}, []);
|
|
1100
1116
|
useEffect(() => {
|
|
1101
1117
|
if (!childElement) return;
|
|
1102
1118
|
if (hasSplitRef.current) return;
|
|
@@ -1526,6 +1542,7 @@ var SplitText = forwardRef(function SplitText2({
|
|
|
1526
1542
|
clearTimeout(resizeTimerRef.current);
|
|
1527
1543
|
resizeTimerRef.current = null;
|
|
1528
1544
|
}
|
|
1545
|
+
lockCurrentRenderedLines(currentElement);
|
|
1529
1546
|
pendingFullResplitRef.current = true;
|
|
1530
1547
|
let resplitWidth;
|
|
1531
1548
|
const targets = resolveAutoSplitTargets(currentElement);
|
|
@@ -1607,6 +1624,7 @@ var SplitText = forwardRef(function SplitText2({
|
|
|
1607
1624
|
autoSplit,
|
|
1608
1625
|
childTreeVersion,
|
|
1609
1626
|
data,
|
|
1627
|
+
lockCurrentRenderedLines,
|
|
1610
1628
|
measureAndSetData,
|
|
1611
1629
|
resolveLineMeasureWidth
|
|
1612
1630
|
]);
|
|
@@ -1707,14 +1725,28 @@ var SplitText = forwardRef(function SplitText2({
|
|
|
1707
1725
|
}
|
|
1708
1726
|
if (resizeTimerRef.current) {
|
|
1709
1727
|
clearTimeout(resizeTimerRef.current);
|
|
1728
|
+
resizeTimerRef.current = null;
|
|
1729
|
+
}
|
|
1730
|
+
const debounceMs = resolveResplitDebounceMs(
|
|
1731
|
+
optionsRef.current?.resplitDebounceMs
|
|
1732
|
+
);
|
|
1733
|
+
if (debounceMs <= 0) {
|
|
1734
|
+
lockCurrentRenderedLines(child2);
|
|
1735
|
+
pendingFullResplitRef.current = true;
|
|
1736
|
+
measureAndSetData(
|
|
1737
|
+
true,
|
|
1738
|
+
splitLines ? lineMeasureWidth : currentWidth
|
|
1739
|
+
);
|
|
1740
|
+
return;
|
|
1710
1741
|
}
|
|
1711
1742
|
resizeTimerRef.current = setTimeout(() => {
|
|
1743
|
+
lockCurrentRenderedLines(child2);
|
|
1712
1744
|
pendingFullResplitRef.current = true;
|
|
1713
1745
|
measureAndSetData(
|
|
1714
1746
|
true,
|
|
1715
1747
|
splitLines ? lineMeasureWidth : currentWidth
|
|
1716
1748
|
);
|
|
1717
|
-
},
|
|
1749
|
+
}, debounceMs);
|
|
1718
1750
|
};
|
|
1719
1751
|
resizeObserverRef.current = new ResizeObserver((entries) => {
|
|
1720
1752
|
let changed = false;
|
|
@@ -1755,6 +1787,7 @@ var SplitText = forwardRef(function SplitText2({
|
|
|
1755
1787
|
}, [
|
|
1756
1788
|
autoSplit,
|
|
1757
1789
|
data,
|
|
1790
|
+
lockCurrentRenderedLines,
|
|
1758
1791
|
measureAndSetData,
|
|
1759
1792
|
measureLineFingerprintForWidth,
|
|
1760
1793
|
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,5 @@
|
|
|
1
1
|
import { waitForFontsReady, createViewportObserver, reapplyInitialStyles, reapplyInitialClasses } from './chunk-PA22PLRB.js';
|
|
2
|
-
import { splitText, normalizeToPromise } from './chunk-
|
|
2
|
+
import { splitText, normalizeToPromise } from './chunk-44MU2I5B.js';
|
|
3
3
|
import { forwardRef, useRef, useCallback, useState, useLayoutEffect, useEffect, isValidElement, createElement } from 'react';
|
|
4
4
|
|
|
5
5
|
var SplitText = forwardRef(
|