Add comprehensive frontend UI and distributed infrastructure

Frontend Enhancements:
- Complete React TypeScript frontend with modern UI components
- Distributed workflows management interface with real-time updates
- Socket.IO integration for live agent status monitoring
- Agent management dashboard with cluster visualization
- Project management interface with metrics and task tracking
- Responsive design with proper error handling and loading states

Backend Infrastructure:
- Distributed coordinator for multi-agent workflow orchestration
- Cluster management API with comprehensive agent operations
- Enhanced database models for agents and projects
- Project service for filesystem-based project discovery
- Performance monitoring and metrics collection
- Comprehensive API documentation and error handling

Documentation:
- Complete distributed development guide (README_DISTRIBUTED.md)
- Comprehensive development report with architecture insights
- System configuration templates and deployment guides

The platform now provides a complete web interface for managing the distributed AI cluster
with real-time monitoring, workflow orchestration, and agent coordination capabilities.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-07-10 08:41:59 +10:00
parent fc0eec91ef
commit 85bf1341f3
28348 changed files with 2646896 additions and 69 deletions

View File

@@ -0,0 +1,5 @@
const MotionGlobalConfig = {
skipAnimations: false,
};
export { MotionGlobalConfig };

View File

@@ -0,0 +1,21 @@
function addUniqueItem(arr, item) {
if (arr.indexOf(item) === -1)
arr.push(item);
}
function removeItem(arr, item) {
const index = arr.indexOf(item);
if (index > -1)
arr.splice(index, 1);
}
// Adapted from array-move
function moveItem([...arr], fromIndex, toIndex) {
const startIndex = fromIndex < 0 ? arr.length + fromIndex : fromIndex;
if (startIndex >= 0 && startIndex < arr.length) {
const endIndex = toIndex < 0 ? arr.length + toIndex : toIndex;
const [item] = arr.splice(fromIndex, 1);
arr.splice(endIndex, 0, item);
}
return arr;
}
export { addUniqueItem, moveItem, removeItem };

View File

@@ -0,0 +1,3 @@
const clamp = (min, max, v) => Math.min(Math.max(v, min), max);
export { clamp };

View File

@@ -0,0 +1,19 @@
import { frame, cancelFrame } from '../frameloop/frame.mjs';
/**
* Timeout defined in ms
*/
function delay(callback, timeout) {
const start = performance.now();
const checkElapsed = ({ timestamp }) => {
const elapsed = timestamp - start;
if (elapsed >= timeout) {
cancelFrame(checkElapsed);
callback(elapsed - timeout);
}
};
frame.read(checkElapsed, true);
return () => cancelFrame(checkElapsed);
}
export { delay };

View File

@@ -0,0 +1,9 @@
const distance = (a, b) => Math.abs(a - b);
function distance2D(a, b) {
// Multi-dimensional
const xDelta = distance(a.x, b.x);
const yDelta = distance(a.y, b.y);
return Math.sqrt(xDelta ** 2 + yDelta ** 2);
}
export { distance, distance2D };

View File

@@ -0,0 +1,18 @@
import { noop } from './noop.mjs';
let warning = noop;
let invariant = noop;
if (process.env.NODE_ENV !== "production") {
warning = (check, message) => {
if (!check && typeof console !== "undefined") {
console.warn(message);
}
};
invariant = (check, message) => {
if (!check) {
throw new Error(message);
}
};
}
export { invariant, warning };

View File

@@ -0,0 +1,6 @@
// Fixes https://github.com/framer/motion/issues/2270
const getContextWindow = ({ current }) => {
return current ? current.ownerDocument.defaultView : null;
};
export { getContextWindow };

View File

@@ -0,0 +1,42 @@
// Adapted from https://gist.github.com/mjackson/5311256
function hueToRgb(p, q, t) {
if (t < 0)
t += 1;
if (t > 1)
t -= 1;
if (t < 1 / 6)
return p + (q - p) * 6 * t;
if (t < 1 / 2)
return q;
if (t < 2 / 3)
return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
function hslaToRgba({ hue, saturation, lightness, alpha }) {
hue /= 360;
saturation /= 100;
lightness /= 100;
let red = 0;
let green = 0;
let blue = 0;
if (!saturation) {
red = green = blue = lightness;
}
else {
const q = lightness < 0.5
? lightness * (1 + saturation)
: lightness + saturation - lightness * saturation;
const p = 2 * lightness - q;
red = hueToRgb(p, q, hue + 1 / 3);
green = hueToRgb(p, q, hue);
blue = hueToRgb(p, q, hue - 1 / 3);
}
return {
red: Math.round(red * 255),
green: Math.round(green * 255),
blue: Math.round(blue * 255),
alpha,
};
}
export { hslaToRgba };

View File

@@ -0,0 +1,92 @@
import { invariant } from './errors.mjs';
import { color } from '../value/types/color/index.mjs';
import { clamp } from './clamp.mjs';
import { mix } from './mix.mjs';
import { mixColor } from './mix-color.mjs';
import { mixComplex, mixArray, mixObject } from './mix-complex.mjs';
import { pipe } from './pipe.mjs';
import { progress } from './progress.mjs';
import { noop } from './noop.mjs';
const mixNumber = (from, to) => (p) => mix(from, to, p);
function detectMixerFactory(v) {
if (typeof v === "number") {
return mixNumber;
}
else if (typeof v === "string") {
return color.test(v) ? mixColor : mixComplex;
}
else if (Array.isArray(v)) {
return mixArray;
}
else if (typeof v === "object") {
return mixObject;
}
return mixNumber;
}
function createMixers(output, ease, customMixer) {
const mixers = [];
const mixerFactory = customMixer || detectMixerFactory(output[0]);
const numMixers = output.length - 1;
for (let i = 0; i < numMixers; i++) {
let mixer = mixerFactory(output[i], output[i + 1]);
if (ease) {
const easingFunction = Array.isArray(ease) ? ease[i] || noop : ease;
mixer = pipe(easingFunction, mixer);
}
mixers.push(mixer);
}
return mixers;
}
/**
* Create a function that maps from a numerical input array to a generic output array.
*
* Accepts:
* - Numbers
* - Colors (hex, hsl, hsla, rgb, rgba)
* - Complex (combinations of one or more numbers or strings)
*
* ```jsx
* const mixColor = interpolate([0, 1], ['#fff', '#000'])
*
* mixColor(0.5) // 'rgba(128, 128, 128, 1)'
* ```
*
* TODO Revist this approach once we've moved to data models for values,
* probably not needed to pregenerate mixer functions.
*
* @public
*/
function interpolate(input, output, { clamp: isClamp = true, ease, mixer } = {}) {
const inputLength = input.length;
invariant(inputLength === output.length, "Both input and output ranges must be the same length");
/**
* If we're only provided a single input, we can just make a function
* that returns the output.
*/
if (inputLength === 1)
return () => output[0];
// If input runs highest -> lowest, reverse both arrays
if (input[0] > input[inputLength - 1]) {
input = [...input].reverse();
output = [...output].reverse();
}
const mixers = createMixers(output, ease, mixer);
const numMixers = mixers.length;
const interpolator = (v) => {
let i = 0;
if (numMixers > 1) {
for (; i < input.length - 2; i++) {
if (v < input[i + 1])
break;
}
}
const progressInRange = progress(input[i], input[i + 1], v);
return mixers[i](progressInRange);
};
return isClamp
? (v) => interpolator(clamp(input[0], input[inputLength - 1], v))
: interpolator;
}
export { interpolate };

View File

@@ -0,0 +1,3 @@
const isBrowser = typeof document !== "undefined";
export { isBrowser };

View File

@@ -0,0 +1,6 @@
/**
* Check if value is a numerical string, ie a string that is purely a number eg "100" or "-100.1"
*/
const isNumericalString = (v) => /^\-?\d*\.?\d+$/.test(v);
export { isNumericalString };

View File

@@ -0,0 +1,7 @@
function isRefObject(ref) {
return (ref &&
typeof ref === "object" &&
Object.prototype.hasOwnProperty.call(ref, "current"));
}
export { isRefObject };

View File

@@ -0,0 +1,6 @@
/**
* Check if the value is a zero value string like "0px" or "0%"
*/
const isZeroValueString = (v) => /^0[^.\s]+$/.test(v);
export { isZeroValueString };

View File

@@ -0,0 +1,10 @@
function memo(callback) {
let result;
return () => {
if (result === undefined)
result = callback();
return result;
};
}
export { memo };

View File

@@ -0,0 +1,40 @@
import { mix } from './mix.mjs';
import { invariant } from './errors.mjs';
import { hslaToRgba } from './hsla-to-rgba.mjs';
import { hex } from '../value/types/color/hex.mjs';
import { rgba } from '../value/types/color/rgba.mjs';
import { hsla } from '../value/types/color/hsla.mjs';
// Linear color space blending
// Explained https://www.youtube.com/watch?v=LKnqECcg6Gw
// Demonstrated http://codepen.io/osublake/pen/xGVVaN
const mixLinearColor = (from, to, v) => {
const fromExpo = from * from;
return Math.sqrt(Math.max(0, v * (to * to - fromExpo) + fromExpo));
};
const colorTypes = [hex, rgba, hsla];
const getColorType = (v) => colorTypes.find((type) => type.test(v));
function asRGBA(color) {
const type = getColorType(color);
invariant(Boolean(type), `'${color}' is not an animatable color. Use the equivalent color code instead.`);
let model = type.parse(color);
if (type === hsla) {
// TODO Remove this cast - needed since Framer Motion's stricter typing
model = hslaToRgba(model);
}
return model;
}
const mixColor = (from, to) => {
const fromRGBA = asRGBA(from);
const toRGBA = asRGBA(to);
const blended = { ...fromRGBA };
return (v) => {
blended.red = mixLinearColor(fromRGBA.red, toRGBA.red, v);
blended.green = mixLinearColor(fromRGBA.green, toRGBA.green, v);
blended.blue = mixLinearColor(fromRGBA.blue, toRGBA.blue, v);
blended.alpha = mix(fromRGBA.alpha, toRGBA.alpha, v);
return rgba.transform(blended);
};
};
export { mixColor, mixLinearColor };

View File

@@ -0,0 +1,64 @@
import { mix } from './mix.mjs';
import { mixColor } from './mix-color.mjs';
import { pipe } from './pipe.mjs';
import { warning } from './errors.mjs';
import { color } from '../value/types/color/index.mjs';
import { complex, analyseComplexValue } from '../value/types/complex/index.mjs';
const mixImmediate = (origin, target) => (p) => `${p > 0 ? target : origin}`;
function getMixer(origin, target) {
if (typeof origin === "number") {
return (v) => mix(origin, target, v);
}
else if (color.test(origin)) {
return mixColor(origin, target);
}
else {
return origin.startsWith("var(")
? mixImmediate(origin, target)
: mixComplex(origin, target);
}
}
const mixArray = (from, to) => {
const output = [...from];
const numValues = output.length;
const blendValue = from.map((fromThis, i) => getMixer(fromThis, to[i]));
return (v) => {
for (let i = 0; i < numValues; i++) {
output[i] = blendValue[i](v);
}
return output;
};
};
const mixObject = (origin, target) => {
const output = { ...origin, ...target };
const blendValue = {};
for (const key in output) {
if (origin[key] !== undefined && target[key] !== undefined) {
blendValue[key] = getMixer(origin[key], target[key]);
}
}
return (v) => {
for (const key in blendValue) {
output[key] = blendValue[key](v);
}
return output;
};
};
const mixComplex = (origin, target) => {
const template = complex.createTransformer(target);
const originStats = analyseComplexValue(origin);
const targetStats = analyseComplexValue(target);
const canInterpolate = originStats.numVars === targetStats.numVars &&
originStats.numColors === targetStats.numColors &&
originStats.numNumbers >= targetStats.numNumbers;
if (canInterpolate) {
return pipe(mixArray(originStats.values, targetStats.values), template);
}
else {
warning(true, `Complex values '${origin}' and '${target}' too different to mix. Ensure all colors are of the same type, and that each contains the same quantity of number and color values. Falling back to instant transition.`);
return mixImmediate(origin, target);
}
};
export { mixArray, mixComplex, mixObject };

View File

@@ -0,0 +1,24 @@
/*
Value in range from progress
Given a lower limit and an upper limit, we return the value within
that range as expressed by progress (usually a number from 0 to 1)
So progress = 0.5 would change
from -------- to
to
from ---- to
E.g. from = 10, to = 20, progress = 0.5 => 15
@param [number]: Lower limit of range
@param [number]: Upper limit of range
@param [number]: The progress between lower and upper limits expressed 0-1
@return [number]: Value as calculated from progress within range (not limited within range)
*/
const mix = (from, to, progress) => -progress * from + progress * to + from;
export { mix };

View File

@@ -0,0 +1,3 @@
const noop = (any) => any;
export { noop };

View File

@@ -0,0 +1,9 @@
import { fillOffset } from './fill.mjs';
function defaultOffset(arr) {
const offset = [0];
fillOffset(offset, arr.length - 1);
return offset;
}
export { defaultOffset };

View File

@@ -0,0 +1,12 @@
import { mix } from '../mix.mjs';
import { progress } from '../progress.mjs';
function fillOffset(offset, remaining) {
const min = offset[offset.length - 1];
for (let i = 1; i <= remaining; i++) {
const offsetProgress = progress(0, remaining, i);
offset.push(mix(min, 1, offsetProgress));
}
}
export { fillOffset };

View File

@@ -0,0 +1,5 @@
function convertOffsetToTimes(offset, duration) {
return offset.map((o) => o * duration);
}
export { convertOffsetToTimes };

View File

@@ -0,0 +1,11 @@
/**
* Pipe
* Compose other transformers to run linearily
* pipe(min(20), max(40))
* @param {...functions} transformers
* @return {function}
*/
const combineFunctions = (a, b) => (v) => b(a(v));
const pipe = (...transformers) => transformers.reduce(combineFunctions);
export { pipe };

View File

@@ -0,0 +1,18 @@
/*
Progress within given range
Given a lower limit and an upper limit, we return the progress
(expressed as a number 0-1) represented by the given value, and
limit that progress to within 0-1.
@param [number]: Lower limit
@param [number]: Upper limit
@param [number]: Value to find progress within given range
@return [number]: Progress of value within range as expressed 0-1
*/
const progress = (from, to, value) => {
const toFromDifference = to - from;
return toFromDifference === 0 ? 1 : (value - from) / toFromDifference;
};
export { progress };

View File

@@ -0,0 +1,19 @@
import { isBrowser } from '../is-browser.mjs';
import { hasReducedMotionListener, prefersReducedMotion } from './state.mjs';
function initPrefersReducedMotion() {
hasReducedMotionListener.current = true;
if (!isBrowser)
return;
if (window.matchMedia) {
const motionMediaQuery = window.matchMedia("(prefers-reduced-motion)");
const setReducedMotionPreferences = () => (prefersReducedMotion.current = motionMediaQuery.matches);
motionMediaQuery.addListener(setReducedMotionPreferences);
setReducedMotionPreferences();
}
else {
prefersReducedMotion.current = false;
}
}
export { initPrefersReducedMotion };

View File

@@ -0,0 +1,5 @@
// Does this device prefer reduced motion? Returns `null` server-side.
const prefersReducedMotion = { current: null };
const hasReducedMotionListener = { current: false };
export { hasReducedMotionListener, prefersReducedMotion };

View File

@@ -0,0 +1,19 @@
import { useContext } from 'react';
import { MotionConfigContext } from '../../context/MotionConfigContext.mjs';
import { useReducedMotion } from './use-reduced-motion.mjs';
function useReducedMotionConfig() {
const reducedMotionPreference = useReducedMotion();
const { reducedMotion } = useContext(MotionConfigContext);
if (reducedMotion === "never") {
return false;
}
else if (reducedMotion === "always") {
return true;
}
else {
return reducedMotionPreference;
}
}
export { useReducedMotionConfig };

View File

@@ -0,0 +1,47 @@
import { useState } from 'react';
import { initPrefersReducedMotion } from './index.mjs';
import { warnOnce } from '../warn-once.mjs';
import { hasReducedMotionListener, prefersReducedMotion } from './state.mjs';
/**
* A hook that returns `true` if we should be using reduced motion based on the current device's Reduced Motion setting.
*
* This can be used to implement changes to your UI based on Reduced Motion. For instance, replacing motion-sickness inducing
* `x`/`y` animations with `opacity`, disabling the autoplay of background videos, or turning off parallax motion.
*
* It will actively respond to changes and re-render your components with the latest setting.
*
* ```jsx
* export function Sidebar({ isOpen }) {
* const shouldReduceMotion = useReducedMotion()
* const closedX = shouldReduceMotion ? 0 : "-100%"
*
* return (
* <motion.div animate={{
* opacity: isOpen ? 1 : 0,
* x: isOpen ? 0 : closedX
* }} />
* )
* }
* ```
*
* @return boolean
*
* @public
*/
function useReducedMotion() {
/**
* Lazy initialisation of prefersReducedMotion
*/
!hasReducedMotionListener.current && initPrefersReducedMotion();
const [shouldReduceMotion] = useState(prefersReducedMotion.current);
if (process.env.NODE_ENV !== "production") {
warnOnce(shouldReduceMotion !== true, "You have Reduced Motion enabled on your device. Animations may not appear as expected.");
}
/**
* TODO See if people miss automatically updating shouldReduceMotion setting
*/
return shouldReduceMotion;
}
export { useReducedMotion };

View File

@@ -0,0 +1,11 @@
import { isKeyframesTarget } from '../animation/utils/is-keyframes-target.mjs';
const isCustomValue = (v) => {
return Boolean(v && typeof v === "object" && v.mix && v.toValue);
};
const resolveFinalValueInKeyframes = (v) => {
// TODO maybe throw if v.length - 1 is placeholder token?
return isKeyframesTarget(v) ? v[v.length - 1] || 0 : v;
};
export { isCustomValue, resolveFinalValueInKeyframes };

View File

@@ -0,0 +1,14 @@
function shallowCompare(next, prev) {
if (!Array.isArray(prev))
return false;
const prevLength = prev.length;
if (prevLength !== next.length)
return false;
for (let i = 0; i < prevLength; i++) {
if (prev[i] !== next[i])
return false;
}
return true;
}
export { shallowCompare };

View File

@@ -0,0 +1,40 @@
import { addUniqueItem, removeItem } from './array.mjs';
class SubscriptionManager {
constructor() {
this.subscriptions = [];
}
add(handler) {
addUniqueItem(this.subscriptions, handler);
return () => removeItem(this.subscriptions, handler);
}
notify(a, b, c) {
const numSubscriptions = this.subscriptions.length;
if (!numSubscriptions)
return;
if (numSubscriptions === 1) {
/**
* If there's only a single handler we can just call it without invoking a loop.
*/
this.subscriptions[0](a, b, c);
}
else {
for (let i = 0; i < numSubscriptions; i++) {
/**
* Check whether the handler exists before firing as it's possible
* the subscriptions were modified during this loop running.
*/
const handler = this.subscriptions[i];
handler && handler(a, b, c);
}
}
}
getSize() {
return this.subscriptions.length;
}
clear() {
this.subscriptions.length = 0;
}
}
export { SubscriptionManager };

View File

@@ -0,0 +1,10 @@
/**
* Converts seconds to milliseconds
*
* @param seconds - Time in seconds.
* @return milliseconds - Converted time in milliseconds.
*/
const secondsToMilliseconds = (seconds) => seconds * 1000;
const millisecondsToSeconds = (milliseconds) => milliseconds / 1000;
export { millisecondsToSeconds, secondsToMilliseconds };

View File

@@ -0,0 +1,21 @@
import { interpolate } from './interpolate.mjs';
const isCustomValueType = (v) => {
return v && typeof v === "object" && v.mix;
};
const getMixer = (v) => (isCustomValueType(v) ? v.mix : undefined);
function transform(...args) {
const useImmediate = !Array.isArray(args[0]);
const argOffset = useImmediate ? 0 : -1;
const inputValue = args[0 + argOffset];
const inputRange = args[1 + argOffset];
const outputRange = args[2 + argOffset];
const options = args[3 + argOffset];
const interpolator = interpolate(inputRange, outputRange, {
mixer: getMixer(outputRange[0]),
...options,
});
return useImmediate ? interpolator(inputValue) : interpolator;
}
export { transform };

View File

@@ -0,0 +1,21 @@
import { useRef, useContext, useEffect } from 'react';
import { MotionConfigContext } from '../context/MotionConfigContext.mjs';
import { frame, cancelFrame } from '../frameloop/frame.mjs';
function useAnimationFrame(callback) {
const initialTimestamp = useRef(0);
const { isStatic } = useContext(MotionConfigContext);
useEffect(() => {
if (isStatic)
return;
const provideTimeSinceStart = ({ timestamp, delta }) => {
if (!initialTimestamp.current)
initialTimestamp.current = timestamp;
callback(timestamp - initialTimestamp.current, delta);
};
frame.update(provideTimeSinceStart, true);
return () => cancelFrame(provideTimeSinceStart);
}, [callback]);
}
export { useAnimationFrame };

View File

@@ -0,0 +1,18 @@
import { useRef } from 'react';
/**
* Creates a constant value over the lifecycle of a component.
*
* Even if `useMemo` is provided an empty array as its final argument, it doesn't offer
* a guarantee that it won't re-run for performance reasons later on. By using `useConstant`
* you can ensure that initialisers don't execute twice or more.
*/
function useConstant(init) {
const ref = useRef(null);
if (ref.current === null) {
ref.current = init();
}
return ref.current;
}
export { useConstant };

View File

@@ -0,0 +1,47 @@
import { useRef, useState, useCallback } from 'react';
import { wrap } from './wrap.mjs';
/**
* Cycles through a series of visual properties. Can be used to toggle between or cycle through animations. It works similar to `useState` in React. It is provided an initial array of possible states, and returns an array of two arguments.
*
* An index value can be passed to the returned `cycle` function to cycle to a specific index.
*
* ```jsx
* import * as React from "react"
* import { motion, useCycle } from "framer-motion"
*
* export const MyComponent = () => {
* const [x, cycleX] = useCycle(0, 50, 100)
*
* return (
* <motion.div
* animate={{ x: x }}
* onTap={() => cycleX()}
* />
* )
* }
* ```
*
* @param items - items to cycle through
* @returns [currentState, cycleState]
*
* @public
*/
function useCycle(...items) {
const index = useRef(0);
const [item, setItem] = useState(items[index.current]);
const runCycle = useCallback((next) => {
index.current =
typeof next !== "number"
? wrap(0, items.length, index.current + 1)
: next;
setItem(items[index.current]);
},
// The array will change on each call, but by putting items.length at
// the front of this array, we guarantee the dependency comparison will match up
// eslint-disable-next-line react-hooks/exhaustive-deps
[items.length, ...items]);
return [item, runCycle];
}
export { useCycle };

View File

@@ -0,0 +1,19 @@
import { useState, useCallback } from 'react';
import { useIsMounted } from './use-is-mounted.mjs';
import { frame } from '../frameloop/frame.mjs';
function useForceUpdate() {
const isMounted = useIsMounted();
const [forcedRenderCount, setForcedRenderCount] = useState(0);
const forceRender = useCallback(() => {
isMounted.current && setForcedRenderCount(forcedRenderCount + 1);
}, [forcedRenderCount]);
/**
* Defer this to the end of the next animation frame in case there are multiple
* synchronous calls.
*/
const deferredForceRender = useCallback(() => frame.postRender(forceRender), [forceRender]);
return [deferredForceRender, forcedRenderCount];
}
export { useForceUpdate };

View File

@@ -0,0 +1,23 @@
import { useState, useEffect } from 'react';
import { inView } from '../render/dom/viewport/index.mjs';
function useInView(ref, { root, margin, amount, once = false } = {}) {
const [isInView, setInView] = useState(false);
useEffect(() => {
if (!ref.current || (once && isInView))
return;
const onEnter = () => {
setInView(true);
return once ? undefined : () => setInView(false);
};
const options = {
root: (root && root.current) || undefined,
margin,
amount,
};
return inView(ref.current, onEnter, options);
}, [root, ref, margin, once, amount]);
return isInView;
}
export { useInView };

View File

@@ -0,0 +1,5 @@
const instantAnimationState = {
current: false,
};
export { instantAnimationState };

View File

@@ -0,0 +1,41 @@
import { useRef, useEffect } from 'react';
import { useInstantLayoutTransition } from '../projection/use-instant-layout-transition.mjs';
import { useForceUpdate } from './use-force-update.mjs';
import { instantAnimationState } from './use-instant-transition-state.mjs';
import { frame } from '../frameloop/frame.mjs';
function useInstantTransition() {
const [forceUpdate, forcedRenderCount] = useForceUpdate();
const startInstantLayoutTransition = useInstantLayoutTransition();
const unlockOnFrameRef = useRef();
useEffect(() => {
/**
* Unblock after two animation frames, otherwise this will unblock too soon.
*/
frame.postRender(() => frame.postRender(() => {
/**
* If the callback has been called again after the effect
* triggered this 2 frame delay, don't unblock animations. This
* prevents the previous effect from unblocking the current
* instant transition too soon. This becomes more likely when
* used in conjunction with React.startTransition().
*/
if (forcedRenderCount !== unlockOnFrameRef.current)
return;
instantAnimationState.current = false;
}));
}, [forcedRenderCount]);
return (callback) => {
startInstantLayoutTransition(() => {
instantAnimationState.current = true;
forceUpdate();
callback();
unlockOnFrameRef.current = forcedRenderCount + 1;
});
};
}
function disableInstantTransitions() {
instantAnimationState.current = false;
}
export { disableInstantTransitions, useInstantTransition };

View File

@@ -0,0 +1,15 @@
import { useRef } from 'react';
import { useIsomorphicLayoutEffect } from './use-isomorphic-effect.mjs';
function useIsMounted() {
const isMounted = useRef(false);
useIsomorphicLayoutEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
return isMounted;
}
export { useIsMounted };

View File

@@ -0,0 +1,6 @@
import { useLayoutEffect, useEffect } from 'react';
import { isBrowser } from './is-browser.mjs';
const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect;
export { useIsomorphicLayoutEffect };

View File

@@ -0,0 +1,13 @@
import { useInsertionEffect } from 'react';
function useMotionValueEvent(value, event, callback) {
/**
* useInsertionEffect will create subscriptions before any other
* effects will run. Effects run upwards through the tree so it
* can be that binding a useLayoutEffect higher up the tree can
* miss changes from lower down the tree.
*/
useInsertionEffect(() => value.on(event, callback), [value, event, callback]);
}
export { useMotionValueEvent };

View File

@@ -0,0 +1,7 @@
import { useEffect } from 'react';
function useUnmountEffect(callback) {
return useEffect(() => () => callback(), []);
}
export { useUnmountEffect };

View File

@@ -0,0 +1,11 @@
/*
Convert velocity into velocity per second
@param [number]: Unit per frame
@param [number]: Frame duration in ms
*/
function velocityPerSecond(velocity, frameDuration) {
return frameDuration ? velocity * (1000 / frameDuration) : 0;
}
export { velocityPerSecond };

View File

@@ -0,0 +1,11 @@
const warned = new Set();
function warnOnce(condition, message, element) {
if (condition || warned.has(message))
return;
console.warn(message);
if (element)
console.warn(element);
warned.add(message);
}
export { warnOnce };

View File

@@ -0,0 +1,6 @@
const wrap = (min, max, v) => {
const rangeSize = max - min;
return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min;
};
export { wrap };