diff --git a/apps/web/public/images/pig-compute-field.webp b/apps/web/public/images/pig-compute-field.webp new file mode 100644 index 0000000..213103c Binary files /dev/null and b/apps/web/public/images/pig-compute-field.webp differ diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 9c39caf..cdf3ac9 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -11,7 +11,6 @@ import { IdentityProvider, useIdentityQuery } from '@/lib/identity'; import { LayoutProvider } from '@/lib/layout'; import { PiggyContextProvider } from '@/lib/piggy-context'; import { PlatformAudioProvider } from '@/lib/audio'; -import { AudioControl } from '@/components/AudioControl'; import { Shell } from '@/components/Shell'; import { SignIn } from '@/pages/SignIn'; import { CreateProfile } from '@/pages/CreateProfile'; @@ -108,25 +107,14 @@ export function App() { } /** - * The signed-out screens render outside Shell, so they get no app header — and - * therefore no way to stop the music. Audio a visitor cannot switch off is the - * worst version of this feature, so every unauthenticated screen gets the - * control pinned in a corner. + * The signed-out screens render outside Shell. Their shared AuthShell supplies + * the public header and its music control; keeping this boundary component + * means the auth gate does not need to know anything about that presentation. * * Learn is not wrapped: it brings its own chrome and already hosts one. */ function SignedOut({ children }: { children: React.ReactNode }) { - return ( - <> - {children} -
- -
- - ); + return children; } /** diff --git a/apps/web/src/components/AuthShell.tsx b/apps/web/src/components/AuthShell.tsx new file mode 100644 index 0000000..48a6945 --- /dev/null +++ b/apps/web/src/components/AuthShell.tsx @@ -0,0 +1,37 @@ +/** + * Public auth composition: generated compute field, grain and real form UI. + * + * The artwork is decoration only. Auth copy and controls stay in the normal + * document flow, so a missing image changes the mood rather than the task. + */ +import type { ReactNode } from 'react'; +import { PublicHeader } from './PublicHeader'; + +export function AuthShell({ children, onSignIn }: { children: ReactNode; onSignIn?: () => void }) { + return ( +
+ +
+
+
+ +
+
+
+ +
+
{children}
+
+
+
+ ); +} diff --git a/apps/web/src/components/PublicHeader.tsx b/apps/web/src/components/PublicHeader.tsx new file mode 100644 index 0000000..83cda5c --- /dev/null +++ b/apps/web/src/components/PublicHeader.tsx @@ -0,0 +1,63 @@ +/** + * The small amount of chrome shared by every public PIG screen. + * + * These routes deliberately live outside the authenticated Shell, but they + * should still feel like the same product. Keeping the wordmark, Learn link, + * sign-in affordance and music control here prevents the auth and shared + * Learn pages from drifting into separate mini-sites. + */ +import { AudioControl } from './AudioControl'; +import { PiggyLogo } from './PiggyMark'; +import { cn } from './ui'; + +type PublicDestination = 'learn' | 'sign-in'; + +export function PublicHeader({ + current, + onSignIn, +}: { + current?: PublicDestination; + onSignIn?: () => void; +}) { + const itemClass = (destination: PublicDestination) => + cn( + 'tap relative inline-flex items-center px-2 text-sm font-medium transition-colors', + current === destination ? 'text-fg' : 'text-muted hover:text-fg', + current === destination && + 'after:absolute after:inset-x-2 after:bottom-1 after:h-px after:bg-current', + ); + + return ( +
+
+ + + + + +
+
+ ); +} diff --git a/apps/web/src/index.css b/apps/web/src/index.css index e3b6a94..ee94810 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -177,6 +177,82 @@ background-attachment: fixed; } + /* + * The auth surface is intentionally monochrome even if a signed-in user has + * selected another accent. It is public brand chrome, and forcing the local + * dark token set keeps controls legible without changing the stored theme. + */ + .auth-shell { + --bg: 240 10% 3%; + --surface: 240 7% 6%; + --surface-2: 240 5% 11%; + --border: 240 4% 22%; + --fg: 0 0% 98%; + --muted: 240 5% 67%; + --accent: 0 0% 98%; + --accent-fg: 0 0% 98%; + --accent-on: 240 6% 10%; + --accent-subtle: 240 4% 16%; + color-scheme: dark; + } + + .auth-field-motion { + animation: auth-field-drift 26s ease-in-out infinite alternate; + will-change: transform; + } + + .auth-visual { + opacity: 0.52; + } + + .auth-visual::after { + position: absolute; + inset: 0; + content: ''; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.92' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.72'/%3E%3C/svg%3E"); + background-size: 180px 180px; + mix-blend-mode: soft-light; + opacity: 0.14; + } + + .auth-visual-vignette { + background: + linear-gradient(to bottom, hsl(var(--bg) / 0.18), hsl(var(--bg) / 0.72) 34%, hsl(var(--bg)) 100%), + linear-gradient(to right, hsl(var(--bg) / 0.18), hsl(var(--bg) / 0.42)); + } + + @media (min-width: 1024px) { + .auth-visual { + opacity: 1; + } + + .auth-visual-vignette { + background: + linear-gradient(to right, transparent 48%, hsl(var(--bg) / 0.28) 78%, hsl(var(--bg) / 0.72)), + linear-gradient(to bottom, hsl(var(--bg) / 0.06), transparent 18%, hsl(var(--bg) / 0.28)); + } + } + + @keyframes auth-field-drift { + 0% { + transform: translate3d(-1.4%, -0.8%, 0) scale(1.045); + } + 50% { + transform: translate3d(0.8%, 0.6%, 0) scale(1.075); + } + 100% { + transform: translate3d(-0.3%, 1.1%, 0) scale(1.055); + } + } + + @media (prefers-reduced-motion: reduce) { + .auth-field-motion { + animation: none; + transform: scale(1.045); + will-change: auto; + } + } + /* Tabular figures keep money and percentages from jittering as they update, which matters on a dashboard that refreshes. */ .nums { diff --git a/apps/web/src/pages/CreateProfile.tsx b/apps/web/src/pages/CreateProfile.tsx index 25f7b05..1ca6109 100644 --- a/apps/web/src/pages/CreateProfile.tsx +++ b/apps/web/src/pages/CreateProfile.tsx @@ -9,8 +9,8 @@ import { useState } from 'react'; import { TEAMS, TEAM_DESCRIPTIONS, TEAM_LABELS, type Team } from '@pig/core'; import { ApiError, post, type PublicConfig } from '@/lib/api'; -import { Button, Card, CardContent, Input } from '@/components/ui'; -import { PiggyMark } from '@/components/PiggyMark'; +import { Button, Input } from '@/components/ui'; +import { AuthShell } from '@/components/AuthShell'; import { usePageTitle } from '@/lib/title'; export function CreateProfile({ @@ -51,110 +51,109 @@ export function CreateProfile({ } return ( -
-
-
- -
-

Set up your profile

-

- You're already authenticated. This creates a PIG profile only after workspace access is verified. -

-
+ +
+
+

+ Private workspace +

+

+ Set up your profile +

+

+ You're already authenticated. This creates a PIG profile only after workspace access is + verified. +

- - -
- + + - + -
- Which team are you on? -
- {TEAMS.map((value) => ( - - ))} -
-

- You can be added to more teams later. -

-
- - {config.inviteRequired ? ( -
-
+ {error ? ( +

+ {error} +

+ ) : null} + + +
-
+ ); } diff --git a/apps/web/src/pages/Learn.tsx b/apps/web/src/pages/Learn.tsx index db803c5..da3b360 100644 --- a/apps/web/src/pages/Learn.tsx +++ b/apps/web/src/pages/Learn.tsx @@ -55,8 +55,7 @@ import { usePageTitle } from '@/lib/title'; import { usePiggyContext } from '@/lib/piggy-context'; import { toPiggyPageRoute } from '@pig/core'; import { Badge, Button, Card, EmptyState, Skeleton } from '@/components/ui'; -import { AudioControl } from '@/components/AudioControl'; -import { PiggyMark } from '@/components/PiggyMark'; +import { PublicHeader } from '@/components/PublicHeader'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { AddResourceDialog } from '@/components/learn/AddResourceDialog'; import { LearnAccessHero } from '@/components/learn/LearnAccessHero'; @@ -414,21 +413,7 @@ function CodeHolderView({ function AnonFrame({ children }: { children: ReactNode }) { return (
-
- - - pig - - - {/* The anonymous page renders outside Shell, so it does not get the - app header — but it DOES get the music, and music with no way to - stop it is the worst version of this feature. */} - - - -
+
{children}
diff --git a/apps/web/src/pages/Register.tsx b/apps/web/src/pages/Register.tsx index 124c5ed..00c7a2c 100644 --- a/apps/web/src/pages/Register.tsx +++ b/apps/web/src/pages/Register.tsx @@ -11,8 +11,8 @@ import { useState } from 'react'; import { ArrowLeft } from 'lucide-react'; import { TEAMS, TEAM_DESCRIPTIONS, TEAM_LABELS, type Team } from '@pig/core'; import { ApiError, getSupabase, post, type PublicConfig } from '@/lib/api'; -import { Button, Card, CardContent, Input } from '@/components/ui'; -import { PiggyMark } from '@/components/PiggyMark'; +import { Button, Input } from '@/components/ui'; +import { AuthShell } from '@/components/AuthShell'; import { usePageTitle } from '@/lib/title'; export function Register({ @@ -66,9 +66,7 @@ export function Register({ } onRegistered(); } catch (err) { - setError( - err instanceof ApiError ? err.message : 'Could not create your account. Try again.', - ); + setError(err instanceof ApiError ? err.message : 'Could not create your account. Try again.'); } finally { setBusy(false); } @@ -76,160 +74,163 @@ export function Register({ if (!config.canSelfRegister) { return ( - - - -

Registration is not open here

-

- This deployment cannot create accounts on its own. Ask an administrator to - provision one for you, then sign in. -

- -
-
-
+ +
+

Registration is not open here

+

+ This deployment cannot create accounts on its own. Ask an administrator to provision one + for you, then sign in. +

+ +
+
); } return ( - -
- -
-

Create your account

-

- Use an email you control. A valid PIG invite is required before the server creates workspace access. + +

+
+

+ Private workspace

-

+

+ Create your account +

+

+ Use an email you control. A valid PIG invite is required before the server creates + workspace access. +

+

This flow does not enable open registration on the shared identity provider.

+ +
+ + + + + + + + + + +
+ Which team are you on? +
+ {TEAMS.map((value) => ( + + ))} +
+
+ + {error ? ( +

+ {error} +

+ ) : null} + + + + +
- - - -
- - - - - - - - - - -
- Which team are you on? -
- {TEAMS.map((value) => ( - - ))} -
-
- - {error ? ( -

- {error} -

- ) : null} - - - - -
-
-
- - ); -} - -function Centered({ children }: { children: React.ReactNode }) { - return ( -
-
{children}
-
+ ); } diff --git a/apps/web/src/pages/SignIn.tsx b/apps/web/src/pages/SignIn.tsx index b8f6212..e056f52 100644 --- a/apps/web/src/pages/SignIn.tsx +++ b/apps/web/src/pages/SignIn.tsx @@ -12,10 +12,10 @@ * mistakes. */ import { useState } from 'react'; -import { KeyRound, Mail } from 'lucide-react'; +import { Mail } from 'lucide-react'; import { getSupabase, type PublicConfig } from '@/lib/api'; -import { Button, Card, CardContent, Input } from '@/components/ui'; -import { PiggyMark } from '@/components/PiggyMark'; +import { Button, Input } from '@/components/ui'; +import { AuthShell } from '@/components/AuthShell'; import { usePageTitle } from '@/lib/title'; type Method = 'password' | 'link'; @@ -75,159 +75,154 @@ export function SignIn({ } return ( -
-
-
- -
-

pig

-

Prime Intellect Growth

+ +
+ {status === 'sent' ? ( +
+ + + +
+

Check your email

+

+ A sign-in link is on its way to {email}. It expires shortly, so use it soon. +

+
+
-
- - - - {status === 'sent' ? ( -
- -
-

Check your email

-

- A sign-in link is on its way to {email}. It expires shortly, so use it - soon. -

-
+ ) : ( + <> +
+

+ Private workspace +

+

+ Sign in to PIG +

+

+ Authentication stays with the deployment's identity provider. PIG never stores your + password. +

+
+
+ {( + [ + { key: 'password', label: 'Password' }, + { key: 'link', label: 'Email link' }, + ] as const + ).map((option) => ( -
- ) : ( - <> -
-

- Private workspace + ))} +

+ +
+ + + {method === 'password' ? ( + + ) : null} + + + + {status === 'error' ? ( +

+ {message} +

+ ) : null} + + {config.canSelfRegister ? ( +
+

+ Have a PIG invite code but no account? Registration remains closed on the shared + identity provider.

-

Sign in to PIG

-

- Authentication stays with the deployment's identity provider. PIG never stores your password. -

-
-
- {( - [ - { key: 'password', label: 'Password', icon: KeyRound }, - { key: 'link', label: 'Email link', icon: Mail }, - ] as const - ).map((option) => ( - - ))} -
- - - - - {method === 'password' ? ( - - ) : null} - - - - {status === 'error' ? ( -

- {message} -

- ) : null} - - {config.canSelfRegister ? ( -
-

- Have a PIG invite code but no account? Registration remains closed on the shared identity provider. -

- -
- ) : config.inviteRequired ? ( -

- This workspace is invite-only. Ask an administrator to provision access; this screen never opens self-registration. -

- ) : null} -
- - )} - - + Create an account + +
+ ) : config.inviteRequired ? ( +

+ This workspace is invite-only. Ask an administrator to provision access; this + screen never opens self-registration. +

+ ) : null} + + + )}
-
+ ); }