fetta 1.3.4 → 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
@@ -85,6 +85,9 @@ const result = splitText(element, options);
85
85
  | `onSplit` | `function` | — | Callback after initial split |
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
+ | `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 |
88
91
 
89
92
  #### Return Value
90
93
 
@@ -110,12 +113,15 @@ import { SplitText } from 'fetta/react';
110
113
  | `children` | `ReactElement` | — | Single element to split |
111
114
  | `onSplit` | `function` | — | Called after text is split |
112
115
  | `onResize` | `function` | — | Called on autoSplit re-split |
113
- | `options` | `object` | — | Split options (type, classes, mask, propIndex) |
116
+ | `options` | `object` | — | Split options (type, classes, mask, propIndex, disableKerning) |
114
117
  | `autoSplit` | `boolean` | `false` | Re-split on container resize |
115
118
  | `revertOnComplete` | `boolean` | `false` | Revert after animation completes |
116
119
  | `inView` | `boolean \| InViewOptions` | `false` | Enable viewport detection |
117
120
  | `onInView` | `function` | — | Called when element enters viewport |
118
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 |
119
125
 
120
126
  #### InView Options
121
127
 
@@ -146,13 +152,15 @@ import { SplitText } from 'fetta/react';
146
152
 
147
153
  ```tsx
148
154
  <SplitText
149
- onSplit={({ words }) => {
150
- words.forEach(w => (w.style.opacity = '0'));
155
+ options={{ type: 'words' }}
156
+ initialStyles={{
157
+ words: { opacity: '0', transform: 'translateY(20px)' }
151
158
  }}
152
- inView={{ amount: 0.5, once: true }}
159
+ inView={{ amount: 0.5 }}
153
160
  onInView={({ words }) => {
154
- animate(words, { opacity: 1, y: [20, 0] }, { delay: stagger(0.03) });
161
+ animate(words, { opacity: 1, y: 0 }, { delay: stagger(0.03) });
155
162
  }}
163
+ resetOnLeave
156
164
  >
157
165
  <p>Animates when scrolled into view</p>
158
166
  </SplitText>
@@ -33,6 +33,10 @@ var BREAK_CHARS = /* @__PURE__ */ new Set([
33
33
  "\u2015"
34
34
  // horizontal bar (U+2015)
35
35
  ]);
36
+ var CONTEXTUAL_SCRIPT_REGEX = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF\u0590-\u05FF\uFB1D-\uFB4F\u0E00-\u0E7F\u0900-\u097F\u0980-\u09FF\u0A00-\u0A7F\u0A80-\u0AFF\u0B00-\u0B7F\u0B80-\u0BFF\u0C00-\u0C7F\u0C80-\u0CFF\u0D00-\u0D7F]/;
37
+ function hasContextualScript(chars) {
38
+ return chars.some((char) => CONTEXTUAL_SCRIPT_REGEX.test(char));
39
+ }
36
40
  var INLINE_ELEMENTS = /* @__PURE__ */ new Set([
37
41
  "a",
38
42
  "abbr",
@@ -407,6 +411,32 @@ function createMaskWrapper(display = "inline-block") {
407
411
  wrapper.style.overflow = "clip";
408
412
  return wrapper;
409
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
+ }
410
440
  function groupIntoLines(elements, element) {
411
441
  const fontSize = parseFloat(getComputedStyle(element).fontSize);
412
442
  const tolerance = Math.max(5, fontSize * 0.3);
@@ -433,6 +463,7 @@ function groupIntoLines(elements, element) {
433
463
  return lineGroups;
434
464
  }
435
465
  function performSplit(element, measuredWords, charClass, wordClass, lineClass, splitChars, splitWords, splitLines, options) {
466
+ var _a, _b;
436
467
  element.textContent = "";
437
468
  const allChars = [];
438
469
  const allWords = [];
@@ -584,6 +615,8 @@ function performSplit(element, measuredWords, charClass, wordClass, lineClass, s
584
615
  for (const wordSpan of allWords) {
585
616
  const wordChars = Array.from(wordSpan.querySelectorAll(`.${charClass}`));
586
617
  if (wordChars.length < 2) continue;
618
+ const charStringsForCheck = wordChars.map((c) => c.textContent || "");
619
+ if (hasContextualScript(charStringsForCheck)) continue;
587
620
  const styleGroups = [];
588
621
  const firstCharStyles = getComputedStyle(wordChars[0]);
589
622
  let currentKey = buildKerningStyleKey(firstCharStyles);
@@ -630,6 +663,7 @@ function performSplit(element, measuredWords, charClass, wordClass, lineClass, s
630
663
  const lastChar = lastCharSpan.textContent || "";
631
664
  const firstChar = firstCharSpan.textContent || "";
632
665
  if (!lastChar || !firstChar) continue;
666
+ if (hasContextualScript([lastChar, firstChar])) continue;
633
667
  const styles = getComputedStyle(firstCharSpan);
634
668
  const kerningMap = measureKerning(element, firstCharSpan, [lastChar, " ", firstChar], styles);
635
669
  let totalKerning = 0;
@@ -650,6 +684,7 @@ function performSplit(element, measuredWords, charClass, wordClass, lineClass, s
650
684
  if (!prevText || !currText) continue;
651
685
  const lastChar = prevText[prevText.length - 1];
652
686
  const firstChar = currText[0];
687
+ if (hasContextualScript([lastChar, firstChar])) continue;
653
688
  const styles = getComputedStyle(currWord);
654
689
  const kerningMap = measureKerning(element, currWord, [lastChar, " ", firstChar], styles);
655
690
  let totalKerning = 0;
@@ -730,12 +765,34 @@ function performSplit(element, measuredWords, charClass, wordClass, lineClass, s
730
765
  element.appendChild(lineSpan);
731
766
  }
732
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
+ }
733
780
  return {
734
781
  chars: allChars,
735
782
  words: splitWords ? allWords : [],
736
783
  lines: allLines
737
784
  };
738
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
+ }
739
796
  return {
740
797
  chars: allChars,
741
798
  words: splitWords ? allWords : [],
@@ -787,6 +844,12 @@ function performSplit(element, measuredWords, charClass, wordClass, lineClass, s
787
844
  element.appendChild(lineSpan);
788
845
  }
789
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
+ }
790
853
  return { chars: [], words: [], lines: allLines };
791
854
  } else {
792
855
  const fullText = measuredWords.map((w) => w.chars.map((c) => c.char).join("")).join(" ");
@@ -806,7 +869,9 @@ function splitText(element, {
806
869
  onSplit,
807
870
  revertOnComplete = false,
808
871
  propIndex = false,
809
- disableKerning = false
872
+ disableKerning = false,
873
+ initialStyles,
874
+ initialClasses
810
875
  } = {}) {
811
876
  var _a;
812
877
  if (!(element instanceof HTMLElement)) {
@@ -857,7 +922,7 @@ function splitText(element, {
857
922
  splitChars,
858
923
  splitWords,
859
924
  splitLines,
860
- { propIndex, mask, ariaHidden: !trackAncestors, disableKerning }
925
+ { propIndex, mask, ariaHidden: !trackAncestors, disableKerning, initialStyles, initialClasses }
861
926
  );
862
927
  currentChars = chars;
863
928
  currentWords = words;
@@ -932,7 +997,7 @@ function splitText(element, {
932
997
  splitChars,
933
998
  splitWords,
934
999
  splitLines,
935
- { propIndex, mask, ariaHidden: !trackAncestors, disableKerning }
1000
+ { propIndex, mask, ariaHidden: !trackAncestors, disableKerning, initialStyles, initialClasses }
936
1001
  );
937
1002
  currentChars = result.chars;
938
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-BXFBPQNP.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-BXFBPQNP.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.4",
3
+ "version": "1.4.0",
4
4
  "description": "Text splitting library with kerning compensation for animations",
5
5
  "type": "module",
6
6
  "sideEffects": false,