fix mobile screen issues

This commit is contained in:
2026-08-24 13:14:22 +05:30
parent dcf9770ada
commit 02eb48af99
34 changed files with 1214 additions and 291 deletions

View File

@@ -4,7 +4,7 @@ import { Sparkles, ChevronDown, ChevronUp, Sliders, Loader2, Star } from 'lucide
import { useCreateJobPosting, useUpdateJobPosting, useGenerateJobDescription, useJobPosting, useRoleCategories, useCreateRoleCategory } from '@/lib/krowHooks';
import { toast } from 'react-hot-toast';
import { ROLE_CATEGORIES } from '@/lib/roleCategories';
import { CERT_OPTIONS, CRITERIA_LABELS, ENGLISH_LEVELS, defaultPosition } from '@/lib/positionModel';
import { CERT_OPTIONS, CRITERIA_LABELS, ENGLISH_LEVELS, defaultPosition, toPositionPayload } from '@/lib/positionModel';
import { SkillRequirementsField } from '@/components/krow/SkillRequirementsField';
import { SkillSurface } from '@/components/skills/SkillSurface';
import OwliverAvatar from '@/components/krow/OwliverAvatar';
@@ -130,11 +130,11 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
// Create draft if not exists
let id = draftId;
if (!id) {
const draft = await createJob.mutateAsync({ ...form, status: 'draft', pay_range_min: Number(form.pay_range_min) || 0, pay_range_max: Number(form.pay_range_max) || 0 });
const draft = await createJob.mutateAsync(toPositionPayload(form, { status: 'draft' }));
id = draft.id;
setDraftId(id);
}
const result = await generateDesc.mutateAsync({ data: { ...form, pay_range_min: Number(form.pay_range_min) || 0, pay_range_max: Number(form.pay_range_max) || 0 }, draftId: id });
const result = await generateDesc.mutateAsync({ data: toPositionPayload(form), draftId: id });
setAiResult(result);
toast.success('Job description generated!');
} catch {
@@ -153,11 +153,7 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
}
try {
const payload = {
...form,
status,
company: String(form.company || '').trim(),
pay_range_min: Number(form.pay_range_min) || 0,
pay_range_max: Number(form.pay_range_max) || 0,
...toPositionPayload(form, { status }),
...(aiResult ? { description: aiResult.description, responsibilities: aiResult.responsibilities, qualifications: aiResult.qualifications, nice_to_haves: aiResult.nice_to_haves, ai_generated: true } : {}),
};
let createdId = draftId;
@@ -190,12 +186,15 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
<div className={embedded ? '' : 'max-w-3xl mx-auto'}>
{/* Header */}
{!embedded && (
<div className="flex items-center justify-between mb-6">
<div>
<div className="flex items-start justify-between gap-4 mb-6">
<div className="min-w-0">
<h1 className="text-[22px] font-bold text-[#111827]">Create a Position</h1>
<p className="text-[13px] text-[#6B7280] mt-0.5">Set hiring criteria once — KROW interviews and ranks every applicant automatically.</p>
</div>
<button onClick={() => navigate('/admin/positions')} className="text-[13px] text-[#6B7280] hover:text-[#111827]">Cancel</button>
{/* `shrink-0` and a real gap: without them the subtitle's intrinsic
width pushed Cancel against the edge of a phone screen with the
title wrapping underneath it. */}
<button onClick={() => navigate('/admin/positions')} className="shrink-0 pt-1 text-[13px] text-[#6B7280] hover:text-[#111827]">Cancel</button>
</div>
)}

View File

@@ -1,13 +1,19 @@
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { hasAdminSession } from '@/lib/admin/session';
import { useAuth } from '@/lib/AuthContext';
/**
* Gate on the Admin console.
*
* Without this, `/admin` renders the Control Center for anyone who types the URL,
* because the demo's data client reports the seeded user as permanently signed in
* (see session.js). The console is meant to be entered through its own sign-in,
* so the guard sends an unauthenticated visitor to the login page.
* It asks the auth context, which reflects `GET /me` — a real question to the
* server about a real session — rather than the `sessionStorage` flag it used
* to read. That flag could be set from the browser console; a session row keyed
* by an HttpOnly cookie cannot.
*
* This guard is deliberately kept even though it now sits inside
* `ProtectedRoute`, which checks the same thing. It is not redundant defence in
* depth for its own sake: it is the component that owns "the Admin console is
* entered through its own sign-in", and it is where a role check will go in
* Phase 3D, when being signed in stops being sufficient to be here.
*
* The attempted path travels along in location state, so signing in returns you
* to where you were going rather than dropping you on the Control Center. That
@@ -16,8 +22,14 @@ import { hasAdminSession } from '@/lib/admin/session';
*/
export default function AdminRoute() {
const location = useLocation();
const { isAuthenticated, isLoadingAuth, authChecked } = useAuth();
if (!hasAdminSession()) {
// ProtectedRoute has already waited for the check, so this is belt and
// braces: without it a direct render of this route would redirect to the
// login page during the first tick and bounce a signed-in operator out.
if (isLoadingAuth || !authChecked) return null;
if (!isAuthenticated) {
return <Navigate to="/admin/login" replace state={{ from: location.pathname }} />;
}

View File

@@ -250,7 +250,7 @@ export default function AdminAnalytics() {
/>
<Surface variant="solid" radius="lg" padding="none" elevation="xs" className="overflow-hidden border border-border">
<div className="overflow-x-auto">
<table className="w-full border-collapse text-body-sm">
<table className="w-full min-w-[36rem] border-collapse text-body-sm">
<caption className="sr-only">Hires, quality, speed and review outcome by role</caption>
<thead>
<tr className="border-b border-border bg-surface-subtle">

View File

@@ -325,7 +325,7 @@ export default function AdminCandidatesAnalysis() {
<SectionTitle id="fit" title="Position fit" meta={`${positionFit.length} open roles`} />
<Surface variant="solid" radius="lg" padding="none" elevation="xs" className="overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full border-collapse text-body-sm">
<table className="w-full min-w-[40rem] border-collapse text-body-sm">
<caption className="sr-only">
Applicant supply against candidates clearing the bar, per open role
</caption>

View File

@@ -922,7 +922,7 @@ export default function ControlCenter() {
>
<div className="overflow-hidden rounded-xl border border-border bg-surface">
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<table className="w-full min-w-[36rem] border-collapse">
<caption className="sr-only">Most recent platform activity</caption>
<thead>
<tr className="border-b border-border bg-surface-subtle">

View File

@@ -4,14 +4,27 @@ import { AlertCircle, ArrowRight, Loader2, Eye, EyeOff, Sparkles, CheckCircle2 }
import { cn } from '@/lib/utils';
import { KROW_LOGO_URL } from '@/assets/brand';
import { Checkbox } from '@/components/ds';
import { base44 } from '@/api/base44Client';
import { startAdminSession } from '@/lib/admin/session';
import { useAuth } from '@/lib/AuthContext';
const DEMO_EMAIL = 'demo@krow.app';
/**
* The one message every failed sign-in shows.
*
* The API answers a wrong password, an unknown address, an account with no
* password and a suspended account with the same 401 and the same body, so that
* a stranger cannot learn which addresses are registered. Rendering the server's
* message verbatim would be equally correct; this is the same statement in the
* product's own voice, and — importantly — it is used for every failure, so this
* page does not reintroduce the distinction the API removes.
*/
const GENERIC_AUTH_ERROR =
'That email and password do not match an account. Check both and try again.';
export default function AdminLogin() {
const navigate = useNavigate();
const location = useLocation();
const { login } = useAuth();
const from = location.state?.from;
const returnTo = typeof from === 'string' && from.startsWith('/admin') && from !== '/admin/login'
@@ -28,8 +41,11 @@ export default function AdminLogin() {
const next = {};
if (!form.email.trim()) next.email = 'Enter your work email';
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) next.email = 'Enter a valid email address';
// Only "did you type anything". A client-side length rule here would be a
// guess at the server's policy that goes stale the moment the policy moves,
// and it tells someone with a valid short legacy password that their own
// password is invalid.
if (!form.password) next.password = 'Enter your password';
else if (form.password.length < 8) next.password = 'Passwords are at least 8 characters';
setErrors(next);
return Object.keys(next).length === 0;
};
@@ -40,17 +56,26 @@ export default function AdminLogin() {
if (!validate()) return;
setStatus('loading');
await new Promise((r) => setTimeout(r, 900));
if (form.password.toLowerCase() === 'wrong') {
try {
// The session is created server-side and arrives as an HttpOnly cookie.
// Nothing on this page sees, stores or forwards a token — there is no
// token here to mishandle.
await login({
email: form.email.trim(),
password: form.password,
rememberMe: form.remember,
});
} catch (error) {
setStatus('idle');
setAuthError('That email and password do not match an administrator account.');
// One message for every rejection. A rate-limited attempt is the single
// exception worth distinguishing, because "wait a moment" is advice the
// person can act on and it reveals nothing about any account.
setAuthError(error?.status === 429
? (error.message || 'Too many sign-in attempts. Wait a few minutes and try again.')
: GENERIC_AUTH_ERROR);
return;
}
await base44.auth.updateMe({ account_type: 'admin' });
localStorage.setItem('activeRole', 'admin');
startAdminSession();
setStatus('success');
setTimeout(() => navigate(returnTo, { replace: true }), 320);
};
@@ -192,7 +217,7 @@ export default function AdminLogin() {
<button
type="button"
disabled={busy}
onClick={() => setAuthError('Password resets are disabled in the demo.')}
onClick={() => setAuthError('Password resets are not available yet — ask an administrator to set a new password.')}
className="text-[#D97706] hover:text-[#B45309] font-semibold transition-colors"
>
Forgot password?
@@ -213,7 +238,8 @@ export default function AdminLogin() {
</form>
<p className="text-center text-xs text-[#94A3B8] leading-relaxed pt-2">
Demo environment — enter any 8+ character password to sign in. Use <span className="text-[#64748B] font-semibold">wrong</span> to test error validation.
Sign in with your Krow account. Passwords are set by an administrator with
the <span className="text-[#64748B] font-semibold">setpassword</span> utility.
</p>
</div>
</div>

View File

@@ -91,8 +91,10 @@ export default function AdminProfile() {
<AdminPage
title="Admin Profile"
subtitle="Manage your platform credentials, system permissions and security governance."
/* Three buttons need 295px; a 320px phone has 273 after the page gutters,
so the row wraps instead of pushing Sign Out off the edge. */
actions={
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-2">
<button
onClick={() => navigate('/admin/settings')}
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-white border border-[#E0E4EA] text-[#101820] text-[13px] font-semibold hover:bg-[#F8F8F8] transition-all shadow-xs"
@@ -173,13 +175,13 @@ export default function AdminProfile() {
</div>
{/* Quick stats in Hero */}
<div className="flex items-center gap-3 border-t md:border-t-0 md:border-l border-white/20 pt-4 md:pt-0 md:pl-6">
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<div className="flex flex-wrap items-center gap-3 border-t md:border-t-0 md:border-l md:flex-nowrap border-white/20 pt-4 md:pt-0 md:pl-6">
<div className="flex-1 md:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Scopes</p>
<p className="text-2xl font-bold text-white mt-0.5">{PERMISSIONS.length}</p>
<span className="text-[10px] text-blue-200">Full Control</span>
</div>
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<div className="flex-1 md:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Security Level</p>
<p className="text-2xl font-bold text-emerald-300 mt-0.5">Tier 1</p>
<span className="text-[10px] text-blue-200">Max Privileges</span>

View File

@@ -55,18 +55,18 @@ export default function AdminSettings() {
</p>
</div>
<div className="flex items-center gap-3 shrink-0">
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<div className="flex flex-wrap items-center gap-3 sm:flex-nowrap sm:shrink-0">
<div className="flex-1 sm:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Active Staff</p>
<p className="text-2xl font-bold text-white mt-0.5">{staff.length}</p>
<span className="text-[10px] text-blue-200">On Platform</span>
</div>
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<div className="flex-1 sm:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Positions</p>
<p className="text-2xl font-bold text-emerald-300 mt-0.5">{postings.length}</p>
<span className="text-[10px] text-blue-200">Active Roles</span>
</div>
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<div className="flex-1 sm:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Clients</p>
<p className="text-2xl font-bold text-sky-300 mt-0.5">{clientCount}</p>
<span className="text-[10px] text-blue-200">Organizations</span>

View File

@@ -83,8 +83,8 @@ export default function AdminWorkspace() {
</p>
</div>
<div className="flex items-center gap-3 shrink-0">
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<div className="flex flex-wrap items-center gap-3 sm:flex-nowrap sm:shrink-0">
<div className="flex-1 sm:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Registered Skills</p>
<p className="text-2xl font-bold text-white mt-0.5">{skills.length}</p>
<span className="text-[10px] text-emerald-300 font-medium">{activeSkills} Active</span>
@@ -92,7 +92,7 @@ export default function AdminWorkspace() {
{/* Was "Training Paths". Replaced rather than dropped: the slot is
useful, and the split of agent skills across the two lists is
what this page actually governs. */}
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<div className="flex-1 sm:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Skill Kinds</p>
<p className="text-2xl font-bold text-sky-300 mt-0.5">{owliverCount + boardCount}</p>
<span className="text-[10px] text-blue-200">{owliverCount} Owliver · {boardCount} Board</span>

View File

@@ -197,7 +197,12 @@ function AgentTableRow({ agent, shipped, overridden, onOpen, onAction }) {
<button
type="button"
onClick={onOpen}
className="font-heading text-body-sm font-semibold text-ink-1 hover:text-krow-blue transition-colors text-left focus-visible:outline-none"
/* On a phone the identity column claims the width of the name
rather than stacking "Control Center Agent" three words tall,
which it can afford because the table already scrolls inside
its own container there. Released at `sm`, so every wider
layout distributes its columns exactly as it did before. */
className="whitespace-nowrap sm:whitespace-normal font-heading text-body-sm font-semibold text-ink-1 hover:text-krow-blue transition-colors text-left focus-visible:outline-none"
>
{agent.name}
</button>
@@ -493,7 +498,7 @@ export default function AdminWorkspaceAgents() {
/* Clean Modern Directory Table View */
<div className="overflow-hidden rounded-2xl border border-border bg-surface shadow-xs">
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<table className="w-full min-w-[48rem] text-left border-collapse">
<thead>
<tr className="border-b border-border bg-surface-subtle/60 text-[11px] font-semibold uppercase tracking-wider text-ink-4">
<th className="py-3 pl-4 pr-3 font-semibold">Agent</th>