@udixio/ui-react 2.10.13 → 2.10.14

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.
Files changed (173) hide show
  1. package/package.json +4 -1
  2. package/.eslintrc.mjs +0 -22
  3. package/.storybook/main.ts +0 -20
  4. package/.storybook/preview.ts +0 -1
  5. package/CHANGELOG.md +0 -1144
  6. package/postcss.config.mjs +0 -5
  7. package/src/index.css +0 -4
  8. package/src/index.ts +0 -1
  9. package/src/lib/components/AnchorPositioner.tsx +0 -185
  10. package/src/lib/components/Button.tsx +0 -208
  11. package/src/lib/components/Card.tsx +0 -47
  12. package/src/lib/components/Carousel.tsx +0 -437
  13. package/src/lib/components/CarouselItem.tsx +0 -61
  14. package/src/lib/components/Checkbox.tsx +0 -120
  15. package/src/lib/components/Chip.tsx +0 -341
  16. package/src/lib/components/Chips.tsx +0 -331
  17. package/src/lib/components/ContextMenu.tsx +0 -109
  18. package/src/lib/components/DatePicker.tsx +0 -432
  19. package/src/lib/components/Divider.tsx +0 -20
  20. package/src/lib/components/Fab.tsx +0 -127
  21. package/src/lib/components/FabMenu.tsx +0 -239
  22. package/src/lib/components/IconButton.tsx +0 -146
  23. package/src/lib/components/Menu.tsx +0 -88
  24. package/src/lib/components/MenuGroup.tsx +0 -34
  25. package/src/lib/components/MenuHeadline.tsx +0 -9
  26. package/src/lib/components/MenuItem.tsx +0 -215
  27. package/src/lib/components/NavigationRail.tsx +0 -186
  28. package/src/lib/components/NavigationRailItem.tsx +0 -227
  29. package/src/lib/components/ProgressIndicator.tsx +0 -214
  30. package/src/lib/components/SideSheet.tsx +0 -135
  31. package/src/lib/components/Slider.tsx +0 -374
  32. package/src/lib/components/Snackbar.tsx +0 -77
  33. package/src/lib/components/Switch.tsx +0 -107
  34. package/src/lib/components/Tab.tsx +0 -123
  35. package/src/lib/components/TabGroup.tsx +0 -66
  36. package/src/lib/components/TabGroupContext.tsx +0 -16
  37. package/src/lib/components/TabPanel.tsx +0 -27
  38. package/src/lib/components/TabPanels.tsx +0 -76
  39. package/src/lib/components/Tabs.tsx +0 -105
  40. package/src/lib/components/TextField.tsx +0 -586
  41. package/src/lib/components/Tooltip.tsx +0 -217
  42. package/src/lib/components/index.ts +0 -34
  43. package/src/lib/config/config.interface.ts +0 -9
  44. package/src/lib/config/define-config.ts +0 -16
  45. package/src/lib/config/index.ts +0 -2
  46. package/src/lib/effects/AnimateOnScroll.ts +0 -391
  47. package/src/lib/effects/State.tsx +0 -90
  48. package/src/lib/effects/SyncedFixedWrapper.tsx +0 -62
  49. package/src/lib/effects/ThemeProvider.tsx +0 -174
  50. package/src/lib/effects/block-scroll.effect.tsx +0 -313
  51. package/src/lib/effects/custom-scroll/custom-scroll.effect.tsx +0 -407
  52. package/src/lib/effects/custom-scroll/custom-scroll.interface.ts +0 -29
  53. package/src/lib/effects/custom-scroll/custom-scroll.style.ts +0 -32
  54. package/src/lib/effects/custom-scroll/index.ts +0 -3
  55. package/src/lib/effects/index.ts +0 -7
  56. package/src/lib/effects/ripple/RippleEffect.tsx +0 -116
  57. package/src/lib/effects/ripple/index.tsx +0 -1
  58. package/src/lib/effects/scrollDriven.ts +0 -239
  59. package/src/lib/effects/smooth-scroll.effect.tsx +0 -112
  60. package/src/lib/effects/theme.worker.ts +0 -97
  61. package/src/lib/hooks/index.ts +0 -10
  62. package/src/lib/hooks/useTooltipTrigger.ts +0 -270
  63. package/src/lib/icon/icon.tsx +0 -125
  64. package/src/lib/icon/index.ts +0 -1
  65. package/src/lib/index.ts +0 -8
  66. package/src/lib/interfaces/button.interface.ts +0 -65
  67. package/src/lib/interfaces/card.interface.ts +0 -11
  68. package/src/lib/interfaces/carousel-item.interface.ts +0 -12
  69. package/src/lib/interfaces/carousel.interface.ts +0 -41
  70. package/src/lib/interfaces/checkbox.interface.ts +0 -39
  71. package/src/lib/interfaces/chip.interface.ts +0 -97
  72. package/src/lib/interfaces/chips.interface.ts +0 -37
  73. package/src/lib/interfaces/date-picker.interface.ts +0 -79
  74. package/src/lib/interfaces/divider.interface.ts +0 -7
  75. package/src/lib/interfaces/fab-menu.interface.ts +0 -12
  76. package/src/lib/interfaces/fab.interface.ts +0 -27
  77. package/src/lib/interfaces/icon-button.interface.ts +0 -38
  78. package/src/lib/interfaces/index.ts +0 -26
  79. package/src/lib/interfaces/menu-group.interface.ts +0 -13
  80. package/src/lib/interfaces/menu-item.interface.ts +0 -29
  81. package/src/lib/interfaces/menu.interface.ts +0 -19
  82. package/src/lib/interfaces/navigation-rail-item.interface.ts +0 -39
  83. package/src/lib/interfaces/navigation-rail.interface.ts +0 -39
  84. package/src/lib/interfaces/progress-indicator.interface.ts +0 -41
  85. package/src/lib/interfaces/side-sheet.interface.tsx +0 -28
  86. package/src/lib/interfaces/slider.interface.ts +0 -27
  87. package/src/lib/interfaces/snackbar.interface.ts +0 -13
  88. package/src/lib/interfaces/switch.interface.ts +0 -14
  89. package/src/lib/interfaces/tab-group.interface.ts +0 -13
  90. package/src/lib/interfaces/tab-panels.interface.ts +0 -21
  91. package/src/lib/interfaces/tab.interface.ts +0 -31
  92. package/src/lib/interfaces/tabs.interface.ts +0 -22
  93. package/src/lib/interfaces/text-field.interface.ts +0 -61
  94. package/src/lib/interfaces/tooltip.interface.ts +0 -61
  95. package/src/lib/styles/button.style.ts +0 -136
  96. package/src/lib/styles/card.style.ts +0 -29
  97. package/src/lib/styles/carousel-item.style.ts +0 -24
  98. package/src/lib/styles/carousel.style.ts +0 -22
  99. package/src/lib/styles/checkbox.style.ts +0 -64
  100. package/src/lib/styles/chip.style.ts +0 -62
  101. package/src/lib/styles/chips.style.ts +0 -20
  102. package/src/lib/styles/date-picker.style.ts +0 -43
  103. package/src/lib/styles/divider.style.ts +0 -31
  104. package/src/lib/styles/fab-menu.style.ts +0 -29
  105. package/src/lib/styles/fab.style.ts +0 -49
  106. package/src/lib/styles/icon-button.style.ts +0 -168
  107. package/src/lib/styles/index.ts +0 -25
  108. package/src/lib/styles/menu-group.style.ts +0 -34
  109. package/src/lib/styles/menu-headline.style.ts +0 -20
  110. package/src/lib/styles/menu-item.style.ts +0 -45
  111. package/src/lib/styles/menu.style.ts +0 -32
  112. package/src/lib/styles/navigation-rail-item.style.ts +0 -56
  113. package/src/lib/styles/navigation-rail.style.ts +0 -36
  114. package/src/lib/styles/progress-indicator.style.ts +0 -72
  115. package/src/lib/styles/side-sheet.style.ts +0 -45
  116. package/src/lib/styles/slider.style.ts +0 -41
  117. package/src/lib/styles/snackbar.style.ts +0 -26
  118. package/src/lib/styles/switch.style.ts +0 -67
  119. package/src/lib/styles/tab-panels.style.ts +0 -35
  120. package/src/lib/styles/tab.style.ts +0 -78
  121. package/src/lib/styles/tabs.style.ts +0 -22
  122. package/src/lib/styles/text-field.style.ts +0 -115
  123. package/src/lib/styles/tooltip.style.ts +0 -48
  124. package/src/lib/utils/component-helper.ts +0 -134
  125. package/src/lib/utils/component.ts +0 -34
  126. package/src/lib/utils/index.ts +0 -7
  127. package/src/lib/utils/string.ts +0 -9
  128. package/src/lib/utils/styles/classnames.ts +0 -49
  129. package/src/lib/utils/styles/get-classname.ts +0 -96
  130. package/src/lib/utils/styles/index.ts +0 -4
  131. package/src/lib/utils/styles/use-classnames.ts +0 -25
  132. package/src/stories/action/button.stories.tsx +0 -86
  133. package/src/stories/action/fab.stories.tsx +0 -54
  134. package/src/stories/action/icon-button.stories.tsx +0 -134
  135. package/src/stories/assets/accessibility.png +0 -0
  136. package/src/stories/assets/accessibility.svg +0 -5
  137. package/src/stories/assets/addon-library.png +0 -0
  138. package/src/stories/assets/assets.png +0 -0
  139. package/src/stories/assets/context.png +0 -0
  140. package/src/stories/assets/discord.svg +0 -15
  141. package/src/stories/assets/docs.png +0 -0
  142. package/src/stories/assets/figma-plugin.png +0 -0
  143. package/src/stories/assets/github.svg +0 -3
  144. package/src/stories/assets/share.png +0 -0
  145. package/src/stories/assets/styling.png +0 -0
  146. package/src/stories/assets/testing.png +0 -0
  147. package/src/stories/assets/theming.png +0 -0
  148. package/src/stories/assets/tutorials.svg +0 -12
  149. package/src/stories/assets/youtube.svg +0 -4
  150. package/src/stories/communication/ProgressIndicator.stories.tsx +0 -57
  151. package/src/stories/communication/SnackBar.stories.tsx +0 -32
  152. package/src/stories/communication/tool-tip.stories.tsx +0 -133
  153. package/src/stories/containment/card.stories.tsx +0 -42
  154. package/src/stories/containment/carousel.stories.tsx +0 -65
  155. package/src/stories/containment/divider.stories.tsx +0 -35
  156. package/src/stories/containment/slide-sheet.stories.tsx +0 -45
  157. package/src/stories/effect/smooth-scroll.stories.tsx +0 -54
  158. package/src/stories/navigation/navigation-rail/navigation-rail-item.stories.tsx +0 -65
  159. package/src/stories/navigation/navigation-rail/navigation-rail.stories.tsx +0 -122
  160. package/src/stories/navigation/tabs/tab.stories.tsx +0 -57
  161. package/src/stories/navigation/tabs/tabs.stories.tsx +0 -102
  162. package/src/stories/selection/slider.stories.tsx +0 -85
  163. package/src/stories/selection/switch.stories.tsx +0 -46
  164. package/src/stories/text-inputs/text-field.stories.tsx +0 -135
  165. package/src/tests/Button.spec.tsx +0 -67
  166. package/src/tests/useClassNames.spec.tsx +0 -82
  167. package/src/udixio.css +0 -120
  168. package/theme.config.ts +0 -7
  169. package/tsconfig.json +0 -16
  170. package/tsconfig.lib.json +0 -51
  171. package/tsconfig.spec.json +0 -37
  172. package/tsconfig.storybook.json +0 -38
  173. package/vite.config.ts +0 -96
@@ -1,217 +0,0 @@
1
- import { cloneElement, isValidElement, useEffect, useRef } from 'react';
2
- import { MotionProps } from '../utils';
3
- import { Button } from './Button';
4
- import { AnchorPositioner } from './AnchorPositioner';
5
- import { ToolTipInterface } from '../interfaces';
6
- import { useToolTipStyle } from '../styles';
7
- import { AnimatePresence, motion } from 'motion/react';
8
- import { useTooltipTrigger } from '../hooks';
9
-
10
- /**
11
- * Tooltips display brief labels or messages
12
- * @status beta
13
- * @category Communication
14
- * @devx
15
- * - `content` overrides `title`/`text`/`buttons` for fully custom content.
16
- * - Supports controlled `isOpen` plus `openDelay`/`closeDelay`.
17
- * @a11y
18
- * - Provides `role="tooltip"` and `aria-describedby` when open.
19
- */
20
- export const Tooltip = ({
21
- variant = 'plain',
22
- buttons,
23
- className,
24
- children,
25
- title,
26
- text,
27
- content,
28
- position: positionProp,
29
- targetRef,
30
- ref,
31
- trigger = ['hover', 'focus'],
32
- transition,
33
- openDelay = 400,
34
- closeDelay = 150,
35
- isOpen: isOpenProp,
36
- defaultOpen = false,
37
- onOpenChange,
38
- id,
39
- anchorRef,
40
- ...props
41
- }: MotionProps<ToolTipInterface>) => {
42
- const defaultPosition = variant === 'rich' ? 'bottom-right' : 'bottom';
43
- const effectivePosition = positionProp || defaultPosition;
44
-
45
- transition = { duration: 0.3, ...transition };
46
-
47
- if (!children && !targetRef) {
48
- throw new Error('Tooltip must have a child or a targetRef');
49
- }
50
-
51
- if (buttons && !Array.isArray(buttons)) {
52
- buttons = [buttons];
53
- }
54
-
55
- const internalRef = useRef<HTMLElement | null>(null);
56
- const resolvedRef = targetRef || internalRef;
57
- const positioningRef = anchorRef || resolvedRef;
58
-
59
- // Use the trigger hook for state management and accessibility
60
- const { triggerProps, tooltipProps, isOpen } = useTooltipTrigger({
61
- trigger,
62
- isOpen: isOpenProp,
63
- defaultOpen,
64
- onOpenChange,
65
- openDelay,
66
- closeDelay,
67
- id,
68
- });
69
-
70
- // Apply trigger props to the target element
71
- const enhancedChildren =
72
- !targetRef && isValidElement(children)
73
- ? cloneElement(children, {
74
- ref: internalRef,
75
- ...triggerProps,
76
- // Merge event handlers if the child already has them
77
- onMouseEnter: (e: React.MouseEvent) => {
78
- triggerProps.onMouseEnter();
79
- (children.props as any)?.onMouseEnter?.(e);
80
- },
81
- onMouseLeave: (e: React.MouseEvent) => {
82
- triggerProps.onMouseLeave();
83
- (children.props as any)?.onMouseLeave?.(e);
84
- },
85
- onFocus: (e: React.FocusEvent) => {
86
- triggerProps.onFocus();
87
- (children.props as any)?.onFocus?.(e);
88
- },
89
- onBlur: (e: React.FocusEvent) => {
90
- triggerProps.onBlur();
91
- (children.props as any)?.onBlur?.(e);
92
- },
93
- onClick: (e: React.MouseEvent) => {
94
- triggerProps.onClick();
95
- (children.props as any)?.onClick?.(e);
96
- },
97
- onKeyDown: (e: React.KeyboardEvent) => {
98
- triggerProps.onKeyDown(e);
99
- (children.props as any)?.onKeyDown?.(e);
100
- },
101
- } as any)
102
- : children;
103
-
104
- // Attach trigger handlers when using targetRef (no direct child to clone)
105
- useEffect(() => {
106
- if (!targetRef) return;
107
- const element = targetRef.current;
108
- if (!element) return;
109
-
110
- const handleMouseEnter = () => triggerProps.onMouseEnter();
111
- const handleMouseLeave = () => triggerProps.onMouseLeave();
112
- const handleFocus = () => triggerProps.onFocus();
113
- const handleBlur = () => triggerProps.onBlur();
114
- const handleClick = () => triggerProps.onClick();
115
- const handleKeyDown = (event: KeyboardEvent) =>
116
- triggerProps.onKeyDown(event as unknown as React.KeyboardEvent);
117
-
118
- element.addEventListener('mouseenter', handleMouseEnter);
119
- element.addEventListener('mouseleave', handleMouseLeave);
120
- element.addEventListener('focus', handleFocus, true);
121
- element.addEventListener('blur', handleBlur, true);
122
- element.addEventListener('click', handleClick);
123
- element.addEventListener('keydown', handleKeyDown);
124
-
125
- if (triggerProps['aria-describedby']) {
126
- element.setAttribute(
127
- 'aria-describedby',
128
- triggerProps['aria-describedby'],
129
- );
130
- } else {
131
- element.removeAttribute('aria-describedby');
132
- }
133
-
134
- return () => {
135
- element.removeEventListener('mouseenter', handleMouseEnter);
136
- element.removeEventListener('mouseleave', handleMouseLeave);
137
- element.removeEventListener('focus', handleFocus, true);
138
- element.removeEventListener('blur', handleBlur, true);
139
- element.removeEventListener('click', handleClick);
140
- element.removeEventListener('keydown', handleKeyDown);
141
- };
142
- }, [targetRef, triggerProps]);
143
-
144
- const styles = useToolTipStyle({
145
- variant,
146
- buttons,
147
- className,
148
- title,
149
- text,
150
- position: effectivePosition,
151
- trigger,
152
- targetRef: targetRef as any,
153
- children: children as any,
154
- });
155
-
156
- const variants = {
157
- open: {
158
- opacity: 1,
159
- height: 'auto',
160
- },
161
- close: {
162
- opacity: 0,
163
- height: 16,
164
- },
165
- };
166
-
167
- return (
168
- <>
169
- {enhancedChildren}
170
- <AnimatePresence>
171
- {isOpen && (
172
- <AnchorPositioner
173
- anchorRef={positioningRef}
174
- position={effectivePosition}
175
- >
176
- <motion.div
177
- initial={'close'}
178
- variants={variants}
179
- animate={'open'}
180
- transition={{ duration: transition.duration }}
181
- exit={'close'}
182
- className={styles.toolTip}
183
- {...props}
184
- {...tooltipProps}
185
- >
186
- <div className={styles.container}>
187
- {content ? (
188
- <div className={styles.content}>{content}</div>
189
- ) : (
190
- <>
191
- {title && <div className={styles.subHead}>{title}</div>}
192
- {text && (
193
- <div className={styles.supportingText}>{text}</div>
194
- )}
195
- {buttons && (
196
- <div className={styles.actions}>
197
- {Array.isArray(buttons) &&
198
- buttons.map((buttonArgs, index) => (
199
- <Button
200
- key={index}
201
- size={'small'}
202
- variant={'text'}
203
- {...buttonArgs}
204
- />
205
- ))}
206
- </div>
207
- )}
208
- </>
209
- )}
210
- </div>
211
- </motion.div>
212
- </AnchorPositioner>
213
- )}
214
- </AnimatePresence>
215
- </>
216
- );
217
- };
@@ -1,34 +0,0 @@
1
-
2
- export * from './AnchorPositioner';
3
- export * from './Button';
4
- export * from './Card';
5
- export * from './Card';
6
- export * from './Carousel';
7
- export * from './CarouselItem';
8
- export * from './Checkbox';
9
- export * from './Chip';
10
- export * from './ContextMenu';
11
- export * from './Chips';
12
- export * from './Divider';
13
- export * from './Fab';
14
- export * from './FabMenu';
15
- export * from './IconButton';
16
- export * from './Menu';
17
- export * from './MenuItem';
18
- export * from './MenuGroup';
19
- export * from './MenuHeadline';
20
- export * from './ProgressIndicator';
21
- export * from './Slider';
22
- export * from './SideSheet';
23
- export * from './Snackbar';
24
- export * from './Switch';
25
- export * from './Tab';
26
- export * from './Tabs';
27
- export * from './TabGroup';
28
- export * from './TabPanels';
29
- export * from './TabPanel';
30
- export * from './TextField';
31
- export * from './NavigationRailItem';
32
- export * from './NavigationRail';
33
- export * from './Tooltip';
34
- export * from './DatePicker';
@@ -1,9 +0,0 @@
1
- import {
2
- ConfigInterface as ConfigTheme,
3
- FontPluginOptions,
4
- } from '@udixio/theme';
5
- import { TailwindPluginOptions } from '@udixio/tailwind';
6
-
7
- export type ConfigInterface = Omit<ConfigTheme, 'plugins' | 'isDark'> &
8
- TailwindPluginOptions &
9
- FontPluginOptions;
@@ -1,16 +0,0 @@
1
- import { ConfigInterface } from './config.interface';
2
- import {
3
- ConfigInterface as ConfigTheme,
4
- defineConfig as defineConfigTheme,
5
- FontPlugin,
6
- Variants,
7
- } from '@udixio/theme';
8
- import { TailwindPlugin } from '@udixio/tailwind';
9
-
10
- export function defineConfig(configObject: ConfigInterface): ConfigTheme {
11
- return defineConfigTheme({
12
- variant: Variants.Udixio,
13
- ...configObject,
14
- plugins: [new FontPlugin(configObject), new TailwindPlugin(configObject)],
15
- });
16
- }
@@ -1,2 +0,0 @@
1
- export * from './config.interface';
2
- export * from './define-config';
@@ -1,391 +0,0 @@
1
- // Simple script initializer (no React component)
2
-
3
- /**
4
- * AnimateOnScroll
5
- *
6
- * Manages triggers for animations:
7
- * - ScrollDriven animations: use native CSS if supported; otherwise import JS fallback per element set.
8
- * - Other entry/exit animations: handled via IntersectionObserver in JS.
9
- */
10
-
11
- function supportsScrollTimeline(): boolean {
12
- if (typeof window === `undefined`) return false;
13
- try {
14
- // @ts-ignore - CSS may not exist in TS lib
15
- if (window.CSS && typeof window.CSS.supports === `function`) {
16
- // @ts-ignore
17
- return (
18
- CSS.supports(`animation-timeline: view()`) ||
19
- CSS.supports(`animation-timeline: scroll()`) ||
20
- // some older implementations used view-timeline-name
21
- CSS.supports(`view-timeline-name: --a`)
22
- );
23
- }
24
- } catch {}
25
- return false;
26
- }
27
-
28
- function prefersReducedMotion(): boolean {
29
- if (typeof window === `undefined` || !(`matchMedia` in window)) return false;
30
- return window.matchMedia(`(prefers-reduced-motion: reduce)`).matches;
31
- }
32
-
33
- function isScrollDrivenCandidate(el: Element): boolean {
34
- if (!(el instanceof HTMLElement)) return false;
35
- const cls = el.classList;
36
-
37
- return Array.from(cls).some(
38
- (className) =>
39
- className.startsWith('anim-') && className.includes('scroll'),
40
- );
41
- }
42
- function isJsObserverCandidate(el: Element): boolean {
43
- if (!(el instanceof HTMLElement)) return false;
44
- const cls = el.classList;
45
-
46
- return Array.from(cls).some((className) => className.startsWith('anim-'));
47
- }
48
-
49
- function hydrateElement(el: HTMLElement, prefix: string): void {
50
- if (!isScrollDrivenCandidate(el)) return;
51
-
52
- // Map data-anim-scroll to correct axis class if provided
53
- if (el.hasAttribute(`data-${prefix}-scroll`)) {
54
- const raw = (el.getAttribute(`data-${prefix}-scroll`) || ``)
55
- .trim()
56
- .toLowerCase();
57
- const axis =
58
- raw === `x` || raw === `inline`
59
- ? `inline`
60
- : raw === `y` || raw === `block`
61
- ? `block`
62
- : `auto`;
63
- const hasAny =
64
- el.classList.contains(`${prefix}-timeline`) ||
65
- el.classList.contains(`${prefix}-timeline-inline`) ||
66
- el.classList.contains(`${prefix}-timeline-block`) ||
67
- el.classList.contains(`${prefix}-timeline-x`) ||
68
- el.classList.contains(`${prefix}-timeline-y`);
69
- if (!hasAny) {
70
- if (axis === `inline`) el.classList.add(`${prefix}-timeline-inline`);
71
- else if (axis === `block`) el.classList.add(`${prefix}-timeline-block`);
72
- else el.classList.add(`${prefix}-scroll`);
73
- }
74
- }
75
-
76
- // Offsets via data-anim-start / data-anim-end (accepts tokens like "entry 20%", "cover 50%", etc.)
77
- const start = el.getAttribute(`data-${prefix}-start`);
78
- if (start) el.style.setProperty(`--${prefix}-range-start`, start);
79
- const end = el.getAttribute(`data-${prefix}-end`);
80
- if (end) el.style.setProperty(`--${prefix}-range-end`, end);
81
-
82
- // Ensure play state is running unless explicitly paused
83
- const explicitlyPaused =
84
- el.hasAttribute(`data-${prefix}-paused`) ||
85
- el.classList.contains(`${prefix}-paused`);
86
- const alreadyRunning =
87
- el.hasAttribute(`data-${prefix}-run`) ||
88
- el.classList.contains(`${prefix}-run`);
89
- if (!explicitlyPaused && !alreadyRunning) {
90
- el.setAttribute(`data-${prefix}-run`, ``);
91
- }
92
- }
93
-
94
- function queryScrollDrivenCandidates(
95
- root: ParentNode = document,
96
- prefix: string,
97
- ): HTMLElement[] {
98
- // Select any elements that have an animation class and are marked as scroll-driven
99
- const animated = Array.from(
100
- root.querySelectorAll<HTMLElement>(
101
- `[class*="${prefix}-"][class*="-scroll"]`,
102
- ),
103
- );
104
- return animated.filter((el) => isScrollDrivenCandidate(el));
105
- }
106
-
107
- function queryJsObserverCandidates(
108
- root: ParentNode = document,
109
- prefix: string,
110
- ): HTMLElement[] {
111
- // Observe any element that has `${prefix}-in` or `${prefix}-out` even if it's scroll-driven.
112
- // Additionally, observe elements that have at least one non-scroll `${prefix}-*` class
113
- // (e.g., `anim-fade`, `anim-scale-150`) even if they also include scroll-driven classes.
114
- // This ensures default IN animations are triggered.
115
- const animated = Array.from(
116
- root.querySelectorAll<HTMLElement>(`[class*="${prefix}-"]`),
117
- );
118
-
119
- const reserved = new Set([
120
- `${prefix}-run`,
121
- `${prefix}-in`,
122
- `${prefix}-out`,
123
- `${prefix}-in-run`,
124
- `${prefix}-out-run`,
125
- `${prefix}-paused`,
126
- `${prefix}-timeline`,
127
- `${prefix}-timeline-inline`,
128
- `${prefix}-timeline-block`,
129
- `${prefix}-timeline-x`,
130
- `${prefix}-timeline-y`,
131
- `${prefix}-scroll`,
132
- ]);
133
-
134
- return animated.filter((el) => {
135
- if (!(el instanceof HTMLElement)) return false;
136
- const cls = el.classList;
137
- const hasInOut =
138
- cls.contains(`${prefix}-in`) || cls.contains(`${prefix}-out`);
139
- if (hasInOut) return true;
140
-
141
- // Check if element has any non-scroll animation class (not in reserved set and not containing "scroll")
142
- const hasNonScrollAnim = Array.from(cls).some(
143
- (c) =>
144
- c.startsWith(`${prefix}-`) && !c.includes('scroll') && !reserved.has(c),
145
- );
146
-
147
- if (hasNonScrollAnim) return true;
148
-
149
- // Otherwise only observe if it's not scroll-driven at all
150
- return !isScrollDrivenCandidate(el);
151
- });
152
- }
153
-
154
- // Utility: identify presence of in/out classes
155
- function hasOutClass(cls: DOMTokenList, prefix: string): boolean {
156
- return Array.from(cls).some(
157
- (className) => className.startsWith(prefix) && className.includes('-out'),
158
- );
159
- }
160
- // Utility: set run flags for a given direction ("in" or "out"), always ensuring generic run flag exists
161
- function setRunFlag(el: HTMLElement, prefix: string, dir: 'in' | 'out'): void {
162
- el.setAttribute(`data-${prefix}-run`, ``);
163
- el.setAttribute(`data-${prefix}-${dir}-run`, ``);
164
- }
165
-
166
- // Utility: reset run flags and restart animation timeline without changing computed styles
167
- function resetRunFlags(
168
- el: HTMLElement,
169
- prefix: string,
170
- direction?: 'in' | 'out',
171
- ): void {
172
- const currentAnimationName = el.style.animationName;
173
- el.style.animationName = 'none';
174
- el.removeAttribute(`data-${prefix}-run`);
175
- if (!direction) {
176
- el.removeAttribute(`data-${prefix}-in-run`);
177
- el.removeAttribute(`data-${prefix}-out-run`);
178
- } else {
179
- el.removeAttribute(`data-${prefix}-${direction}-run`);
180
- }
181
- void (el as HTMLElement).offsetWidth; // reflow to restart animations
182
- el.style.animationName = currentAnimationName;
183
- }
184
-
185
- // IO thresholds centralized for clarity
186
- const IO_THRESHOLD: number[] = [0, 0.2];
187
-
188
- // Track which elements have animation lifecycle listeners attached
189
- const listenersAttached = new WeakSet<Element>();
190
-
191
- function addAnimationLifecycle(el: HTMLElement, prefix: string): void {
192
- if (listenersAttached.has(el)) return;
193
- listenersAttached.add(el);
194
-
195
- const onStart = (e: AnimationEvent) => {
196
- if (e.target !== el) return;
197
- // Only mark as animating if this animation was initiated by our run flags.
198
- // This avoids setting data-{prefix}-animating during hydration or passive CSS animations
199
- // which would block the initial in/out trigger from IntersectionObserver.
200
- if (
201
- el.hasAttribute(`data-${prefix}-in-run`) ||
202
- el.hasAttribute(`data-${prefix}-out-run`)
203
- ) {
204
- el.setAttribute(`data-${prefix}-animating`, ``);
205
- }
206
- };
207
-
208
- const onEndOrCancel = (e: AnimationEvent) => {
209
- if (e.target !== el) return;
210
- // If an IN animation just finished, persist a completion flag so it won't replay on upward scroll
211
-
212
- el.removeAttribute(`data-${prefix}-animating`);
213
- };
214
-
215
- el.addEventListener('animationstart', onStart as EventListener);
216
- el.addEventListener('animationend', onEndOrCancel as EventListener);
217
- el.addEventListener('animationcancel', onEndOrCancel as EventListener);
218
- }
219
-
220
- export type AnimateOnScrollOptions = {
221
- prefix?: string;
222
- once?: boolean;
223
- };
224
-
225
- export function initAnimateOnScroll(
226
- options: AnimateOnScrollOptions = {},
227
- ): () => void {
228
- const { prefix = 'anim', once = true } = options;
229
-
230
- if (prefersReducedMotion()) {
231
- return () => {};
232
- }
233
-
234
- const cssSupported = supportsScrollTimeline();
235
-
236
- // Setup JS observers for non-scroll-driven animations
237
- const observed = new WeakSet<Element>();
238
-
239
- // Track scroll direction to prevent triggering IN when scrolling up
240
- let lastScrollY =
241
- typeof window !== 'undefined'
242
- ? window.pageYOffset || window.scrollY || 0
243
- : 0;
244
- let scrollingDown = true; // default allow initial IN
245
- const onScrollDir = () => {
246
- const y = window.pageYOffset || window.scrollY || 0;
247
- scrollingDown = y >= lastScrollY;
248
- lastScrollY = y;
249
- };
250
- if (typeof window !== 'undefined') {
251
- window.addEventListener('scroll', onScrollDir, { passive: true });
252
- }
253
-
254
- const io = new IntersectionObserver(
255
- (entries) => {
256
- for (const entry of entries) {
257
- const el = entry.target as HTMLElement;
258
-
259
- if (!isJsObserverCandidate(el)) continue;
260
-
261
- // If an animation is in progress, avoid re-triggering or flipping direction
262
- if (el.hasAttribute(`data-${prefix}-animating`)) continue;
263
-
264
- const isOut = hasOutClass(el.classList, prefix);
265
-
266
- if (entry.isIntersecting) {
267
- if (isOut) {
268
- resetRunFlags(el, prefix, 'out');
269
- }
270
- setRunFlag(el, prefix, 'in');
271
-
272
- if (once) io.unobserve(el);
273
- } else {
274
- if (!once) {
275
- if (!scrollingDown) {
276
- resetRunFlags(el, prefix, 'in');
277
- }
278
-
279
- if (isOut) {
280
- setRunFlag(el, prefix, 'out');
281
- }
282
- }
283
- }
284
- }
285
- },
286
- { threshold: IO_THRESHOLD },
287
- );
288
-
289
- const observeJsCandidates = (root?: ParentNode) => {
290
- const candidates = queryJsObserverCandidates(root || document, prefix);
291
- for (const el of candidates) {
292
- if (observed.has(el)) continue;
293
- observed.add(el);
294
- io.observe(el);
295
- addAnimationLifecycle(el, prefix);
296
- }
297
- };
298
-
299
- // Initial observe
300
- observeJsCandidates();
301
-
302
- // Scroll-driven branch per support state
303
- let cleanupScrollDriven: void | (() => void);
304
- let mo: MutationObserver | null = null;
305
- let rafId: number | null = null;
306
-
307
- if (cssSupported) {
308
- const schedule = () => {
309
- if (rafId != null) return;
310
- rafId = requestAnimationFrame(() => {
311
- rafId = null;
312
- const els = queryScrollDrivenCandidates(undefined, prefix);
313
- for (const el of els) hydrateElement(el, prefix);
314
- });
315
- };
316
-
317
- // Initial hydration
318
- schedule();
319
-
320
- mo = new MutationObserver((muts) => {
321
- for (const m of muts) {
322
- if (m.type === `attributes`) {
323
- const t = m.target;
324
- if (t instanceof HTMLElement) {
325
- hydrateElement(t as HTMLElement, prefix);
326
- if (isJsObserverCandidate(t)) {
327
- if (!observed.has(t)) {
328
- observed.add(t);
329
- io.observe(t);
330
- addAnimationLifecycle(t as HTMLElement, prefix);
331
- }
332
- }
333
- }
334
- } else if (m.type === `childList`) {
335
- if (m.addedNodes && m.addedNodes.length) {
336
- for (const node of Array.from(m.addedNodes)) {
337
- if (node instanceof HTMLElement) {
338
- const sds = queryScrollDrivenCandidates(node, prefix);
339
- for (const el of sds) hydrateElement(el, prefix);
340
- observeJsCandidates(node);
341
- }
342
- }
343
- }
344
- }
345
- }
346
- });
347
-
348
- mo.observe(document.documentElement, {
349
- subtree: true,
350
- childList: true,
351
- attributes: true,
352
- attributeFilter: [
353
- `class`,
354
- `data-${prefix}-scroll`,
355
- `data-${prefix}-start`,
356
- `data-${prefix}-end`,
357
- `data-${prefix}-paused`,
358
- `data-${prefix}-run`,
359
- ],
360
- });
361
-
362
- cleanupScrollDriven = () => {
363
- if (rafId != null) cancelAnimationFrame(rafId);
364
- if (mo) mo.disconnect();
365
- };
366
- } else {
367
- let stop: void | (() => void);
368
- const existing = queryScrollDrivenCandidates(undefined, prefix);
369
- if (existing.length > 0) {
370
- import(`./scrollDriven`).then((m) => {
371
- stop = m.initScrollViewFallback({ once });
372
- });
373
- }
374
- cleanupScrollDriven = () => {
375
- if (typeof stop === `function`) (stop as () => void)();
376
- };
377
- }
378
-
379
- // Public cleanup
380
- return () => {
381
- if (cleanupScrollDriven) cleanupScrollDriven();
382
- if (typeof window !== 'undefined') {
383
- window.removeEventListener('scroll', onScrollDir as EventListener);
384
- }
385
- io.disconnect();
386
- };
387
- }
388
-
389
- // Backward-compatible alias name (non-React):
390
- export const AnimateOnScrollInit = initAnimateOnScroll;
391
- export const animateOnScroll = initAnimateOnScroll;