mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-16 02:05:48 -07:00
Updated Login UI
This commit is contained in:
@@ -1,7 +1,17 @@
|
|||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { Box, TextField, Button, Typography } from "@mui/material";
|
import { Box, TextField, Button, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Borealis MagicUI Login
|
||||||
|
* - Preserves original auth + MFA logic
|
||||||
|
* - Adds MagicUI-inspired visuals:
|
||||||
|
* • Aurora gradient background
|
||||||
|
* • Flickering Grid backdrop
|
||||||
|
* • ShineBorder card container
|
||||||
|
* - Minimal, component-local CSS (no external deps)
|
||||||
|
*/
|
||||||
export default function Login({ onLogin }) {
|
export default function Login({ onLogin }) {
|
||||||
|
// ----------------- Original state & logic -----------------
|
||||||
const [username, setUsername] = useState("admin");
|
const [username, setUsername] = useState("admin");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
@@ -156,179 +166,337 @@ export default function Login({ onLogin }) {
|
|||||||
? "Multi-Factor Authentication"
|
? "Multi-Factor Authentication"
|
||||||
: "Borealis - Automation Platform";
|
: "Borealis - Automation Platform";
|
||||||
|
|
||||||
|
// ----------------- UI helpers -----------------
|
||||||
|
const FieldLabel = ({ children }) => (
|
||||||
|
<Typography variant="caption" sx={{ color: "var(--text-dim)", letterSpacing: ".04em", display: "block", mt: 1.4, mb: 0.1 }}>
|
||||||
|
{children}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<>
|
||||||
sx={{
|
{/* Component-scoped styles for MagicUI effects */}
|
||||||
display: "flex",
|
<style>{`
|
||||||
justifyContent: "center",
|
:root {
|
||||||
alignItems: "center",
|
--bg: #0b0e14;
|
||||||
height: "100vh",
|
--panel: #0f1320;
|
||||||
backgroundColor: "#2b2b2b",
|
--text: #e6f1ff;
|
||||||
}}
|
--text-dim: #9ab0c8;
|
||||||
>
|
--accent: #58a6ff;
|
||||||
<Box
|
--accent-2: #a78bfa; /* purple */
|
||||||
component="form"
|
--accent-3: #22d3ee; /* cyan */
|
||||||
onSubmit={step === "mfa" ? handleMfaSubmit : handleCredentialsSubmit}
|
--radius-xl: 20px;
|
||||||
sx={{
|
--card-w: min(92vw, 420px);
|
||||||
display: "flex",
|
}
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
width: 320,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/Borealis_Logo.png"
|
|
||||||
alt="Borealis Logo"
|
|
||||||
style={{ width: "120px", marginBottom: "16px" }}
|
|
||||||
/>
|
|
||||||
<Typography variant="h6" sx={{ mb: 2, textAlign: "center" }}>
|
|
||||||
{formTitle}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{step === "credentials" ? (
|
.login-wrap {
|
||||||
<>
|
position: relative;
|
||||||
<TextField
|
display: grid;
|
||||||
label="Username"
|
place-items: center;
|
||||||
variant="outlined"
|
min-height: 100vh;
|
||||||
fullWidth
|
background: radial-gradient(1200px 800px at 80% 20%, rgba(75,0,130,.18), transparent 60%),
|
||||||
value={username}
|
radial-gradient(900px 700px at 10% 90%, rgba(0,128,128,.16), transparent 60%),
|
||||||
disabled={isSubmitting}
|
linear-gradient(160deg, #0b0e14 0%, #0a0f1f 50%, #0b0e14 100%);
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
overflow: hidden;
|
||||||
margin="normal"
|
isolation: isolate;
|
||||||
/>
|
}
|
||||||
<TextField
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
variant="outlined"
|
|
||||||
fullWidth
|
|
||||||
value={password}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
{error && (
|
|
||||||
<Typography color="error" sx={{ mt: 1 }}>
|
|
||||||
{error}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="contained"
|
|
||||||
fullWidth
|
|
||||||
disabled={isSubmitting}
|
|
||||||
sx={{ mt: 2, bgcolor: "#58a6ff", "&:hover": { bgcolor: "#1d82d3" } }}
|
|
||||||
>
|
|
||||||
{isSubmitting ? "Signing In..." : "Login"}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{mfaStage === "setup" ? (
|
|
||||||
<>
|
|
||||||
<Typography variant="body2" sx={{ color: "#ccc", textAlign: "center", mb: 2 }}>
|
|
||||||
Scan the QR code with your authenticator app, then enter the 6-digit code to complete setup for {username}.
|
|
||||||
</Typography>
|
|
||||||
{setupQr ? (
|
|
||||||
<Box sx={{ display: "flex", justifyContent: "center", mb: 1.5 }}>
|
|
||||||
<img
|
|
||||||
src={setupQr}
|
|
||||||
alt="MFA enrollment QR code"
|
|
||||||
style={{ width: "180px", height: "180px" }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
) : null}
|
|
||||||
{formattedSecret ? (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
bgcolor: "#1d1d1d",
|
|
||||||
borderRadius: 1,
|
|
||||||
px: 2,
|
|
||||||
py: 1,
|
|
||||||
mb: 1.5,
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="caption" sx={{ color: "#999" }}>
|
|
||||||
Manual code
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
sx={{
|
|
||||||
fontFamily: "monospace",
|
|
||||||
letterSpacing: "0.3rem",
|
|
||||||
color: "#fff",
|
|
||||||
mt: 0.5,
|
|
||||||
textAlign: "center",
|
|
||||||
wordBreak: "break-word",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{formattedSecret}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
) : null}
|
|
||||||
{setupUri ? (
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
sx={{
|
|
||||||
color: "#888",
|
|
||||||
mb: 2,
|
|
||||||
wordBreak: "break-all",
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{setupUri}
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Typography variant="body2" sx={{ color: "#ccc", textAlign: "center", mb: 2 }}>
|
|
||||||
Enter the 6-digit code from your authenticator app for {username}.
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TextField
|
/* Aurora blobs */
|
||||||
label="6-digit code"
|
.aurora {
|
||||||
variant="outlined"
|
position: absolute;
|
||||||
fullWidth
|
inset: -20% -20% -20% -20%;
|
||||||
value={mfaCode}
|
pointer-events: none;
|
||||||
onChange={onCodeChange}
|
z-index: 0;
|
||||||
disabled={isSubmitting}
|
filter: blur(40px) saturate(120%);
|
||||||
margin="normal"
|
opacity: .7;
|
||||||
inputProps={{
|
background:
|
||||||
inputMode: "numeric",
|
radial-gradient(650px 380px at 10% 20%, rgba(88,166,255,.25), transparent 60%),
|
||||||
pattern: "[0-9]*",
|
radial-gradient(700px 420px at 80% 10%, rgba(167,139,250,.18), transparent 60%),
|
||||||
maxLength: 6,
|
radial-gradient(600px 360px at 70% 90%, rgba(34,211,238,.18), transparent 60%);
|
||||||
style: { letterSpacing: "0.4rem", textAlign: "center", fontSize: "1.2rem" }
|
animation: auroraShift 22s ease-in-out infinite alternate;
|
||||||
}}
|
}
|
||||||
autoComplete="one-time-code"
|
@keyframes auroraShift {
|
||||||
/>
|
0% { transform: translate3d(-3%, -2%, 0) scale(1.02); }
|
||||||
{error && (
|
50% { transform: translate3d(2%, 1%, 0) scale(1.04); }
|
||||||
<Typography color="error" sx={{ mt: 1, textAlign: "center" }}>
|
100% { transform: translate3d(-1%, 3%, 0) scale(1.03); }
|
||||||
{error}
|
}
|
||||||
|
|
||||||
|
/* Flickering grid */
|
||||||
|
.grid {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(255,255,255,0.04) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255,255,255,0.04) 1px, transparent 1px);
|
||||||
|
background-size: 32px 32px, 32px 32px;
|
||||||
|
mask-image: radial-gradient(60% 60% at 50% 50%, black 60%, transparent 100%);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.grid::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
repeating-radial-gradient(circle at 30% 30%, rgba(88,166,255,0.04) 0 2px, transparent 2px 6px);
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
animation: flicker 4s infinite steps(60);
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
@keyframes flicker {
|
||||||
|
0%, 19%, 21%, 23%, 25%, 54%, 56%, 100% { opacity: .75 }
|
||||||
|
20%, 24%, 55% { opacity: .45 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shine border container */
|
||||||
|
.shine {
|
||||||
|
position: relative;
|
||||||
|
width: var(--card-w);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
padding: 1px; /* border thickness */
|
||||||
|
background:
|
||||||
|
conic-gradient(from 0deg,
|
||||||
|
rgba(88,166,255,.0),
|
||||||
|
rgba(88,166,255,.45),
|
||||||
|
rgba(34,211,238,.0),
|
||||||
|
rgba(167,139,250,.45),
|
||||||
|
rgba(34,211,238,.0),
|
||||||
|
rgba(88,166,255,.45),
|
||||||
|
rgba(167,139,250,.0));
|
||||||
|
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask-composite: exclude;
|
||||||
|
/* rotation disabled per bugfix */
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(255,255,255,0.06) inset,
|
||||||
|
0 10px 30px rgba(0,0,0,.5);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
.shine-inner {
|
||||||
|
border-radius: inherit;
|
||||||
|
background: linear-gradient(180deg, rgba(17,22,36,.9), rgba(11,14,20,.9));
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
padding: 28px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.brand img { height: 32px; width: auto; }
|
||||||
|
.brand-title {
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .04em;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: .9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: var(--text);
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helper {
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: linear-gradient(135deg, #34d399, #22d3ee);
|
||||||
|
color: #0b0e14;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: none;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 8px 18px rgba(88,166,255,.25);
|
||||||
|
}
|
||||||
|
.submit:hover {
|
||||||
|
filter: brightness(1.05);
|
||||||
|
box-shadow: 0 10px 22px rgba(88,166,255,.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted-btn {
|
||||||
|
color: var(--accent);
|
||||||
|
text-transform: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono-box {
|
||||||
|
background: #0f172a;
|
||||||
|
border: 1px solid rgba(255,255,255,.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.mono-text {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
letter-spacing: .28rem;
|
||||||
|
color: #e5e7eb;
|
||||||
|
text-align: center;
|
||||||
|
user-select: all;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #ff6b6b;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: .9rem;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
<div className="login-wrap">
|
||||||
|
{/* Background layers */}
|
||||||
|
<div className="aurora" />
|
||||||
|
<div className="grid" />
|
||||||
|
|
||||||
|
{/* Card with ShineBorder */}
|
||||||
|
<div className="shine">
|
||||||
|
<div className="shine-inner">
|
||||||
|
<Box
|
||||||
|
component="form"
|
||||||
|
onSubmit={step === "mfa" ? handleMfaSubmit : handleCredentialsSubmit}
|
||||||
|
sx={{ display: "flex", flexDirection: "column", gap: 1 }}
|
||||||
|
>
|
||||||
|
<div className="brand">
|
||||||
|
<img src="/Borealis_Logo_Full.png" alt="Borealis Automation" style={{ width: "345px", height: "auto" }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Typography variant="h5" className="title">
|
||||||
|
{step === "mfa" ? "Multi‑Factor Authentication" : ""}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
|
||||||
<Button
|
{step === "credentials" ? (
|
||||||
type="submit"
|
<>
|
||||||
variant="contained"
|
<FieldLabel>Username</FieldLabel>
|
||||||
fullWidth
|
<TextField
|
||||||
disabled={isSubmitting || mfaCode.length < 6}
|
placeholder="you@borealis"
|
||||||
sx={{ mt: 2, bgcolor: "#58a6ff", "&:hover": { bgcolor: "#1d82d3" } }}
|
variant="outlined"
|
||||||
>
|
fullWidth
|
||||||
{isSubmitting ? "Verifying..." : "Verify Code"}
|
value={username}
|
||||||
</Button>
|
disabled={isSubmitting}
|
||||||
<Button
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
type="button"
|
margin="dense"
|
||||||
variant="text"
|
InputProps={{ sx: { borderRadius: 2 } }}
|
||||||
fullWidth
|
/>
|
||||||
disabled={isSubmitting}
|
|
||||||
onClick={handleBackToLogin}
|
<FieldLabel>Password</FieldLabel>
|
||||||
sx={{ mt: 1, color: "#58a6ff" }}
|
<TextField
|
||||||
>
|
type="password"
|
||||||
Use a different account
|
variant="outlined"
|
||||||
</Button>
|
fullWidth
|
||||||
</>
|
value={password}
|
||||||
)}
|
disabled={isSubmitting}
|
||||||
</Box>
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
</Box>
|
margin="dense"
|
||||||
|
InputProps={{ sx: { borderRadius: 2 } }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
className="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Signing In..." : "Sign in"}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{mfaStage === "setup" ? (
|
||||||
|
<>
|
||||||
|
<Typography variant="body2" className="helper">
|
||||||
|
Scan the QR code with your authenticator app, then enter the 6‑digit code to
|
||||||
|
complete setup for <strong>{username}</strong>.
|
||||||
|
</Typography>
|
||||||
|
{setupQr ? (
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "center", mb: 1.5 }}>
|
||||||
|
<img
|
||||||
|
src={setupQr}
|
||||||
|
alt="MFA enrollment QR code"
|
||||||
|
style={{ width: 180, height: 180, borderRadius: 12, boxShadow: "0 6px 18px rgba(0,0,0,.4)" }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
{formattedSecret ? (
|
||||||
|
<div className="mono-box">
|
||||||
|
<FieldLabel>Manual code</FieldLabel>
|
||||||
|
<div className="mono-text">{formattedSecret}</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{setupUri ? (
|
||||||
|
<Typography variant="caption" sx={{ color: "var(--text-dim)", wordBreak: "break-all", textAlign: "center" }}>
|
||||||
|
{setupUri}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2" className="helper">
|
||||||
|
Enter the 6‑digit code from your authenticator app for <strong>{username}</strong>.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FieldLabel>One‑time code</FieldLabel>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
value={mfaCode}
|
||||||
|
onChange={(e) => {
|
||||||
|
const raw = e.target.value || "";
|
||||||
|
const digits = raw.replace(/\D/g, "").slice(0, 6);
|
||||||
|
setMfaCode(digits);
|
||||||
|
}}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
margin="dense"
|
||||||
|
inputProps={{
|
||||||
|
inputMode: "numeric",
|
||||||
|
pattern: "[0-9]*",
|
||||||
|
maxLength: 6,
|
||||||
|
style: { letterSpacing: "0.4rem", textAlign: "center", fontSize: "1.15rem" }
|
||||||
|
}}
|
||||||
|
autoComplete="one-time-code"
|
||||||
|
placeholder="• • • • • •"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
className="submit"
|
||||||
|
disabled={isSubmitting || mfaCode.length < 6}
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Verifying..." : "Verify code"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="text"
|
||||||
|
className="muted-btn"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={handleBackToLogin}
|
||||||
|
>
|
||||||
|
Use a different account
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user