Skip to content

Commit eb55c78

Browse files
committed
fix: improve ux and ui
1 parent e014130 commit eb55c78

File tree

7 files changed

+676
-356
lines changed

7 files changed

+676
-356
lines changed

services/app/src/App.tsx

Lines changed: 121 additions & 48 deletions
Large diffs are not rendered by default.

services/app/src/access-admin/App.tsx

Lines changed: 419 additions & 267 deletions
Large diffs are not rendered by default.

services/app/src/components/SecurityReportsTable.tsx

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,16 @@ export function SecurityReportsTable({
1717
}: Props) {
1818
if (loading) {
1919
return (
20-
<div className="flex items-center justify-center py-16">
21-
<div className="h-5 w-5 animate-spin rounded-full border-2 border-slate-300 border-t-transparent" />
22-
<span className="ml-3 text-sm text-slate-300">Loading security reports</span>
20+
<div className="flex items-center justify-center rounded-xl border border-slate-200 bg-white/80 px-4 py-10 text-sm text-slate-600 shadow-sm dark:border-slate-700 dark:bg-slate-900/70 dark:text-slate-300">
21+
<div className="h-5 w-5 animate-spin rounded-full border-2 border-emerald-500 border-t-transparent" />
22+
<span className="ml-3">Loading security reports</span>
2323
</div>
2424
);
2525
}
2626

2727
if (error) {
2828
return (
29-
<div className="rounded-lg border border-red-500/40 bg-red-500/10 px-4 py-3 text-sm text-red-100">
29+
<div className="rounded-xl border border-red-500/40 bg-red-50/80 px-4 py-3 text-sm text-red-900 shadow-sm dark:border-red-500/40 dark:bg-red-500/10 dark:text-red-100">
3030
<div className="font-semibold">Failed to load reports</div>
3131
<div className="mt-1 opacity-90">{error}</div>
3232
</div>
@@ -35,41 +35,41 @@ export function SecurityReportsTable({
3535

3636
if (!reports.length) {
3737
return (
38-
<div className="rounded-lg border border-slate-700/60 bg-slate-900/60 px-4 py-6 text-sm text-slate-300">
38+
<div className="rounded-xl border border-slate-200 bg-white/80 px-4 py-6 text-sm text-slate-600 shadow-sm dark:border-slate-700 dark:bg-slate-900/70 dark:text-slate-300">
3939
No security reports match the current filters.
4040
</div>
4141
);
4242
}
4343

4444
return (
45-
<div className="overflow-hidden rounded-xl border border-slate-700/70 bg-slate-900/70 shadow-sm">
46-
<table className="min-w-full divide-y divide-slate-700/80">
47-
<thead className="bg-slate-900/90">
45+
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white/90 shadow-sm dark:border-slate-700/70 dark:bg-slate-900/70">
46+
<table className="min-w-full divide-y divide-slate-200 dark:divide-slate-700/80">
47+
<thead className="bg-slate-50/90 text-slate-500 dark:bg-slate-900/90 dark:text-slate-400">
4848
<tr>
49-
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-400">
49+
<th className="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wide">
5050
Package
5151
</th>
52-
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-400">
52+
<th className="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wide">
5353
Version
5454
</th>
55-
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-400">
55+
<th className="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wide">
5656
Status
5757
</th>
58-
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-400">
58+
<th className="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wide">
5959
Risk score
6060
</th>
61-
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-400">
61+
<th className="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wide">
6262
Risk level
6363
</th>
64-
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-400">
64+
<th className="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wide">
6565
Last scan
6666
</th>
67-
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-400">
67+
<th className="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wide">
6868
Summary
6969
</th>
7070
</tr>
7171
</thead>
72-
<tbody className="divide-y divide-slate-800/80">
72+
<tbody className="divide-y divide-slate-100 dark:divide-slate-800/80">
7373
{reports.map((report) => {
7474
const lastScan = new Date(report.lastScanAt);
7575
const lastScanLabel = Number.isNaN(lastScan.getTime())
@@ -79,28 +79,28 @@ export function SecurityReportsTable({
7979
return (
8080
<tr
8181
key={`${report.packageName}@${report.version}`}
82-
className="hover:bg-slate-800/60 cursor-pointer"
82+
className="cursor-pointer bg-white/80 text-xs text-slate-800 transition hover:bg-slate-50 dark:bg-slate-900/60 dark:text-slate-200 dark:hover:bg-slate-800/70"
8383
onClick={onRowClick ? () => onRowClick(report) : undefined}
8484
>
85-
<td className="whitespace-nowrap px-4 py-2 text-sm font-mono text-emerald-200">
85+
<td className="whitespace-nowrap px-4 py-2.5 text-sm font-mono text-emerald-700 dark:text-emerald-200">
8686
{report.packageName}
8787
</td>
88-
<td className="whitespace-nowrap px-4 py-2 text-xs font-mono text-slate-200">
88+
<td className="whitespace-nowrap px-4 py-2.5 font-mono text-slate-700 dark:text-slate-200">
8989
{report.version}
9090
</td>
91-
<td className="whitespace-nowrap px-4 py-2 text-xs">
91+
<td className="whitespace-nowrap px-4 py-2.5">
9292
<StatusBadge status={report.status} />
9393
</td>
94-
<td className="whitespace-nowrap px-4 py-2 text-xs text-slate-200">
94+
<td className="whitespace-nowrap px-4 py-2.5 text-slate-800 dark:text-slate-200">
9595
{report.riskScore.toFixed(0)}
9696
</td>
97-
<td className="whitespace-nowrap px-4 py-2 text-xs">
97+
<td className="whitespace-nowrap px-4 py-2.5">
9898
<RiskLevelTag level={report.riskLevel} />
9999
</td>
100-
<td className="whitespace-nowrap px-4 py-2 text-xs text-slate-300">
100+
<td className="whitespace-nowrap px-4 py-2.5 text-slate-600 dark:text-slate-300">
101101
{lastScanLabel}
102102
</td>
103-
<td className="max-w-xl px-4 py-2 text-xs text-slate-200">
103+
<td className="max-w-xl px-4 py-2.5 text-slate-700 dark:text-slate-200">
104104
<div className="line-clamp-2 leading-relaxed">{report.summary}</div>
105105
</td>
106106
</tr>

services/app/src/main.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
33
import App from './App';
44
import AccessAdminApp from './access-admin/App';
55
import RequestAccessApp from './request-access/App';
6+
import { ThemeProvider } from './theme';
67
import './index.css';
78

89
function Router() {
@@ -22,6 +23,8 @@ function Router() {
2223

2324
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
2425
<React.StrictMode>
25-
<Router />
26+
<ThemeProvider>
27+
<Router />
28+
</ThemeProvider>
2629
</React.StrictMode>
2730
);

services/app/src/request-access/App.tsx

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,28 +43,28 @@ function App() {
4343
};
4444

4545
return (
46-
<div className="min-h-screen bg-slate-950 text-slate-50 flex items-center justify-center px-4">
47-
<div className="w-full max-w-md rounded-xl border border-slate-800 bg-slate-950/90 p-5 shadow-2xl shadow-black/60">
46+
<div className="min-h-screen bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-50 flex items-center justify-center px-4">
47+
<div className="w-full max-w-md rounded-xl border border-slate-200 bg-white/90 p-5 shadow-xl dark:border-slate-700 dark:bg-slate-900/90">
4848
<div className="mb-4">
49-
<div className="text-sm font-semibold tracking-wide text-emerald-400">
49+
<div className="text-sm font-semibold tracking-wide text-emerald-600 dark:text-emerald-400">
5050
NoPackageMalware
5151
</div>
52-
<p className="mt-1 text-xs text-slate-400">
52+
<p className="mt-1 text-xs text-slate-600 dark:text-slate-400">
5353
Because npm install shouldn't feel like Russian roulette.
5454
</p>
55-
<h1 className="mt-2 text-lg font-semibold text-slate-50">
55+
<h1 className="mt-2 text-lg font-semibold text-slate-900 dark:text-slate-50">
5656
Request access token
5757
</h1>
58-
<p className="mt-1 text-xs text-slate-400">
59-
Submit your email to request an access token for the secure npm registry.
60-
An administrator will review your request and contact you with further
58+
<p className="mt-1 text-xs text-slate-600 dark:text-slate-400">
59+
Submit your email to request an access token for the secure npm registry. An
60+
administrator will review your request and contact you with further
6161
instructions.
6262
</p>
6363
</div>
6464

6565
<form onSubmit={handleSubmit} className="space-y-3 text-sm">
6666
<div>
67-
<label className="block text-xs font-semibold uppercase tracking-wide text-slate-400">
67+
<label className="block text-xs font-semibold uppercase tracking-wide text-slate-600 dark:text-slate-400">
6868
Work email
6969
</label>
7070
<input
@@ -73,41 +73,41 @@ function App() {
7373
placeholder="you@example.com"
7474
value={email}
7575
onChange={(e) => setEmail(e.target.value)}
76-
className="mt-1 block w-full rounded-md border border-slate-700 bg-slate-950 px-3 py-1.5 text-sm text-slate-50 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
76+
className="mt-1 block w-full rounded-md border border-slate-300 bg-white px-3 py-1.5 text-sm text-slate-900 shadow-sm placeholder:text-slate-400 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-50 dark:placeholder:text-slate-500"
7777
/>
7878
</div>
7979
<div>
80-
<label className="block text-xs font-semibold uppercase tracking-wide text-slate-400">
80+
<label className="block text-xs font-semibold uppercase tracking-wide text-slate-600 dark:text-slate-400">
8181
Organization (optional)
8282
</label>
8383
<input
8484
type="text"
8585
placeholder="Acme Corp"
8686
value={orgName}
8787
onChange={(e) => setOrgName(e.target.value)}
88-
className="mt-1 block w-full rounded-md border border-slate-700 bg-slate-950 px-3 py-1.5 text-sm text-slate-50 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
88+
className="mt-1 block w-full rounded-md border border-slate-300 bg-white px-3 py-1.5 text-sm text-slate-900 shadow-sm placeholder:text-slate-400 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-50 dark:placeholder:text-slate-500"
8989
/>
90-
<p className="mt-1 text-xs text-slate-500">
90+
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400">
9191
If omitted, we'll infer it from your email domain when possible.
9292
</p>
9393
</div>
9494

9595
{status && (
96-
<div className="text-xs text-emerald-300">
96+
<div className="rounded-md border border-emerald-500/50 bg-emerald-50/90 px-3 py-2 text-xs text-emerald-900 shadow-sm dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-100">
9797
{status}
9898
</div>
9999
)}
100100

101101
{error && (
102-
<div className="text-xs text-rose-300">
102+
<div className="rounded-md border border-rose-500/60 bg-rose-50/90 px-3 py-2 text-xs text-rose-900 shadow-sm dark:border-rose-500/50 dark:bg-rose-500/10 dark:text-rose-100">
103103
{error}
104104
</div>
105105
)}
106106

107107
<button
108108
type="submit"
109109
disabled={submitting}
110-
className="mt-1 inline-flex w-full items-center justify-center rounded-lg bg-emerald-500 px-3 py-1.5 text-sm font-medium text-emerald-950 shadow-sm hover:bg-emerald-400 disabled:cursor-not-allowed disabled:opacity-60"
110+
className="mt-1 inline-flex w-full items-center justify-center rounded-lg bg-emerald-600 px-3 py-1.5 text-sm font-medium text-emerald-50 shadow-sm transition hover:bg-emerald-500 disabled:cursor-not-allowed disabled:opacity-60"
111111
>
112112
{submitting ? 'Submitting...' : 'Request access'}
113113
</button>

services/app/src/theme.tsx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import React, {
2+
createContext,
3+
useContext,
4+
useEffect,
5+
useState,
6+
type ReactNode
7+
} from 'react';
8+
9+
export type Theme = 'light' | 'dark';
10+
11+
interface ThemeContextValue {
12+
theme: Theme;
13+
setTheme: (theme: Theme) => void;
14+
toggleTheme: () => void;
15+
}
16+
17+
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
18+
19+
const STORAGE_KEY = 'no-package-malware-theme';
20+
21+
function getPreferredTheme(): Theme {
22+
if (typeof window === 'undefined') {
23+
return 'dark';
24+
}
25+
26+
try {
27+
const stored = window.localStorage.getItem(STORAGE_KEY) as Theme | null;
28+
if (stored === 'light' || stored === 'dark') {
29+
return stored;
30+
}
31+
} catch {
32+
// ignore
33+
}
34+
35+
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
36+
return 'light';
37+
}
38+
39+
return 'dark';
40+
}
41+
42+
export function ThemeProvider({ children }: { children: ReactNode }) {
43+
const [theme, setThemeState] = useState<Theme>('dark');
44+
45+
// Initialize from user/system preference on first mount
46+
useEffect(() => {
47+
const preferred = getPreferredTheme();
48+
setThemeState(preferred);
49+
}, []);
50+
51+
// Sync document class + localStorage whenever theme changes
52+
useEffect(() => {
53+
if (typeof document === 'undefined') return;
54+
55+
const root = document.documentElement;
56+
57+
if (theme === 'dark') {
58+
root.classList.add('dark');
59+
} else {
60+
root.classList.remove('dark');
61+
}
62+
63+
try {
64+
window.localStorage.setItem(STORAGE_KEY, theme);
65+
} catch {
66+
// ignore
67+
}
68+
}, [theme]);
69+
70+
const setTheme = (next: Theme) => {
71+
setThemeState(next);
72+
};
73+
74+
const toggleTheme = () => {
75+
setThemeState((prev) => (prev === 'dark' ? 'light' : 'dark'));
76+
};
77+
78+
return (
79+
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
80+
{children}
81+
</ThemeContext.Provider>
82+
);
83+
}
84+
85+
export function useTheme(): ThemeContextValue {
86+
const ctx = useContext(ThemeContext);
87+
if (!ctx) {
88+
throw new Error('useTheme must be used within a ThemeProvider');
89+
}
90+
return ctx;
91+
}

services/app/tailwind.config.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/** @type {import('tailwindcss').Config} */
22
module.exports = {
3+
darkMode: 'class',
34
content: ['./index.html', './src/**/*.{ts,tsx}'],
45
theme: {
56
extend: {}

0 commit comments

Comments
 (0)