Add the web app, seed data, and user-selectable theming
apps/web — React, Vite, Tailwind, shadcn-idiom components. Mobile Safari is a first-class target, not an afterthought: - Two navigation treatments rather than one compromise. A bottom tab bar on phones, because the top of a large phone is out of thumb reach; a persistent sidebar from lg upward, so an iPad in portrait gets it too. - Safe-area insets throughout, so the tab bar clears the home indicator and the last row of a list is actually reachable. - Inputs are pinned to a 16px minimum, which is the correct fix for Safari zooming on focus. user-scalable=no is not used: it breaks pinch-zoom for everyone and recent iOS ignores it anyway. - The pipeline board becomes a stage picker on phones. An eight-column board scrolling horizontally on a 390px screen is technically responsive and practically useless. Theming: users pick an accent and the whole interface re-tints. Accent values live once, in @pig/core, and are written onto the root element at runtime — there is no CSS copy to drift from the TypeScript. Preferences are stored server-side so they follow a person between laptop and phone, mirrored into localStorage only so the pre-paint script can avoid a white flash. Status colours stay fixed regardless of accent: if "at risk" re-tinted to whatever someone picked, the signal would be gone. Seed data is public research, every record carrying a confidence grade and a source URL. No email addresses are seeded or inferred — none are published, and guessing them from a name and a domain is unreliable and rude. Authorship is not promoted to employment: contributors, residency participants and alumni are recorded as what the evidence actually shows, and a name that could not be sourced at all is listed as unresolved rather than invented. Three defects found and fixed by actually running it rather than assuming: 1. The seed was not idempotent. onConflictDoNothing() with no target is a no-op without a matching unique constraint, so a second run duplicated 27 contacts. There is deliberately no unique index on (account, name) — two people at one company can share a name — so idempotency is enforced in the seed instead of by bending the schema. 2. /capacity scrolled sideways on a phone. Grid items default to min-width:auto and `truncate` sets nowrap, so a long title became unshrinkable content and widened the track. Fixed with min-w-0 on every truncating grid child. 3. The idle-capacity alert silently failed to fire at exactly 80% utilisation, losing a float comparison against a 0.2 threshold. Moved to 0.15, which is also a more sensible line for "worth attention". The worked example is tuned to teach rather than to flatter: 70% sold at a 53% markup lands at +6.7% margin with 20% still idle, so both the healthy number and the alert are visible. Drop the sold share to 55% and the same block goes underwater — that sensitivity is the argument for the product. Verified in a real browser at 393px and 1440px, light and dark: zero horizontal overflow on every route, zero console errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Sign-in.
|
||||
*
|
||||
* Magic link only. PIG stores no passwords, and adding a password field would
|
||||
* mean either storing one or pretending to — both worse than an email link for
|
||||
* an internal tool used by a couple of dozen people.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { getSupabase, type PublicConfig } from '@/lib/api';
|
||||
import { Button, Card, CardContent, Input } from '@/components/ui';
|
||||
import { PiggyMark } from '@/components/PiggyMark';
|
||||
|
||||
export function SignIn({ config }: { config: PublicConfig }) {
|
||||
const [email, setEmail] = useState('');
|
||||
const [status, setStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle');
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
async function submit(event: React.FormEvent) {
|
||||
event.preventDefault();
|
||||
const supabase = getSupabase();
|
||||
if (!supabase) {
|
||||
setStatus('error');
|
||||
setMessage('Authentication is not configured on this deployment.');
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('sending');
|
||||
const { error } = await supabase.auth.signInWithOtp({
|
||||
email,
|
||||
options: { emailRedirectTo: window.location.origin },
|
||||
});
|
||||
|
||||
if (error) {
|
||||
setStatus('error');
|
||||
// Supabase returns a clear message for rate limits and disabled signup,
|
||||
// both of which the person can act on, so it is shown rather than hidden.
|
||||
setMessage(error.message);
|
||||
return;
|
||||
}
|
||||
setStatus('sent');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-dvh items-center justify-center bg-bg px-6 py-12">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="mb-8 flex flex-col items-center gap-3 text-center">
|
||||
<PiggyMark className="h-14 w-14 text-fg" title="pig" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold lowercase tracking-tight">pig</h1>
|
||||
<p className="text-sm text-muted">Prime Intellect Growth</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-5">
|
||||
{status === 'sent' ? (
|
||||
<div className="space-y-2 text-center">
|
||||
<p className="font-medium">Check your email</p>
|
||||
<p className="text-sm text-muted">
|
||||
A sign-in link is on its way to {email}. It expires shortly, so use it soon.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={submit} className="space-y-3">
|
||||
<label className="block">
|
||||
<span className="mb-1 block text-sm font-medium">Email</span>
|
||||
<Input
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="you@company.com"
|
||||
autoComplete="email"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</label>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
disabled={status === 'sending'}
|
||||
>
|
||||
{status === 'sending' ? 'Sending…' : 'Email me a sign-in link'}
|
||||
</Button>
|
||||
{status === 'error' ? (
|
||||
<p className="text-sm text-danger">{message}</p>
|
||||
) : null}
|
||||
{config.inviteRequired ? (
|
||||
<p className="text-center text-xs text-muted">
|
||||
PIG is invite-only. An account alone does not grant access.
|
||||
</p>
|
||||
) : null}
|
||||
</form>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user