diff --git a/package.json b/package.json index ee3e057e7..c9cb776cc 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,8 @@ "@bfchain/util": "^5.0.0", "@bfmeta/sign-util": "^1.3.10", "@biochain/bio-sdk": "workspace:*", + "@biochain/key-ui": "workspace:*", + "@biochain/key-utils": "workspace:*", "@biochain/plugin-navigation-sync": "workspace:*", "@bnqkl/server-util": "^1.3.4", "@bnqkl/wallet-sdk": "^0.23.8", diff --git a/packages/key-ui/src/alert/Alert.stories.tsx b/packages/key-ui/src/alert/Alert.stories.tsx new file mode 100644 index 000000000..7df77057d --- /dev/null +++ b/packages/key-ui/src/alert/Alert.stories.tsx @@ -0,0 +1,75 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { Alert } from './Alert' + +const meta: Meta = { + title: 'Components/Alert', + component: Alert, + parameters: { + layout: 'padded', + }, + tags: ['autodocs'], + argTypes: { + variant: { + control: 'select', + options: ['info', 'success', 'warning', 'error'], + }, + }, +} + +export default meta +type Story = StoryObj + +const InfoIcon = () => ( + + + + +) + +export const Info: Story = { + args: { + variant: 'info', + children: 'This is an informational message.', + icon: , + }, +} + +export const Success: Story = { + args: { + variant: 'success', + children: 'Your transaction was successful!', + icon: , + }, +} + +export const Warning: Story = { + args: { + variant: 'warning', + children: 'Please review before proceeding.', + icon: , + }, +} + +export const Error: Story = { + args: { + variant: 'error', + children: 'Something went wrong. Please try again.', + icon: , + }, +} + +export const WithTitle: Story = { + args: { + variant: 'warning', + title: 'Important Notice', + children: 'Your session will expire in 5 minutes.', + icon: , + }, +} + +export const NoIcon: Story = { + args: { + variant: 'info', + children: 'Alert without an icon.', + }, +} diff --git a/packages/key-ui/src/alert/Alert.tsx b/packages/key-ui/src/alert/Alert.tsx new file mode 100644 index 000000000..e9ddd61f0 --- /dev/null +++ b/packages/key-ui/src/alert/Alert.tsx @@ -0,0 +1,37 @@ +'use client' +import * as React from 'react' +import { cn } from '@biochain/key-utils' + +export type AlertVariant = 'info' | 'success' | 'warning' | 'error' + +export interface AlertProps { + variant?: AlertVariant + title?: string + children: React.ReactNode + className?: string + icon?: React.ReactNode +} + +const variantStyles: Record = { + info: 'bg-info/10 text-info', + success: 'bg-success/10 text-success', + warning: 'bg-warning/10 text-warning', + error: 'bg-destructive/10 text-destructive', +} + +export function Alert({ variant = 'info', title, children, className, icon }: AlertProps) { + return ( +
+ {icon &&
{icon}
} +
+ {title &&

{title}

} +
{children}
+
+
+ ) +} + +export namespace Alert { + export type Props = AlertProps + export type Variant = AlertVariant +} diff --git a/packages/key-ui/src/alert/index.ts b/packages/key-ui/src/alert/index.ts new file mode 100644 index 000000000..2665bfe3f --- /dev/null +++ b/packages/key-ui/src/alert/index.ts @@ -0,0 +1,2 @@ +export { Alert } from './Alert' +export type { AlertProps, AlertVariant } from './Alert' diff --git a/packages/key-ui/src/empty-state/EmptyState.stories.tsx b/packages/key-ui/src/empty-state/EmptyState.stories.tsx new file mode 100644 index 000000000..2d7f258da --- /dev/null +++ b/packages/key-ui/src/empty-state/EmptyState.stories.tsx @@ -0,0 +1,54 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { EmptyState } from './EmptyState' + +const meta: Meta = { + title: 'Components/EmptyState', + component: EmptyState, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} + +export default meta +type Story = StoryObj + +const PlaceholderIcon = () => ( + + + +) + +export const Default: Story = { + args: { + title: 'No items found', + description: 'Get started by creating your first item.', + }, +} + +export const WithIcon: Story = { + args: { + icon: , + title: 'No transactions', + description: 'Your transaction history will appear here.', + }, +} + +export const WithAction: Story = { + args: { + icon: , + title: 'No wallet connected', + description: 'Connect your wallet to get started.', + action: ( + + ), + }, +} + +export const Minimal: Story = { + args: { + title: 'Nothing here yet', + }, +} diff --git a/packages/key-ui/src/empty-state/EmptyState.tsx b/packages/key-ui/src/empty-state/EmptyState.tsx new file mode 100644 index 000000000..14c4641bd --- /dev/null +++ b/packages/key-ui/src/empty-state/EmptyState.tsx @@ -0,0 +1,30 @@ +'use client' +import * as React from 'react' +import { cn } from '@biochain/key-utils' + +export interface EmptyStateProps { + icon?: React.ReactNode + title: string + description?: string + action?: React.ReactNode + className?: string + testId?: string +} + +export function EmptyState({ icon, title, description, action, className, testId }: EmptyStateProps) { + return ( +
+ {icon &&
{icon}
} +

{title}

+ {description &&

{description}

} + {action &&
{action}
} +
+ ) +} + +export namespace EmptyState { + export type Props = EmptyStateProps +} diff --git a/packages/key-ui/src/empty-state/index.ts b/packages/key-ui/src/empty-state/index.ts new file mode 100644 index 000000000..9d843f54e --- /dev/null +++ b/packages/key-ui/src/empty-state/index.ts @@ -0,0 +1,2 @@ +export { EmptyState } from './EmptyState' +export type { EmptyStateProps } from './EmptyState' diff --git a/packages/key-ui/src/gradient-button/GradientButton.stories.tsx b/packages/key-ui/src/gradient-button/GradientButton.stories.tsx new file mode 100644 index 000000000..2cda3140b --- /dev/null +++ b/packages/key-ui/src/gradient-button/GradientButton.stories.tsx @@ -0,0 +1,104 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { GradientButton } from './GradientButton' + +const meta: Meta = { + title: 'Components/GradientButton', + component: GradientButton, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + variant: { + control: 'select', + options: ['blue', 'purple', 'red', 'mint'], + }, + size: { + control: 'select', + options: ['sm', 'md', 'lg'], + }, + }, +} + +export default meta +type Story = StoryObj + +export const Purple: Story = { + args: { + variant: 'purple', + children: 'Continue', + }, +} + +export const Blue: Story = { + args: { + variant: 'blue', + children: 'Connect Wallet', + }, +} + +export const Red: Story = { + args: { + variant: 'red', + children: 'Disconnect', + }, +} + +export const Mint: Story = { + args: { + variant: 'mint', + children: 'Confirm', + }, +} + +export const Loading: Story = { + args: { + variant: 'purple', + children: 'Processing...', + loading: true, + }, +} + +export const Disabled: Story = { + args: { + variant: 'purple', + children: 'Disabled', + disabled: true, + }, +} + +export const FullWidth: Story = { + args: { + variant: 'purple', + children: 'Full Width Button', + fullWidth: true, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export const AllSizes: Story = { + render: () => ( +
+ Small + Medium + Large +
+ ), +} + +export const AllVariants: Story = { + render: () => ( +
+ Purple + Blue + Red + Mint +
+ ), +} diff --git a/packages/key-ui/src/gradient-button/GradientButton.tsx b/packages/key-ui/src/gradient-button/GradientButton.tsx new file mode 100644 index 000000000..33664e1c3 --- /dev/null +++ b/packages/key-ui/src/gradient-button/GradientButton.tsx @@ -0,0 +1,81 @@ +'use client' +import * as React from 'react' +import { cn } from '@biochain/key-utils' + +export type GradientButtonVariant = 'blue' | 'purple' | 'red' | 'mint' +export type GradientButtonSize = 'sm' | 'md' | 'lg' + +export interface GradientButtonProps extends React.ButtonHTMLAttributes { + variant?: GradientButtonVariant + size?: GradientButtonSize + fullWidth?: boolean + loading?: boolean + loadingIcon?: React.ReactNode + children: React.ReactNode +} + +const variantStyles: Record = { + blue: 'bg-gradient-blue hover:opacity-90', + purple: 'bg-gradient-purple hover:opacity-90', + red: 'bg-gradient-red hover:opacity-90', + mint: 'bg-gradient-mint hover:opacity-90', +} + +const sizeStyles: Record = { + sm: 'h-9 px-4 text-sm has-[svg]:ps-3 @xs:h-10 @xs:px-5 @xs:has-[svg]:ps-4', + md: 'h-11 px-6 text-base has-[svg]:ps-4 @xs:h-12 @xs:px-8 @xs:has-[svg]:ps-5', + lg: 'h-12 px-8 text-lg has-[svg]:ps-5 @xs:h-14 @xs:px-10 @xs:has-[svg]:ps-6', +} + +const DefaultLoadingIcon = () => ( + + + +) + +export const GradientButton = React.forwardRef( + ( + { + className, + variant = 'purple', + size = 'md', + fullWidth = false, + loading = false, + loadingIcon, + children, + disabled, + ...props + }, + ref, + ) => { + const LoadingIconComponent = loadingIcon ?? + + return ( + + ) + }, +) + +GradientButton.displayName = 'GradientButton' + +export namespace GradientButton { + export type Props = GradientButtonProps + export type Variant = GradientButtonVariant + export type Size = GradientButtonSize +} diff --git a/packages/key-ui/src/gradient-button/index.ts b/packages/key-ui/src/gradient-button/index.ts new file mode 100644 index 000000000..d2eae8969 --- /dev/null +++ b/packages/key-ui/src/gradient-button/index.ts @@ -0,0 +1,2 @@ +export { GradientButton } from './GradientButton' +export type { GradientButtonProps, GradientButtonVariant, GradientButtonSize } from './GradientButton' diff --git a/packages/key-ui/src/icon-circle/IconCircle.stories.tsx b/packages/key-ui/src/icon-circle/IconCircle.stories.tsx new file mode 100644 index 000000000..01241944a --- /dev/null +++ b/packages/key-ui/src/icon-circle/IconCircle.stories.tsx @@ -0,0 +1,84 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { IconCircle } from './IconCircle' + +const meta: Meta = { + title: 'Components/IconCircle', + component: IconCircle, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + variant: { + control: 'select', + options: ['primary', 'secondary', 'success', 'warning', 'error'], + }, + size: { + control: 'select', + options: ['sm', 'md', 'lg'], + }, + }, +} + +export default meta +type Story = StoryObj + +const ShieldIcon = () => ( + + + +) + +export const Primary: Story = { + args: { + icon: , + variant: 'primary', + size: 'lg', + }, +} + +export const Success: Story = { + args: { + icon: , + variant: 'success', + size: 'lg', + }, +} + +export const Warning: Story = { + args: { + icon: , + variant: 'warning', + size: 'lg', + }, +} + +export const Error: Story = { + args: { + icon: , + variant: 'error', + size: 'lg', + }, +} + +export const AllSizes: Story = { + render: () => ( +
+ } size="sm" /> + } size="md" /> + } size="lg" /> +
+ ), +} + +export const AllVariants: Story = { + render: () => ( +
+ } variant="primary" /> + } variant="secondary" /> + } variant="success" /> + } variant="warning" /> + } variant="error" /> +
+ ), +} diff --git a/packages/key-ui/src/icon-circle/IconCircle.tsx b/packages/key-ui/src/icon-circle/IconCircle.tsx new file mode 100644 index 000000000..e87f1c277 --- /dev/null +++ b/packages/key-ui/src/icon-circle/IconCircle.tsx @@ -0,0 +1,48 @@ +'use client' +import * as React from 'react' +import { cn } from '@biochain/key-utils' + +export type IconCircleVariant = 'primary' | 'secondary' | 'success' | 'warning' | 'error' +export type IconCircleSize = 'sm' | 'md' | 'lg' + +export interface IconCircleProps { + icon: React.ReactNode + variant?: IconCircleVariant + size?: IconCircleSize + className?: string +} + +const variantStyles: Record = { + primary: 'bg-primary/10 text-primary', + secondary: 'bg-secondary text-secondary-foreground', + success: 'bg-green-500/10 text-green-500', + warning: 'bg-yellow-500/10 text-yellow-500', + error: 'bg-destructive/10 text-destructive', +} + +const sizeStyles: Record = { + sm: { container: 'size-10', icon: 'size-5' }, + md: { container: 'size-14', icon: 'size-7' }, + lg: { container: 'size-16', icon: 'size-8' }, +} + +export function IconCircle({ icon, variant = 'primary', size = 'lg', className }: IconCircleProps) { + return ( +
+
{icon}
+
+ ) +} + +export namespace IconCircle { + export type Props = IconCircleProps + export type Variant = IconCircleVariant + export type Size = IconCircleSize +} diff --git a/packages/key-ui/src/icon-circle/index.ts b/packages/key-ui/src/icon-circle/index.ts new file mode 100644 index 000000000..328412c83 --- /dev/null +++ b/packages/key-ui/src/icon-circle/index.ts @@ -0,0 +1,2 @@ +export { IconCircle } from './IconCircle' +export type { IconCircleProps, IconCircleVariant, IconCircleSize } from './IconCircle' diff --git a/packages/key-ui/src/index.ts b/packages/key-ui/src/index.ts index f6a652907..217c94b2a 100644 --- a/packages/key-ui/src/index.ts +++ b/packages/key-ui/src/index.ts @@ -1,3 +1,11 @@ export * from './address-display' export * from './amount-display' export * from './token-icon' +export * from './skeleton' +export * from './empty-state' +export * from './alert' +export * from './icon-circle' +export * from './step-indicator' +export * from './gradient-button' +export * from './loading-spinner' +export * from './qr-code' diff --git a/packages/key-ui/src/loading-spinner/LoadingSpinner.stories.tsx b/packages/key-ui/src/loading-spinner/LoadingSpinner.stories.tsx new file mode 100644 index 000000000..751b48a1f --- /dev/null +++ b/packages/key-ui/src/loading-spinner/LoadingSpinner.stories.tsx @@ -0,0 +1,58 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { LoadingSpinner } from './LoadingSpinner' + +const meta: Meta = { + title: 'Components/LoadingSpinner', + component: LoadingSpinner, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + size: { + control: 'select', + options: ['sm', 'md', 'lg'], + }, + }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + size: 'md', + }, +} + +export const Small: Story = { + args: { + size: 'sm', + }, +} + +export const Large: Story = { + args: { + size: 'lg', + }, +} + +export const AllSizes: Story = { + render: () => ( +
+ + + +
+ ), +} + +export const CustomColor: Story = { + render: () => ( +
+ + + +
+ ), +} diff --git a/packages/key-ui/src/loading-spinner/LoadingSpinner.tsx b/packages/key-ui/src/loading-spinner/LoadingSpinner.tsx new file mode 100644 index 000000000..3e5e0dfe4 --- /dev/null +++ b/packages/key-ui/src/loading-spinner/LoadingSpinner.tsx @@ -0,0 +1,59 @@ +'use client' +import * as React from 'react' +import { cn } from '@biochain/key-utils' + +export type LoadingSpinnerSize = 'sm' | 'md' | 'lg' + +export interface LoadingSpinnerProps { + className?: string + size?: LoadingSpinnerSize + fullScreen?: boolean + label?: string +} + +const sizeClasses: Record = { + sm: 'size-4', + md: 'size-8', + lg: 'size-12', +} + +export function LoadingSpinner({ className, size = 'md', fullScreen = false, label = 'Loading...' }: LoadingSpinnerProps) { + const spinner = ( + + + + + ) + + if (fullScreen) { + return ( +
+ {spinner} +
+ ) + } + + return spinner +} + +export namespace LoadingSpinner { + export type Props = LoadingSpinnerProps + export type Size = LoadingSpinnerSize +} diff --git a/packages/key-ui/src/loading-spinner/index.ts b/packages/key-ui/src/loading-spinner/index.ts new file mode 100644 index 000000000..25550c31d --- /dev/null +++ b/packages/key-ui/src/loading-spinner/index.ts @@ -0,0 +1,2 @@ +export { LoadingSpinner } from './LoadingSpinner' +export type { LoadingSpinnerProps, LoadingSpinnerSize } from './LoadingSpinner' diff --git a/packages/key-ui/src/qr-code/QRCode.stories.tsx b/packages/key-ui/src/qr-code/QRCode.stories.tsx new file mode 100644 index 000000000..ff3d40385 --- /dev/null +++ b/packages/key-ui/src/qr-code/QRCode.stories.tsx @@ -0,0 +1,64 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { QRCode, AddressQRCode } from './QRCode' + +const meta: Meta = { + title: 'Components/QRCode', + component: QRCode, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + value: 'https://example.com', + size: 200, + }, +} + +export const Small: Story = { + args: { + value: 'https://example.com', + size: 120, + }, +} + +export const Large: Story = { + args: { + value: 'https://example.com', + size: 300, + }, +} + +export const WithLogo: Story = { + args: { + value: 'https://example.com', + size: 200, + logoUrl: 'https://cryptologos.cc/logos/ethereum-eth-logo.png', + logoSize: 40, + }, +} + +export const EthereumAddress: StoryObj = { + render: () => ( + + ), +} + +export const BitcoinAddress: StoryObj = { + render: () => ( + + ), +} diff --git a/packages/key-ui/src/qr-code/QRCode.tsx b/packages/key-ui/src/qr-code/QRCode.tsx new file mode 100644 index 000000000..ec469dd03 --- /dev/null +++ b/packages/key-ui/src/qr-code/QRCode.tsx @@ -0,0 +1,109 @@ +'use client' +import * as React from 'react' +import { cn } from '@biochain/key-utils' + +export interface QRCodeProps { + value: string + size?: number + bgColor?: string + fgColor?: string + level?: 'L' | 'M' | 'Q' | 'H' + includeMargin?: boolean + logoUrl?: string + logoSize?: number + className?: string + renderFn?: (props: { value: string; size: number; level: string }) => React.ReactNode +} + +export function QRCode({ + value, + size = 200, + level = 'M', + logoUrl, + logoSize = 40, + className, + renderFn, +}: QRCodeProps) { + return ( +
+ {renderFn ? ( + renderFn({ value, size, level }) + ) : ( +
+ QR: {value.slice(0, 20)}... +
+ )} + {logoUrl && ( +
+ Logo +
+ )} +
+ ) +} + +export interface AddressQRCodeProps { + address: string + chain?: string + size?: number + className?: string + renderFn?: QRCodeProps['renderFn'] +} + +export function AddressQRCode({ + address, + chain, + size = 200, + className, + renderFn, +}: AddressQRCodeProps) { + const getQRValue = () => { + switch (chain) { + case 'ethereum': + return `ethereum:${address}` + case 'bitcoin': + return `bitcoin:${address}` + case 'tron': + return address + default: + return address + } + } + + return ( + + ) +} + +export namespace QRCode { + export type Props = QRCodeProps +} + +export namespace AddressQRCode { + export type Props = AddressQRCodeProps +} diff --git a/packages/key-ui/src/qr-code/index.ts b/packages/key-ui/src/qr-code/index.ts new file mode 100644 index 000000000..4f75340cd --- /dev/null +++ b/packages/key-ui/src/qr-code/index.ts @@ -0,0 +1,2 @@ +export { QRCode, AddressQRCode } from './QRCode' +export type { QRCodeProps, AddressQRCodeProps } from './QRCode' diff --git a/packages/key-ui/src/skeleton/Skeleton.stories.tsx b/packages/key-ui/src/skeleton/Skeleton.stories.tsx new file mode 100644 index 000000000..d0e72080a --- /dev/null +++ b/packages/key-ui/src/skeleton/Skeleton.stories.tsx @@ -0,0 +1,45 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { Skeleton, SkeletonText, SkeletonCard, SkeletonList } from './Skeleton' + +const meta: Meta = { + title: 'Components/Skeleton', + component: Skeleton, + parameters: { + layout: 'padded', + }, + tags: ['autodocs'], +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => , +} + +export const Circle: Story = { + render: () => , +} + +export const Text: Story = { + render: () => , +} + +export const Card: Story = { + render: () => , +} + +export const List: Story = { + render: () => , +} + +export const CustomSizes: Story = { + render: () => ( +
+ + + + +
+ ), +} diff --git a/packages/key-ui/src/skeleton/Skeleton.tsx b/packages/key-ui/src/skeleton/Skeleton.tsx new file mode 100644 index 000000000..dd563da9b --- /dev/null +++ b/packages/key-ui/src/skeleton/Skeleton.tsx @@ -0,0 +1,66 @@ +'use client' +import * as React from 'react' +import { cn } from '@biochain/key-utils' + +export interface SkeletonProps { + className?: string +} + +export function Skeleton({ className }: SkeletonProps) { + return
+} + +export interface SkeletonTextProps { + lines?: number + className?: string +} + +export function SkeletonText({ lines = 3, className }: SkeletonTextProps) { + return ( +
+ {Array.from({ length: lines }).map((_, i) => ( + + ))} +
+ ) +} + +export function SkeletonCard({ className }: SkeletonProps) { + return ( +
+
+ +
+ + +
+
+ +
+ ) +} + +export function SkeletonList({ count = 3, className }: { count?: number; className?: string }) { + return ( +
+ {Array.from({ length: count }).map((_, i) => ( +
+ +
+ + +
+ +
+ ))} +
+ ) +} + +export namespace Skeleton { + export type Props = SkeletonProps +} + +export namespace SkeletonText { + export type Props = SkeletonTextProps +} diff --git a/packages/key-ui/src/skeleton/index.ts b/packages/key-ui/src/skeleton/index.ts new file mode 100644 index 000000000..7de247b64 --- /dev/null +++ b/packages/key-ui/src/skeleton/index.ts @@ -0,0 +1,2 @@ +export { Skeleton, SkeletonText, SkeletonCard, SkeletonList } from './Skeleton' +export type { SkeletonProps, SkeletonTextProps } from './Skeleton' diff --git a/packages/key-ui/src/step-indicator/StepIndicator.stories.tsx b/packages/key-ui/src/step-indicator/StepIndicator.stories.tsx new file mode 100644 index 000000000..61a4c4e70 --- /dev/null +++ b/packages/key-ui/src/step-indicator/StepIndicator.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { StepIndicator, ProgressSteps } from './StepIndicator' + +const meta: Meta = { + title: 'Components/StepIndicator', + component: StepIndicator, + parameters: { + layout: 'padded', + }, + tags: ['autodocs'], +} + +export default meta +type Story = StoryObj + +const steps = ['Connect', 'Confirm', 'Complete'] + +export const Step1: Story = { + args: { + steps, + currentStep: 0, + }, +} + +export const Step2: Story = { + args: { + steps, + currentStep: 1, + }, +} + +export const Step3: Story = { + args: { + steps, + currentStep: 2, + }, +} + +export const Completed: Story = { + args: { + steps, + currentStep: 3, + }, +} + +export const ManySteps: Story = { + args: { + steps: ['Step 1', 'Step 2', 'Step 3', 'Step 4', 'Step 5'], + currentStep: 2, + }, +} + +export const ProgressBars: StoryObj = { + render: () => ( +
+ + + + +
+ ), +} diff --git a/packages/key-ui/src/step-indicator/StepIndicator.tsx b/packages/key-ui/src/step-indicator/StepIndicator.tsx new file mode 100644 index 000000000..2421f41ee --- /dev/null +++ b/packages/key-ui/src/step-indicator/StepIndicator.tsx @@ -0,0 +1,91 @@ +'use client' +import * as React from 'react' +import { cn } from '@biochain/key-utils' + +export interface StepIndicatorProps { + steps: string[] + currentStep: number + className?: string +} + +export function StepIndicator({ steps, currentStep, className }: StepIndicatorProps) { + return ( +
+ {steps.map((step, index) => { + const isCompleted = index < currentStep + const isCurrent = index === currentStep + const isLast = index === steps.length - 1 + + return ( + +
+
+ {isCompleted ? ( + + + + ) : ( + index + 1 + )} +
+ + {step} + +
+ {!isLast && ( +
+ )} + + ) + })} +
+ ) +} + +export interface ProgressStepsProps { + total: number + current: number + className?: string +} + +export function ProgressSteps({ total, current, className }: ProgressStepsProps) { + return ( +
+ {Array.from({ length: total }).map((_, i) => ( +
+ ))} +
+ ) +} + +export namespace StepIndicator { + export type Props = StepIndicatorProps +} + +export namespace ProgressSteps { + export type Props = ProgressStepsProps +} diff --git a/packages/key-ui/src/step-indicator/index.ts b/packages/key-ui/src/step-indicator/index.ts new file mode 100644 index 000000000..1bd2d14ba --- /dev/null +++ b/packages/key-ui/src/step-indicator/index.ts @@ -0,0 +1,2 @@ +export { StepIndicator, ProgressSteps } from './StepIndicator' +export type { StepIndicatorProps, ProgressStepsProps } from './StepIndicator' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82899a25d..97dc6f308 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,12 @@ importers: '@biochain/bio-sdk': specifier: workspace:* version: link:packages/bio-sdk + '@biochain/key-ui': + specifier: workspace:* + version: link:packages/key-ui + '@biochain/key-utils': + specifier: workspace:* + version: link:packages/key-utils '@biochain/plugin-navigation-sync': specifier: workspace:* version: link:packages/plugin-navigation-sync diff --git a/src/components/common/index.ts b/src/components/common/index.ts index ada351771..3661a6d33 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -1,14 +1,47 @@ -export { GradientButton, gradientButtonVariants, type GradientButtonProps } from './gradient-button' -export { LoadingSpinner } from './loading-spinner' -export { EmptyState } from './empty-state' -export { Skeleton, SkeletonText, SkeletonCard, SkeletonList } from './skeleton' +// Re-export from @biochain/key-ui +export { + Skeleton, + SkeletonText, + SkeletonCard, + SkeletonList, + EmptyState, + Alert, + IconCircle, + StepIndicator, + ProgressSteps, + GradientButton, + LoadingSpinner, + QRCode, + AddressQRCode, +} from '@biochain/key-ui' + +export type { + SkeletonProps, + SkeletonTextProps, + EmptyStateProps, + AlertProps, + AlertVariant, + IconCircleProps, + IconCircleVariant, + IconCircleSize, + StepIndicatorProps, + ProgressStepsProps, + GradientButtonProps, + GradientButtonVariant, + GradientButtonSize, + LoadingSpinnerProps, + LoadingSpinnerSize, + QRCodeProps, + AddressQRCodeProps, +} from '@biochain/key-ui' + +// Keep local components that are not yet migrated export { AmountDisplay, AmountWithFiat, formatAmount } from './amount-display' export { AnimatedNumber, AnimatedAmount } from './animated-number' export { TimeDisplay, formatDate, formatDateTime, formatTime, toDate } from './time-display' -export { QRCode, AddressQRCode } from './qr-code' -export { IconCircle } from './icon-circle' export { FormField } from './form-field' -export { Alert } from './alert' -export { StepIndicator, ProgressSteps } from './step-indicator' export { ErrorBoundary } from './error-boundary' export { CopyableText } from './copyable-text' + +// Legacy export for gradientButtonVariants (cva) +export { gradientButtonVariants } from './gradient-button'