@udixio/ui-react 2.9.6 → 2.9.8
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/CHANGELOG.md +34 -0
- package/dist/index.cjs +3 -3
- package/dist/index.js +3393 -2755
- package/dist/lib/components/Chip.d.ts.map +1 -1
- package/dist/lib/components/Chips.d.ts.map +1 -1
- package/dist/lib/effects/State.d.ts.map +1 -1
- package/dist/lib/effects/smooth-scroll.effect.d.ts +35 -11
- package/dist/lib/effects/smooth-scroll.effect.d.ts.map +1 -1
- package/dist/lib/styles/card.style.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/lib/components/Chip.tsx +2 -1
- package/src/lib/components/Chips.tsx +57 -16
- package/src/lib/effects/State.tsx +6 -2
- package/src/lib/effects/smooth-scroll.effect.tsx +81 -125
- package/src/lib/styles/card.style.ts +4 -1
- package/src/stories/effect/smooth-scroll.stories.tsx +34 -25
- package/tsconfig.json +0 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chip.d.ts","sourceRoot":"","sources":["../../../src/lib/components/Chip.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAc,UAAU,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAO9C;;;;GAIG;AACH,eAAO,MAAM,IAAI,GAAI,4MAqBlB,UAAU,CAAC,aAAa,CAAC,
|
|
1
|
+
{"version":3,"file":"Chip.d.ts","sourceRoot":"","sources":["../../../src/lib/components/Chip.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAc,UAAU,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAO9C;;;;GAIG;AACH,eAAO,MAAM,IAAI,GAAI,4MAqBlB,UAAU,CAAC,aAAa,CAAC,4CA2S3B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chips.d.ts","sourceRoot":"","sources":["../../../src/lib/components/Chips.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAY,cAAc,EAAE,MAAM,eAAe,CAAC;AAMzD,eAAO,MAAM,KAAK,GAAI,sEAOnB,UAAU,CAAC,cAAc,CAAC,
|
|
1
|
+
{"version":3,"file":"Chips.d.ts","sourceRoot":"","sources":["../../../src/lib/components/Chips.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAY,cAAc,EAAE,MAAM,eAAe,CAAC;AAMzD,eAAO,MAAM,KAAK,GAAI,sEAOnB,UAAU,CAAC,cAAc,CAAC,4CAiT5B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"State.d.ts","sourceRoot":"","sources":["../../../src/lib/effects/State.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,kBAAkB,EAGlB,UAAU,EACX,MAAM,UAAU,CAAC;AAGlB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,KAAK,CAAC;IACZ,KAAK,EAAE;QACL,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,CAAC,EACX,MAAM,GACN,oBAAoB,GACpB,aAAa,GACb,aAAa,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;KAC7B,CAAC;IACF,MAAM,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC;IAC9B,QAAQ,EAAE,CAAC,YAAY,CAAC,CAAC;CAC1B;AAED,eAAO,MAAM,KAAK,GAAI,kDAKnB,UAAU,CAAC,cAAc,CAAC,
|
|
1
|
+
{"version":3,"file":"State.d.ts","sourceRoot":"","sources":["../../../src/lib/effects/State.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,kBAAkB,EAGlB,UAAU,EACX,MAAM,UAAU,CAAC;AAGlB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,KAAK,CAAC;IACZ,KAAK,EAAE;QACL,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,CAAC,EACX,MAAM,GACN,oBAAoB,GACpB,aAAa,GACb,aAAa,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;KAC7B,CAAC;IACF,MAAM,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC;IAC9B,QAAQ,EAAE,CAAC,YAAY,CAAC,CAAC;CAC1B;AAED,eAAO,MAAM,KAAK,GAAI,kDAKnB,UAAU,CAAC,cAAc,CAAC,4CAyC5B,CAAC;AAYF,eAAO,MAAM,aAAa;cA9DJ,OAAO;;eATd,MAAM;qBAEb,MAAM,GACN,oBAAoB,GACpB,aAAa,GACb,aAAa;gBACL,MAAM;YACV,KAAK,CAAC,aAAa;;;kCAmE9B,CAAC"}
|
|
@@ -1,14 +1,38 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
export type SmoothScrollProps = {
|
|
3
|
+
/**
|
|
4
|
+
* Duration of the scroll animation in seconds or as a CSS string (e.g., '1s', '500ms').
|
|
5
|
+
* Default: 1.2
|
|
6
|
+
*/
|
|
7
|
+
transition?: number | string;
|
|
8
|
+
/**
|
|
9
|
+
* Easing function for the scroll animation.
|
|
10
|
+
* Default: easeOutQuint
|
|
11
|
+
*/
|
|
12
|
+
easing?: (t: number) => number;
|
|
13
|
+
/**
|
|
14
|
+
* Scroll orientation.
|
|
15
|
+
* Default: 'vertical'
|
|
16
|
+
*/
|
|
17
|
+
orientation?: 'vertical' | 'horizontal';
|
|
18
|
+
/**
|
|
19
|
+
* Enable smooth scrolling on touch devices.
|
|
20
|
+
* Default: false (native touch scrolling is usually preferred)
|
|
21
|
+
*/
|
|
22
|
+
smoothTouch?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Multiplier for touch scroll sensitivity.
|
|
25
|
+
* Default: 2
|
|
26
|
+
*/
|
|
27
|
+
touchMultiplier?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Children elements (optional, component works at document level)
|
|
30
|
+
*/
|
|
31
|
+
children?: ReactNode;
|
|
32
|
+
};
|
|
3
33
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Rework it later (e.g., via Lenis or another solution) before using it in production.
|
|
34
|
+
* SmoothScroll component using Lenis for smooth scrolling.
|
|
35
|
+
* This component enables smooth scrolling at the document level.
|
|
7
36
|
*/
|
|
8
|
-
export declare const SmoothScroll: ({ transition, orientation,
|
|
9
|
-
transition?: {
|
|
10
|
-
ease: "linear" | "easeIn" | "easeOut" | "easeInOut" | "circIn" | "circOut" | "circInOut" | "backIn" | "backOut" | "backInOut" | "anticipate" | ((t: number) => number);
|
|
11
|
-
duration?: number;
|
|
12
|
-
};
|
|
13
|
-
} & ReactProps<CustomScrollInterface>) => import("react/jsx-runtime").JSX.Element | null;
|
|
37
|
+
export declare const SmoothScroll: ({ transition, easing, orientation, smoothTouch, touchMultiplier, children, }: SmoothScrollProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
14
38
|
//# sourceMappingURL=smooth-scroll.effect.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"smooth-scroll.effect.d.ts","sourceRoot":"","sources":["../../../src/lib/effects/smooth-scroll.effect.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"smooth-scroll.effect.d.ts","sourceRoot":"","sources":["../../../src/lib/effects/smooth-scroll.effect.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqB,SAAS,EAAE,MAAM,OAAO,CAAC;AAGrD,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/B;;;OAGG;IACH,WAAW,CAAC,EAAE,UAAU,GAAG,YAAY,CAAC;IACxC;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;OAEG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,8EAO1B,iBAAiB,mDAmDnB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"card.style.d.ts","sourceRoot":"","sources":["../../../src/lib/styles/card.style.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EACL,KAAK,kBAAkB,EAIxB,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"card.style.d.ts","sourceRoot":"","sources":["../../../src/lib/styles/card.style.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EACL,KAAK,kBAAkB,EAIxB,MAAM,UAAU,CAAC;AAiBlB,eAAO,MAAM,SAAS;;;;;;;;;;2CAAuD,CAAC;AAE9E,eAAO,MAAM,YAAY;;;;;;2CAGxB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@udixio/ui-react",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"motion": "^12.23.0",
|
|
29
29
|
"tailwind-merge": "^3.3.1",
|
|
30
30
|
"uuid": "^11.0.3",
|
|
31
|
+
"lenis": "^1.2.3",
|
|
31
32
|
"@fortawesome/fontawesome-svg-core": "^7.0.0",
|
|
32
33
|
"@fortawesome/free-regular-svg-icons": "^7.0.0",
|
|
33
34
|
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
|
@@ -36,8 +37,8 @@
|
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"react": "^19.1.1",
|
|
38
39
|
"react-dom": "^19.1.1",
|
|
39
|
-
"@udixio/theme": "2.1.
|
|
40
|
-
"@udixio/tailwind": "2.4.
|
|
40
|
+
"@udixio/theme": "2.1.11",
|
|
41
|
+
"@udixio/tailwind": "2.4.9"
|
|
41
42
|
},
|
|
42
43
|
"repository": {
|
|
43
44
|
"type": "git",
|
|
@@ -114,7 +114,8 @@ export const Chip = ({
|
|
|
114
114
|
}
|
|
115
115
|
};
|
|
116
116
|
|
|
117
|
-
const isInteractive =
|
|
117
|
+
const isInteractive =
|
|
118
|
+
!!onToggle || !!onRemove || !!onClick || !!href || !!editable;
|
|
118
119
|
|
|
119
120
|
if (activated) {
|
|
120
121
|
icon = faCheck;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react';
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { ReactProps } from '../utils';
|
|
3
3
|
import { ChipItem, ChipsInterface } from '../interfaces';
|
|
4
4
|
import { useChipsStyle } from '../styles';
|
|
@@ -34,12 +34,19 @@ export const Chips = ({
|
|
|
34
34
|
|
|
35
35
|
React.useEffect(() => {
|
|
36
36
|
if (isFocused) {
|
|
37
|
-
|
|
37
|
+
if (variant == 'input') {
|
|
38
|
+
ghostChipRef.current?.focus();
|
|
39
|
+
} else {
|
|
40
|
+
ref.current?.focus();
|
|
41
|
+
}
|
|
38
42
|
}
|
|
39
43
|
}, [isFocused]);
|
|
40
44
|
|
|
41
45
|
const chipRefs = React.useRef<(HTMLElement | null)[]>([]);
|
|
42
46
|
|
|
47
|
+
// Guard to prevent multiple chip creation from fast typing
|
|
48
|
+
const isCreatingRef = React.useRef(false);
|
|
49
|
+
|
|
43
50
|
const updateItems = React.useCallback(
|
|
44
51
|
(updater: (prev: ChipItem[]) => ChipItem[]) => {
|
|
45
52
|
onItemsChange?.(updater(list));
|
|
@@ -64,6 +71,10 @@ export const Chips = ({
|
|
|
64
71
|
(seedLabel = '') => {
|
|
65
72
|
if (variant !== 'input') return;
|
|
66
73
|
|
|
74
|
+
// Guard against multiple rapid creations
|
|
75
|
+
if (isCreatingRef.current) return;
|
|
76
|
+
isCreatingRef.current = true;
|
|
77
|
+
|
|
67
78
|
const newItem: ChipItem = {
|
|
68
79
|
label: seedLabel,
|
|
69
80
|
} as ChipItem;
|
|
@@ -77,6 +88,8 @@ export const Chips = ({
|
|
|
77
88
|
|
|
78
89
|
requestAnimationFrame(() => {
|
|
79
90
|
setSelectedChip(newId);
|
|
91
|
+
// Reset guard after chip is selected
|
|
92
|
+
isCreatingRef.current = false;
|
|
80
93
|
});
|
|
81
94
|
},
|
|
82
95
|
[variant, onItemsChange, list, getInternalId],
|
|
@@ -102,6 +115,11 @@ export const Chips = ({
|
|
|
102
115
|
}, [selectedChip, list, getInternalId]);
|
|
103
116
|
|
|
104
117
|
// MODE ITEMS (source de vérité locale ou contrôlée)
|
|
118
|
+
|
|
119
|
+
const ghostChipRef = useRef<HTMLButtonElement>(null);
|
|
120
|
+
|
|
121
|
+
const isGhostChip = (isFocused || list.length === 0) && variant === 'input';
|
|
122
|
+
|
|
105
123
|
return (
|
|
106
124
|
<div
|
|
107
125
|
ref={ref}
|
|
@@ -114,7 +132,9 @@ export const Chips = ({
|
|
|
114
132
|
setIsFocused(true);
|
|
115
133
|
}
|
|
116
134
|
}}
|
|
117
|
-
onBlur={() =>
|
|
135
|
+
onBlur={() => {
|
|
136
|
+
setIsFocused(false);
|
|
137
|
+
}}
|
|
118
138
|
onKeyDown={(e) => {
|
|
119
139
|
if (variant !== 'input') return;
|
|
120
140
|
|
|
@@ -160,13 +180,7 @@ export const Chips = ({
|
|
|
160
180
|
setSelectedChip(elId);
|
|
161
181
|
return;
|
|
162
182
|
}
|
|
163
|
-
|
|
164
183
|
if (isContainerFocused) {
|
|
165
|
-
if (key === 'Enter') {
|
|
166
|
-
e.preventDefault();
|
|
167
|
-
createAndStartEdit('');
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
184
|
if (key === 'Backspace') {
|
|
171
185
|
e.preventDefault();
|
|
172
186
|
// Focus last chip if any
|
|
@@ -176,12 +190,6 @@ export const Chips = ({
|
|
|
176
190
|
}
|
|
177
191
|
return;
|
|
178
192
|
}
|
|
179
|
-
// Start creation when typing a printable character
|
|
180
|
-
if (key.length === 1 && !e.altKey && !e.ctrlKey && !e.metaKey) {
|
|
181
|
-
createAndStartEdit(key);
|
|
182
|
-
e.preventDefault();
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
193
|
}
|
|
186
194
|
}}
|
|
187
195
|
>
|
|
@@ -203,7 +211,7 @@ export const Chips = ({
|
|
|
203
211
|
onEditCancel: () => {
|
|
204
212
|
setIsFocused(true);
|
|
205
213
|
},
|
|
206
|
-
onChange: (
|
|
214
|
+
onChange: () => {
|
|
207
215
|
if (chipRefs.current.length == index + 1) {
|
|
208
216
|
const el = ref.current!;
|
|
209
217
|
requestAnimationFrame(() => {
|
|
@@ -275,6 +283,39 @@ export const Chips = ({
|
|
|
275
283
|
</style>
|
|
276
284
|
</>
|
|
277
285
|
)}
|
|
286
|
+
{isGhostChip && (
|
|
287
|
+
<Chip
|
|
288
|
+
ref={ghostChipRef}
|
|
289
|
+
className="opacity-0"
|
|
290
|
+
draggable={draggable}
|
|
291
|
+
editable={true}
|
|
292
|
+
editing={true}
|
|
293
|
+
onChange={(v) => {
|
|
294
|
+
v = v.replace(/( )+/g, ' ').trim();
|
|
295
|
+
console.log('Ghost chip onChange', v, !!v);
|
|
296
|
+
if (v) {
|
|
297
|
+
createAndStartEdit(v);
|
|
298
|
+
} else {
|
|
299
|
+
if (list.length > 0) {
|
|
300
|
+
const el = chipRefs.current[list.length - 1] as any;
|
|
301
|
+
el?.focus?.();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}}
|
|
305
|
+
onEditCommit={() => {
|
|
306
|
+
// Ghost chip doesn't commit - it creates a new chip via onChange
|
|
307
|
+
}}
|
|
308
|
+
onBlur={() => {
|
|
309
|
+
setIsFocused(false);
|
|
310
|
+
}}
|
|
311
|
+
onFocus={(e: React.FocusEvent) => {
|
|
312
|
+
setIsFocused(true);
|
|
313
|
+
e.stopPropagation();
|
|
314
|
+
}}
|
|
315
|
+
>
|
|
316
|
+
|
|
317
|
+
</Chip>
|
|
318
|
+
)}
|
|
278
319
|
</div>
|
|
279
320
|
);
|
|
280
321
|
};
|
|
@@ -44,9 +44,13 @@ export const State = ({
|
|
|
44
44
|
if (ref.current && stateClassName !== 'state-layer') {
|
|
45
45
|
const groupName = !stateClassName.includes('[')
|
|
46
46
|
? 'group'
|
|
47
|
-
: stateClassName.split('[')[1].split(']')[0];
|
|
47
|
+
: 'group/' + stateClassName.split('[')[1].split(']')[0];
|
|
48
|
+
|
|
49
|
+
// On échappe le slash pour le sélecteur CSS
|
|
50
|
+
const safeGroupName = groupName.replace(/\//g, '\\/');
|
|
51
|
+
|
|
48
52
|
const furthestGroupState = ref.current.closest(
|
|
49
|
-
`.${
|
|
53
|
+
`.${safeGroupName}:not(.${safeGroupName} .${safeGroupName})`,
|
|
50
54
|
);
|
|
51
55
|
groupStateRef.current = furthestGroupState as HTMLElement | null;
|
|
52
56
|
}
|
|
@@ -1,142 +1,98 @@
|
|
|
1
|
-
import { useEffect, useRef,
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { useEffect, useRef, ReactNode } from 'react';
|
|
2
|
+
import Lenis from 'lenis';
|
|
3
|
+
|
|
4
|
+
export type SmoothScrollProps = {
|
|
5
|
+
/**
|
|
6
|
+
* Duration of the scroll animation in seconds or as a CSS string (e.g., '1s', '500ms').
|
|
7
|
+
* Default: 1.2
|
|
8
|
+
*/
|
|
9
|
+
transition?: number | string;
|
|
10
|
+
/**
|
|
11
|
+
* Easing function for the scroll animation.
|
|
12
|
+
* Default: easeOutQuint
|
|
13
|
+
*/
|
|
14
|
+
easing?: (t: number) => number;
|
|
15
|
+
/**
|
|
16
|
+
* Scroll orientation.
|
|
17
|
+
* Default: 'vertical'
|
|
18
|
+
*/
|
|
19
|
+
orientation?: 'vertical' | 'horizontal';
|
|
20
|
+
/**
|
|
21
|
+
* Enable smooth scrolling on touch devices.
|
|
22
|
+
* Default: false (native touch scrolling is usually preferred)
|
|
23
|
+
*/
|
|
24
|
+
smoothTouch?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Multiplier for touch scroll sensitivity.
|
|
27
|
+
* Default: 2
|
|
28
|
+
*/
|
|
29
|
+
touchMultiplier?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Children elements (optional, component works at document level)
|
|
32
|
+
*/
|
|
33
|
+
children?: ReactNode;
|
|
34
|
+
};
|
|
6
35
|
|
|
7
36
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Rework it later (e.g., via Lenis or another solution) before using it in production.
|
|
37
|
+
* SmoothScroll component using Lenis for smooth scrolling.
|
|
38
|
+
* This component enables smooth scrolling at the document level.
|
|
11
39
|
*/
|
|
12
40
|
export const SmoothScroll = ({
|
|
13
|
-
transition,
|
|
41
|
+
transition = 1.2,
|
|
42
|
+
easing,
|
|
14
43
|
orientation = 'vertical',
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
| 'easeOut'
|
|
22
|
-
| 'easeInOut'
|
|
23
|
-
| 'circIn'
|
|
24
|
-
| 'circOut'
|
|
25
|
-
| 'circInOut'
|
|
26
|
-
| 'backIn'
|
|
27
|
-
| 'backOut'
|
|
28
|
-
| 'backInOut'
|
|
29
|
-
| 'anticipate'
|
|
30
|
-
| ((t: number) => number);
|
|
31
|
-
duration?: number;
|
|
32
|
-
};
|
|
33
|
-
} & ReactProps<CustomScrollInterface>) => {
|
|
34
|
-
// Target value (instant), driven by wheel/touch/keyboard or native scroll sync
|
|
35
|
-
const [scrollY, setScrollY] = useState(0);
|
|
36
|
-
|
|
37
|
-
const [el, setEl] = useState<HTMLHtmlElement>();
|
|
38
|
-
|
|
39
|
-
const isScrolling = useRef(false);
|
|
40
|
-
const scrollTimeoutRef = useRef<NodeJS.Timeout>();
|
|
41
|
-
const lastAppliedYRef = useRef(0);
|
|
42
|
-
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
setEl(document as unknown as HTMLHtmlElement);
|
|
45
|
-
const y = document.documentElement.scrollTop;
|
|
46
|
-
setScrollY(y);
|
|
47
|
-
lastAppliedYRef.current = y;
|
|
48
|
-
}, []);
|
|
44
|
+
smoothTouch = false,
|
|
45
|
+
touchMultiplier = 2,
|
|
46
|
+
children,
|
|
47
|
+
}: SmoothScrollProps) => {
|
|
48
|
+
const lenisRef = useRef<Lenis | null>(null);
|
|
49
|
+
const rafRef = useRef<number | null>(null);
|
|
49
50
|
|
|
50
|
-
// Sync native scroll (e.g., scrollbar, programmatic) back to target after a small delay
|
|
51
51
|
useEffect(() => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// Drive the spring when target changes
|
|
65
|
-
const currentY = useRef<number | null>();
|
|
66
|
-
const animationRef = useRef<AnimationPlaybackControls | null>(null);
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
const y = scrollY;
|
|
69
|
-
|
|
70
|
-
if (animationRef.current) {
|
|
71
|
-
animationRef.current.stop();
|
|
72
|
-
animationRef.current = null;
|
|
52
|
+
// Parse duration from string if needed (e.g., '1s' -> 1, '500ms' -> 0.5)
|
|
53
|
+
let duration: number;
|
|
54
|
+
if (typeof transition === 'string') {
|
|
55
|
+
if (transition.endsWith('ms')) {
|
|
56
|
+
duration = parseFloat(transition) / 1000;
|
|
57
|
+
} else if (transition.endsWith('s')) {
|
|
58
|
+
duration = parseFloat(transition);
|
|
59
|
+
} else {
|
|
60
|
+
duration = parseFloat(transition) || 1.2;
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
duration = transition;
|
|
73
64
|
}
|
|
74
65
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
duration
|
|
81
|
-
|
|
66
|
+
// Default easing: easeOutQuint
|
|
67
|
+
const defaultEasing = (t: number) => 1 - Math.pow(1 - t, 5);
|
|
68
|
+
|
|
69
|
+
// Initialize Lenis
|
|
70
|
+
lenisRef.current = new Lenis({
|
|
71
|
+
duration,
|
|
72
|
+
easing: easing ?? defaultEasing,
|
|
73
|
+
orientation,
|
|
74
|
+
smoothWheel: true,
|
|
75
|
+
touchMultiplier,
|
|
76
|
+
syncTouch: smoothTouch,
|
|
77
|
+
});
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
// Animation frame loop
|
|
80
|
+
const raf = (time: number) => {
|
|
81
|
+
lenisRef.current?.raf(time);
|
|
82
|
+
rafRef.current = requestAnimationFrame(raf);
|
|
83
|
+
};
|
|
88
84
|
|
|
89
|
-
|
|
90
|
-
// Avoid micro-movements causing extra layout work
|
|
91
|
-
const rounded = Math.round(value * 1000) / 1000;
|
|
92
|
-
const last = lastAppliedYRef.current;
|
|
93
|
-
if (Math.abs(rounded - last) < 0.1) return;
|
|
94
|
-
lastAppliedYRef.current = rounded;
|
|
85
|
+
rafRef.current = requestAnimationFrame(raf);
|
|
95
86
|
|
|
96
|
-
|
|
97
|
-
html.scrollTo({ top: rounded });
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
onComplete: () => {
|
|
101
|
-
scrollTimeoutRef.current = setTimeout(() => {
|
|
102
|
-
isScrolling.current = false;
|
|
103
|
-
}, 300);
|
|
104
|
-
animationRef.current = null;
|
|
105
|
-
},
|
|
106
|
-
});
|
|
87
|
+
// Cleanup
|
|
107
88
|
return () => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
animationRef.current.stop();
|
|
111
|
-
animationRef.current = null;
|
|
89
|
+
if (rafRef.current !== null) {
|
|
90
|
+
cancelAnimationFrame(rafRef.current);
|
|
112
91
|
}
|
|
92
|
+
lenisRef.current?.destroy();
|
|
93
|
+
lenisRef.current = null;
|
|
113
94
|
};
|
|
114
|
-
}, [
|
|
115
|
-
|
|
116
|
-
if (!el) return null;
|
|
117
|
-
|
|
118
|
-
return (
|
|
119
|
-
<BlockScroll
|
|
120
|
-
touch={false}
|
|
121
|
-
el={el as unknown as HTMLElement}
|
|
122
|
-
onScroll={(scroll) => {
|
|
123
|
-
if (
|
|
124
|
-
'deltaY' in scroll &&
|
|
125
|
-
scroll.deltaY !== 0 &&
|
|
126
|
-
el &&
|
|
127
|
-
scrollY !== null
|
|
128
|
-
) {
|
|
129
|
-
let y = scrollY + scroll.deltaY;
|
|
130
|
-
const html = el.querySelector('html');
|
|
131
|
-
if (html) {
|
|
132
|
-
y = Math.min(y, html.scrollHeight - html.clientHeight);
|
|
133
|
-
}
|
|
134
|
-
y = Math.max(y, 0);
|
|
135
|
-
setScrollY(y);
|
|
95
|
+
}, [transition, easing, orientation, smoothTouch, touchMultiplier]);
|
|
136
96
|
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
}}
|
|
140
|
-
></BlockScroll>
|
|
141
|
-
);
|
|
97
|
+
return children ? <>{children}</> : null;
|
|
142
98
|
};
|
|
@@ -11,10 +11,13 @@ const cardConfig: ClassNameComponent<CardInterface> = ({
|
|
|
11
11
|
isInteractive,
|
|
12
12
|
}) => ({
|
|
13
13
|
card: classNames(
|
|
14
|
-
'
|
|
14
|
+
' rounded-xl overflow-hidden ',
|
|
15
15
|
variant === 'outlined' && 'bg-surface border border-outline-variant',
|
|
16
16
|
variant === 'elevated' && 'bg-surface-container-low shadow-1',
|
|
17
17
|
variant === 'filled' && 'bg-surface-container-highest',
|
|
18
|
+
{
|
|
19
|
+
'group/card': isInteractive,
|
|
20
|
+
},
|
|
18
21
|
),
|
|
19
22
|
});
|
|
20
23
|
|
|
@@ -2,42 +2,51 @@ import type { Meta, StoryObj } from '@storybook/react';
|
|
|
2
2
|
import { SmoothScroll } from '../../';
|
|
3
3
|
import { JSX } from 'react/jsx-runtime';
|
|
4
4
|
|
|
5
|
-
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
|
6
5
|
const meta = {
|
|
7
6
|
title: 'effect/SmoothScroll',
|
|
8
7
|
component: SmoothScroll,
|
|
9
|
-
parameters: {
|
|
10
|
-
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
|
|
11
|
-
},
|
|
12
|
-
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
|
|
8
|
+
parameters: {},
|
|
13
9
|
tags: ['autodocs'],
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
argTypes: {
|
|
11
|
+
transition: {
|
|
12
|
+
control: { type: 'text' },
|
|
13
|
+
description: 'Duration of the scroll animation (e.g., "1s", "500ms", or number in seconds)',
|
|
14
|
+
},
|
|
15
|
+
orientation: {
|
|
16
|
+
control: { type: 'select' },
|
|
17
|
+
options: ['vertical', 'horizontal'],
|
|
18
|
+
},
|
|
19
|
+
smoothTouch: {
|
|
20
|
+
control: { type: 'boolean' },
|
|
21
|
+
description: 'Enable smooth scrolling on touch devices',
|
|
22
|
+
},
|
|
23
|
+
touchMultiplier: {
|
|
24
|
+
control: { type: 'number' },
|
|
25
|
+
description: 'Multiplier for touch scroll sensitivity',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
16
28
|
} satisfies Meta<typeof SmoothScroll>;
|
|
17
29
|
|
|
18
30
|
export default meta;
|
|
19
31
|
type Story = StoryObj<typeof meta>;
|
|
20
32
|
|
|
21
|
-
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
|
|
22
|
-
|
|
23
33
|
const createSmoothScrollStory = () => {
|
|
24
34
|
const SmoothScrollStory: () => JSX.Element = () => (
|
|
25
|
-
|
|
26
|
-
<SmoothScroll transition=
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
</div>
|
|
35
|
+
<>
|
|
36
|
+
<SmoothScroll transition="1s" />
|
|
37
|
+
<div className="h-52 bg-primary" />
|
|
38
|
+
<div className="h-52 bg-secondary" />
|
|
39
|
+
<div className="h-52 bg-tertiary" />
|
|
40
|
+
<div className="h-52 bg-primary" />
|
|
41
|
+
<div className="h-52 bg-secondary" />
|
|
42
|
+
<div className="h-52 bg-tertiary" />
|
|
43
|
+
<div className="h-52 bg-primary" />
|
|
44
|
+
<div className="h-52 bg-secondary" />
|
|
45
|
+
<div className="h-52 bg-tertiary" />
|
|
46
|
+
<div className="h-52 bg-primary" />
|
|
47
|
+
<div className="h-52 bg-secondary" />
|
|
48
|
+
<div className="h-52 bg-tertiary" />
|
|
49
|
+
</>
|
|
41
50
|
);
|
|
42
51
|
|
|
43
52
|
return SmoothScrollStory;
|