Update to common styling

This commit is contained in:
2026-03-06 21:10:42 -05:00
parent 1ea5a4391e
commit 21440fad57
7 changed files with 345 additions and 92 deletions

View File

@@ -4,6 +4,9 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>StrafesNET Dev Portal</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>

View File

@@ -8,6 +8,7 @@ import {
Typography
} from '@mui/material';
import Header from "./components/Header.tsx";
import AnimatedBackground from "./components/AnimatedBackground.tsx";
import ApiKeyDialog from "./components/applications/dialog/ApiKeyDialog.tsx";
import ApplicationFormDialog from "./components/applications/dialog/ApplicationFormDialog.tsx";
import ApplicationMenu from "./components/applications/ApplicationMenu.tsx";
@@ -142,10 +143,9 @@ const AppContent = () => {
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<ApplicationProvider permissions={user.permissions}>
<Box sx={{
display: 'flex',
flexDirection: 'column',
}}>
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', position: 'relative' }}>
<AnimatedBackground />
<Box sx={{ position: 'relative', zIndex: 1, display: 'flex', flexDirection: 'column' }}>
<AppUI user={user} isAdmin={isAdmin} />
{/* Notifications */}
@@ -168,6 +168,7 @@ const AppContent = () => {
{notification.message}
</Alert>
</Snackbar>
</Box>
</Box>
</ApplicationProvider>
</ThemeProvider>

View File

@@ -0,0 +1,34 @@
import { Box } from '@mui/material';
import { surface } from '../theme/colors';
const AnimatedBackground: React.FC = () => {
return (
<Box
sx={{
position: 'fixed',
inset: 0,
zIndex: 0,
overflow: 'hidden',
pointerEvents: 'none',
background: `
radial-gradient(ellipse 80% 60% at 50% -20%, rgba(124, 58, 237, 0.08) 0%, transparent 100%),
radial-gradient(ellipse 60% 40% at 80% 80%, rgba(34, 211, 238, 0.04) 0%, transparent 100%),
${surface.base}
`,
}}
>
<Box
sx={{
position: 'absolute',
inset: 0,
opacity: 0.03,
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`,
backgroundRepeat: 'repeat',
backgroundSize: '128px 128px',
}}
/>
</Box>
);
};
export default AnimatedBackground;

View File

@@ -24,6 +24,7 @@ import {
import {UserInfo} from "../types";
import {useConfig} from "../context/ConfigContext.tsx";
import RateLimitDisplay from './RateLimitDisplay';
import { primary, border, fill, text } from '../theme/colors';
interface HeaderProps {
user: UserInfo;
@@ -64,9 +65,18 @@ const Header: React.FC<HeaderProps> = ({ user, onCreateAppClick, isAdmin, adminV
return (
<>
<AppBar position="static" elevation={0}>
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
<AppBar position="sticky">
<Toolbar sx={{ gap: 1 }}>
<Typography
variant="h6"
component="div"
sx={{
flexGrow: 1,
fontWeight: 700,
letterSpacing: '-0.02em',
color: text.secondary,
}}
>
StrafesNET Developer Portal
</Typography>
@@ -82,15 +92,17 @@ const Header: React.FC<HeaderProps> = ({ user, onCreateAppClick, isAdmin, adminV
startIcon={adminView ? <AppsIcon /> : <AdminIcon />}
onClick={onToggleAdminView}
sx={{
backgroundColor: adminView
? 'rgba(149, 128, 255, 0.15)'
: 'rgba(255, 255, 255, 0.08)',
px: 1.5,
py: 0.75,
borderRadius: 2,
backgroundColor: adminView ? fill.primaryStrong : fill.default,
border: `1px solid ${adminView ? border.primaryStrong : border.default}`,
color: adminView ? primary.main : text.tertiary,
'&:hover': {
backgroundColor: adminView
? 'rgba(149, 128, 255, 0.25)'
: 'rgba(255, 255, 255, 0.15)',
backgroundColor: adminView ? fill.primaryActive : fill.hover,
borderColor: border.primaryMedium,
color: primary.main,
},
mr: 2,
}}
>
{adminView ? 'My Apps' : 'Admin'}
@@ -105,11 +117,17 @@ const Header: React.FC<HeaderProps> = ({ user, onCreateAppClick, isAdmin, adminV
onClick={onCreateAppClick}
disabled={!user.active}
sx={{
backgroundColor: 'rgba(255, 255, 255, 0.08)',
px: 1.5,
py: 0.75,
borderRadius: 2,
backgroundColor: fill.default,
border: `1px solid ${border.default}`,
color: text.tertiary,
'&:hover': {
backgroundColor: 'rgba(255, 255, 255, 0.15)',
backgroundColor: fill.hover,
borderColor: border.primaryMedium,
color: primary.main,
},
mr: 2
}}
>
Create App
@@ -123,14 +141,22 @@ const Header: React.FC<HeaderProps> = ({ user, onCreateAppClick, isAdmin, adminV
aria-controls={open ? 'profile-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
sx={{
p: 0.5,
transition: 'all 0.15s ease',
'&:hover': { boxShadow: `0 0 12px rgba(167, 139, 250, 0.3)` },
}}
>
<Avatar
sx={{
width: 40,
height: 40,
width: 32,
height: 32,
cursor: 'pointer',
opacity: user.active ? 1 : 0.6,
border: !user.active ? '2px solid #f44336' : 'none'
border: !user.active ? `2px solid #f87171` : 'none',
backgroundColor: primary.dark,
fontSize: '0.75rem',
fontWeight: 700,
}}
alt="Profile Photo"
src={ user.avatar_url }

View File

@@ -1,25 +1,9 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { ThemeProvider, createTheme, CssBaseline } from '@mui/material'
import App from './App.tsx'
// Create a custom theme (customize colors as needed)
const theme = createTheme({
palette: {
primary: {
main: '#1976d2',
},
secondary: {
main: '#dc004e',
},
},
})
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ThemeProvider theme={theme}>
<CssBaseline /> {/* Provides baseline CSS normalization */}
<App />
</ThemeProvider>
<App />
</StrictMode>,
)
)

84
web/src/theme/colors.ts Normal file
View File

@@ -0,0 +1,84 @@
/* ─── Core palette ─────────────────────────────────────────────────────
* Single source of truth for all colors used across the UI.
* Components should import from here instead of hardcoding hex values.
* ──────────────────────────────────────────────────────────────────── */
// Brand / accent
export const primary = {
main: '#a78bfa',
light: '#c4b5fd',
dark: '#7c3aed',
darker: '#6d28d9',
mid: '#8b5cf6',
} as const;
export const secondary = {
main: '#22d3ee',
light: '#67e8f9',
dark: '#0891b2',
} as const;
// Semantic
export const semantic = {
error: '#f87171',
warning: '#fbbf24',
success: '#4ade80',
info: '#38bdf8',
} as const;
// Surfaces
export const surface = {
base: '#09090b',
raised: '#18181b',
raisedAlpha: 'rgba(24, 24, 27, 0.6)',
raisedSolid: 'rgba(24, 24, 27, 0.95)',
overlay: 'rgba(0, 0, 0, 0.6)',
appBar: 'rgba(9, 9, 11, 0.8)',
} as const;
// Text hierarchy (lightest → dimmest)
export const text = {
primary: '#fafafa',
secondary: '#d4d4d8',
tertiary: '#a1a1aa',
muted: '#71717a',
dim: '#52525b',
faint: '#3f3f46',
} as const;
// Borders & dividers
export const border = {
subtle: 'rgba(255, 255, 255, 0.04)',
default: 'rgba(255, 255, 255, 0.06)',
medium: 'rgba(255, 255, 255, 0.08)',
strong: 'rgba(255, 255, 255, 0.1)',
primarySubtle: 'rgba(167, 139, 250, 0.1)',
primaryDefault: 'rgba(167, 139, 250, 0.15)',
primaryMedium: 'rgba(167, 139, 250, 0.2)',
primaryStrong: 'rgba(167, 139, 250, 0.3)',
} as const;
// Interactive surface fills
export const fill = {
subtle: 'rgba(255, 255, 255, 0.03)',
default: 'rgba(255, 255, 255, 0.04)',
hover: 'rgba(255, 255, 255, 0.06)',
primaryHover: 'rgba(167, 139, 250, 0.06)',
primaryActive: 'rgba(167, 139, 250, 0.08)',
primaryStrong: 'rgba(167, 139, 250, 0.1)',
} as const;
/* ─── Gradient presets ────────────────────────────────────────────── */
export const gradients = {
brand: `linear-gradient(135deg, ${primary.dark}, ${secondary.main})`,
button: `linear-gradient(135deg, ${primary.dark} 0%, ${primary.main} 100%)`,
buttonHover: `linear-gradient(135deg, ${primary.darker} 0%, ${primary.mid} 100%)`,
} as const;
/* ─── Glow / shadow presets ───────────────────────────────────────── */
export const glow = {
brand: `0 0 12px rgba(124, 58, 237, 0.5)`,
button: `0 0 20px rgba(124, 58, 237, 0.3)`,
} as const;

View File

@@ -1,99 +1,220 @@
import { createTheme } from '@mui/material';
import { primary, secondary, semantic, surface, text, border, fill, gradients, glow } from './colors';
export const createAppTheme = () => createTheme({
palette: {
mode: 'dark',
primary: {
main: '#9580ff', // Purple instead of blue
main: primary.main,
light: primary.light,
dark: primary.dark,
},
secondary: {
main: '#80ffea', // Teal/cyan
main: secondary.main,
light: secondary.light,
dark: secondary.dark,
},
background: {
paper: '#1e1e1e', // Darker paper background
default: '#121212', // Very dark background
},
error: {
main: '#ff80bf', // Pink-ish error
},
warning: {
main: '#ffca80', // Soft orange
},
success: {
main: '#8aff80', // Light green
},
info: {
main: '#80ffea', // Teal
default: surface.base,
paper: surface.raised,
},
error: { main: semantic.error },
warning: { main: semantic.warning },
success: { main: semantic.success },
info: { main: semantic.info },
text: {
primary: '#ffffff',
secondary: 'rgba(255, 255, 255, 0.7)',
primary: text.primary,
secondary: text.tertiary,
},
divider: border.default,
},
shape: {
borderRadius: 12,
},
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
fontSize: 14,
h4: { fontWeight: 700, letterSpacing: '-0.02em' },
h5: { fontWeight: 700, letterSpacing: '-0.02em' },
h6: { fontWeight: 700, letterSpacing: '-0.01em' },
},
components: {
MuiCard: {
MuiCssBaseline: {
styleOverrides: {
root: {
backgroundColor: '#1e1e1e', // Slightly lighter than default background
backgroundImage: 'none', // Remove any gradient
boxShadow: '0 0 10px rgba(0, 0, 0, 0.5)', // Stronger shadow for depth
body: {
backgroundColor: surface.base,
backgroundImage: `radial-gradient(ellipse 80% 50% at 50% -20%, rgba(120, 60, 255, 0.15), transparent)`,
},
},
},
MuiAppBar: {
styleOverrides: {
root: {
backgroundColor: '#272727', // Slightly lighter app bar
},
},
},
MuiAccordion: {
styleOverrides: {
root: {
backgroundColor: '#1e1e1e',
backgroundImage: 'none',
backgroundColor: surface.appBar,
backdropFilter: 'blur(16px)',
boxShadow: 'none',
borderBottom: `1px solid ${border.default}`,
},
},
},
MuiPaper: {
styleOverrides: {
root: {
backgroundImage: 'none', // Remove any gradient
backgroundImage: 'none',
backgroundColor: surface.raisedAlpha,
backdropFilter: 'blur(12px)',
border: `1px solid ${border.default}`,
},
outlined: {
borderColor: 'rgba(255, 255, 255, 0.12)',
},
},
MuiCard: {
styleOverrides: {
root: {
backgroundImage: 'none',
backgroundColor: surface.raisedAlpha,
backdropFilter: 'blur(12px)',
border: `1px solid ${border.default}`,
boxShadow: 'none',
transition: 'border-color 0.15s ease',
'&:hover': {
borderColor: border.primaryDefault,
},
},
},
},
MuiDialog: {
styleOverrides: {
paper: {
backgroundColor: '#1e1e1e',
backgroundColor: surface.raisedSolid,
backdropFilter: 'blur(20px)',
border: `1px solid ${border.default}`,
},
},
},
MuiTableContainer: {
styleOverrides: {
root: {
backgroundImage: 'none',
borderRadius: 12,
},
},
},
MuiTableHead: {
styleOverrides: {
root: {
'& .MuiTableCell-head': {
backgroundColor: fill.subtle,
color: text.tertiary,
fontWeight: 600,
fontSize: '0.75rem',
textTransform: 'uppercase',
letterSpacing: '0.05em',
borderBottom: `1px solid ${border.default}`,
padding: '12px 16px',
},
},
},
},
MuiTableBody: {
styleOverrides: {
root: {
'& .MuiTableRow-root': {
transition: 'background-color 0.15s ease',
'&:hover': {
backgroundColor: fill.primaryHover,
},
},
'& .MuiTableCell-body': {
borderBottom: `1px solid ${border.subtle}`,
padding: '14px 16px',
fontSize: '0.875rem',
color: text.secondary,
},
},
},
},
MuiTab: {
styleOverrides: {
root: {
textTransform: 'none',
fontWeight: 500,
fontSize: '0.875rem',
minHeight: 44,
'&.Mui-selected': { color: primary.main },
},
},
},
MuiTabs: {
styleOverrides: {
indicator: {
backgroundColor: primary.main,
height: 2,
borderRadius: 1,
},
},
},
MuiButton: {
styleOverrides: {
root: {
textTransform: 'none',
fontWeight: 600,
borderRadius: 8,
},
contained: {
boxShadow: 'none',
background: gradients.button,
'&:hover': {
boxShadow: glow.button,
background: gradients.buttonHover,
},
},
outlined: {
borderColor: border.primaryStrong,
color: primary.main,
'&:hover': {
borderColor: primary.main,
backgroundColor: fill.primaryActive,
},
},
},
},
MuiTextField: {
styleOverrides: {
root: {
'& .MuiOutlinedInput-root': {
borderRadius: 8,
'& fieldset': { borderColor: border.strong },
'&:hover fieldset': { borderColor: border.primaryStrong },
'&.Mui-focused fieldset': { borderColor: primary.main },
},
},
},
},
MuiChip: {
styleOverrides: {
root: {
fontWeight: 600,
fontSize: '0.75rem',
borderRadius: 6,
},
},
},
MuiAccordion: {
styleOverrides: {
root: {
backgroundImage: 'none',
backgroundColor: surface.raisedAlpha,
border: `1px solid ${border.default}`,
boxShadow: 'none',
},
},
},
MuiDivider: {
styleOverrides: {
root: {
borderColor: 'rgba(255, 255, 255, 0.12)',
},
},
},
MuiButton: {
styleOverrides: {
contained: {
boxShadow: '0 3px 5px 2px rgba(0, 0, 0, .3)',
},
},
},
MuiContainer: {
styleOverrides: {
maxWidthXl: {
maxWidth: '100% !important', // Make sure container is full width
paddingLeft: '24px',
paddingRight: '24px',
borderColor: border.default,
},
},
},
},
});
});