feat: add app UI primitives (Button, Badge, FieldRow, Toast)
This commit is contained in:
34
app/src/components/ui/Badge.tsx
Normal file
34
app/src/components/ui/Badge.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
interface BadgeProps {
|
||||||
|
children: ReactNode
|
||||||
|
color: string
|
||||||
|
bg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Badge({ children, color, bg }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '4px 10px',
|
||||||
|
borderRadius: 999,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 600,
|
||||||
|
color,
|
||||||
|
background: bg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const STATUS_COLORS = {
|
||||||
|
active: { color: '#14F195', bg: 'rgba(20,241,149,.15)' },
|
||||||
|
inactive: { color: '#8A8FA3', bg: 'rgba(138,143,163,.16)' },
|
||||||
|
awaitingConfirm: { color: '#F5B23E', bg: 'rgba(245,178,62,.15)' },
|
||||||
|
disputed: { color: '#FF7A59', bg: 'rgba(255,122,89,.15)' },
|
||||||
|
complete: { color: '#4FD1E0', bg: 'rgba(79,209,224,.15)' },
|
||||||
|
cancelled: { color: '#8A8FA3', bg: 'rgba(138,143,163,.16)' },
|
||||||
|
}
|
||||||
52
app/src/components/ui/Button.tsx
Normal file
52
app/src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import type { CSSProperties, ReactNode, ButtonHTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
export type ButtonVariant = 'primary' | 'outline' | 'danger' | 'ghost'
|
||||||
|
|
||||||
|
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
variant?: ButtonVariant
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantStyles: Record<ButtonVariant, CSSProperties> = {
|
||||||
|
primary: {
|
||||||
|
background: 'linear-gradient(135deg, var(--acc), var(--acc2))',
|
||||||
|
color: '#0b0613',
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
background: 'transparent',
|
||||||
|
border: '1px solid var(--bd)',
|
||||||
|
color: 'var(--tx)',
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
background: 'transparent',
|
||||||
|
border: '1px solid var(--danger)',
|
||||||
|
color: 'var(--danger)',
|
||||||
|
},
|
||||||
|
ghost: {
|
||||||
|
background: 'var(--bg3)',
|
||||||
|
border: '1px solid var(--bd)',
|
||||||
|
color: 'var(--tx)',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Button({ variant = 'primary', children, style, disabled, ...rest }: ButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...rest}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{
|
||||||
|
padding: '11px 18px',
|
||||||
|
borderRadius: 'var(--radius)',
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 13,
|
||||||
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: disabled ? 0.5 : 1,
|
||||||
|
...variantStyles[variant],
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
75
app/src/components/ui/FieldRow.tsx
Normal file
75
app/src/components/ui/FieldRow.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
interface FieldRowProps {
|
||||||
|
label: string
|
||||||
|
children: ReactNode
|
||||||
|
last?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FieldRow({ label, children, last }: FieldRowProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 18,
|
||||||
|
padding: '11px 0',
|
||||||
|
borderBottom: last ? 'none' : '1px solid var(--bdSoft)',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: 12.5, color: 'var(--mut)', fontWeight: 500, flexShrink: 0, paddingTop: 2 }}>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'right', minWidth: 0 }}>{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MonoChip({
|
||||||
|
value,
|
||||||
|
onCopy,
|
||||||
|
onClick,
|
||||||
|
sub,
|
||||||
|
}: {
|
||||||
|
value: string
|
||||||
|
onCopy?: () => void
|
||||||
|
onClick?: () => void
|
||||||
|
sub?: string
|
||||||
|
}) {
|
||||||
|
function copy() {
|
||||||
|
navigator.clipboard.writeText(value)
|
||||||
|
onCopy?.()
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 7, justifyContent: 'flex-end' }}>
|
||||||
|
<span
|
||||||
|
onClick={onClick}
|
||||||
|
style={{
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
fontSize: 12.5,
|
||||||
|
color: onClick ? 'var(--acc2light)' : 'var(--tx)',
|
||||||
|
cursor: onClick ? 'pointer' : 'default',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={copy}
|
||||||
|
style={{
|
||||||
|
fontSize: 9.5,
|
||||||
|
color: 'var(--mut)',
|
||||||
|
padding: '3px 6px',
|
||||||
|
borderRadius: 5,
|
||||||
|
background: 'var(--bg3)',
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
COPY
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{sub && <div style={{ fontSize: 11.5, color: 'var(--mut)', marginTop: 3 }}>{sub}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
59
app/src/components/ui/Toast.tsx
Normal file
59
app/src/components/ui/Toast.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { createContext, useCallback, useContext, useState, type ReactNode } from 'react'
|
||||||
|
|
||||||
|
const ToastCtx = createContext<(msg: string) => void>(() => {})
|
||||||
|
|
||||||
|
export function useToast() {
|
||||||
|
return useContext(ToastCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ToastProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [msg, setMsg] = useState('')
|
||||||
|
|
||||||
|
const show = useCallback((m: string) => {
|
||||||
|
setMsg(m)
|
||||||
|
setTimeout(() => setMsg(''), 2800)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastCtx.Provider value={show}>
|
||||||
|
{children}
|
||||||
|
{msg && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
left: '50%',
|
||||||
|
bottom: 30,
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
zIndex: 90,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 10,
|
||||||
|
padding: '12px 18px',
|
||||||
|
background: 'var(--bg2)',
|
||||||
|
border: '1px solid var(--accBd)',
|
||||||
|
borderRadius: 12,
|
||||||
|
boxShadow: '0 16px 40px rgba(0,0,0,.55)',
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 500,
|
||||||
|
animation: 'scToast .22s ease',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: 'var(--acc2)',
|
||||||
|
boxShadow: '0 0 8px var(--acc2)',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{msg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ToastCtx.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user