arbret/frontend/app/styles/shared.ts
counterweight e8d0ee2eca
refactor: import React types directly instead of namespace
Replace 'import React from "react"' with direct imports of
CSSProperties and ChangeEvent. This eliminates unused imports
and follows modern React patterns where the namespace import
is not required for JSX (React 17+).
2025-12-23 12:23:32 +01:00

840 lines
21 KiB
TypeScript

import { CSSProperties } from "react";
/**
* Design tokens - centralized values for consistency.
*/
const tokens = {
// Font families
fontSans: "'DM Sans', system-ui, sans-serif",
fontSerif: "'Instrument Serif', Georgia, serif",
fontMono: "'DM Mono', monospace",
// Colors
white: "#fff",
textPrimary: "#fff",
textSecondary: "rgba(255, 255, 255, 0.7)",
textMuted: "rgba(255, 255, 255, 0.5)",
textDim: "rgba(255, 255, 255, 0.4)",
textDisabled: "rgba(255, 255, 255, 0.3)",
// Accent colors
accent: "#a78bfa",
accentIndigo: "rgba(99, 102, 241, 0.9)",
accentIndigoMuted: "rgba(129, 140, 248, 0.9)",
// Status colors
success: "rgba(34, 197, 94, 0.9)",
successBg: "rgba(34, 197, 94, 0.2)",
successBorder: "rgba(34, 197, 94, 0.3)",
error: "#f87171",
errorBg: "rgba(239, 68, 68, 0.15)",
errorBorder: "rgba(239, 68, 68, 0.3)",
// Surfaces
surfaceBg: "rgba(255, 255, 255, 0.03)",
surfaceBorder: "rgba(255, 255, 255, 0.08)",
surfaceHover: "rgba(255, 255, 255, 0.05)",
inputBg: "rgba(255, 255, 255, 0.05)",
inputBorder: "rgba(255, 255, 255, 0.1)",
// Gradients
pageGradient: "linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #2d1b4e 100%)",
primaryGradient: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
// Shadows
cardShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)",
buttonShadow: "0 4px 14px rgba(99, 102, 241, 0.4)",
// Border radius
radiusSm: "6px",
radiusMd: "8px",
radiusLg: "12px",
radiusXl: "16px",
radius2xl: "20px",
radius3xl: "24px",
} as const;
type StyleRecord = Record<string, CSSProperties>;
// =============================================================================
// Layout Styles
// =============================================================================
export const layoutStyles: StyleRecord = {
/** Full-page main container with gradient background */
main: {
minHeight: "100vh",
background: tokens.pageGradient,
display: "flex",
flexDirection: "column",
},
/** Centered loading indicator */
loader: {
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontFamily: tokens.fontSans,
color: tokens.textMuted,
fontSize: "1.125rem",
},
/** Content area - centered (for cards/forms) */
contentCentered: {
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "2rem",
},
/** Content area - scrollable (for tables/lists) */
contentScrollable: {
flex: 1,
padding: "2rem",
overflowY: "auto",
},
/** Max-width container for page content */
pageContainer: {
maxWidth: "1200px",
margin: "0 auto",
width: "100%",
},
};
// =============================================================================
// Header Styles (for nav/app header - used by Header component)
// =============================================================================
export const headerStyles: StyleRecord = {
header: {
padding: "1.5rem 2rem",
borderBottom: `1px solid ${tokens.surfaceBorder}`,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
},
nav: {
display: "flex",
alignItems: "center",
gap: "0.75rem",
},
navLink: {
fontFamily: tokens.fontSans,
color: tokens.textMuted,
fontSize: "0.875rem",
textDecoration: "none",
transition: "color 0.2s",
},
navDivider: {
color: "rgba(255, 255, 255, 0.2)",
fontSize: "0.75rem",
},
navCurrent: {
fontFamily: tokens.fontSans,
color: tokens.accent,
fontSize: "0.875rem",
fontWeight: 600,
},
userInfo: {
display: "flex",
alignItems: "center",
gap: "1rem",
},
userEmail: {
fontFamily: tokens.fontSans,
color: "rgba(255, 255, 255, 0.6)",
fontSize: "0.875rem",
},
logoutBtn: {
fontFamily: tokens.fontSans,
padding: "0.5rem 1rem",
fontSize: "0.875rem",
fontWeight: 500,
background: tokens.surfaceHover,
color: tokens.textSecondary,
border: `1px solid ${tokens.inputBorder}`,
borderRadius: tokens.radiusMd,
cursor: "pointer",
transition: "all 0.2s",
},
};
// =============================================================================
// Typography Styles
// =============================================================================
export const typographyStyles: StyleRecord = {
/** Large page title - serif font */
pageTitle: {
fontFamily: tokens.fontSerif,
fontSize: "2rem",
fontWeight: 400,
color: tokens.white,
margin: 0,
letterSpacing: "-0.02em",
},
/** Page subtitle text */
pageSubtitle: {
fontFamily: tokens.fontSans,
color: tokens.textMuted,
marginTop: "0.5rem",
fontSize: "0.95rem",
},
/** Section title - uppercase, smaller */
sectionTitle: {
fontFamily: tokens.fontSans,
fontSize: "0.875rem",
fontWeight: 600,
color: "rgba(255, 255, 255, 0.8)",
margin: 0,
textTransform: "uppercase",
letterSpacing: "0.05em",
},
/** Section title - muted variant */
sectionTitleMuted: {
fontFamily: tokens.fontSans,
fontSize: "1.1rem",
fontWeight: 500,
color: tokens.textMuted,
marginBottom: "1rem",
},
/** Section hint text */
sectionHint: {
fontFamily: tokens.fontSans,
fontSize: "0.8rem",
color: tokens.textDim,
margin: 0,
},
};
// =============================================================================
// Card Styles
// =============================================================================
export const cardStyles: StyleRecord = {
/** Standard card container */
card: {
background: tokens.surfaceBg,
backdropFilter: "blur(10px)",
border: `1px solid ${tokens.surfaceBorder}`,
borderRadius: tokens.radius3xl,
padding: "2.5rem",
boxShadow: tokens.cardShadow,
},
/** Card header section */
cardHeader: {
marginBottom: "2rem",
},
/** Card title - serif, large */
cardTitle: {
fontFamily: tokens.fontSerif,
fontSize: "2rem",
fontWeight: 400,
color: tokens.white,
margin: 0,
letterSpacing: "-0.02em",
},
/** Card subtitle */
cardSubtitle: {
fontFamily: tokens.fontSans,
color: tokens.textMuted,
marginTop: "0.5rem",
fontSize: "0.95rem",
},
/** Smaller table/section card */
tableCard: {
background: tokens.surfaceBg,
backdropFilter: "blur(10px)",
border: `1px solid ${tokens.surfaceBorder}`,
borderRadius: tokens.radius2xl,
padding: "1.5rem",
boxShadow: tokens.cardShadow,
},
};
// =============================================================================
// Table Styles
// =============================================================================
export const tableStyles: StyleRecord = {
tableHeader: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "1rem",
flexWrap: "wrap",
gap: "1rem",
},
tableTitle: {
fontFamily: tokens.fontSerif,
fontSize: "1.5rem",
fontWeight: 400,
color: tokens.white,
margin: 0,
},
totalCount: {
fontFamily: tokens.fontSans,
fontSize: "0.875rem",
color: tokens.textDim,
},
tableWrapper: {
overflowX: "auto",
},
table: {
width: "100%",
borderCollapse: "collapse",
fontFamily: tokens.fontSans,
},
th: {
textAlign: "left",
padding: "0.75rem 1rem",
fontSize: "0.75rem",
fontWeight: 600,
color: tokens.textDim,
textTransform: "uppercase",
letterSpacing: "0.05em",
borderBottom: `1px solid ${tokens.surfaceBorder}`,
},
tr: {
borderBottom: "1px solid rgba(255, 255, 255, 0.04)",
},
td: {
padding: "0.875rem 1rem",
fontSize: "0.875rem",
color: tokens.textSecondary,
},
tdMono: {
padding: "0.875rem 1rem",
fontSize: "0.875rem",
color: tokens.white,
fontFamily: tokens.fontMono,
},
tdNum: {
padding: "0.875rem 1rem",
fontSize: "0.875rem",
color: "rgba(255, 255, 255, 0.9)",
fontFamily: tokens.fontMono,
},
tdDate: {
padding: "0.875rem 1rem",
fontSize: "0.75rem",
color: tokens.textDim,
},
emptyRow: {
padding: "2rem 1rem",
textAlign: "center",
color: tokens.textDisabled,
fontSize: "0.875rem",
},
errorRow: {
padding: "2rem 1rem",
textAlign: "center",
color: tokens.error,
fontSize: "0.875rem",
},
};
// =============================================================================
// Pagination Styles
// =============================================================================
export const paginationStyles: StyleRecord = {
pagination: {
display: "flex",
justifyContent: "center",
alignItems: "center",
gap: "1rem",
marginTop: "1rem",
paddingTop: "1rem",
borderTop: "1px solid rgba(255, 255, 255, 0.06)",
},
pageBtn: {
fontFamily: tokens.fontSans,
padding: "0.5rem 1rem",
fontSize: "1rem",
background: tokens.surfaceHover,
color: tokens.textSecondary,
border: `1px solid ${tokens.inputBorder}`,
borderRadius: tokens.radiusMd,
cursor: "pointer",
transition: "all 0.2s",
},
pageInfo: {
fontFamily: tokens.fontSans,
fontSize: "0.875rem",
color: tokens.textMuted,
},
};
// =============================================================================
// Form/Input Styles
// =============================================================================
export const formStyles: StyleRecord = {
form: {
display: "flex",
flexDirection: "column",
gap: "1.25rem",
},
field: {
display: "flex",
flexDirection: "column",
gap: "0.5rem",
},
label: {
fontFamily: tokens.fontSans,
color: tokens.textSecondary,
fontSize: "0.875rem",
fontWeight: 500,
},
input: {
fontFamily: tokens.fontSans,
padding: "0.875rem 1rem",
fontSize: "1rem",
background: tokens.inputBg,
border: `1px solid ${tokens.inputBorder}`,
borderRadius: tokens.radiusLg,
color: tokens.white,
outline: "none",
transition: "border-color 0.2s, box-shadow 0.2s",
},
inputError: {
border: `1px solid ${tokens.errorBorder}`,
boxShadow: `0 0 0 2px ${tokens.errorBg}`,
},
inputReadOnly: {
background: "rgba(255, 255, 255, 0.02)",
color: tokens.textMuted,
cursor: "not-allowed",
},
textarea: {
fontFamily: tokens.fontSans,
width: "100%",
padding: "0.75rem",
background: tokens.inputBg,
border: `1px solid ${tokens.inputBorder}`,
borderRadius: tokens.radiusMd,
color: tokens.white,
fontSize: "0.875rem",
minHeight: "80px",
resize: "vertical" as const,
},
select: {
fontFamily: tokens.fontSans,
fontSize: "0.9rem",
padding: "0.75rem",
background: tokens.inputBg,
border: `1px solid ${tokens.inputBorder}`,
borderRadius: tokens.radiusMd,
color: tokens.white,
cursor: "pointer",
},
hint: {
fontFamily: tokens.fontSans,
fontSize: "0.75rem",
color: tokens.textDim,
fontStyle: "italic",
},
errorText: {
fontFamily: tokens.fontSans,
fontSize: "0.75rem",
color: "#fca5a5",
},
charCount: {
fontFamily: tokens.fontSans,
fontSize: "0.75rem",
color: tokens.textDim,
textAlign: "right" as const,
marginTop: "0.25rem",
},
charCountWarning: {
color: tokens.error,
},
};
// =============================================================================
// Button Styles
// =============================================================================
export const buttonStyles: StyleRecord = {
/** Primary action button with gradient */
primaryButton: {
fontFamily: tokens.fontSans,
padding: "1rem",
fontSize: "1rem",
fontWeight: 600,
background: tokens.primaryGradient,
color: tokens.white,
border: "none",
borderRadius: tokens.radiusLg,
cursor: "pointer",
transition: "transform 0.2s, box-shadow 0.2s",
boxShadow: tokens.buttonShadow,
},
/** Secondary/cancel button */
secondaryButton: {
fontFamily: tokens.fontSans,
fontSize: "0.85rem",
padding: "0.6rem 1rem",
background: tokens.surfaceHover,
color: tokens.textSecondary,
border: `1px solid ${tokens.inputBorder}`,
borderRadius: tokens.radiusMd,
cursor: "pointer",
transition: "all 0.2s",
},
/** Accent button (indigo tinted) */
accentButton: {
fontFamily: tokens.fontSans,
fontSize: "0.9rem",
fontWeight: 500,
padding: "0.75rem 1.5rem",
background: "rgba(99, 102, 241, 0.3)",
color: tokens.white,
border: "1px solid rgba(99, 102, 241, 0.5)",
borderRadius: tokens.radiusMd,
cursor: "pointer",
},
/** Danger/destructive button */
dangerButton: {
fontFamily: tokens.fontSans,
fontSize: "0.75rem",
padding: "0.4rem 0.75rem",
background: tokens.errorBg,
color: "rgba(239, 68, 68, 0.9)",
border: `1px solid ${tokens.errorBorder}`,
borderRadius: tokens.radiusSm,
cursor: "pointer",
},
/** Disabled button modifier */
buttonDisabled: {
opacity: 0.5,
cursor: "not-allowed",
boxShadow: "none",
},
};
// =============================================================================
// Badge/Status Styles
// =============================================================================
export const badgeStyles: StyleRecord = {
/** Base badge style */
badge: {
fontFamily: tokens.fontSans,
fontSize: "0.7rem",
fontWeight: 500,
padding: "0.25rem 0.5rem",
borderRadius: "4px",
textTransform: "uppercase",
display: "inline-block",
},
/** Ready/primary status */
badgeReady: {
background: "rgba(99, 102, 241, 0.2)",
color: tokens.accentIndigoMuted,
},
/** Success/spent status */
badgeSuccess: {
background: tokens.successBg,
color: tokens.success,
},
/** Error/revoked status */
badgeError: {
background: "rgba(239, 68, 68, 0.2)",
color: "rgba(239, 68, 68, 0.9)",
},
/** Booked/active status */
badgeBooked: {
background: "rgba(99, 102, 241, 0.15)",
color: tokens.accentIndigoMuted,
},
};
// =============================================================================
// Banner/Alert Styles
// =============================================================================
export const bannerStyles: StyleRecord = {
errorBanner: {
fontFamily: tokens.fontSans,
fontSize: "0.875rem",
padding: "1rem",
background: tokens.errorBg,
border: `1px solid ${tokens.errorBorder}`,
borderRadius: tokens.radiusMd,
color: tokens.error,
marginBottom: "1rem",
},
successBanner: {
fontFamily: tokens.fontSans,
fontSize: "0.875rem",
padding: "1rem",
background: "rgba(34, 197, 94, 0.15)",
border: `1px solid ${tokens.successBorder}`,
borderRadius: tokens.radiusMd,
color: "#4ade80",
marginBottom: "1rem",
},
};
// =============================================================================
// Modal Styles
// =============================================================================
export const modalStyles: StyleRecord = {
modalOverlay: {
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "rgba(0, 0, 0, 0.7)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1000,
},
modal: {
background: "#1a1a3e",
border: `1px solid ${tokens.inputBorder}`,
borderRadius: tokens.radiusXl,
padding: "2rem",
width: "90%",
maxWidth: "400px",
boxShadow: tokens.cardShadow,
},
modalTitle: {
fontFamily: tokens.fontSerif,
fontSize: "1.5rem",
fontWeight: 400,
color: tokens.white,
margin: "0 0 1.5rem 0",
},
modalError: {
fontFamily: tokens.fontSans,
fontSize: "0.85rem",
padding: "0.75rem",
background: tokens.errorBg,
border: `1px solid ${tokens.errorBorder}`,
borderRadius: tokens.radiusMd,
color: tokens.error,
marginBottom: "1rem",
},
modalActions: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "1rem",
},
modalActionsRight: {
display: "flex",
gap: "0.75rem",
},
};
// =============================================================================
// Toast Styles
// =============================================================================
export const toastStyles: StyleRecord = {
toast: {
position: "fixed",
top: "1.5rem",
right: "1.5rem",
padding: "1rem 1.5rem",
borderRadius: tokens.radiusLg,
fontFamily: tokens.fontSans,
fontSize: "0.875rem",
fontWeight: 500,
zIndex: 1000,
boxShadow: "0 10px 25px rgba(0, 0, 0, 0.3)",
},
toastSuccess: {
background: "rgba(34, 197, 94, 0.9)",
color: tokens.white,
},
toastError: {
background: "rgba(239, 68, 68, 0.9)",
color: tokens.white,
},
};
// =============================================================================
// Trade Card Styles (shared between trades and admin/trades pages)
// =============================================================================
export const tradeCardStyles: StyleRecord = {
/** Container for list of trade cards */
tradeList: {
display: "flex",
flexDirection: "column",
gap: "0.75rem",
},
/** Individual trade card */
tradeCard: {
background: tokens.surfaceBg,
border: `1px solid ${tokens.surfaceBorder}`,
borderRadius: tokens.radiusLg,
padding: "1.25rem",
transition: "all 0.2s",
},
/** Trade card header - flex row for info + actions */
tradeHeader: {
display: "flex",
justifyContent: "space-between",
alignItems: "flex-start",
gap: "1rem",
},
/** Left side info container */
tradeInfo: {
display: "flex",
flexDirection: "column",
gap: "0.25rem",
},
/** Trade date/time display */
tradeTime: {
fontFamily: tokens.fontSans,
fontSize: "1rem",
fontWeight: 500,
color: tokens.white,
marginBottom: "0.5rem",
},
/** Trade amount details row */
tradeDetails: {
display: "flex",
alignItems: "center",
gap: "0.5rem",
flexWrap: "wrap",
},
/** BUY/SELL direction badge (base - color applied inline) */
directionBadge: {
fontFamily: tokens.fontSans,
fontSize: "0.7rem",
fontWeight: 600,
padding: "0.2rem 0.5rem",
borderRadius: "4px",
textTransform: "uppercase",
},
/** EUR amount display */
amount: {
fontFamily: tokens.fontMono,
fontSize: "0.95rem",
color: tokens.white,
fontWeight: 500,
},
/** Arrow between EUR and sats */
arrow: {
color: tokens.textDisabled,
fontSize: "0.8rem",
},
/** Sats amount in Bitcoin orange */
satsAmount: {
fontFamily: tokens.fontMono,
fontSize: "0.9rem",
color: "#f7931a", // Bitcoin orange
},
/** Rate info row */
rateRow: {
display: "flex",
alignItems: "center",
gap: "0.5rem",
marginTop: "0.25rem",
flexWrap: "wrap",
},
/** Rate label text */
rateLabel: {
fontFamily: tokens.fontSans,
fontSize: "0.75rem",
color: tokens.textDim,
},
/** Rate value in monospace */
rateValue: {
fontFamily: tokens.fontMono,
fontSize: "0.8rem",
color: tokens.textSecondary,
},
/** Button group container */
buttonGroup: {
display: "flex",
gap: "0.5rem",
},
/** Empty state for no trades */
emptyState: {
fontFamily: tokens.fontSans,
color: tokens.textDim,
textAlign: "center" as const,
padding: "3rem",
},
};
// =============================================================================
// Misc Utility Styles
// =============================================================================
export const utilityStyles: StyleRecord = {
/** Horizontal divider */
divider: {
height: "1px",
background: tokens.surfaceBorder,
margin: "0.75rem 0",
},
/** Empty state container */
emptyState: {
fontFamily: tokens.fontSans,
color: tokens.textDim,
textAlign: "center" as const,
padding: "1rem 0",
},
/** Read-only badge for form labels */
readOnlyBadge: {
fontSize: "0.7rem",
fontWeight: 500,
color: tokens.textDim,
background: tokens.surfaceBorder,
padding: "0.15rem 0.5rem",
borderRadius: "4px",
textTransform: "uppercase",
letterSpacing: "0.05em",
},
/** Flex row with gap */
buttonRow: {
display: "flex",
gap: "0.75rem",
},
/** Filter group container */
filterGroup: {
display: "flex",
alignItems: "center",
gap: "1rem",
},
};
// =============================================================================
// Combined sharedStyles (backwards compatible export)
// =============================================================================
/**
* @deprecated Use individual style exports (layoutStyles, cardStyles, etc.) for better tree-shaking.
* This combined export is maintained for backwards compatibility during migration.
*/
export const sharedStyles: StyleRecord = {
// Layout
main: layoutStyles.main,
loader: layoutStyles.loader,
content: layoutStyles.contentCentered,
// Header
header: headerStyles.header,
nav: headerStyles.nav,
navLink: headerStyles.navLink,
navDivider: headerStyles.navDivider,
navCurrent: headerStyles.navCurrent,
userInfo: headerStyles.userInfo,
userEmail: headerStyles.userEmail,
logoutBtn: headerStyles.logoutBtn,
// Common
errorBanner: bannerStyles.errorBanner,
cancelButton: buttonStyles.secondaryButton,
emptyState: utilityStyles.emptyState,
};