Skip to content

Commit ffbea8e

Browse files
authored
feat(key-ui): add common components and integrate with main app (#215)
Add new components to @biochain/key-ui: - Skeleton, SkeletonText, SkeletonCard, SkeletonList - EmptyState - Alert (info/success/warning/error variants) - IconCircle (with size and variant options) - StepIndicator, ProgressSteps - GradientButton (blue/purple/red/mint variants) - LoadingSpinner - QRCode, AddressQRCode Update main app: - Add @biochain/key-ui and @biochain/key-utils as dependencies - Re-export migrated components from src/components/common/index.ts All 71 Storybook tests passing.
1 parent c2f2381 commit ffbea8e

28 files changed

Lines changed: 1140 additions & 8 deletions

‎package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@
7070
"@bfchain/util": "^5.0.0",
7171
"@bfmeta/sign-util": "^1.3.10",
7272
"@biochain/bio-sdk": "workspace:*",
73+
"@biochain/key-ui": "workspace:*",
74+
"@biochain/key-utils": "workspace:*",
7375
"@biochain/plugin-navigation-sync": "workspace:*",
7476
"@bnqkl/server-util": "^1.3.4",
7577
"@bnqkl/wallet-sdk": "^0.23.8",
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite'
2+
import { Alert } from './Alert'
3+
4+
const meta: Meta<typeof Alert> = {
5+
title: 'Components/Alert',
6+
component: Alert,
7+
parameters: {
8+
layout: 'padded',
9+
},
10+
tags: ['autodocs'],
11+
argTypes: {
12+
variant: {
13+
control: 'select',
14+
options: ['info', 'success', 'warning', 'error'],
15+
},
16+
},
17+
}
18+
19+
export default meta
20+
type Story = StoryObj<typeof meta>
21+
22+
const InfoIcon = () => (
23+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
24+
<circle cx="12" cy="12" r="10" />
25+
<path d="M12 16v-4M12 8h.01" />
26+
</svg>
27+
)
28+
29+
export const Info: Story = {
30+
args: {
31+
variant: 'info',
32+
children: 'This is an informational message.',
33+
icon: <InfoIcon />,
34+
},
35+
}
36+
37+
export const Success: Story = {
38+
args: {
39+
variant: 'success',
40+
children: 'Your transaction was successful!',
41+
icon: <InfoIcon />,
42+
},
43+
}
44+
45+
export const Warning: Story = {
46+
args: {
47+
variant: 'warning',
48+
children: 'Please review before proceeding.',
49+
icon: <InfoIcon />,
50+
},
51+
}
52+
53+
export const Error: Story = {
54+
args: {
55+
variant: 'error',
56+
children: 'Something went wrong. Please try again.',
57+
icon: <InfoIcon />,
58+
},
59+
}
60+
61+
export const WithTitle: Story = {
62+
args: {
63+
variant: 'warning',
64+
title: 'Important Notice',
65+
children: 'Your session will expire in 5 minutes.',
66+
icon: <InfoIcon />,
67+
},
68+
}
69+
70+
export const NoIcon: Story = {
71+
args: {
72+
variant: 'info',
73+
children: 'Alert without an icon.',
74+
},
75+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use client'
2+
import * as React from 'react'
3+
import { cn } from '@biochain/key-utils'
4+
5+
export type AlertVariant = 'info' | 'success' | 'warning' | 'error'
6+
7+
export interface AlertProps {
8+
variant?: AlertVariant
9+
title?: string
10+
children: React.ReactNode
11+
className?: string
12+
icon?: React.ReactNode
13+
}
14+
15+
const variantStyles: Record<AlertVariant, string> = {
16+
info: 'bg-info/10 text-info',
17+
success: 'bg-success/10 text-success',
18+
warning: 'bg-warning/10 text-warning',
19+
error: 'bg-destructive/10 text-destructive',
20+
}
21+
22+
export function Alert({ variant = 'info', title, children, className, icon }: AlertProps) {
23+
return (
24+
<div role="alert" className={cn('flex gap-3 rounded-lg p-3 text-sm', variantStyles[variant], className)}>
25+
{icon && <div className="mt-0.5 size-4 shrink-0">{icon}</div>}
26+
<div className="flex-1">
27+
{title && <p className="font-medium">{title}</p>}
28+
<div className={title ? 'mt-1' : ''}>{children}</div>
29+
</div>
30+
</div>
31+
)
32+
}
33+
34+
export namespace Alert {
35+
export type Props = AlertProps
36+
export type Variant = AlertVariant
37+
}

‎packages/key-ui/src/alert/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { Alert } from './Alert'
2+
export type { AlertProps, AlertVariant } from './Alert'
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite'
2+
import { EmptyState } from './EmptyState'
3+
4+
const meta: Meta<typeof EmptyState> = {
5+
title: 'Components/EmptyState',
6+
component: EmptyState,
7+
parameters: {
8+
layout: 'centered',
9+
},
10+
tags: ['autodocs'],
11+
}
12+
13+
export default meta
14+
type Story = StoryObj<typeof meta>
15+
16+
const PlaceholderIcon = () => (
17+
<svg className="size-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
18+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
19+
</svg>
20+
)
21+
22+
export const Default: Story = {
23+
args: {
24+
title: 'No items found',
25+
description: 'Get started by creating your first item.',
26+
},
27+
}
28+
29+
export const WithIcon: Story = {
30+
args: {
31+
icon: <PlaceholderIcon />,
32+
title: 'No transactions',
33+
description: 'Your transaction history will appear here.',
34+
},
35+
}
36+
37+
export const WithAction: Story = {
38+
args: {
39+
icon: <PlaceholderIcon />,
40+
title: 'No wallet connected',
41+
description: 'Connect your wallet to get started.',
42+
action: (
43+
<button className="bg-primary text-primary-foreground rounded-full px-6 py-2 font-medium">
44+
Connect Wallet
45+
</button>
46+
),
47+
},
48+
}
49+
50+
export const Minimal: Story = {
51+
args: {
52+
title: 'Nothing here yet',
53+
},
54+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use client'
2+
import * as React from 'react'
3+
import { cn } from '@biochain/key-utils'
4+
5+
export interface EmptyStateProps {
6+
icon?: React.ReactNode
7+
title: string
8+
description?: string
9+
action?: React.ReactNode
10+
className?: string
11+
testId?: string
12+
}
13+
14+
export function EmptyState({ icon, title, description, action, className, testId }: EmptyStateProps) {
15+
return (
16+
<div
17+
{...(testId && { 'data-testid': testId })}
18+
className={cn('flex flex-col items-center justify-center px-4 py-12 text-center', className)}
19+
>
20+
{icon && <div className="text-muted-foreground mb-4">{icon}</div>}
21+
<h3 className="text-foreground text-base font-medium @xs:text-lg">{title}</h3>
22+
{description && <p className="text-muted-foreground mt-1 max-w-xs text-sm @xs:text-base">{description}</p>}
23+
{action && <div className="mt-6">{action}</div>}
24+
</div>
25+
)
26+
}
27+
28+
export namespace EmptyState {
29+
export type Props = EmptyStateProps
30+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { EmptyState } from './EmptyState'
2+
export type { EmptyStateProps } from './EmptyState'
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite'
2+
import { GradientButton } from './GradientButton'
3+
4+
const meta: Meta<typeof GradientButton> = {
5+
title: 'Components/GradientButton',
6+
component: GradientButton,
7+
parameters: {
8+
layout: 'centered',
9+
},
10+
tags: ['autodocs'],
11+
argTypes: {
12+
variant: {
13+
control: 'select',
14+
options: ['blue', 'purple', 'red', 'mint'],
15+
},
16+
size: {
17+
control: 'select',
18+
options: ['sm', 'md', 'lg'],
19+
},
20+
},
21+
}
22+
23+
export default meta
24+
type Story = StoryObj<typeof meta>
25+
26+
export const Purple: Story = {
27+
args: {
28+
variant: 'purple',
29+
children: 'Continue',
30+
},
31+
}
32+
33+
export const Blue: Story = {
34+
args: {
35+
variant: 'blue',
36+
children: 'Connect Wallet',
37+
},
38+
}
39+
40+
export const Red: Story = {
41+
args: {
42+
variant: 'red',
43+
children: 'Disconnect',
44+
},
45+
}
46+
47+
export const Mint: Story = {
48+
args: {
49+
variant: 'mint',
50+
children: 'Confirm',
51+
},
52+
}
53+
54+
export const Loading: Story = {
55+
args: {
56+
variant: 'purple',
57+
children: 'Processing...',
58+
loading: true,
59+
},
60+
}
61+
62+
export const Disabled: Story = {
63+
args: {
64+
variant: 'purple',
65+
children: 'Disabled',
66+
disabled: true,
67+
},
68+
}
69+
70+
export const FullWidth: Story = {
71+
args: {
72+
variant: 'purple',
73+
children: 'Full Width Button',
74+
fullWidth: true,
75+
},
76+
decorators: [
77+
(Story) => (
78+
<div className="w-80">
79+
<Story />
80+
</div>
81+
),
82+
],
83+
}
84+
85+
export const AllSizes: Story = {
86+
render: () => (
87+
<div className="flex items-center gap-4">
88+
<GradientButton size="sm">Small</GradientButton>
89+
<GradientButton size="md">Medium</GradientButton>
90+
<GradientButton size="lg">Large</GradientButton>
91+
</div>
92+
),
93+
}
94+
95+
export const AllVariants: Story = {
96+
render: () => (
97+
<div className="flex flex-wrap gap-4">
98+
<GradientButton variant="purple">Purple</GradientButton>
99+
<GradientButton variant="blue">Blue</GradientButton>
100+
<GradientButton variant="red">Red</GradientButton>
101+
<GradientButton variant="mint">Mint</GradientButton>
102+
</div>
103+
),
104+
}

0 commit comments

Comments
 (0)