mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
chore(root): prepare workspace for alpha (#276)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
// A (very crude) utility to merge class names
|
||||
// Usually I'd use something like `clsx` or `classnames` but this is ok for our simple use case.
|
||||
// It just makes the billions of Tailwind classes a little easier to read.
|
||||
export function cn(...classes: (string | undefined)[]): string {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
import { createContext, forwardRef, useCallback, useContext, useEffect, useRef, useSyncExternalStore } from 'react';
|
||||
|
||||
export type StateHookFn<TProps = any, TState = any> = (props: TProps) => TState;
|
||||
|
||||
export type PropsHookFn<TProps = any, TState = any, TResultProps = any> = (
|
||||
props: TProps,
|
||||
state: TState
|
||||
) => TResultProps;
|
||||
|
||||
export type RenderFn<TProps = any, TState = any> = (props: TProps, state: TState) => ReactElement;
|
||||
|
||||
const Context = createContext<any>(null);
|
||||
|
||||
/**
|
||||
* Generic factory function to create connected components following the hooks pattern
|
||||
* inspired by Adobe React Spectrum and Base UI architectures.
|
||||
*
|
||||
* @param useStateHook - Hook that provides component state
|
||||
* @param usePropsHook - Hook that enhances props with state-derived values
|
||||
* @param defaultRender - Default render function for the component
|
||||
* @param displayName - Display name for React DevTools
|
||||
* @returns Connected component with customizable render prop
|
||||
*/
|
||||
export function toConnectedComponent<
|
||||
TProps extends Record<string, any>,
|
||||
TState,
|
||||
TResultProps extends Record<string, any>,
|
||||
TRenderFn extends RenderFn<TResultProps, TState>,
|
||||
>(
|
||||
useStateHook: StateHookFn<TProps, TState>,
|
||||
usePropsHook: PropsHookFn<TProps, TState, TResultProps>,
|
||||
defaultRender: TRenderFn,
|
||||
displayName: string,
|
||||
): ConnectedComponent<TProps, TRenderFn> {
|
||||
const ConnectedComponent = forwardRef<HTMLElement, TProps & { render?: TRenderFn }>(
|
||||
({ render = defaultRender, ...props }, ref) => {
|
||||
const propsWithRef = ref ? { ...props, ref } : props;
|
||||
const connectedState = useStateHook(propsWithRef as unknown as TProps);
|
||||
const connectedProps = usePropsHook(propsWithRef as unknown as TProps, connectedState);
|
||||
return <Context.Provider value={connectedState}>{render(connectedProps, connectedState)}</Context.Provider>;
|
||||
},
|
||||
);
|
||||
|
||||
ConnectedComponent.displayName = displayName;
|
||||
|
||||
return ConnectedComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type helper to infer the component type from the factory
|
||||
*/
|
||||
export type ConnectedComponent<TProps extends Record<string, any>, TRenderFn extends RenderFn<any, any>> = React.ForwardRefExoticComponent<
|
||||
React.PropsWithoutRef<TProps & { render?: TRenderFn }> & React.RefAttributes<HTMLElement>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Factory function to create context-based components that don't use toConnectedComponent
|
||||
* These components rely on context provided by a parent component.
|
||||
*
|
||||
* @param usePropsHook - Hook that enhances props with context-derived values
|
||||
* @param defaultRender - Default render function for the component
|
||||
* @param displayName - Display name for React DevTools
|
||||
* @returns Context-based component with customizable render prop
|
||||
*/
|
||||
export function toContextComponent<
|
||||
TProps extends Record<string, any>,
|
||||
TResultProps extends Record<string, any>,
|
||||
TRenderFn extends (props: TResultProps, context: any) => ReactElement,
|
||||
>(
|
||||
usePropsHook: (props: TProps, context: ReturnType<StateHookFn<TProps>>) => TResultProps,
|
||||
defaultRender: TRenderFn,
|
||||
displayName: string,
|
||||
): ContextComponent<TProps, TRenderFn> {
|
||||
const ContextComponent = forwardRef<HTMLElement, TProps & { render?: TRenderFn }>(
|
||||
({ render = defaultRender, ...props }, ref) => {
|
||||
const context = useContext(Context);
|
||||
const propsWithRef = ref ? { ...props, ref } : props;
|
||||
const contextProps = usePropsHook(propsWithRef as unknown as TProps, context);
|
||||
return render(contextProps, context);
|
||||
},
|
||||
);
|
||||
|
||||
ContextComponent.displayName = displayName;
|
||||
|
||||
return ContextComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type helper to infer the context component type from the factory
|
||||
*/
|
||||
export type ContextComponent<
|
||||
TProps extends Record<string, any>,
|
||||
TRenderFn extends (props: any, context: any) => ReactElement,
|
||||
> = React.ForwardRefExoticComponent<
|
||||
React.PropsWithoutRef<TProps & { render?: TRenderFn }> & React.RefAttributes<any>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Hook that manages a CoreClass instance and triggers re-renders when state changes.
|
||||
* Uses useSyncExternalStore for optimal performance with external state subscriptions.
|
||||
*/
|
||||
export function useCore<
|
||||
T extends {
|
||||
subscribe: (callback: (state: any) => void) => () => void;
|
||||
getState: () => any;
|
||||
setState: (state: any) => void;
|
||||
},
|
||||
>(CoreClass: new () => T, state: any): ReturnType<T['getState']> {
|
||||
const coreRef = useRef<T | null>(null);
|
||||
const snapshotRef = useRef<any>(null);
|
||||
|
||||
// Initialize the core instance
|
||||
if (!coreRef.current) {
|
||||
coreRef.current = new CoreClass();
|
||||
snapshotRef.current = coreRef.current.getState();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
coreRef.current?.setState(state);
|
||||
}, [...Object.values(state)]);
|
||||
|
||||
// Use useSyncExternalStore to subscribe to state changes
|
||||
useSyncExternalStore(
|
||||
useCallback((onStoreChange) => {
|
||||
if (!coreRef.current) return () => {};
|
||||
return coreRef.current.subscribe((newState) => {
|
||||
snapshotRef.current = newState;
|
||||
onStoreChange();
|
||||
});
|
||||
}, []),
|
||||
useCallback(() => {
|
||||
return snapshotRef.current;
|
||||
}, []),
|
||||
() => null, // server snapshot
|
||||
);
|
||||
|
||||
return coreRef.current.getState();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import * as React from 'react';
|
||||
|
||||
type PossibleRef<T> = React.Ref<T> | undefined;
|
||||
|
||||
/**
|
||||
* Set a given ref to a given value
|
||||
* This utility takes care of different types of refs: callback refs and RefObject(s)
|
||||
*/
|
||||
function setRef<T>(ref: PossibleRef<T>, value: T): (() => void) | void | undefined {
|
||||
if (typeof ref === 'function') {
|
||||
return ref(value);
|
||||
} else if (ref !== null && ref !== undefined) {
|
||||
(ref as React.MutableRefObject<T>).current = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A utility to compose multiple refs together
|
||||
* Accepts callback refs and RefObject(s)
|
||||
*/
|
||||
function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
|
||||
return (node) => {
|
||||
let hasCleanup = false;
|
||||
const cleanups = refs.map((ref) => {
|
||||
const cleanup = setRef(ref, node);
|
||||
if (!hasCleanup && typeof cleanup == 'function') {
|
||||
hasCleanup = true;
|
||||
}
|
||||
return cleanup;
|
||||
});
|
||||
|
||||
// React <19 will log an error to the console if a callback ref returns a
|
||||
// value. We don't use ref cleanups internally so this will only happen if a
|
||||
// user's ref callback returns a value, which we only expect if they are
|
||||
// using the cleanup functionality added in React 19.
|
||||
if (hasCleanup) {
|
||||
return () => {
|
||||
for (let i = 0; i < cleanups.length; i++) {
|
||||
const cleanup = cleanups[i];
|
||||
if (typeof cleanup == 'function') {
|
||||
cleanup();
|
||||
} else {
|
||||
setRef(refs[i], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom hook that composes multiple refs
|
||||
* Accepts callback refs and RefObject(s)
|
||||
*/
|
||||
function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return React.useCallback(composeRefs(...refs), refs);
|
||||
}
|
||||
|
||||
export { composeRefs, useComposedRefs };
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
/**
|
||||
* Custom React hook for observing DOM mutations using MutationObserver
|
||||
*
|
||||
* @param target - Element to observe (ref or element)
|
||||
* @param callback - Function called when mutations occur
|
||||
* @param options - MutationObserver options
|
||||
*/
|
||||
export function useMutationObserver<T extends Element = Element>(
|
||||
target: RefObject<T> | T | null | undefined,
|
||||
callback: MutationCallback,
|
||||
options: MutationObserverInit = {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
},
|
||||
): void {
|
||||
const callbackRef = useRef(callback);
|
||||
|
||||
useEffect(() => {
|
||||
callbackRef.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = target && 'current' in target ? target.current : target;
|
||||
|
||||
if (!element || !(element instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((mutations, obs) => {
|
||||
callbackRef.current(mutations, obs);
|
||||
});
|
||||
|
||||
observer.observe(element, options);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [target, options]);
|
||||
}
|
||||
Reference in New Issue
Block a user