diff --git a/src/packages/components/lazy-motion/.gitignore b/src/packages/components/lazy-motion/.gitignore new file mode 100644 index 0000000..73ac1e6 --- /dev/null +++ b/src/packages/components/lazy-motion/.gitignore @@ -0,0 +1,42 @@ + +# dependencies +node_modules + +# build +dist +dist-ssr +build +out +*.tsbuildinfo + +# testing & coverage +coverage +coverage-ts + +# env +.env + +# Editor directories and files +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +*.local +*.pem +.idea +.todo +.DS_Store + +# vendors +.turbo +.vercel + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* diff --git a/src/packages/components/lazy-motion/CHANGELOG.md b/src/packages/components/lazy-motion/CHANGELOG.md new file mode 100644 index 0000000..ce56d13 --- /dev/null +++ b/src/packages/components/lazy-motion/CHANGELOG.md @@ -0,0 +1,8 @@ +# @renderui/lazy-motion + +## 1.0.0 + +### Patch changes + +- Added abstraction providers for framer-motion dom animationa and dom max features +- Added provider compount component diff --git a/src/packages/components/lazy-motion/bun.lockb b/src/packages/components/lazy-motion/bun.lockb new file mode 100755 index 0000000..fb024d1 Binary files /dev/null and b/src/packages/components/lazy-motion/bun.lockb differ diff --git a/src/packages/components/lazy-motion/package.json b/src/packages/components/lazy-motion/package.json new file mode 100644 index 0000000..b3194c6 --- /dev/null +++ b/src/packages/components/lazy-motion/package.json @@ -0,0 +1,48 @@ +{ + "author": { + "email": "lovro.zagar5@gmail.com", + "name": "Lovro Žagar" + }, + "devDependencies": { + "bunchee": "^5.5.1", + "framer-motion": "^11.11.10", + "react": "19.0.0-rc-a960b92c-20240819", + "react-dom": "19.0.0-rc-a960b92c-20240819", + "types-react": "^19.0.0-rc.1", + "types-react-dom": "^19.0.0-rc.1", + "typescript": "^5.5.4" + }, + "exports": { + ".": { + "import": { + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + } + } + }, + "files": ["dist"], + "license": "MIT", + "main": "./dist/index.js", + "module": "./dist/index.js", + "name": "@renderui/lazy-motion", + "peerDependencies": { + "framer-motion": "^11.11.10", + "react": ">=18", + "react-dom": ">=18" + }, + "private": false, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/lovrozagar/renderui.git" + }, + "scripts": { + "build": "bunchee -m" + }, + "sideEffects": false, + "type": "module", + "types": "./dist/index.d.ts", + "version": "1.0.0" +} diff --git a/src/packages/components/lazy-motion/src/components/lazy-motion-dom-animation-provider.tsx b/src/packages/components/lazy-motion/src/components/lazy-motion-dom-animation-provider.tsx new file mode 100644 index 0000000..80ecf82 --- /dev/null +++ b/src/packages/components/lazy-motion/src/components/lazy-motion-dom-animation-provider.tsx @@ -0,0 +1,17 @@ +import { LazyMotion } from "framer-motion" +import type { LazyMotionProviderProps } from "../types/lazy-motion-provider" + +const LazyMotionDomAnimationProvider = (props: LazyMotionProviderProps) => { + const { children, strict = true } = props + + return ( + import("../lib/dom-animation").then((res) => res.default)} + strict={strict} + > + {children} + + ) +} + +export { LazyMotionDomAnimationProvider } diff --git a/src/packages/components/lazy-motion/src/components/lazy-motion-dom-max-provider.tsx b/src/packages/components/lazy-motion/src/components/lazy-motion-dom-max-provider.tsx new file mode 100644 index 0000000..3eb70f5 --- /dev/null +++ b/src/packages/components/lazy-motion/src/components/lazy-motion-dom-max-provider.tsx @@ -0,0 +1,17 @@ +import { LazyMotion } from "framer-motion" +import type { LazyMotionProviderProps } from "../types/lazy-motion-provider" + +const LazyMotionDomMaxProvider = (props: LazyMotionProviderProps) => { + const { children, strict = true } = props + + return ( + import("../lib/dom-max").then((res) => res.default)} + strict={strict} + > + {children} + + ) +} + +export { LazyMotionDomMaxProvider } diff --git a/src/packages/components/lazy-motion/src/components/lazy-motion-provider.tsx b/src/packages/components/lazy-motion/src/components/lazy-motion-provider.tsx new file mode 100644 index 0000000..4c484a9 --- /dev/null +++ b/src/packages/components/lazy-motion/src/components/lazy-motion-provider.tsx @@ -0,0 +1,15 @@ +import { LazyMotionDomAnimationProvider } from "./lazy-motion-dom-animation-provider" +import { LazyMotionDomMaxProvider } from "./lazy-motion-dom-max-provider" + +type LazyMotionProviderCompoundComponent = JSX.Element & { + DomAnimation: typeof LazyMotionDomAnimationProvider + DomMax: typeof LazyMotionDomMaxProvider +} + +/* biome-ignore lint/complexity/noUselessFragments: */ +const LazyMotionProvider = (<>) as LazyMotionProviderCompoundComponent + +LazyMotionProvider.DomAnimation = LazyMotionDomAnimationProvider +LazyMotionProvider.DomMax = LazyMotionDomMaxProvider + +export { LazyMotionProvider } diff --git a/src/packages/components/lazy-motion/src/index.ts b/src/packages/components/lazy-motion/src/index.ts new file mode 100644 index 0000000..d0a45e1 --- /dev/null +++ b/src/packages/components/lazy-motion/src/index.ts @@ -0,0 +1,4 @@ +export { LazyMotionDomAnimationProvider } from "./components/lazy-motion-dom-animation-provider" +export { LazyMotionDomMaxProvider } from "./components/lazy-motion-dom-max-provider" +export { LazyMotionProvider } from "./components/lazy-motion-provider" +export type { LazyMotionProviderProps } from "./types/lazy-motion-provider" diff --git a/src/packages/components/lazy-motion/src/lib/dom-animation.ts b/src/packages/components/lazy-motion/src/lib/dom-animation.ts new file mode 100644 index 0000000..c25206e --- /dev/null +++ b/src/packages/components/lazy-motion/src/lib/dom-animation.ts @@ -0,0 +1 @@ +export { domAnimation as default } from 'framer-motion' diff --git a/src/packages/components/lazy-motion/src/lib/dom-max.ts b/src/packages/components/lazy-motion/src/lib/dom-max.ts new file mode 100644 index 0000000..b61a58f --- /dev/null +++ b/src/packages/components/lazy-motion/src/lib/dom-max.ts @@ -0,0 +1 @@ +export { domMax as default } from "framer-motion" diff --git a/src/packages/components/lazy-motion/src/types/lazy-motion-provider.ts b/src/packages/components/lazy-motion/src/types/lazy-motion-provider.ts new file mode 100644 index 0000000..6c9d661 --- /dev/null +++ b/src/packages/components/lazy-motion/src/types/lazy-motion-provider.ts @@ -0,0 +1,6 @@ +import type { LazyMotion } from "framer-motion" +import type { ComponentPropsWithRef } from "react" + +type LazyMotionProviderProps = Omit, "features"> + +export type { LazyMotionProviderProps } diff --git a/src/packages/components/lazy-motion/tsconfig.json b/src/packages/components/lazy-motion/tsconfig.json new file mode 100644 index 0000000..d2db219 --- /dev/null +++ b/src/packages/components/lazy-motion/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../../tsconfig.json", + "include": ["src"] +} diff --git a/src/packages/components/ripple/.gitignore b/src/packages/components/ripple/.gitignore new file mode 100644 index 0000000..73ac1e6 --- /dev/null +++ b/src/packages/components/ripple/.gitignore @@ -0,0 +1,42 @@ + +# dependencies +node_modules + +# build +dist +dist-ssr +build +out +*.tsbuildinfo + +# testing & coverage +coverage +coverage-ts + +# env +.env + +# Editor directories and files +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +*.local +*.pem +.idea +.todo +.DS_Store + +# vendors +.turbo +.vercel + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* diff --git a/src/packages/components/ripple/CHANGELOG.md b/src/packages/components/ripple/CHANGELOG.md new file mode 100644 index 0000000..f1d6077 --- /dev/null +++ b/src/packages/components/ripple/CHANGELOG.md @@ -0,0 +1,7 @@ +# @renderui/ripple + +## 1.0.0 + +### Patch changes + +- Added ripple component diff --git a/src/packages/components/ripple/bun.lockb b/src/packages/components/ripple/bun.lockb new file mode 100755 index 0000000..7ddc47e Binary files /dev/null and b/src/packages/components/ripple/bun.lockb differ diff --git a/src/packages/components/ripple/package.json b/src/packages/components/ripple/package.json new file mode 100644 index 0000000..bc74cf0 --- /dev/null +++ b/src/packages/components/ripple/package.json @@ -0,0 +1,56 @@ +{ + "author": { + "email": "lovro.zagar5@gmail.com", + "name": "Lovro Žagar" + }, + "devDependencies": { + "@renderui/lazy-motion": ">=1.0.0", + "@renderui/sub-layer": ">=1.0.0", + "@renderui/utils": ">=0.2.6", + "@renderui/hooks": ">=1.0.0", + "bunchee": "^5.5.1", + "framer-motion": "^11.11.10", + "react": "19.0.0-rc-a960b92c-20240819", + "react-dom": "19.0.0-rc-a960b92c-20240819", + "types-react": "^19.0.0-rc.1", + "types-react-dom": "^19.0.0-rc.1", + "typescript": "^5.5.4" + }, + "exports": { + ".": { + "import": { + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + } + } + }, + "files": ["dist"], + "license": "MIT", + "main": "./dist/index.js", + "module": "./dist/index.js", + "name": "@renderui/ripple", + "peerDependencies": { + "@renderui/lazy-motion": ">=1.0.0", + "@renderui/sub-layer": ">=1.0.0", + "@renderui/utils": ">=0.2.6", + "@renderui/hooks": ">=1.0.0", + "framer-motion": "^11.11.10", + "react": ">=18", + "react-dom": ">=18" + }, + "private": false, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/lovrozagar/renderui.git" + }, + "scripts": { + "build": "bunchee -m" + }, + "sideEffects": false, + "type": "module", + "types": "./dist/index.d.ts", + "version": "1.0.0" +} diff --git a/src/packages/components/ripple/src/components/ripple.tsx b/src/packages/components/ripple/src/components/ripple.tsx new file mode 100644 index 0000000..35ba765 --- /dev/null +++ b/src/packages/components/ripple/src/components/ripple.tsx @@ -0,0 +1,29 @@ +"use client" + +import { LazyMotionDomAnimationProvider } from "@renderui/lazy-motion" +import { SubLayer } from "@renderui/sub-layer" +import { getOptionalObject } from "@renderui/utils" +import { AnimatePresence, m } from "framer-motion" +import { useRipple } from "../hooks/use-ripple" +import type { RippleProps } from "../types/ripple" + +const Ripple = (props: RippleProps) => { + const { subLayerProps, ...otherProps } = getOptionalObject(props) + + const { ripples, internalSubLayerRef, addRippleOnPress, getRippleRipplesProps } = + useRipple(otherProps) + + return ( + + {ripples.map((ripple) => ( + + + + + + ))} + + ) +} + +export { Ripple } diff --git a/src/packages/components/ripple/src/constants/constants.ts b/src/packages/components/ripple/src/constants/constants.ts new file mode 100644 index 0000000..fc378a6 --- /dev/null +++ b/src/packages/components/ripple/src/constants/constants.ts @@ -0,0 +1,49 @@ +const DEFAULT_RIPPLE_CLASSNAME = "_ripple bg-current" + +const RIPPLE_ANIMATION_END_OPACITY = 0 +const RIPPLE_ANIMATION_START_SCALE = 0 +const RIPPLE_ANIMATION_END_DEFAULT_SCALE = 2 + +const RIPPLE_DEFAULT_OPACITY = 0.25 +const RIPPLE_DEFAULT_DURATION_MULTIPLIER = 1 +const RIPPLE_DEFAULT_SCALE_MULTIPLIER = 1 + +const RIPPLE_BASE_MULTIPLIER = 0.01 +const RIPPLE_MIDDLE_DURATION = 0.25 +const RIPPLE_SIZE_100 = 100 +const RIPPLE_OVER_100_DURATION = 0.75 +const RIPPLE_UNDER_100_DURATION = 0.675 + +const RIPPLE_RIPPLE_BASE_STYLE = { + backgroundColor: "current", + position: "absolute", + borderRadius: "100%", + transformOrigin: "center", + pointerEvents: "none", + zIndex: "0", +} as const + +const KEYBOARD_RIPPLE_DATASET_ATTRIBUTE = "data-keyboard-pressed" + +const KEYBOARD_RIPPLE_MUTATION_OBSERVER_OPTIONS = { + attributes: true, + attributeFilter: [KEYBOARD_RIPPLE_DATASET_ATTRIBUTE], +} + +export { + DEFAULT_RIPPLE_CLASSNAME, + KEYBOARD_RIPPLE_MUTATION_OBSERVER_OPTIONS, + RIPPLE_ANIMATION_END_DEFAULT_SCALE, + RIPPLE_ANIMATION_END_OPACITY, + RIPPLE_ANIMATION_START_SCALE, + RIPPLE_BASE_MULTIPLIER, + RIPPLE_DEFAULT_DURATION_MULTIPLIER, + RIPPLE_DEFAULT_OPACITY, + RIPPLE_DEFAULT_SCALE_MULTIPLIER, + RIPPLE_MIDDLE_DURATION, + RIPPLE_OVER_100_DURATION, + RIPPLE_RIPPLE_BASE_STYLE, + RIPPLE_SIZE_100, + RIPPLE_UNDER_100_DURATION, + KEYBOARD_RIPPLE_DATASET_ATTRIBUTE, +} diff --git a/src/packages/components/ripple/src/hooks/use-keyboard-ripple.ts b/src/packages/components/ripple/src/hooks/use-keyboard-ripple.ts new file mode 100644 index 0000000..29fddaa --- /dev/null +++ b/src/packages/components/ripple/src/hooks/use-keyboard-ripple.ts @@ -0,0 +1,51 @@ +import { useMutationObserver } from "@renderui/hooks" +import { type Dispatch, type RefObject, type SetStateAction, useCallback } from "react" +import { KEYBOARD_RIPPLE_DATASET_ATTRIBUTE, KEYBOARD_RIPPLE_MUTATION_OBSERVER_OPTIONS } from "../constants/constants" +import type { RippleRipple } from "../types/ripple-ripple" +import { createRipple } from "../utils/create-ripple" + +function useKeyboardRipple( + ref: RefObject, + setRipples: Dispatch>, +) { + const addRippleOnKeyboardPress = useCallback( + (height: number, width: number) => { + const newRipple = createRipple({ type: "keyboard", width, height }) + + setRipples((previousRipples) => [...previousRipples, newRipple]) + + return newRipple.key + }, + [setRipples], + ) + + const mutationHandler = useCallback( + (mutations: MutationRecord[]) => { + const element = ref.current?.parentElement + const parentElement = element + + if (!parentElement) return + + mutations.forEach((mutation) => { + if (mutation.attributeName !== KEYBOARD_RIPPLE_DATASET_ATTRIBUTE) return + + const parentDatasetState = parentElement.dataset.pressed + + if (parentDatasetState === "true") return + + addRippleOnKeyboardPress(element.clientHeight, element.clientWidth) + }) + }, + [ref, addRippleOnKeyboardPress], + ) + + const parentRef = ref.current?.parentElement ?? null + + useMutationObserver({ + node: parentRef, + options: KEYBOARD_RIPPLE_MUTATION_OBSERVER_OPTIONS, + callback: mutationHandler, + }) +} + +export { useKeyboardRipple } diff --git a/src/packages/components/ripple/src/hooks/use-press-ripple.ts b/src/packages/components/ripple/src/hooks/use-press-ripple.ts new file mode 100644 index 0000000..f34b1a4 --- /dev/null +++ b/src/packages/components/ripple/src/hooks/use-press-ripple.ts @@ -0,0 +1,45 @@ +import { + type Dispatch, + type MouseEvent, + type MouseEventHandler, + type SetStateAction, + useCallback, + useEffect, + useState, +} from "react" +import type { RippleRipple } from "../types/ripple-ripple" +import { createRipple } from "../utils/create-ripple" + +function usePressRipple( + setRipples: Dispatch>, + isDisabled: boolean | undefined = undefined, +) { + const [isRaised, setIsRaised] = useState(false) + + useEffect(() => { + let timeoutId: NodeJS.Timeout | undefined = undefined + + if (isDisabled) { + timeoutId = setTimeout(() => setIsRaised(true), 0) + } else { + setIsRaised(false) + } + + return () => clearTimeout(timeoutId) + }, [isDisabled]) + + return useCallback( + (event: React.MouseEvent) => { + if (isRaised) return "" + + const newRipple = createRipple({ type: "pointer", event: event }) + + setRipples((previousRipples) => [...previousRipples, newRipple]) + + return newRipple.key + }, + [isRaised, setRipples], + ) as unknown as MouseEventHandler +} + +export { usePressRipple } diff --git a/src/packages/components/ripple/src/hooks/use-ripple.ts b/src/packages/components/ripple/src/hooks/use-ripple.ts new file mode 100644 index 0000000..e1ad074 --- /dev/null +++ b/src/packages/components/ripple/src/hooks/use-ripple.ts @@ -0,0 +1,115 @@ +"use client" + +import { cn } from "@renderui/utils" +import type { AnimationDefinition } from "framer-motion" +import { type Key, type RefObject, useRef, useState } from "react" +import { + DEFAULT_RIPPLE_CLASSNAME, + RIPPLE_ANIMATION_END_DEFAULT_SCALE, + RIPPLE_ANIMATION_END_OPACITY, + RIPPLE_ANIMATION_START_SCALE, + RIPPLE_DEFAULT_OPACITY, + RIPPLE_RIPPLE_BASE_STYLE, +} from "../constants/constants" +import type { RippleProps } from "../types/ripple" +import type { RippleRipple } from "../types/ripple-ripple" +import { getRippleDuration } from "../utils/get-ripple-duration" +import { useKeyboardRipple } from "./use-keyboard-ripple" +import { usePressRipple } from "./use-press-ripple" + +type UseRippleReturnType = { + ripples: RippleRipple[] + internalSubLayerRef: RefObject + addRippleOnPress: ReturnType + /* biome-ignore lint/suspicious/noExplicitAny: avoid external module reference error */ + getRippleRipplesProps: (ripple: RippleRipple) => any +} + +function useRipple(props: Omit): UseRippleReturnType { + const { + ref, + opacity: opacityProp, + duration: durationProp, + scale: scaleProp, + transition: transitionProp, + initial: initialProp, + animate: animateProp, + exit: exitProp, + style: styleProp, + className, + isDisabled, + onAnimationComplete: onAnimationCompleteProp, + ...restProps + } = props + + const internalSubLayerRef = useRef(null) + const [ripples, setRipples] = useState([]) + + const addRippleOnPress = usePressRipple(setRipples, isDisabled) + + useKeyboardRipple(internalSubLayerRef, setRipples) + + const clearRipple = (key: Key) => { + setRipples((previousState) => previousState.filter((ripple) => ripple.key !== key)) + } + + const onAnimationComplete = (ripple: RippleRipple, definition: AnimationDefinition) => { + clearRipple(ripple.key) + + if (onAnimationCompleteProp) { + onAnimationCompleteProp(definition) + } + } + + const getRippleRipplesProps = (ripple: RippleRipple) => { + const duration = durationProp ?? getRippleDuration(ripple.size) + + const scale = scaleProp ?? RIPPLE_ANIMATION_END_DEFAULT_SCALE + + const initial = initialProp ?? { + transform: `scale(${RIPPLE_ANIMATION_START_SCALE})`, + opacity: opacityProp ?? RIPPLE_DEFAULT_OPACITY, + } + + const animate = animateProp ?? { + transform: `scale(${scale})`, + opacity: RIPPLE_ANIMATION_END_OPACITY, + } + + const exit = exitProp ?? { opacity: RIPPLE_ANIMATION_END_OPACITY } + + const transition = { duration, ...transitionProp } + + const style = { + ...RIPPLE_RIPPLE_BASE_STYLE, + top: ripple.y, + left: ripple.x, + width: `${ripple.size}px`, + height: `${ripple.size}px`, + ...styleProp, + } + + return { + ref, + initial, + animate, + exit, + transition, + style, + "data-slot": "ripple", + className: cn(DEFAULT_RIPPLE_CLASSNAME, className), + onAnimationComplete: (definition: AnimationDefinition) => + onAnimationComplete(ripple, definition), + ...restProps, + } + } + + return { + ripples, + internalSubLayerRef, + addRippleOnPress, + getRippleRipplesProps, + } +} + +export { useRipple } diff --git a/src/packages/components/ripple/src/index.ts b/src/packages/components/ripple/src/index.ts new file mode 100644 index 0000000..188b117 --- /dev/null +++ b/src/packages/components/ripple/src/index.ts @@ -0,0 +1,3 @@ +export { Ripple } from "./components/ripple"; +export type { RippleProps } from "./types/ripple"; + diff --git a/src/packages/components/ripple/src/types/ripple-ripple.ts b/src/packages/components/ripple/src/types/ripple-ripple.ts new file mode 100644 index 0000000..496491b --- /dev/null +++ b/src/packages/components/ripple/src/types/ripple-ripple.ts @@ -0,0 +1,10 @@ +import type { Key } from "react" + +type RippleRipple = { + key: Key + x: number + y: number + size: number +} + +export type { RippleRipple } diff --git a/src/packages/components/ripple/src/types/ripple.ts b/src/packages/components/ripple/src/types/ripple.ts new file mode 100644 index 0000000..eafd4be --- /dev/null +++ b/src/packages/components/ripple/src/types/ripple.ts @@ -0,0 +1,18 @@ +import type { SubLayerProps } from "@renderui/sub-layer" +import type { Simplify } from "@renderui/utils" +import type { m } from "framer-motion" +import type { ComponentPropsWithRef } from "react" + +type RipplePrimitiveProps = ComponentPropsWithRef + +type RippleCustomProps = { + isDisabled?: boolean + opacity?: number + duration?: number + scale?: number + subLayerProps?: SubLayerProps +} + +type RippleProps = Simplify + +export type { RippleProps } diff --git a/src/packages/components/ripple/src/utils/create-ripple.ts b/src/packages/components/ripple/src/utils/create-ripple.ts new file mode 100644 index 0000000..b346648 --- /dev/null +++ b/src/packages/components/ripple/src/utils/create-ripple.ts @@ -0,0 +1,49 @@ +import type { MouseEvent } from "react" + +type CreateRipplePointerProps = { + type: "pointer" + event: React.MouseEvent +} + +type CreateRippleKeyboardProps = { + type: "keyboard" + width: number + height: number +} + +type CreateRippleProps = CreateRipplePointerProps | CreateRippleKeyboardProps + +function createRipple(props: CreateRippleProps) { + const { type } = props + + const rippleKey = crypto.randomUUID() + + const size = + type === "keyboard" + ? Math.max(props.width, props.height) + : Math.max(props.event.clientX, props.event.clientY) + + const rect = + type === "keyboard" ? { x: 0, y: 0 } : props.event.currentTarget.getBoundingClientRect() + + const coordinates = + type === "keyboard" + ? { + /* center keyboard ripple in middle of element */ + x: 0, + y: -props.width / 2 + props.height / 2, + } + : { + /* origin point is the exact pointer click coordinate */ + x: props.event.clientX - rect.x - size / 2, + y: props.event.clientY - rect.y - size / 2, + } + + return { + key: rippleKey, + size, + ...coordinates, + } +} + +export { createRipple } diff --git a/src/packages/components/ripple/src/utils/get-ripple-duration.ts b/src/packages/components/ripple/src/utils/get-ripple-duration.ts new file mode 100644 index 0000000..ad33e22 --- /dev/null +++ b/src/packages/components/ripple/src/utils/get-ripple-duration.ts @@ -0,0 +1,21 @@ +import { + RIPPLE_BASE_MULTIPLIER, + RIPPLE_MIDDLE_DURATION, + RIPPLE_OVER_100_DURATION, + RIPPLE_SIZE_100, + RIPPLE_UNDER_100_DURATION, +} from "../constants/constants" + +function clamp(value: number, min: number, max: number) { + return Math.min(Math.max(value, min), max) +} + +function getRippleDuration(rippleSize: number) { + return clamp( + RIPPLE_BASE_MULTIPLIER * rippleSize, + RIPPLE_MIDDLE_DURATION, + rippleSize > RIPPLE_SIZE_100 ? RIPPLE_OVER_100_DURATION : RIPPLE_UNDER_100_DURATION, + ) +} + +export { getRippleDuration } diff --git a/src/packages/components/ripple/tsconfig.json b/src/packages/components/ripple/tsconfig.json new file mode 100644 index 0000000..d2db219 --- /dev/null +++ b/src/packages/components/ripple/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../../tsconfig.json", + "include": ["src"] +} diff --git a/src/packages/hooks/shared/.gitignore b/src/packages/hooks/shared/.gitignore index 0b62d33..73ac1e6 100644 --- a/src/packages/hooks/shared/.gitignore +++ b/src/packages/hooks/shared/.gitignore @@ -1,43 +1,42 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz +node_modules -# testing -/coverage +# build +dist +dist-ssr +build +out +*.tsbuildinfo -# next.js -/.next/ -/out/ +# testing & coverage +coverage +coverage-ts -# production -/build -/dist +# env +.env -# misc -.DS_Store +# Editor directories and files +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +*.local *.pem +.idea +.todo +.DS_Store -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env -.env*.local - -# turbo +# vendors .turbo - -# vercel .vercel -# typescript -*.tsbuildinfo - -# coverage-ts -coverage-ts +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* diff --git a/src/packages/hooks/shared/CHANGELOG.md b/src/packages/hooks/shared/CHANGELOG.md index 6137aac..44de6a9 100644 --- a/src/packages/hooks/shared/CHANGELOG.md +++ b/src/packages/hooks/shared/CHANGELOG.md @@ -1,7 +1,7 @@ -# @renderui/utils +# @renderui/hooks -## 0.2.4 +## 0.1.8 ### Patch changes -- Refactor and export shared utility functions +- Added useFreshRef and useMutationObserverHooks, refactor from @renderui/core diff --git a/src/packages/hooks/shared/bun.lockb b/src/packages/hooks/shared/bun.lockb new file mode 100755 index 0000000..d0c8412 Binary files /dev/null and b/src/packages/hooks/shared/bun.lockb differ diff --git a/src/packages/hooks/shared/package.json b/src/packages/hooks/shared/package.json index 2c895e6..ab41f7b 100644 --- a/src/packages/hooks/shared/package.json +++ b/src/packages/hooks/shared/package.json @@ -1,45 +1,46 @@ { - "author": { - "email": "lovro.zagar5@gmail.com", - "name": "Lovro Žagar" - }, - "devDependencies": { - "bunchee": "^5.5.1", - "react": "19.0.0-rc-a960b92c-20240819", - "react-dom": "19.0.0-rc-a960b92c-20240819", - "types-react": "^19.0.0-rc.1", - "types-react-dom": "^19.0.0-rc.1", - "typescript": "^5.5.4" - }, - "exports": { - ".": { - "import": { - "default": "./dist/index.js", - "types": "./dist/index.d.ts" - } - } - }, - "files": ["dist"], - "license": "MIT", - "main": "./dist/index.js", - "module": "./dist/index.js", - "name": "@renderui/hooks", - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "private": false, - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "https://github.com/lovrozagar/renderui" - }, - "scripts": { - "build": "bunchee -m" - }, - "type": "module", - "types": "./dist/index.d.ts", - "version": "0.1.7" + "author": { + "email": "lovro.zagar5@gmail.com", + "name": "Lovro Žagar" + }, + "devDependencies": { + "bunchee": "^5.5.1", + "react": "19.0.0-rc-a960b92c-20240819", + "react-dom": "19.0.0-rc-a960b92c-20240819", + "types-react": "^19.0.0-rc.1", + "types-react-dom": "^19.0.0-rc.1", + "typescript": "^5.5.4" + }, + "exports": { + ".": { + "import": { + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + } + } + }, + "files": ["dist"], + "license": "MIT", + "main": "./dist/index.js", + "module": "./dist/index.js", + "name": "@renderui/hooks", + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "private": false, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/lovrozagar/renderui.git" + }, + "scripts": { + "build": "bunchee -m" + }, + "sideEffects": false, + "type": "module", + "types": "./dist/index.d.ts", + "version": "1.0.0" } diff --git a/src/packages/hooks/shared/src/index.ts b/src/packages/hooks/shared/src/index.ts new file mode 100644 index 0000000..d57dbd0 --- /dev/null +++ b/src/packages/hooks/shared/src/index.ts @@ -0,0 +1,5 @@ +export { useFreshRef, type UseFreshRefReturn } from "./use-fresh-ref/use-fresh-ref" +export { + useMutationObserver, + type UseMutationObserverProps, +} from "./use-mutation-observer/use-mutation-observer" diff --git a/src/packages/hooks/shared/src/use-fresh-ref/use-fresh-ref.ts b/src/packages/hooks/shared/src/use-fresh-ref/use-fresh-ref.ts new file mode 100644 index 0000000..5566d78 --- /dev/null +++ b/src/packages/hooks/shared/src/use-fresh-ref/use-fresh-ref.ts @@ -0,0 +1,17 @@ +"use client" + +import { useEffect, useRef } from "react" + +function useFreshRef(value: T) { + const ref = useRef(value) + + useEffect(() => { + ref.current = value + }, [value]) + + return ref +} + +type UseFreshRefReturn = ReturnType + +export { useFreshRef, type UseFreshRefReturn } diff --git a/src/packages/hooks/shared/src/use-mutation-observer/use-mutation-observer.ts b/src/packages/hooks/shared/src/use-mutation-observer/use-mutation-observer.ts new file mode 100644 index 0000000..5f9eff6 --- /dev/null +++ b/src/packages/hooks/shared/src/use-mutation-observer/use-mutation-observer.ts @@ -0,0 +1,43 @@ +"use client" + +import { useEffect, useRef } from "react" +import { useFreshRef } from "../use-fresh-ref/use-fresh-ref" + +type UseMutationObserverProps = { + node: T | null + callback: MutationCallback + options: MutationObserverInit + enabled?: boolean +} + +function useMutationObserver(props: UseMutationObserverProps) { + const { node, callback, options, enabled } = props + + const freshCallback = useFreshRef(callback) + const freshOptions = useFreshRef(options) + const observerRef = useRef(null) + + /* biome-ignore lint/correctness/useExhaustiveDependencies: using fresh ref pattern, ref dep not needed */ + useEffect(() => { + if (!node || !enabled) { + if (!observerRef.current) return + + observerRef.current.disconnect() + + return + } + + if (observerRef.current) return + + observerRef.current = new MutationObserver(freshCallback.current) + observerRef.current.observe(node, freshOptions.current) + + return () => { + if (observerRef.current) { + observerRef.current.disconnect() + } + } + }, [node, enabled]) +} + +export { useMutationObserver, type UseMutationObserverProps } diff --git a/src/packages/hooks/shared/tsconfig.json b/src/packages/hooks/shared/tsconfig.json index 9bcd127..d2db219 100644 --- a/src/packages/hooks/shared/tsconfig.json +++ b/src/packages/hooks/shared/tsconfig.json @@ -1,4 +1,4 @@ { - "extends": "../../../../tsconfig.json", - "include": ["src", "index.ts"] + "extends": "../../../../tsconfig.json", + "include": ["src"] }