mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-03-03 05:55:03 +01:00
feat(ds): Replace lottie colors in Spinner (#18001)
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { hexToRgbaArray } from '@trezor/utils';
|
||||
|
||||
// @ts-expect-error
|
||||
type JsonValue = string | number | boolean | null | Record<string, JsonValue> | JsonValue[];
|
||||
|
||||
type LottieAnimation = JsonValue;
|
||||
|
||||
const areColorsClose = (a: number[], b: number[], tolerance = 0.01) =>
|
||||
a.length === 4 && b.length === 4 && a.every((v, i) => Math.abs(v - b[i]) < tolerance);
|
||||
|
||||
export const recolorLottieAnimation = (
|
||||
data: LottieAnimation,
|
||||
replacements: { from: string; to: string }[] | null,
|
||||
): LottieAnimation => {
|
||||
if (replacements === null || replacements.length === 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const colorPairs = replacements.map(({ from, to }) => ({
|
||||
from: hexToRgbaArray(from),
|
||||
to: hexToRgbaArray(to),
|
||||
}));
|
||||
|
||||
const cloned = JSON.parse(JSON.stringify(data));
|
||||
|
||||
const findReplacement = (color: number[]): number[] | null => {
|
||||
for (const { from, to } of colorPairs) {
|
||||
if (areColorsClose(color, from)) {
|
||||
return to;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const walk = (node: any) => {
|
||||
if (Array.isArray(node)) {
|
||||
// Find RGBA quartets in row (e.g. in gradients)
|
||||
for (let i = 0; i <= node.length - 4; i++) {
|
||||
const segment = node.slice(i, i + 4);
|
||||
if (segment.every(n => typeof n === 'number')) {
|
||||
const replacement = findReplacement(segment);
|
||||
if (replacement) {
|
||||
node.splice(i, 4, ...replacement);
|
||||
i += 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node.forEach(walk);
|
||||
} else if (typeof node === 'object' && node !== null) {
|
||||
for (const key in node) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (node.hasOwnProperty(key)) {
|
||||
const value = node[key];
|
||||
|
||||
// Simple rgba color
|
||||
if (
|
||||
Array.isArray(value) &&
|
||||
value.length === 4 &&
|
||||
value.every(n => typeof n === 'number')
|
||||
) {
|
||||
const replacement = findReplacement(value);
|
||||
if (replacement) {
|
||||
node[key] = replacement;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
walk(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk(cloned);
|
||||
|
||||
return cloned;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ArgTypes, Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { Spinner as SpinnerComponent, SpinnerProps, allowedSpinnerFrameProps } from './Spinner';
|
||||
import { getFramePropsStory } from '../../../utils/frameProps';
|
||||
@@ -9,45 +9,54 @@ const meta: Meta = {
|
||||
} as Meta;
|
||||
export default meta;
|
||||
|
||||
export const Default: StoryObj<SpinnerProps> = {
|
||||
args: {
|
||||
size: 50,
|
||||
...getFramePropsStory(allowedSpinnerFrameProps).args,
|
||||
const args: Partial<SpinnerProps> | undefined = {
|
||||
size: 50,
|
||||
bodyColor: undefined,
|
||||
warningBackgroundColor: undefined,
|
||||
warningForegroundColor: undefined,
|
||||
isGrey: false,
|
||||
...getFramePropsStory(allowedSpinnerFrameProps).args,
|
||||
};
|
||||
const argTypes: Partial<ArgTypes<SpinnerProps>> | undefined = {
|
||||
bodyColor: {
|
||||
control: { type: 'color' },
|
||||
},
|
||||
argTypes: {
|
||||
className: {
|
||||
control: false,
|
||||
warningBackgroundColor: {
|
||||
control: { type: 'color' },
|
||||
},
|
||||
warningForegroundColor: {
|
||||
control: { type: 'color' },
|
||||
},
|
||||
className: {
|
||||
control: false,
|
||||
},
|
||||
isGrey: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
...getFramePropsStory(allowedSpinnerFrameProps).argTypes,
|
||||
},
|
||||
...getFramePropsStory(allowedSpinnerFrameProps).argTypes,
|
||||
};
|
||||
|
||||
export const Default: StoryObj<SpinnerProps> = {
|
||||
args,
|
||||
argTypes,
|
||||
};
|
||||
|
||||
export const Success: StoryObj<SpinnerProps> = {
|
||||
args: {
|
||||
size: 50,
|
||||
...args,
|
||||
hasFinished: true,
|
||||
hasStartAnimation: true,
|
||||
...getFramePropsStory(allowedSpinnerFrameProps).args,
|
||||
},
|
||||
argTypes: {
|
||||
className: {
|
||||
control: false,
|
||||
},
|
||||
...getFramePropsStory(allowedSpinnerFrameProps).argTypes,
|
||||
},
|
||||
argTypes,
|
||||
};
|
||||
|
||||
export const Error: StoryObj<SpinnerProps> = {
|
||||
args: {
|
||||
size: 50,
|
||||
...args,
|
||||
hasError: true,
|
||||
hasStartAnimation: true,
|
||||
...getFramePropsStory(allowedSpinnerFrameProps).args,
|
||||
},
|
||||
argTypes: {
|
||||
className: {
|
||||
control: false,
|
||||
},
|
||||
...getFramePropsStory(allowedSpinnerFrameProps).argTypes,
|
||||
},
|
||||
argTypes,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import Lottie from 'lottie-react';
|
||||
import styled from 'styled-components';
|
||||
import styled, { useTheme } from 'styled-components';
|
||||
|
||||
import animationEnd from './animationData/refresh-spinner-end-success.json';
|
||||
import animationWarn from './animationData/refresh-spinner-end-warning.json';
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
withFrameProps,
|
||||
} from '../../../utils/frameProps';
|
||||
import { TransientProps } from '../../../utils/transientProps';
|
||||
import { recolorLottieAnimation } from '../../animations/recolorLottieAnimation';
|
||||
|
||||
export const allowedSpinnerFrameProps = ['margin'] as const satisfies FramePropsKeys[];
|
||||
type AllowedFrameProps = Pick<FrameProps, (typeof allowedSpinnerFrameProps)[number]>;
|
||||
@@ -31,6 +32,12 @@ const StyledLottie = styled(Lottie)<
|
||||
${withFrameProps}
|
||||
`;
|
||||
|
||||
const ORIGIN_COLORS_IN_ANIMATION = {
|
||||
BODY: '#00854DFF',
|
||||
WARNING_BACKGROUND: '#f7bf2f',
|
||||
WARNING_FOREGROUND: '#ffffff',
|
||||
};
|
||||
|
||||
export type SpinnerProps = AllowedFrameProps & {
|
||||
size?: number;
|
||||
isGrey?: boolean;
|
||||
@@ -39,6 +46,9 @@ export type SpinnerProps = AllowedFrameProps & {
|
||||
hasError?: boolean;
|
||||
className?: string;
|
||||
'data-testid'?: string;
|
||||
bodyColor?: string | null;
|
||||
warningBackgroundColor?: string | null;
|
||||
warningForegroundColor?: string | null;
|
||||
};
|
||||
|
||||
export const Spinner = ({
|
||||
@@ -49,8 +59,16 @@ export const Spinner = ({
|
||||
hasError,
|
||||
className,
|
||||
'data-testid': dataTest,
|
||||
bodyColor,
|
||||
warningBackgroundColor,
|
||||
warningForegroundColor,
|
||||
...rest
|
||||
}: SpinnerProps) => {
|
||||
const theme = useTheme();
|
||||
const defaultBodyColor = theme.baseContentBrand;
|
||||
const defaultWarningColor = theme.baseContentWarning;
|
||||
const defaultWarningForegroundColor = theme.baseContentReversePrimary;
|
||||
|
||||
const frameProps = pickAndPrepareFrameProps(rest, allowedSpinnerFrameProps);
|
||||
|
||||
const [hasStarted, setHasStarted] = useState(false);
|
||||
@@ -60,30 +78,43 @@ export const Spinner = ({
|
||||
setHasFinishedRotation(true);
|
||||
};
|
||||
|
||||
// base/content/reverse-primary
|
||||
const colorsReplace = [
|
||||
{ from: ORIGIN_COLORS_IN_ANIMATION.BODY, to: bodyColor ?? defaultBodyColor },
|
||||
{
|
||||
from: ORIGIN_COLORS_IN_ANIMATION.WARNING_BACKGROUND,
|
||||
to: warningBackgroundColor ?? defaultWarningColor,
|
||||
},
|
||||
{
|
||||
from: ORIGIN_COLORS_IN_ANIMATION.WARNING_FOREGROUND,
|
||||
to: warningForegroundColor ?? defaultWarningForegroundColor,
|
||||
},
|
||||
];
|
||||
|
||||
const getProps = () => {
|
||||
if (hasFinished && hasFinishedRotation) {
|
||||
return {
|
||||
animationData: animationEnd,
|
||||
animationData: recolorLottieAnimation(animationEnd, colorsReplace),
|
||||
loop: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (hasError && hasFinishedRotation) {
|
||||
return {
|
||||
animationData: animationWarn,
|
||||
animationData: recolorLottieAnimation(animationWarn, colorsReplace),
|
||||
loop: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (hasStarted || !hasStartAnimation) {
|
||||
return {
|
||||
animationData: animationMiddle,
|
||||
animationData: recolorLottieAnimation(animationMiddle, colorsReplace),
|
||||
onLoopComplete,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
animationData: animationStart,
|
||||
animationData: recolorLottieAnimation(animationStart, colorsReplace),
|
||||
onComplete: () => setHasStarted(true),
|
||||
loop: false,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,9 @@ export { motionAnimation, motionEasing } from './config/motion';
|
||||
export { Checkbox, type CheckboxProps } from './components/form/Checkbox/Checkbox';
|
||||
export * from './components/animations/DeviceAnimation';
|
||||
export * from './components/animations/LottieAnimation';
|
||||
export { recolorLottieAnimation } from './components/animations/recolorLottieAnimation';
|
||||
export { hexToRgbaArray } from '../../utils/src/hexToRgbaArray';
|
||||
export { hexToRgba } from '../../utils/src/hexToRgba';
|
||||
export { AssetLogo, type AssetLogoProps } from './components/AssetLogo/AssetLogo';
|
||||
export * from './components/Flag/Flag';
|
||||
export * from './components/AutoScalingInput/AutoScalingInput';
|
||||
|
||||
11
packages/utils/src/hexToRgbaArray.ts
Normal file
11
packages/utils/src/hexToRgbaArray.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const hexToRgbaArray = (hex: string): number[] => {
|
||||
const norm = hex.replace('#', '');
|
||||
const full = norm.length === 6 ? norm + 'FF' : norm;
|
||||
|
||||
return [
|
||||
parseInt(full.slice(0, 2), 16) / 255,
|
||||
parseInt(full.slice(2, 4), 16) / 255,
|
||||
parseInt(full.slice(4, 6), 16) / 255,
|
||||
parseInt(full.slice(6, 8), 16) / 255,
|
||||
];
|
||||
};
|
||||
@@ -30,7 +30,8 @@ export * from './getWeakRandomId';
|
||||
export * from './getWeakRandomInt';
|
||||
export * from './getWeakRandomNumberInRange';
|
||||
export * from './hasUppercaseLetter';
|
||||
export * from './hexToRgba';
|
||||
export { hexToRgba } from './hexToRgba';
|
||||
export { hexToRgbaArray } from './hexToRgbaArray';
|
||||
export * from './isArrayMember';
|
||||
export * from './isFullPath';
|
||||
export * from './isHex';
|
||||
|
||||
Reference in New Issue
Block a user