arbret/frontend/app/styles/shared.ts

896 lines
22 KiB
TypeScript
Raw Normal View History

import { CSSProperties } from "react";
2025-12-18 23:54:51 +01:00
/**
* Design tokens - centralized values for consistency.
2025-12-18 23:54:51 +01:00
*/
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 */
2025-12-18 23:54:51 +01:00
main: {
minHeight: "100vh",
background: tokens.pageGradient,
2025-12-18 23:54:51 +01:00
display: "flex",
flexDirection: "column",
},
/** Centered loading indicator */
2025-12-18 23:54:51 +01:00
loader: {
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontFamily: tokens.fontSans,
color: tokens.textMuted,
2025-12-18 23:54:51 +01:00
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 = {
2025-12-18 23:54:51 +01:00
header: {
padding: "1.5rem 2rem",
borderBottom: `1px solid ${tokens.surfaceBorder}`,
2025-12-18 23:54:51 +01:00
display: "flex",
justifyContent: "space-between",
alignItems: "center",
},
nav: {
display: "flex",
alignItems: "center",
gap: "0.75rem",
},
navLink: {
fontFamily: tokens.fontSans,
color: tokens.textMuted,
2025-12-18 23:54:51 +01:00
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,
2025-12-18 23:54:51 +01:00
fontSize: "0.875rem",
fontWeight: 600,
},
userInfo: {
display: "flex",
alignItems: "center",
gap: "1rem",
},
userEmail: {
fontFamily: tokens.fontSans,
2025-12-18 23:54:51 +01:00
color: "rgba(255, 255, 255, 0.6)",
fontSize: "0.875rem",
},
logoutBtn: {
fontFamily: tokens.fontSans,
2025-12-18 23:54:51 +01:00
padding: "0.5rem 1rem",
fontSize: "0.875rem",
fontWeight: 500,
background: tokens.surfaceHover,
color: tokens.textSecondary,
border: `1px solid ${tokens.inputBorder}`,
borderRadius: tokens.radiusMd,
2025-12-18 23:54:51 +01:00
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: {
2025-12-18 23:54:51 +01:00
display: "flex",
justifyContent: "space-between",
2025-12-18 23:54:51 +01:00
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",
2025-12-18 23:54:51 +01:00
justifyContent: "center",
alignItems: "center",
gap: "1rem",
marginTop: "1rem",
paddingTop: "1rem",
borderTop: "1px solid rgba(255, 255, 255, 0.06)",
2025-12-18 23:54:51 +01:00
},
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",
},
2025-12-26 23:27:33 +01:00
error: {
fontFamily: tokens.fontSans,
fontSize: "0.75rem",
color: "#fca5a5",
marginTop: "0.25rem",
},
section: {
marginBottom: "2rem",
},
sectionTitle: {
fontFamily: tokens.fontSans,
fontSize: "1rem",
fontWeight: 600,
color: tokens.textSecondary,
marginBottom: "1.25rem",
marginTop: 0,
},
actions: {
display: "flex",
justifyContent: "flex-end",
marginTop: "2rem",
paddingTop: "1.5rem",
borderTop: `1px solid ${tokens.surfaceBorder}`,
},
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",
},
2025-12-26 23:27:33 +01:00
/** Primary button alias (for backwards compatibility) */
primary: {
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,
},
/** Disabled button alias (for backwards compatibility) */
disabled: {
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",
},
2025-12-26 23:27:33 +01:00
/** Success banner alias (for backwards compatibility) */
success: {
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,
2025-12-18 23:54:51 +01:00
};