@matteodicristofalo/text-animations 1.0.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/.storybook/main.ts +22 -0
- package/.storybook/preview.ts +14 -0
- package/README.md +138 -0
- package/package.json +53 -0
- package/rollup.config.mjs +21 -0
- package/src/components/index.ts +2 -0
- package/src/components/split-text-reveal/split-test-reveal.types.ts +12 -0
- package/src/components/split-text-reveal/split-text-reveal.module.scss +26 -0
- package/src/components/split-text-reveal/split-text-reveal.stories.ts +34 -0
- package/src/components/split-text-reveal/split-text-reveal.tsx +61 -0
- package/src/components/split-text-rotate/split-text-rotate.module.scss +34 -0
- package/src/components/split-text-rotate/split-text-rotate.stories.ts +29 -0
- package/src/components/split-text-rotate/split-text-rotate.tsx +60 -0
- package/src/components/split-text-rotate/split-text-rotate.types.ts +9 -0
- package/src/global.d.ts +4 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/intersection-overser/intersection-observer.ts +41 -0
- package/src/index.ts +1 -0
- package/src/utils/numbers.ts +3 -0
- package/src/utils/text.ts +19 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { StorybookConfig } from "@storybook/react-vite";
|
|
2
|
+
import tsconfigPaths from "vite-tsconfig-paths";
|
|
3
|
+
|
|
4
|
+
const config: StorybookConfig = {
|
|
5
|
+
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
|
6
|
+
addons: [
|
|
7
|
+
"@storybook/addon-onboarding",
|
|
8
|
+
"@storybook/addon-essentials",
|
|
9
|
+
"@chromatic-com/storybook",
|
|
10
|
+
"@storybook/addon-interactions",
|
|
11
|
+
],
|
|
12
|
+
framework: {
|
|
13
|
+
name: "@storybook/react-vite",
|
|
14
|
+
options: {},
|
|
15
|
+
},
|
|
16
|
+
viteFinal: async (config) => {
|
|
17
|
+
config.plugins?.push(tsconfigPaths());
|
|
18
|
+
return config;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default config;
|
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# @mdc/text-animations
|
|
2
|
+
|
|
3
|
+
This library provides a set of React components to animate text
|
|
4
|
+
|
|
5
|
+
# Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @mdc/text-animations
|
|
9
|
+
# or
|
|
10
|
+
yarn add @mdc/text-animations
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @mdc/text-animations
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
# Usage
|
|
16
|
+
|
|
17
|
+
The first thing to do in order to use this library is to import it's CSS.
|
|
18
|
+
|
|
19
|
+
The following example is for a Next.js App but you can apply the same concept for different frameworks
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
// app/layout.tsx
|
|
23
|
+
|
|
24
|
+
...
|
|
25
|
+
import "@mdc/text-animations/index.css";
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
export default function RootLayout({
|
|
29
|
+
children,
|
|
30
|
+
}: Readonly<{
|
|
31
|
+
children: React.ReactNode;
|
|
32
|
+
}>) {
|
|
33
|
+
return (
|
|
34
|
+
<html lang="en">
|
|
35
|
+
<body>
|
|
36
|
+
{children}
|
|
37
|
+
</body>
|
|
38
|
+
</html>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Once imported the CSS you can use the React components.
|
|
44
|
+
|
|
45
|
+
## SplitTextRevel
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
"use client";
|
|
49
|
+
|
|
50
|
+
import { SplitTextRevel } from "@mdc/text-animations";
|
|
51
|
+
|
|
52
|
+
export function MyComponent() {
|
|
53
|
+
return (
|
|
54
|
+
<h1>
|
|
55
|
+
<SplitTextRevel text="Hello World" />
|
|
56
|
+
</h1>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
You can also choose how to split the text between one of this options:
|
|
62
|
+
|
|
63
|
+
- char
|
|
64
|
+
- word
|
|
65
|
+
- sentence
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
<SplitTextRevel text="Hello World" splitType="word" />
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
You can also configure the reveal animation by specifying:
|
|
72
|
+
|
|
73
|
+
- duration
|
|
74
|
+
- stagger
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
"use client";
|
|
78
|
+
|
|
79
|
+
import { useMemo } from "react";
|
|
80
|
+
import { SplitTextRevel } from "@mdc/text-animations";
|
|
81
|
+
|
|
82
|
+
export function MyComponent() {
|
|
83
|
+
const revealOptions = useMemo(
|
|
84
|
+
() => ({
|
|
85
|
+
duration: 1,
|
|
86
|
+
stagger: 0.5,
|
|
87
|
+
}),
|
|
88
|
+
[]
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<h1>
|
|
93
|
+
<SplitTextRevel text="Hello World" revealOptions={revealOptions} />
|
|
94
|
+
</h1>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## SplitTextRotate
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
"use client";
|
|
103
|
+
|
|
104
|
+
import { SplitTextRotate } from "@mdc/text-animations";
|
|
105
|
+
|
|
106
|
+
export function MyComponent() {
|
|
107
|
+
return (
|
|
108
|
+
<h1>
|
|
109
|
+
<SplitTextRotate text="Hello World" />
|
|
110
|
+
</h1>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Also here you can configure the aniimation
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
"use client";
|
|
119
|
+
|
|
120
|
+
import { useMemo } from "react";
|
|
121
|
+
import { SplitTextRotate } from "@mdc/text-animations";
|
|
122
|
+
|
|
123
|
+
export function MyComponent() {
|
|
124
|
+
const rotateOptions = useMemo(
|
|
125
|
+
() => ({
|
|
126
|
+
duration: 1,
|
|
127
|
+
stagger: 0.5,
|
|
128
|
+
}),
|
|
129
|
+
[]
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<h1>
|
|
134
|
+
<SplitTextRotate text="Hello World" rotateOptions={rotateOptions} />
|
|
135
|
+
</h1>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@matteodicristofalo/text-animations",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React library to animate text",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "rimraf dist && rollup -c",
|
|
7
|
+
"storybook": "storybook dev -p 6006"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"clsx": "^2.1.1"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@chromatic-com/storybook": "^3.2.2",
|
|
14
|
+
"@rollup/plugin-node-resolve": "^15.3.0",
|
|
15
|
+
"@rollup/plugin-typescript": "^12.1.1",
|
|
16
|
+
"@storybook/addon-essentials": "^8.4.6",
|
|
17
|
+
"@storybook/addon-interactions": "^8.4.6",
|
|
18
|
+
"@storybook/addon-onboarding": "^8.4.6",
|
|
19
|
+
"@storybook/blocks": "^8.4.6",
|
|
20
|
+
"@storybook/react": "^8.4.6",
|
|
21
|
+
"@storybook/react-vite": "^8.4.6",
|
|
22
|
+
"@storybook/test": "^8.4.6",
|
|
23
|
+
"@types/node": "^20",
|
|
24
|
+
"@types/react": "^18",
|
|
25
|
+
"@types/react-dom": "^18",
|
|
26
|
+
"rimraf": "^6.0.1",
|
|
27
|
+
"rollup": "^4.28.0",
|
|
28
|
+
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
29
|
+
"rollup-plugin-postcss": "^4.0.2",
|
|
30
|
+
"sass": "^1.81.0",
|
|
31
|
+
"storybook": "^8.4.6",
|
|
32
|
+
"tslib": "^2.8.1",
|
|
33
|
+
"typescript": "^5.7.2",
|
|
34
|
+
"vite-tsconfig-paths": "^5.1.3"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"react": "^18.3.1",
|
|
38
|
+
"react-dom": "^18.3.1"
|
|
39
|
+
},
|
|
40
|
+
"type": "module",
|
|
41
|
+
"types": "dist/index.d.ts",
|
|
42
|
+
"exports": {
|
|
43
|
+
".": "./dist/index.js",
|
|
44
|
+
"./index.css": "./dist/index.css"
|
|
45
|
+
},
|
|
46
|
+
"author": "Matteo Di Cristofalo",
|
|
47
|
+
"license": "ISC",
|
|
48
|
+
"homepage": "https://github.com/matteodicristofalo/text-animations",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git@github.com:matteodicristofalo/text-animations.git"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import resolve from '@rollup/plugin-node-resolve';
|
|
2
|
+
import typescript from '@rollup/plugin-typescript';
|
|
3
|
+
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
|
|
4
|
+
import postCss from 'rollup-plugin-postcss';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
input: 'src/index.ts',
|
|
8
|
+
output: {
|
|
9
|
+
dir: 'dist',
|
|
10
|
+
format: 'es',
|
|
11
|
+
},
|
|
12
|
+
plugins: [
|
|
13
|
+
postCss({
|
|
14
|
+
extract: true,
|
|
15
|
+
modules: true,
|
|
16
|
+
}),
|
|
17
|
+
typescript(),
|
|
18
|
+
peerDepsExternal(),
|
|
19
|
+
resolve(),
|
|
20
|
+
],
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.text {
|
|
2
|
+
position: relative;
|
|
3
|
+
|
|
4
|
+
.container {
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
|
|
8
|
+
span {
|
|
9
|
+
min-width: 0.25em;
|
|
10
|
+
transform: translateY(100%);
|
|
11
|
+
transition-delay: var(--var-delay);
|
|
12
|
+
transition-duration: var(--var-duration);
|
|
13
|
+
transition-property: transform;
|
|
14
|
+
transition-timing-function: ease-out;
|
|
15
|
+
will-change: transform;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
&.animate {
|
|
20
|
+
.container {
|
|
21
|
+
span {
|
|
22
|
+
transform: translateY(0);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { SplitTextReveal } from "./split-text-reveal";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "SplitTextReveal",
|
|
6
|
+
component: SplitTextReveal,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: "centered",
|
|
9
|
+
},
|
|
10
|
+
argTypes: {
|
|
11
|
+
text: {
|
|
12
|
+
control: "text",
|
|
13
|
+
},
|
|
14
|
+
splitType: {
|
|
15
|
+
control: "radio",
|
|
16
|
+
options: ["char", "word", "sentence"],
|
|
17
|
+
},
|
|
18
|
+
revealOptions: {
|
|
19
|
+
control: {
|
|
20
|
+
type: "object",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
} satisfies Meta<typeof SplitTextReveal>;
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof meta>;
|
|
28
|
+
|
|
29
|
+
export const Default: Story = {
|
|
30
|
+
args: {
|
|
31
|
+
text: "Here is the revealed text",
|
|
32
|
+
splitType: "char",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useMemo, useRef } from "react";
|
|
4
|
+
import { round } from "@utils/numbers";
|
|
5
|
+
import { characters, sentences, words } from "@utils/text";
|
|
6
|
+
import { useIntersectionObserver } from "@hooks";
|
|
7
|
+
import { SplitTextRevealProps } from "./split-test-reveal.types";
|
|
8
|
+
import clsx from "clsx";
|
|
9
|
+
import styles from "./split-text-reveal.module.scss";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_DURATION = 0.5;
|
|
12
|
+
const DEFAULT_STAGGER = 0.01;
|
|
13
|
+
|
|
14
|
+
export function SplitTextReveal({
|
|
15
|
+
text,
|
|
16
|
+
splitType = "char",
|
|
17
|
+
revealOptions,
|
|
18
|
+
}: SplitTextRevealProps) {
|
|
19
|
+
const ref = useRef(null);
|
|
20
|
+
const memoizedOptions = useMemo(() => ({ once: true, threshold: 0.1 }), []);
|
|
21
|
+
const isInView = useIntersectionObserver(ref, memoizedOptions);
|
|
22
|
+
const transitionDuration = revealOptions?.duration || DEFAULT_DURATION;
|
|
23
|
+
const transitionStagger = revealOptions?.stagger || DEFAULT_STAGGER;
|
|
24
|
+
|
|
25
|
+
const splitFn = useMemo(() => {
|
|
26
|
+
switch (splitType) {
|
|
27
|
+
case "sentence":
|
|
28
|
+
return sentences;
|
|
29
|
+
case "word":
|
|
30
|
+
return words;
|
|
31
|
+
case "char":
|
|
32
|
+
return characters;
|
|
33
|
+
}
|
|
34
|
+
}, [splitType]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<span
|
|
38
|
+
ref={ref}
|
|
39
|
+
className={clsx(styles["text"], { [styles["animate"]]: isInView })}
|
|
40
|
+
style={
|
|
41
|
+
{
|
|
42
|
+
"--var-duration": `${transitionDuration}s`,
|
|
43
|
+
} as React.CSSProperties
|
|
44
|
+
}
|
|
45
|
+
>
|
|
46
|
+
{splitFn(text).map((el, i) => (
|
|
47
|
+
<span key={i} className={styles["container"]}>
|
|
48
|
+
<span
|
|
49
|
+
style={
|
|
50
|
+
{
|
|
51
|
+
"--var-delay": `${round(transitionStagger * i, 3)}s`,
|
|
52
|
+
} as React.CSSProperties
|
|
53
|
+
}
|
|
54
|
+
>
|
|
55
|
+
{el}
|
|
56
|
+
</span>
|
|
57
|
+
</span>
|
|
58
|
+
))}
|
|
59
|
+
</span>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
.text {
|
|
2
|
+
display: block;
|
|
3
|
+
overflow: hidden;
|
|
4
|
+
position: relative;
|
|
5
|
+
|
|
6
|
+
.container {
|
|
7
|
+
display: flex;
|
|
8
|
+
|
|
9
|
+
&:nth-of-type(2) {
|
|
10
|
+
position: absolute;
|
|
11
|
+
top: 0;
|
|
12
|
+
transform: translateY(100%);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.char {
|
|
16
|
+
display: inline-block;
|
|
17
|
+
min-width: 0.25em;
|
|
18
|
+
transition-delay: var(--var-delay-out);
|
|
19
|
+
transition-duration: var(--var-duration);
|
|
20
|
+
transition-property: transform;
|
|
21
|
+
transition-timing-function: ease-out;
|
|
22
|
+
will-change: transform;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&:hover {
|
|
27
|
+
.container {
|
|
28
|
+
.char {
|
|
29
|
+
transform: translateY(-100%);
|
|
30
|
+
transition-delay: var(--var-delay-in);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { SplitTextRotate } from "./split-text-rotate";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "SplitTextRotate",
|
|
6
|
+
component: SplitTextRotate,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: "centered",
|
|
9
|
+
},
|
|
10
|
+
argTypes: {
|
|
11
|
+
text: {
|
|
12
|
+
control: "text",
|
|
13
|
+
},
|
|
14
|
+
rotateOptions: {
|
|
15
|
+
control: {
|
|
16
|
+
type: "object",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
} satisfies Meta<typeof SplitTextRotate>;
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof meta>;
|
|
24
|
+
|
|
25
|
+
export const Default: Story = {
|
|
26
|
+
args: {
|
|
27
|
+
text: "Rotating text",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useCallback } from "react";
|
|
4
|
+
import { round } from "@utils/numbers";
|
|
5
|
+
import { characters } from "@utils/text";
|
|
6
|
+
import { SplitTextRotateProps } from "./split-text-rotate.types";
|
|
7
|
+
import styles from "./split-text-rotate.module.scss";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_DURATION = 0.25;
|
|
10
|
+
const DEFAULT_STAGGER = 0.025;
|
|
11
|
+
|
|
12
|
+
export function SplitTextRotate({ text, rotateOptions }: SplitTextRotateProps) {
|
|
13
|
+
const transitionDuration = rotateOptions?.duration || DEFAULT_DURATION;
|
|
14
|
+
const transitionStagger = rotateOptions?.stagger || DEFAULT_STAGGER;
|
|
15
|
+
const splittedText = characters(text);
|
|
16
|
+
|
|
17
|
+
const delays = useCallback(
|
|
18
|
+
(index: number) => {
|
|
19
|
+
const noElementsZeroBased = splittedText.length - 1;
|
|
20
|
+
return {
|
|
21
|
+
in: round(transitionStagger * index, 3),
|
|
22
|
+
out: round(transitionStagger * (noElementsZeroBased - index), 3),
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
[splittedText, transitionStagger]
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<span
|
|
30
|
+
className={styles["text"]}
|
|
31
|
+
style={
|
|
32
|
+
{
|
|
33
|
+
"--var-duration": `${transitionDuration}s`,
|
|
34
|
+
} as React.CSSProperties
|
|
35
|
+
}
|
|
36
|
+
>
|
|
37
|
+
{Array.from({ length: 2 }).map((_, i) => (
|
|
38
|
+
<span key={i} className={styles["container"]}>
|
|
39
|
+
{splittedText.map((char, i) => {
|
|
40
|
+
const charDelays = delays(i);
|
|
41
|
+
return (
|
|
42
|
+
<span
|
|
43
|
+
key={i}
|
|
44
|
+
className={styles["char"]}
|
|
45
|
+
style={
|
|
46
|
+
{
|
|
47
|
+
"--var-delay-in": `${charDelays.in}s`,
|
|
48
|
+
"--var-delay-out": `${charDelays.out}s`,
|
|
49
|
+
} as React.CSSProperties
|
|
50
|
+
}
|
|
51
|
+
>
|
|
52
|
+
{char}
|
|
53
|
+
</span>
|
|
54
|
+
);
|
|
55
|
+
})}
|
|
56
|
+
</span>
|
|
57
|
+
))}
|
|
58
|
+
</span>
|
|
59
|
+
);
|
|
60
|
+
}
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useIntersectionObserver } from "./intersection-overser/intersection-observer";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, RefObject } from "react";
|
|
4
|
+
|
|
5
|
+
type Options = IntersectionObserverInit & {
|
|
6
|
+
once?: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function useIntersectionObserver(
|
|
10
|
+
ref: RefObject<HTMLElement>,
|
|
11
|
+
options?: Options
|
|
12
|
+
): boolean {
|
|
13
|
+
const [isIntersecting, setIntersecting] = useState(false);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!ref.current) return;
|
|
17
|
+
|
|
18
|
+
const observer = new IntersectionObserver(
|
|
19
|
+
([entry]) => {
|
|
20
|
+
if (options?.once) {
|
|
21
|
+
if (entry.isIntersecting) {
|
|
22
|
+
setIntersecting(true);
|
|
23
|
+
observer.disconnect();
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
setIntersecting(entry.isIntersecting);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
threshold: 0.75,
|
|
31
|
+
...options,
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
observer.observe(ref.current);
|
|
36
|
+
|
|
37
|
+
return () => observer.disconnect();
|
|
38
|
+
}, [ref, options]);
|
|
39
|
+
|
|
40
|
+
return isIntersecting;
|
|
41
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./components";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function sentences(text: string) {
|
|
2
|
+
return segment(text, "sentence");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function words(text: string) {
|
|
6
|
+
return segment(text, "word");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function characters(text: string) {
|
|
10
|
+
return segment(text, "grapheme");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function segment(
|
|
14
|
+
text: string,
|
|
15
|
+
granularity: "sentence" | "word" | "grapheme"
|
|
16
|
+
) {
|
|
17
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity });
|
|
18
|
+
return Array.from(segmenter.segment(text), (segment) => segment.segment);
|
|
19
|
+
}
|
package/tsconfig.json
ADDED