fetta 1.3.5 → 1.4.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 CHANGED
@@ -86,6 +86,8 @@ const result = splitText(element, options);
86
86
  | `revertOnComplete` | `boolean` | `false` | Auto-revert when animation completes |
87
87
  | `propIndex` | `boolean` | `false` | Add CSS custom properties: `--char-index`, `--word-index`, `--line-index` |
88
88
  | `disableKerning` | `boolean` | `false` | Skip kerning compensation (no margin adjustments) |
89
+ | `initialStyles` | `object` | — | Apply initial inline styles to chars/words/lines after split |
90
+ | `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines after split |
89
91
 
90
92
  #### Return Value
91
93
 
@@ -117,6 +119,9 @@ import { SplitText } from 'fetta/react';
117
119
  | `inView` | `boolean \| InViewOptions` | `false` | Enable viewport detection |
118
120
  | `onInView` | `function` | — | Called when element enters viewport |
119
121
  | `onLeaveView` | `function` | — | Called when element leaves viewport |
122
+ | `initialStyles` | `object` | — | Apply initial inline styles to chars/words/lines |
123
+ | `initialClasses` | `object` | — | Apply initial CSS classes to chars/words/lines |
124
+ | `resetOnLeave` | `boolean` | `false` | Re-apply initialStyles/initialClasses when leaving viewport |
120
125
 
121
126
  #### InView Options
122
127
 
@@ -147,13 +152,15 @@ import { SplitText } from 'fetta/react';
147
152
 
148
153
  ```tsx
149
154
  <SplitText
150
- onSplit={({ words }) => {
151
- words.forEach(w => (w.style.opacity = '0'));
155
+ options={{ type: 'words' }}
156
+ initialStyles={{
157
+ words: { opacity: '0', transform: 'translateY(20px)' }
152
158
  }}
153
- inView={{ amount: 0.5, once: true }}
159
+ inView={{ amount: 0.5 }}
154
160
  onInView={({ words }) => {
155
- animate(words, { opacity: 1, y: [20, 0] }, { delay: stagger(0.03) });
161
+ animate(words, { opacity: 1, y: 0 }, { delay: stagger(0.03) });
156
162
  }}
163
+ resetOnLeave
157
164
  >
158
165
  <p>Animates when scrolled into view</p>
159
166
  </SplitText>
@@ -411,6 +411,32 @@ function createMaskWrapper(display = "inline-block") {
411
411
  wrapper.style.overflow = "clip";
412
412
  return wrapper;
413
413
  }
414
+ var PROTECTED_STYLES = /* @__PURE__ */ new Set([
415
+ "display",
416
+ "position",
417
+ "textDecoration",
418
+ "fontVariantLigatures"
419
+ ]);
420
+ function applyInitialStyles(elements, style) {
421
+ if (!style || elements.length === 0) return;
422
+ const isFn = typeof style === "function";
423
+ for (let i = 0; i < elements.length; i++) {
424
+ const el = elements[i];
425
+ const styles = isFn ? style(el, i) : style;
426
+ for (const [key, value] of Object.entries(styles)) {
427
+ if (!PROTECTED_STYLES.has(key) && value !== void 0) {
428
+ el.style[key] = value;
429
+ }
430
+ }
431
+ }
432
+ }
433
+ function applyInitialClasses(elements, className) {
434
+ if (!className || elements.length === 0) return;
435
+ const classes = className.split(/\s+/).filter(Boolean);
436
+ for (const el of elements) {
437
+ el.classList.add(...classes);
438
+ }
439
+ }
414
440
  function groupIntoLines(elements, element) {
415
441
  const fontSize = parseFloat(getComputedStyle(element).fontSize);
416
442
  const tolerance = Math.max(5, fontSize * 0.3);
@@ -437,6 +463,7 @@ function groupIntoLines(elements, element) {
437
463
  return lineGroups;
438
464
  }
439
465
  function performSplit(element, measuredWords, charClass, wordClass, lineClass, splitChars, splitWords, splitLines, options) {
466
+ var _a, _b;
440
467
  element.textContent = "";
441
468
  const allChars = [];
442
469
  const allWords = [];
@@ -738,12 +765,34 @@ function performSplit(element, measuredWords, charClass, wordClass, lineClass, s
738
765
  element.appendChild(lineSpan);
739
766
  }
740
767
  });
768
+ if (options == null ? void 0 : options.initialStyles) {
769
+ const { chars, words, lines } = options.initialStyles;
770
+ if (chars) applyInitialStyles(allChars, chars);
771
+ if (words) applyInitialStyles(allWords, words);
772
+ if (lines) applyInitialStyles(allLines, lines);
773
+ }
774
+ if (options == null ? void 0 : options.initialClasses) {
775
+ const { chars, words, lines } = options.initialClasses;
776
+ if (chars) applyInitialClasses(allChars, chars);
777
+ if (words) applyInitialClasses(allWords, words);
778
+ if (lines) applyInitialClasses(allLines, lines);
779
+ }
741
780
  return {
742
781
  chars: allChars,
743
782
  words: splitWords ? allWords : [],
744
783
  lines: allLines
745
784
  };
746
785
  }
786
+ if (options == null ? void 0 : options.initialStyles) {
787
+ const { chars, words } = options.initialStyles;
788
+ if (chars) applyInitialStyles(allChars, chars);
789
+ if (words) applyInitialStyles(allWords, words);
790
+ }
791
+ if (options == null ? void 0 : options.initialClasses) {
792
+ const { chars, words } = options.initialClasses;
793
+ if (chars) applyInitialClasses(allChars, chars);
794
+ if (words) applyInitialClasses(allWords, words);
795
+ }
747
796
  return {
748
797
  chars: allChars,
749
798
  words: splitWords ? allWords : [],
@@ -795,6 +844,12 @@ function performSplit(element, measuredWords, charClass, wordClass, lineClass, s
795
844
  element.appendChild(lineSpan);
796
845
  }
797
846
  });
847
+ if ((_a = options == null ? void 0 : options.initialStyles) == null ? void 0 : _a.lines) {
848
+ applyInitialStyles(allLines, options.initialStyles.lines);
849
+ }
850
+ if ((_b = options == null ? void 0 : options.initialClasses) == null ? void 0 : _b.lines) {
851
+ applyInitialClasses(allLines, options.initialClasses.lines);
852
+ }
798
853
  return { chars: [], words: [], lines: allLines };
799
854
  } else {
800
855
  const fullText = measuredWords.map((w) => w.chars.map((c) => c.char).join("")).join(" ");
@@ -814,7 +869,9 @@ function splitText(element, {
814
869
  onSplit,
815
870
  revertOnComplete = false,
816
871
  propIndex = false,
817
- disableKerning = false
872
+ disableKerning = false,
873
+ initialStyles,
874
+ initialClasses
818
875
  } = {}) {
819
876
  var _a;
820
877
  if (!(element instanceof HTMLElement)) {
@@ -865,7 +922,7 @@ function splitText(element, {
865
922
  splitChars,
866
923
  splitWords,
867
924
  splitLines,
868
- { propIndex, mask, ariaHidden: !trackAncestors, disableKerning }
925
+ { propIndex, mask, ariaHidden: !trackAncestors, disableKerning, initialStyles, initialClasses }
869
926
  );
870
927
  currentChars = chars;
871
928
  currentWords = words;
@@ -940,7 +997,7 @@ function splitText(element, {
940
997
  splitChars,
941
998
  splitWords,
942
999
  splitLines,
943
- { propIndex, mask, ariaHidden: !trackAncestors, disableKerning }
1000
+ { propIndex, mask, ariaHidden: !trackAncestors, disableKerning, initialStyles, initialClasses }
944
1001
  );
945
1002
  currentChars = result.chars;
946
1003
  currentWords = result.words;
package/dist/index.d.ts CHANGED
@@ -46,7 +46,27 @@ interface SplitTextOptions {
46
46
  * Kerning is naturally lost when splitting into inline-block spans.
47
47
  * Use this if you prefer no compensation over imperfect Safari compensation. */
48
48
  disableKerning?: boolean;
49
+ /** Apply initial inline styles to elements after split (and after kerning compensation).
50
+ * Can be a static style object or a function that receives (element, index). */
51
+ initialStyles?: {
52
+ chars?: InitialStyle;
53
+ words?: InitialStyle;
54
+ lines?: InitialStyle;
55
+ };
56
+ /** Apply initial classes to elements after split (and after kerning compensation).
57
+ * Classes are added via classList.add() and support space-separated class names. */
58
+ initialClasses?: {
59
+ chars?: string;
60
+ words?: string;
61
+ lines?: string;
62
+ };
49
63
  }
64
+ /** Style value for initialStyles - a partial CSSStyleDeclaration object */
65
+ type InitialStyleValue = Partial<CSSStyleDeclaration>;
66
+ /** Function that returns styles based on element and index */
67
+ type InitialStyleFn = (element: HTMLElement, index: number) => InitialStyleValue;
68
+ /** Initial style can be a static object or a function */
69
+ type InitialStyle = InitialStyleValue | InitialStyleFn;
50
70
  /**
51
71
  * Result returned by splitText containing arrays of split elements and a revert function.
52
72
  *
@@ -112,6 +132,6 @@ interface SplitTextResult {
112
132
  * });
113
133
  * ```
114
134
  */
115
- declare function splitText(element: HTMLElement, { type, charClass, wordClass, lineClass, mask, autoSplit, onResize, onSplit, revertOnComplete, propIndex, disableKerning, }?: SplitTextOptions): SplitTextResult;
135
+ declare function splitText(element: HTMLElement, { type, charClass, wordClass, lineClass, mask, autoSplit, onResize, onSplit, revertOnComplete, propIndex, disableKerning, initialStyles, initialClasses, }?: SplitTextOptions): SplitTextResult;
116
136
 
117
137
  export { type SplitTextOptions, type SplitTextResult, splitText };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { splitText } from './chunk-HQLYE4Q5.js';
1
+ export { splitText } from './chunk-NTRJ6XDH.js';
package/dist/react.d.ts CHANGED
@@ -2,6 +2,24 @@ import * as react from 'react';
2
2
  import { ReactElement } from 'react';
3
3
  export { SplitTextOptions, SplitTextResult } from './index.js';
4
4
 
5
+ /** Style value for initialStyles - a partial CSSStyleDeclaration object */
6
+ type InitialStyleValue = Partial<CSSStyleDeclaration>;
7
+ /** Function that returns styles based on element and index */
8
+ type InitialStyleFn = (element: HTMLElement, index: number) => InitialStyleValue;
9
+ /** Initial style can be a static object or a function */
10
+ type InitialStyle = InitialStyleValue | InitialStyleFn;
11
+ /** Initial styles configuration for chars, words, and/or lines */
12
+ interface InitialStyles {
13
+ chars?: InitialStyle;
14
+ words?: InitialStyle;
15
+ lines?: InitialStyle;
16
+ }
17
+ /** Initial classes configuration for chars, words, and/or lines */
18
+ interface InitialClasses {
19
+ chars?: string;
20
+ words?: string;
21
+ lines?: string;
22
+ }
5
23
  interface SplitTextOptions {
6
24
  type?: "chars" | "words" | "lines" | "chars,words" | "words,lines" | "chars,lines" | "chars,words,lines";
7
25
  charClass?: string;
@@ -71,6 +89,15 @@ interface SplitTextProps {
71
89
  onInView?: (result: SplitTextElements) => CallbackReturn;
72
90
  /** Called when element leaves viewport */
73
91
  onLeaveView?: (result: SplitTextElements) => CallbackReturn;
92
+ /** Apply initial inline styles to elements after split (and after kerning compensation).
93
+ * Can be a static style object or a function that receives (element, index). */
94
+ initialStyles?: InitialStyles;
95
+ /** Apply initial classes to elements after split (and after kerning compensation).
96
+ * Classes are added via classList.add() and support space-separated class names. */
97
+ initialClasses?: InitialClasses;
98
+ /** Re-apply initialStyles and initialClasses when element leaves viewport.
99
+ * Useful for scroll-triggered animations that should reset when scrolling away. */
100
+ resetOnLeave?: boolean;
74
101
  }
75
102
  /**
76
103
  * React component wrapper for text splitting with kerning compensation.
package/dist/react.js CHANGED
@@ -1,7 +1,27 @@
1
- import { splitText, __spreadProps, __spreadValues, normalizeToPromise } from './chunk-HQLYE4Q5.js';
1
+ import { splitText, __spreadProps, __spreadValues, normalizeToPromise } from './chunk-NTRJ6XDH.js';
2
2
  import { forwardRef, useRef, useCallback, useState, useLayoutEffect, useEffect, isValidElement, cloneElement } from 'react';
3
3
  import { jsx } from 'react/jsx-runtime';
4
4
 
5
+ function reapplyInitialStyles(elements, style) {
6
+ if (!style || elements.length === 0) return;
7
+ const isFn = typeof style === "function";
8
+ for (let i = 0; i < elements.length; i++) {
9
+ const el = elements[i];
10
+ const styles = isFn ? style(el, i) : style;
11
+ for (const [key, value] of Object.entries(styles)) {
12
+ if (value !== void 0) {
13
+ el.style[key] = value;
14
+ }
15
+ }
16
+ }
17
+ }
18
+ function reapplyInitialClasses(elements, className) {
19
+ if (!className || elements.length === 0) return;
20
+ const classes = className.split(/\s+/).filter(Boolean);
21
+ for (const el of elements) {
22
+ el.classList.add(...classes);
23
+ }
24
+ }
5
25
  var SplitText = forwardRef(
6
26
  function SplitText2({
7
27
  children,
@@ -15,7 +35,10 @@ var SplitText = forwardRef(
15
35
  revertOnComplete = false,
16
36
  inView,
17
37
  onInView,
18
- onLeaveView
38
+ onLeaveView,
39
+ initialStyles,
40
+ initialClasses,
41
+ resetOnLeave = false
19
42
  }, forwardedRef) {
20
43
  const containerRef = useRef(null);
21
44
  const mergedRef = useCallback(
@@ -38,6 +61,9 @@ var SplitText = forwardRef(
38
61
  const inViewRef = useRef(inView);
39
62
  const onInViewRef = useRef(onInView);
40
63
  const onLeaveViewRef = useRef(onLeaveView);
64
+ const initialStylesRef = useRef(initialStyles);
65
+ const initialClassesRef = useRef(initialClasses);
66
+ const resetOnLeaveRef = useRef(resetOnLeave);
41
67
  useLayoutEffect(() => {
42
68
  onSplitRef.current = onSplit;
43
69
  onResizeRef.current = onResize;
@@ -46,6 +72,9 @@ var SplitText = forwardRef(
46
72
  inViewRef.current = inView;
47
73
  onInViewRef.current = onInView;
48
74
  onLeaveViewRef.current = onLeaveView;
75
+ initialStylesRef.current = initialStyles;
76
+ initialClassesRef.current = initialClasses;
77
+ resetOnLeaveRef.current = resetOnLeave;
49
78
  });
50
79
  const hasSplitRef = useRef(false);
51
80
  const hasRevertedRef = useRef(false);
@@ -67,6 +96,8 @@ var SplitText = forwardRef(
67
96
  const result = splitText(childElement, __spreadProps(__spreadValues({}, optionsRef.current), {
68
97
  autoSplit,
69
98
  revertOnComplete: revertOnCompleteRef.current,
99
+ initialStyles: initialStylesRef.current,
100
+ initialClasses: initialClassesRef.current,
70
101
  onResize: (resizeResult) => {
71
102
  var _a2;
72
103
  const newSplitTextElements = {
@@ -159,8 +190,25 @@ var SplitText = forwardRef(
159
190
  console.warn("[fetta] Animation rejected, text not reverted");
160
191
  });
161
192
  }
162
- } else if (!isInView && onLeaveViewRef.current && splitResultRef.current) {
163
- onLeaveViewRef.current(splitResultRef.current);
193
+ } else if (!isInView && splitResultRef.current) {
194
+ if (resetOnLeaveRef.current) {
195
+ const { chars, words, lines } = splitResultRef.current;
196
+ const styles = initialStylesRef.current;
197
+ const classes = initialClassesRef.current;
198
+ if (styles) {
199
+ reapplyInitialStyles(chars, styles.chars);
200
+ reapplyInitialStyles(words, styles.words);
201
+ reapplyInitialStyles(lines, styles.lines);
202
+ }
203
+ if (classes) {
204
+ reapplyInitialClasses(chars, classes.chars);
205
+ reapplyInitialClasses(words, classes.words);
206
+ reapplyInitialClasses(lines, classes.lines);
207
+ }
208
+ }
209
+ if (onLeaveViewRef.current) {
210
+ onLeaveViewRef.current(splitResultRef.current);
211
+ }
164
212
  }
165
213
  }, [isInView]);
166
214
  if (!isValidElement(children)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fetta",
3
- "version": "1.3.5",
3
+ "version": "1.4.0",
4
4
  "description": "Text splitting library with kerning compensation for animations",
5
5
  "type": "module",
6
6
  "sideEffects": false,