first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,520 @@
.progress-circle-fills-animate {
will-change: transform;
transform: translateZ(0);
animation: progress-circle-fills-rotate 1s infinite cubic-bezier(.25,.78,.48,.89);
transform-origin: center;
}
.progress-circle-fill-submask-1-animate {
will-change: transform;
transform: translateZ(0);
animation: progress-circle-fill-mask-1 1s infinite linear;
}
.progress-circle-fill-submask-2-animate {
will-change: transform;
transform: translateZ(0);
animation: progress-circle-fill-mask-2 1s infinite linear;
}
@keyframes progress-circle-fill-mask-1 {
0% {
transform: rotate(90deg);
}
1.69% {
transform: rotate(72.3deg);
}
3.39% {
transform: rotate(55.5deg);
}
5.08% {
transform: rotate(40.3deg);
}
6.78% {
transform: rotate(25deg);
}
8.47% {
transform: rotate(10.6deg);
}
10.17% {
transform: rotate(0deg);
}
11.86% {
transform: rotate(0deg);
}
13.56% {
transform: rotate(0deg);
}
15.25% {
transform: rotate(0deg);
}
16.95% {
transform: rotate(0deg);
}
18.64% {
transform: rotate(0deg);
}
20.34% {
transform: rotate(0deg);
}
22.03% {
transform: rotate(0deg);
}
23.73% {
transform: rotate(0deg);
}
25.42% {
transform: rotate(0deg);
}
27.12% {
transform: rotate(0deg);
}
28.81% {
transform: rotate(0deg);
}
30.51% {
transform: rotate(0deg);
}
32.2% {
transform: rotate(0deg);
}
33.9% {
transform: rotate(0deg);
}
35.59% {
transform: rotate(0deg);
}
37.29% {
transform: rotate(0deg);
}
38.98% {
transform: rotate(0deg);
}
40.68% {
transform: rotate(0deg);
}
42.37% {
transform: rotate(5.3deg);
}
44.07% {
transform: rotate(13.4deg);
}
45.76% {
transform: rotate(20.6deg);
}
47.46% {
transform: rotate(29deg);
}
49.15% {
transform: rotate(36.5deg);
}
50.85% {
transform: rotate(42.6deg);
}
52.54% {
transform: rotate(48.8deg);
}
54.24% {
transform: rotate(54.2deg);
}
55.93% {
transform: rotate(59.4deg);
}
57.63% {
transform: rotate(63.2deg);
}
59.32% {
transform: rotate(67.2deg);
}
61.02% {
transform: rotate(70.8deg);
}
62.71% {
transform: rotate(73.8deg);
}
64.41% {
transform: rotate(76.2deg);
}
66.1% {
transform: rotate(78.7deg);
}
67.8% {
transform: rotate(80.6deg);
}
69.49% {
transform: rotate(82.6deg);
}
71.19% {
transform: rotate(83.7deg);
}
72.88% {
transform: rotate(85deg);
}
74.58% {
transform: rotate(86.3deg);
}
76.27% {
transform: rotate(87deg);
}
77.97% {
transform: rotate(87.7deg);
}
79.66% {
transform: rotate(88.3deg);
}
81.36% {
transform: rotate(88.6deg);
}
83.05% {
transform: rotate(89.2deg);
}
84.75% {
transform: rotate(89.2deg);
}
86.44% {
transform: rotate(89.5deg);
}
88.14% {
transform: rotate(89.9deg);
}
89.83% {
transform: rotate(89.7deg);
}
91.53% {
transform: rotate(90.1deg);
}
93.22% {
transform: rotate(90.2deg);
}
94.92% {
transform: rotate(90.1deg);
}
96.61% {
transform: rotate(90deg);
}
98.31% {
transform: rotate(89.8deg);
}
100% {
transform: rotate(90deg);
}
}
@keyframes progress-circle-fill-mask-2 {
0% {
transform: rotate(180deg);
}
1.69% {
transform: rotate(180deg);
}
3.39% {
transform: rotate(180deg);
}
5.08% {
transform: rotate(180deg);
}
6.78% {
transform: rotate(180deg);
}
8.47% {
transform: rotate(180deg);
}
10.17% {
transform: rotate(179.2deg);
}
11.86% {
transform: rotate(164deg);
}
13.56% {
transform: rotate(151.8deg);
}
15.25% {
transform: rotate(140.8deg);
}
16.95% {
transform: rotate(130.3deg);
}
18.64% {
transform: rotate(120.4deg);
}
20.34% {
transform: rotate(110.8deg);
}
22.03% {
transform: rotate(101.6deg);
}
23.73% {
transform: rotate(93.5deg);
}
25.42% {
transform: rotate(85.4deg);
}
27.12% {
transform: rotate(78.1deg);
}
28.81% {
transform: rotate(71.2deg);
}
30.51% {
transform: rotate(89.1deg);
}
32.2% {
transform: rotate(105.5deg);
}
33.9% {
transform: rotate(121.3deg);
}
35.59% {
transform: rotate(135.5deg);
}
37.29% {
transform: rotate(148.4deg);
}
38.98% {
transform: rotate(161deg);
}
40.68% {
transform: rotate(173.5deg);
}
42.37% {
transform: rotate(180deg);
}
44.07% {
transform: rotate(180deg);
}
45.76% {
transform: rotate(180deg);
}
47.46% {
transform: rotate(180deg);
}
49.15% {
transform: rotate(180deg);
}
50.85% {
transform: rotate(180deg);
}
52.54% {
transform: rotate(180deg);
}
54.24% {
transform: rotate(180deg);
}
55.93% {
transform: rotate(180deg);
}
57.63% {
transform: rotate(180deg);
}
59.32% {
transform: rotate(180deg);
}
61.02% {
transform: rotate(180deg);
}
62.71% {
transform: rotate(180deg);
}
64.41% {
transform: rotate(180deg);
}
66.1% {
transform: rotate(180deg);
}
67.8% {
transform: rotate(180deg);
}
69.49% {
transform: rotate(180deg);
}
71.19% {
transform: rotate(180deg);
}
72.88% {
transform: rotate(180deg);
}
74.58% {
transform: rotate(180deg);
}
76.27% {
transform: rotate(180deg);
}
77.97% {
transform: rotate(180deg);
}
79.66% {
transform: rotate(180deg);
}
81.36% {
transform: rotate(180deg);
}
83.05% {
transform: rotate(180deg);
}
84.75% {
transform: rotate(180deg);
}
86.44% {
transform: rotate(180deg);
}
88.14% {
transform: rotate(180deg);
}
89.83% {
transform: rotate(180deg);
}
91.53% {
transform: rotate(180deg);
}
93.22% {
transform: rotate(180deg);
}
94.92% {
transform: rotate(180deg);
}
96.61% {
transform: rotate(180deg);
}
98.31% {
transform: rotate(180deg);
}
100% {
transform: rotate(180deg);
}
}
@keyframes progress-circle-fills-rotate {
0% {transform: rotate(-90deg)}
100% {transform: rotate(270deg)}
}
.progress-bar-indeterminate-animate {
animation: progress-bar-indeterminate 1s infinite;
}
@keyframes progress-bar-indeterminate {
from {
transform: translate(calc(100% * -1));
}
to {
transform: translate(100%);
}
}

View File

@@ -0,0 +1,21 @@
import {ProgressCircle} from './progress-circle';
import React from 'react';
import clsx from 'clsx';
interface FullPageLoaderProps {
className?: string;
screen?: boolean;
}
export function FullPageLoader({className, screen}: FullPageLoaderProps) {
return (
<div
className={clsx(
'flex items-center justify-center flex-auto',
screen ? 'h-screen w-screen' : 'h-full w-full',
className
)}
>
<ProgressCircle isIndeterminate aria-label="Loading page..." />
</div>
);
}

View File

@@ -0,0 +1,6 @@
import React from 'react';
import {ProgressBarBase, ProgressBarBaseProps} from './progress-bar-base';
export function Meter(props: ProgressBarBaseProps) {
return <ProgressBarBase {...props} role="meter progressbar" />;
}

View File

@@ -0,0 +1,110 @@
import React, {CSSProperties, ReactNode, useId} from 'react';
import clsx from 'clsx';
import {InputSize} from '../forms/input-field/input-size';
import {getInputFieldClassNames} from '../forms/input-field/get-input-field-class-names';
import {useNumberFormatter} from '../../i18n/use-number-formatter';
import {clamp} from '../../utils/number/clamp';
export interface ProgressBarBaseProps {
value?: number;
minValue?: number;
maxValue?: number;
className?: string;
showValueLabel?: boolean;
size?: 'xs' | 'sm' | 'md';
labelPosition?: 'top' | 'bottom';
isIndeterminate?: boolean;
label?: ReactNode;
formatOptions?: Intl.NumberFormatOptions;
role?: string;
radius?: string;
trackColor?: string;
trackHeight?: string;
progressColor?: string;
}
export function ProgressBarBase(props: ProgressBarBaseProps) {
let {
value = 0,
minValue = 0,
maxValue = 100,
size = 'md',
label,
showValueLabel = !!label,
isIndeterminate = false,
labelPosition = 'top',
className,
role,
formatOptions = {
style: 'percent',
},
radius = 'rounded',
trackColor = 'bg-primary-light',
progressColor = 'bg-primary',
trackHeight = getSize(size),
} = props;
const id = useId();
value = clamp(value, minValue, maxValue);
const percentage = (value - minValue) / (maxValue - minValue);
const formatter = useNumberFormatter(formatOptions);
let valueLabel = '';
if (!isIndeterminate && showValueLabel) {
const valueToFormat =
formatOptions.style === 'percent' ? percentage : value;
valueLabel = formatter.format(valueToFormat);
}
const barStyle: CSSProperties = {};
if (!isIndeterminate) {
barStyle.width = `${Math.round(percentage * 100)}%`;
}
const style = getInputFieldClassNames({size});
const labelEl = (label || valueLabel) && (
<div className={clsx('flex gap-10 justify-between my-4', style.label)}>
{label && <span id={id}>{label}</span>}
{valueLabel && <div>{valueLabel}</div>}
</div>
);
return (
<div
aria-valuenow={isIndeterminate ? undefined : value}
aria-valuemin={minValue}
aria-valuemax={maxValue}
aria-valuetext={isIndeterminate ? undefined : (valueLabel as string)}
aria-labelledby={label ? id : undefined}
role={role || 'progressbar'}
className={clsx(className, 'min-w-42')}
>
{labelPosition === 'top' && labelEl}
<div className={`${trackHeight} ${radius} ${trackColor} overflow-hidden`}>
<div
className={clsx(
progressColor,
'fill h-full transition-width duration-200 rounded-l',
isIndeterminate && 'progress-bar-indeterminate-animate'
)}
style={barStyle}
/>
</div>
{labelPosition === 'bottom' && labelEl}
</div>
);
}
function getSize(size: InputSize) {
switch (size) {
case 'sm':
return 'h-6';
case 'xs':
return 'h-4';
default:
return 'h-8';
}
}

View File

@@ -0,0 +1,10 @@
import React from 'react';
import {ProgressBarBase, ProgressBarBaseProps} from './progress-bar-base';
interface Props extends ProgressBarBaseProps {
isIndeterminate?: boolean;
}
export function ProgressBar(props: Props) {
return <ProgressBarBase {...props} />;
}

View File

@@ -0,0 +1,159 @@
import React, {ComponentPropsWithoutRef, CSSProperties} from 'react';
import clsx from 'clsx';
import {clamp} from '../../utils/number/clamp';
import {useNumberFormatter} from '../../i18n/use-number-formatter';
export interface ProgressCircleProps extends ComponentPropsWithoutRef<'div'> {
value?: number;
minValue?: number;
maxValue?: number;
size?: 'xs' | 'sm' | 'md' | 'lg' | string;
isIndeterminate?: boolean;
className?: string;
position?: string;
trackColor?: string;
fillColor?: string;
}
export const ProgressCircle = React.forwardRef<
HTMLDivElement,
ProgressCircleProps
>((props, ref) => {
let {
value = 0,
minValue = 0,
maxValue = 100,
size = 'md',
isIndeterminate = false,
className,
position = 'relative',
trackColor,
fillColor = 'border-primary',
...domProps
} = props;
value = clamp(value, minValue, maxValue);
const circleSize = getCircleStyle(size);
const percentage = (value - minValue) / (maxValue - minValue);
const formatter = useNumberFormatter({style: 'percent'});
let valueLabel = '';
if (!isIndeterminate && !valueLabel) {
valueLabel = formatter.format(percentage);
}
const subMask1Style: CSSProperties = {};
const subMask2Style: CSSProperties = {};
if (!isIndeterminate) {
const percentage = ((value - minValue) / (maxValue - minValue)) * 100;
let angle;
if (percentage > 0 && percentage <= 50) {
angle = -180 + (percentage / 50) * 180;
subMask1Style.transform = `rotate(${angle}deg)`;
subMask2Style.transform = 'rotate(-180deg)';
} else if (percentage > 50) {
angle = -180 + ((percentage - 50) / 50) * 180;
subMask1Style.transform = 'rotate(0deg)';
subMask2Style.transform = `rotate(${angle}deg)`;
}
}
return (
<div
{...domProps}
aria-valuenow={isIndeterminate ? undefined : value}
aria-valuemin={minValue}
aria-valuemax={maxValue}
aria-valuetext={isIndeterminate ? undefined : valueLabel}
role="progressbar"
ref={ref}
className={clsx(
'progress-circle',
position,
circleSize,
isIndeterminate && 'indeterminate',
className
)}
>
<div className={clsx(circleSize, trackColor, 'rounded-full border-4')} />
<div
className={clsx(
'fills absolute left-0 top-0 h-full w-full',
isIndeterminate && 'progress-circle-fills-animate'
)}
>
<FillMask
circleSize={circleSize}
subMaskStyle={subMask1Style}
isIndeterminate={isIndeterminate}
className="rotate-180"
fillColor={fillColor}
subMaskClassName={clsx(
isIndeterminate && 'progress-circle-fill-submask-1-animate'
)}
/>
<FillMask
circleSize={circleSize}
subMaskStyle={subMask2Style}
isIndeterminate={isIndeterminate}
fillColor={fillColor}
subMaskClassName={clsx(
isIndeterminate && 'progress-circle-fill-submask-2-animate'
)}
/>
</div>
</div>
);
});
interface FillMaskProps {
className?: string;
circleSize?: string;
subMaskStyle: CSSProperties;
subMaskClassName: string;
isIndeterminate?: boolean;
fillColor?: string;
}
function FillMask({
subMaskStyle,
subMaskClassName,
className,
circleSize,
isIndeterminate,
fillColor,
}: FillMaskProps) {
return (
<div
className={clsx(
'absolute h-full w-1/2 origin-[100%] overflow-hidden',
className
)}
>
<div
className={clsx(
'h-full w-full origin-[100%] rotate-180 overflow-hidden',
!isIndeterminate && 'transition-transform duration-100',
subMaskClassName
)}
style={subMaskStyle}
>
<div className={clsx(circleSize, fillColor, 'rounded-full border-4')} />
</div>
</div>
);
}
function getCircleStyle(size: ProgressCircleProps['size']) {
switch (size) {
case 'xs':
return 'w-20 h-20';
case 'sm':
return 'w-24 h-24';
case 'md':
return 'w-32 h-32';
case 'lg':
return 'w-42 h-42';
default:
return size;
}
}