feat: add app UI primitives (Button, Badge, FieldRow, Toast)

This commit is contained in:
thesn10
2026-06-24 23:36:19 +02:00
parent 5658e4c3b8
commit 5695df6048
4 changed files with 220 additions and 0 deletions

View 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)' },
}

View 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>
)
}

View 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>
)
}

View 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>
)
}