human hand off bug
This commit is contained in:
parent
2855a20140
commit
962f991580
11 changed files with 226 additions and 34 deletions
|
|
@ -23,6 +23,8 @@ from app.schemas import (
|
||||||
InvitePreview,
|
InvitePreview,
|
||||||
LoginRequest,
|
LoginRequest,
|
||||||
PasswordChangeRequest,
|
PasswordChangeRequest,
|
||||||
|
PasswordResetRequest,
|
||||||
|
PasswordResetResponse,
|
||||||
)
|
)
|
||||||
from app.services.member_invitations import get_valid_invitation
|
from app.services.member_invitations import get_valid_invitation
|
||||||
from app.services.welcome_email import send_welcome_email
|
from app.services.welcome_email import send_welcome_email
|
||||||
|
|
@ -306,6 +308,35 @@ async def accept_invite(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reset-password", response_model=PasswordResetResponse)
|
||||||
|
async def reset_password(
|
||||||
|
payload: PasswordResetRequest,
|
||||||
|
session: AsyncSession = Depends(get_postgres_session),
|
||||||
|
) -> PasswordResetResponse:
|
||||||
|
validate_password(payload.new_password)
|
||||||
|
account = await session.scalar(
|
||||||
|
select(Account).where(Account.email == str(payload.email).lower())
|
||||||
|
)
|
||||||
|
password_hash.verify(
|
||||||
|
payload.new_password,
|
||||||
|
account.password_hash if account is not None else dummy_password_hash,
|
||||||
|
)
|
||||||
|
if account is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="No account was found for that email.",
|
||||||
|
)
|
||||||
|
if account.member_status == "invited":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Accept your invitation email to set your password before resetting it.",
|
||||||
|
)
|
||||||
|
account.password_hash = password_hash.hash(payload.new_password)
|
||||||
|
account.must_change_password = False
|
||||||
|
await session.commit()
|
||||||
|
return PasswordResetResponse()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/change-password", response_model=AccountRead)
|
@router.post("/change-password", response_model=AccountRead)
|
||||||
async def change_password(
|
async def change_password(
|
||||||
payload: PasswordChangeRequest,
|
payload: PasswordChangeRequest,
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,15 @@ class PasswordChangeRequest(BaseModel):
|
||||||
new_password: str = Field(min_length=8, max_length=128)
|
new_password: str = Field(min_length=8, max_length=128)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetRequest(BaseModel):
|
||||||
|
email: EmailStr
|
||||||
|
new_password: str = Field(min_length=8, max_length=128)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetResponse(BaseModel):
|
||||||
|
ok: bool = True
|
||||||
|
|
||||||
|
|
||||||
class InvitePreview(BaseModel):
|
class InvitePreview(BaseModel):
|
||||||
work_email: EmailStr
|
work_email: EmailStr
|
||||||
first_name: str
|
first_name: str
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import DashboardPage from "./components/DashboardPage";
|
import DashboardPage from "./components/DashboardPage";
|
||||||
|
import ForgotPasswordPage from "./components/ForgotPasswordPage";
|
||||||
import InviteAcceptPage from "./components/InviteAcceptPage";
|
import InviteAcceptPage from "./components/InviteAcceptPage";
|
||||||
import LoginPage from "./components/LoginPage";
|
import LoginPage from "./components/LoginPage";
|
||||||
import MobileLanding from "./components/MobileLanding";
|
import MobileLanding from "./components/MobileLanding";
|
||||||
|
|
@ -13,6 +14,9 @@ export default function App() {
|
||||||
if (window.location.pathname === "/login") {
|
if (window.location.pathname === "/login") {
|
||||||
return <LoginPage />;
|
return <LoginPage />;
|
||||||
}
|
}
|
||||||
|
if (window.location.pathname === "/forgot-password") {
|
||||||
|
return <ForgotPasswordPage />;
|
||||||
|
}
|
||||||
if (window.location.pathname.startsWith("/invite/")) {
|
if (window.location.pathname.startsWith("/invite/")) {
|
||||||
return <InviteAcceptPage />;
|
return <InviteAcceptPage />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ async function request(path, { method = "GET", payload, token, basicAuth } = {})
|
||||||
|
|
||||||
export const login = (payload) =>
|
export const login = (payload) =>
|
||||||
request("/auth/login", { method: "POST", payload });
|
request("/auth/login", { method: "POST", payload });
|
||||||
|
export const resetPassword = (payload) =>
|
||||||
|
request("/auth/reset-password", { method: "POST", payload });
|
||||||
export const previewInvite = (token) =>
|
export const previewInvite = (token) =>
|
||||||
request(`/auth/invites/${encodeURIComponent(token)}`);
|
request(`/auth/invites/${encodeURIComponent(token)}`);
|
||||||
export const acceptInvite = (token, payload) =>
|
export const acceptInvite = (token, payload) =>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { changePassword } from "../api";
|
import { changePassword } from "../api";
|
||||||
|
import PasswordField from "./PasswordField";
|
||||||
|
|
||||||
function validateForm(form) {
|
function validateForm(form) {
|
||||||
const errors = {};
|
const errors = {};
|
||||||
|
|
@ -79,8 +80,7 @@ export default function ChangePasswordModal({ account, onComplete }) {
|
||||||
<form className="change-password-form" onSubmit={handleSubmit} noValidate>
|
<form className="change-password-form" onSubmit={handleSubmit} noValidate>
|
||||||
<label>
|
<label>
|
||||||
Current password
|
Current password
|
||||||
<input
|
<PasswordField
|
||||||
type="password"
|
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
value={form.current_password}
|
value={form.current_password}
|
||||||
onChange={(event) => updateField("current_password", event.target.value)}
|
onChange={(event) => updateField("current_password", event.target.value)}
|
||||||
|
|
@ -94,8 +94,7 @@ export default function ChangePasswordModal({ account, onComplete }) {
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
New password
|
New password
|
||||||
<input
|
<PasswordField
|
||||||
type="password"
|
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
value={form.new_password}
|
value={form.new_password}
|
||||||
onChange={(event) => updateField("new_password", event.target.value)}
|
onChange={(event) => updateField("new_password", event.target.value)}
|
||||||
|
|
@ -109,8 +108,7 @@ export default function ChangePasswordModal({ account, onComplete }) {
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Confirm new password
|
Confirm new password
|
||||||
<input
|
<PasswordField
|
||||||
type="password"
|
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
value={form.confirm_password}
|
value={form.confirm_password}
|
||||||
onChange={(event) => updateField("confirm_password", event.target.value)}
|
onChange={(event) => updateField("confirm_password", event.target.value)}
|
||||||
|
|
|
||||||
95
frontend/src/components/ForgotPasswordPage.jsx
Normal file
95
frontend/src/components/ForgotPasswordPage.jsx
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { ArrowRight, Mail } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { resetPassword } from "../api";
|
||||||
|
import AuthLayout from "./AuthLayout";
|
||||||
|
import PasswordField from "./PasswordField";
|
||||||
|
|
||||||
|
export default function ForgotPasswordPage() {
|
||||||
|
const [status, setStatus] = useState({ loading: false, error: "", done: false });
|
||||||
|
|
||||||
|
async function handleSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const form = new FormData(event.currentTarget);
|
||||||
|
const password = String(form.get("new_password") || "");
|
||||||
|
const confirm = String(form.get("confirm_password") || "");
|
||||||
|
if (password !== confirm) {
|
||||||
|
setStatus({ loading: false, error: "Passwords do not match.", done: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatus({ loading: true, error: "", done: false });
|
||||||
|
try {
|
||||||
|
await resetPassword({
|
||||||
|
email: form.get("email"),
|
||||||
|
new_password: password,
|
||||||
|
});
|
||||||
|
setStatus({ loading: false, error: "", done: true });
|
||||||
|
} catch (error) {
|
||||||
|
setStatus({ loading: false, error: error.message, done: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthLayout
|
||||||
|
eyebrow="Forgot password"
|
||||||
|
title={<>Set a new password<br /><em>and sign back in.</em></>}
|
||||||
|
description="Enter your account email and choose a new password. Extra email or phone checks are skipped for now."
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<div className="card-heading">
|
||||||
|
<h2>Reset your password</h2>
|
||||||
|
<p>Use the email you sign in with. No verification code is required yet.</p>
|
||||||
|
</div>
|
||||||
|
{status.done ? (
|
||||||
|
<div className="auth-success">
|
||||||
|
<p>Your password has been updated. You can sign in with it now.</p>
|
||||||
|
<a className="button button-coral auth-submit" href="/login">
|
||||||
|
Back to sign in <ArrowRight size={16} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form className="auth-form" onSubmit={handleSubmit}>
|
||||||
|
<label>
|
||||||
|
Email address
|
||||||
|
<div className="input-wrap">
|
||||||
|
<Mail size={16} />
|
||||||
|
<input
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="you@gentic.email"
|
||||||
|
required
|
||||||
|
autoComplete="email"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
New password
|
||||||
|
<PasswordField
|
||||||
|
name="new_password"
|
||||||
|
placeholder="8+ characters"
|
||||||
|
minLength={8}
|
||||||
|
required
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
<small className="field-hint">Use uppercase, lowercase, and a number.</small>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Confirm new password
|
||||||
|
<PasswordField
|
||||||
|
name="confirm_password"
|
||||||
|
placeholder="Repeat your password"
|
||||||
|
minLength={8}
|
||||||
|
required
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{status.error && <p className="form-error" role="alert">{status.error}</p>}
|
||||||
|
<button className="button button-coral auth-submit" disabled={status.loading}>
|
||||||
|
{status.loading ? "Updating…" : "Update password"} <ArrowRight size={16} />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
<p className="auth-switch">Remembered it? <a href="/login">Log in</a></p>
|
||||||
|
</>
|
||||||
|
</AuthLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { ArrowRight, LockKeyhole } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { acceptInvite, previewInvite } from "../api";
|
import { acceptInvite, previewInvite } from "../api";
|
||||||
import AuthLayout from "./AuthLayout";
|
import AuthLayout from "./AuthLayout";
|
||||||
|
import PasswordField from "./PasswordField";
|
||||||
|
|
||||||
function inviteTokenFromPath() {
|
function inviteTokenFromPath() {
|
||||||
const match = window.location.pathname.match(/^\/invite\/([^/]+)/);
|
const match = window.location.pathname.match(/^\/invite\/([^/]+)/);
|
||||||
|
|
@ -89,31 +90,23 @@ export default function InviteAcceptPage() {
|
||||||
<form className="auth-form" onSubmit={handleSubmit}>
|
<form className="auth-form" onSubmit={handleSubmit}>
|
||||||
<label>
|
<label>
|
||||||
Password
|
Password
|
||||||
<div className="input-wrap">
|
<PasswordField
|
||||||
<LockKeyhole size={16} />
|
name="password"
|
||||||
<input
|
placeholder="Create a password"
|
||||||
name="password"
|
required
|
||||||
type="password"
|
minLength={8}
|
||||||
placeholder="Create a password"
|
autoComplete="new-password"
|
||||||
required
|
/>
|
||||||
minLength={8}
|
|
||||||
autoComplete="new-password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Confirm password
|
Confirm password
|
||||||
<div className="input-wrap">
|
<PasswordField
|
||||||
<LockKeyhole size={16} />
|
name="confirm_password"
|
||||||
<input
|
placeholder="Repeat your password"
|
||||||
name="confirm_password"
|
required
|
||||||
type="password"
|
minLength={8}
|
||||||
placeholder="Repeat your password"
|
autoComplete="new-password"
|
||||||
required
|
/>
|
||||||
minLength={8}
|
|
||||||
autoComplete="new-password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</label>
|
</label>
|
||||||
<p className="auth-switch" style={{ marginTop: 0 }}>
|
<p className="auth-switch" style={{ marginTop: 0 }}>
|
||||||
Must include uppercase, lowercase, and a number.
|
Must include uppercase, lowercase, and a number.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { ArrowRight, LockKeyhole, Mail } from "lucide-react";
|
import { ArrowRight, Mail } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { login } from "../api";
|
import { login } from "../api";
|
||||||
import AuthLayout from "./AuthLayout";
|
import AuthLayout from "./AuthLayout";
|
||||||
|
import PasswordField from "./PasswordField";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [status, setStatus] = useState({ loading: false, error: "" });
|
const [status, setStatus] = useState({ loading: false, error: "" });
|
||||||
|
|
@ -33,7 +34,18 @@ export default function LoginPage() {
|
||||||
<div className="card-heading"><h2>Log in to agmail</h2><p>Enter your account details below.</p></div>
|
<div className="card-heading"><h2>Log in to agmail</h2><p>Enter your account details below.</p></div>
|
||||||
<form className="auth-form" onSubmit={handleSubmit}>
|
<form className="auth-form" onSubmit={handleSubmit}>
|
||||||
<label>Email address<div className="input-wrap"><Mail size={16} /><input name="email" type="email" placeholder="you@gentic.email" required autoComplete="email" /></div></label>
|
<label>Email address<div className="input-wrap"><Mail size={16} /><input name="email" type="email" placeholder="you@gentic.email" required autoComplete="email" /></div></label>
|
||||||
<label>Password<div className="input-wrap"><LockKeyhole size={16} /><input name="password" type="password" placeholder="Your password" required autoComplete="current-password" /></div></label>
|
<label>
|
||||||
|
<span className="auth-label-row">
|
||||||
|
Password
|
||||||
|
<a className="auth-forgot" href="/forgot-password">Forgot password?</a>
|
||||||
|
</span>
|
||||||
|
<PasswordField
|
||||||
|
name="password"
|
||||||
|
placeholder="Your password"
|
||||||
|
required
|
||||||
|
autoComplete="current-password"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
{status.error && <p className="form-error" role="alert">{status.error}</p>}
|
{status.error && <p className="form-error" role="alert">{status.error}</p>}
|
||||||
<button className="button button-coral auth-submit" disabled={status.loading}>
|
<button className="button button-coral auth-submit" disabled={status.loading}>
|
||||||
{status.loading ? "Signing in…" : "Log in"} <ArrowRight size={16} />
|
{status.loading ? "Signing in…" : "Log in"} <ArrowRight size={16} />
|
||||||
|
|
|
||||||
41
frontend/src/components/PasswordField.jsx
Normal file
41
frontend/src/components/PasswordField.jsx
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { Eye, EyeOff, LockKeyhole } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function PasswordField({
|
||||||
|
name,
|
||||||
|
placeholder,
|
||||||
|
autoComplete,
|
||||||
|
required,
|
||||||
|
minLength,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
showLock = true,
|
||||||
|
className = "input-wrap",
|
||||||
|
}) {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
{showLock ? <LockKeyhole size={16} /> : null}
|
||||||
|
<input
|
||||||
|
name={name}
|
||||||
|
type={visible ? "text" : "password"}
|
||||||
|
placeholder={placeholder}
|
||||||
|
required={required}
|
||||||
|
minLength={minLength}
|
||||||
|
autoComplete={autoComplete}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="password-peek"
|
||||||
|
aria-label={visible ? "Hide password" : "Show password"}
|
||||||
|
aria-pressed={visible}
|
||||||
|
onClick={() => setVisible((current) => !current)}
|
||||||
|
>
|
||||||
|
{visible ? <EyeOff size={15} /> : <Eye size={15} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import {
|
import {
|
||||||
ArrowRight, Building2, CalendarDays, Eye, LockKeyhole, Mail, Phone, UserRound,
|
ArrowRight, Building2, CalendarDays, Mail, Phone, UserRound,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { register } from "../api";
|
import { register } from "../api";
|
||||||
import AuthLayout from "./AuthLayout";
|
import AuthLayout from "./AuthLayout";
|
||||||
|
import PasswordField from "./PasswordField";
|
||||||
|
|
||||||
export default function SignupPage() {
|
export default function SignupPage() {
|
||||||
const [accountType, setAccountType] = useState("individual");
|
const [accountType, setAccountType] = useState("individual");
|
||||||
|
|
@ -67,7 +68,7 @@ export default function SignupPage() {
|
||||||
</div>
|
</div>
|
||||||
<label>Date of birth<div className="input-wrap"><CalendarDays size={16} /><input name="date_of_birth" type="date" required /></div></label>
|
<label>Date of birth<div className="input-wrap"><CalendarDays size={16} /><input name="date_of_birth" type="date" required /></div></label>
|
||||||
<label>Phone number<div className="input-wrap"><Phone size={16} /><input name="phone_number" type="tel" placeholder="+1 555 123 4567" required /></div></label>
|
<label>Phone number<div className="input-wrap"><Phone size={16} /><input name="phone_number" type="tel" placeholder="+1 555 123 4567" required /></div></label>
|
||||||
<label>Password<div className="input-wrap"><LockKeyhole size={16} /><input name="password" type="password" placeholder="8+ characters" minLength="8" required autoComplete="new-password" /><Eye size={15} /></div><small className="field-hint">Use uppercase, lowercase, and a number.</small></label>
|
<label>Password<PasswordField name="password" placeholder="8+ characters" minLength={8} required autoComplete="new-password" /><small className="field-hint">Use uppercase, lowercase, and a number.</small></label>
|
||||||
{status.error && <p className="form-error" role="alert">{status.error}</p>}
|
{status.error && <p className="form-error" role="alert">{status.error}</p>}
|
||||||
<button className="button button-coral auth-submit" disabled={status.loading}>
|
<button className="button button-coral auth-submit" disabled={status.loading}>
|
||||||
{status.loading ? "Creating account…" : "Create account"} <ArrowRight size={16} />
|
{status.loading ? "Creating account…" : "Create account"} <ArrowRight size={16} />
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,11 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
|
||||||
.input-wrap:focus-within { border-color:var(--coral); box-shadow:0 0 0 3px #ff806c18; }
|
.input-wrap:focus-within { border-color:var(--coral); box-shadow:0 0 0 3px #ff806c18; }
|
||||||
.input-wrap input { min-width:0; flex:1; border:0; outline:0; background:transparent; color:var(--ink); font-size:13px; }
|
.input-wrap input { min-width:0; flex:1; border:0; outline:0; background:transparent; color:var(--ink); font-size:13px; }
|
||||||
.input-wrap input::placeholder { color:#aaa5ab; }
|
.input-wrap input::placeholder { color:#aaa5ab; }
|
||||||
|
.password-peek { margin:0; padding:4px; border:0; background:transparent; color:#a19ba2; display:grid; place-items:center; cursor:pointer; }
|
||||||
|
.password-peek:hover,.password-peek:focus-visible { color:var(--ink); }
|
||||||
|
.auth-label-row { display:flex; align-items:center; justify-content:space-between; gap:10px; }
|
||||||
|
.auth-forgot { color:var(--ink); font-size:11px; font-weight:700; text-decoration:none; }
|
||||||
|
.auth-forgot:hover { color:var(--coral); }
|
||||||
.account-types { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
|
.account-types { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
|
||||||
.type-card { min-height:74px; padding:13px; display:grid; grid-template-columns:20px 1fr; align-items:center; gap:10px; border:1px solid #e5e0dc; border-radius:12px; background:white; cursor:pointer; }
|
.type-card { min-height:74px; padding:13px; display:grid; grid-template-columns:20px 1fr; align-items:center; gap:10px; border:1px solid #e5e0dc; border-radius:12px; background:white; cursor:pointer; }
|
||||||
.type-card.selected { border-color:var(--coral); background:#fff6f2; box-shadow:0 0 0 2px #ff806c12; }
|
.type-card.selected { border-color:var(--coral); background:#fff6f2; box-shadow:0 0 0 2px #ff806c12; }
|
||||||
|
|
@ -499,7 +504,8 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
|
||||||
.change-password-head p { margin:0; font-size:14px; color:#6f6c7b; line-height:1.5; }
|
.change-password-head p { margin:0; font-size:14px; color:#6f6c7b; line-height:1.5; }
|
||||||
.change-password-form { display:grid; gap:14px; }
|
.change-password-form { display:grid; gap:14px; }
|
||||||
.change-password-form label { display:grid; gap:6px; font-size:12px; font-weight:600; color:#4a4652; }
|
.change-password-form label { display:grid; gap:6px; font-size:12px; font-weight:600; color:#4a4652; }
|
||||||
.change-password-form input { padding:10px 12px; border:1px solid #E1DFE7; border-radius:10px; font-size:14px; }
|
.change-password-form .input-wrap { height:44px; margin-top:0; padding:0 12px; }
|
||||||
|
.change-password-form .input-wrap input { font-size:14px; }
|
||||||
.people-drawer-actions { margin-top:8px; display:flex; justify-content:flex-end; gap:10px; }
|
.people-drawer-actions { margin-top:8px; display:flex; justify-content:flex-end; gap:10px; }
|
||||||
.group-avatar { width:32px; height:32px; display:grid; place-items:center; border-radius:50%; font-size:12px; font-weight:700; flex-shrink:0; }
|
.group-avatar { width:32px; height:32px; display:grid; place-items:center; border-radius:50%; font-size:12px; font-weight:700; flex-shrink:0; }
|
||||||
.group-avatar.tone-green { background:#E8F7F3; color:#2d8a72; }
|
.group-avatar.tone-green { background:#E8F7F3; color:#2d8a72; }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue