diff --git a/src/LoginCard.tsx b/src/LoginCard.tsx index d7d65a3..23fde74 100644 --- a/src/LoginCard.tsx +++ b/src/LoginCard.tsx @@ -1,79 +1,65 @@ -function LoginCard() { - - const handleGoogleLogin = () => { - // Replace with your actual backend OAuth endpoint - window.location.href = 'http://localhost:8080/oauth2/authorization/google'; - }; - - return ( -
-
+import { ProgressBar } from './shared/primitives/Progressbar' - {/* Heading */} -
-

- Welcome -

+function LoginCard() { + const handleGoogleLogin = () => { + // Replace with your actual backend OAuth endpoint + window.location.href = 'http://localhost:8080/oauth2/authorization/google' + } -

- Sign in using your Google account -

-
+ return ( +
+
+ {/* Heading */} +
+

Welcome

- {/* Google OAuth Button */} -
- Continue with Google - + {/* Google OAuth Button */} + - {/* Footer */} -

- By continuing, you agree to our Terms of Service and Privacy Policy -

-
-
- ); + {/* Footer */} +

+ By continuing, you agree to our{' '} + + Terms of Service + {' '} + and{' '} + + Privacy Policy + +

+ +
+
+ ) } -export default LoginCard; \ No newline at end of file +export default LoginCard diff --git a/src/index.css b/src/index.css index 4854b10..7573056 100644 --- a/src/index.css +++ b/src/index.css @@ -116,3 +116,13 @@ html[data-theme-loaded='true'] * { border-color 0.25s ease, box-shadow 0.25s ease; } + +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + + 100% { + background-position: -200% 0; + } +} diff --git a/src/shared/primitives/Progressbar/ProgressBar.stories.tsx b/src/shared/primitives/Progressbar/ProgressBar.stories.tsx new file mode 100644 index 0000000..4e87743 --- /dev/null +++ b/src/shared/primitives/Progressbar/ProgressBar.stories.tsx @@ -0,0 +1,82 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' + +import { ProgressBar } from '.' + +const meta = { + title: 'Shared/Primitives/ProgressBar', + + component: ProgressBar, + + tags: ['autodocs'], +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const AutoDanger: Story = { + args: { + value: 0.9, + variant: 'auto', + showValue: true, + }, +} + +export const AutoWarning: Story = { + args: { + value: 0.7, + variant: 'auto', + showValue: true, + }, +} + +export const AutoSuccess: Story = { + args: { + value: 0.4, + variant: 'auto', + showValue: true, + }, +} + +export const Danger: Story = { + args: { + value: 0.8, + variant: 'danger', + }, +} + +export const Warning: Story = { + args: { + value: 0.8, + variant: 'warning', + }, +} + +export const Success: Story = { + args: { + value: 0.8, + variant: 'success', + }, +} + +export const Info: Story = { + args: { + value: 0.8, + variant: 'info', + }, +} + +export const Animated: Story = { + args: { + value: 0, + animated: true, + variant: 'info', + }, +} + +export const ExtraSmall: Story = { + args: { + value: 0.82, + size: 'xs', + }, +} diff --git a/src/shared/primitives/Progressbar/ProgressBar.tsx b/src/shared/primitives/Progressbar/ProgressBar.tsx new file mode 100644 index 0000000..0d66fd5 --- /dev/null +++ b/src/shared/primitives/Progressbar/ProgressBar.tsx @@ -0,0 +1,179 @@ +import { cva, type VariantProps } from 'class-variance-authority' + +const progressTrack = cva( + ` + relative + overflow-hidden + rounded-full + bg-bg-secondary + border + border-border-secondary + `, + { + variants: { + size: { + xs: 'h-1', + sm: 'h-2', + md: 'h-3', + }, + }, + + defaultVariants: { + size: 'md', + }, + } +) + +const progressFill = cva( + ` + h-full + rounded-full + transition-[width] + duration-300 + ease-in-out + relative + overflow-hidden + `, + { + variants: { + variant: { + success: 'bg-text-success', + + warning: 'bg-text-warning', + + danger: 'bg-text-danger', + + info: 'bg-text-info', + }, + + animated: { + true: ` + before:absolute + before:inset-0 + before:bg-[length:200%_100%] + before:bg-gradient-to-r + before:from-transparent + before:via-white/40 + before:to-transparent + before:animate-[shimmer_1.6s_linear_infinite] + `, + + false: '', + }, + }, + } +) + +export interface ProgressBarProps extends VariantProps { + value: number + + variant?: 'auto' | 'danger' | 'warning' | 'success' | 'info' + + label?: string + + showValue?: boolean + + animated?: boolean + + className?: string + + scoreLabel?: string +} + +const resolveAutoVariant = (value: number): 'danger' | 'warning' | 'success' => { + if (value >= 0.85) { + return 'danger' + } + + if (value >= 0.6) { + return 'warning' + } + + return 'success' +} + +export function ProgressBar({ + value, + + variant = 'auto', + + size, + + label, + + scoreLabel, + + showValue = false, + + animated = false, + + className = '', +}: ProgressBarProps) { + const safeValue = Math.min(Math.max(value, 0), 1) + + const resolvedVariant = variant === 'auto' ? resolveAutoVariant(safeValue) : variant + + const percentage = safeValue * 100 + + if (scoreLabel) { + return ( +
+ {scoreLabel} + +
+
+
+
+
+ + + {safeValue.toFixed(2)} + +
+ ) + } + + return ( +
+ {(label || showValue) && ( +
+ {label} + + {showValue && {percentage.toFixed(0)}%} +
+ )} + +
+
+
+
+ ) +} + +export default ProgressBar diff --git a/src/shared/primitives/Progressbar/index.ts b/src/shared/primitives/Progressbar/index.ts new file mode 100644 index 0000000..28c6333 --- /dev/null +++ b/src/shared/primitives/Progressbar/index.ts @@ -0,0 +1,3 @@ +export { ProgressBar } from './ProgressBar' + +export type { ProgressBarProps } from './ProgressBar' diff --git a/src/shared/theme/ThemeProvider.tsx b/src/shared/theme/ThemeProvider.tsx index 5733606..b75baf3 100644 --- a/src/shared/theme/ThemeProvider.tsx +++ b/src/shared/theme/ThemeProvider.tsx @@ -1,58 +1,33 @@ -import { useEffect } from 'react'; -import { themeRegistry } from './themeRegistry'; -import { useThemeStore } from './themeStore'; - -const applyTokens = ( - tokens: object, - prefix = '' -) => { - const root = document.documentElement; - - Object.entries(tokens).forEach(([key, value]) => { - const newPrefix = prefix - ? `${prefix}-${key}` - : key; - - if ( - typeof value === 'object' && - value !== null - ) { - applyTokens(value, newPrefix); - } else if (typeof value === 'string') { - root.style.setProperty( - `--${newPrefix}`, - value - ); - } - }); -}; - -export const ThemeProvider = ({ - children, -}: { - children: React.ReactNode; -}) => { - const themeName = useThemeStore( - (s) => s.themeName - ); - - useEffect(() => { - const resolvedTheme = - themeRegistry[themeName] ?? - themeRegistry.light; - - document.documentElement.setAttribute( - 'data-theme', - resolvedTheme.name - ); - - applyTokens(resolvedTheme.tokens); - - document.documentElement.setAttribute( - 'data-theme-loaded', - 'true' - ); - }, [themeName]); - - return children; -}; \ No newline at end of file +import { useLayoutEffect } from 'react' +import { themeRegistry } from './themeRegistry' +import { useThemeStore } from './themeStore' + +const applyTokens = (tokens: object, prefix = '') => { + const root = document.documentElement + + Object.entries(tokens).forEach(([key, value]) => { + const newPrefix = prefix ? `${prefix}-${key}` : key + + if (typeof value === 'object' && value !== null) { + applyTokens(value, newPrefix) + } else if (typeof value === 'string') { + root.style.setProperty(`--${newPrefix}`, value) + } + }) +} + +export const ThemeProvider = ({ children }: { children: React.ReactNode }) => { + const themeName = useThemeStore((s) => s.themeName) + + useLayoutEffect(() => { + const resolvedTheme = themeRegistry[themeName] ?? themeRegistry.light + + document.documentElement.setAttribute('data-theme', resolvedTheme.name) + + applyTokens(resolvedTheme.tokens) + + document.documentElement.setAttribute('data-theme-loaded', 'true') + }, [themeName]) + + return children +}