OpalShaderShell
Three auth surfaces (`/login`, `/oauth/select-account`, `/oauth/consent`) share one shell that bundles the static auth background, the centered content column, and the beacon plumbing the brand mark needs to paint.
The web app has three auth-adjacent surfaces:
| Route | Component |
|---|---|
apps/web/src/app/(auth)/login/page.tsx | LoginShell |
apps/web/src/app/(oauth)/oauth/select-account/page.tsx | SelectAccountShell |
apps/web/src/app/(oauth)/oauth/consent/page.tsx | AuthorizeShell |
All three are thin aliases over OpalShaderShell —
apps/web/src/components/app-auth/opal-shader-shell.tsx.
The shell exists because writing three independent layouts was the original
approach and it left the brand mark broken on two of them.
What the shell bundles
<OpalShaderShell>
<YourAuthStepComponent />
</OpalShaderShell>…internally expands to:
<TodayStaticBackground variant='auth' />
<BeaconProvider>
<OpalNarrativeMarkPoseProvider>
<main className='relative mx-auto max-w-150 ...'>
{children}
</main>
<OpalNarrativeMarkSlot />
</OpalNarrativeMarkPoseProvider>
</BeaconProvider>The pieces and what each does:
| Piece | Why it's there |
|---|---|
TodayStaticBackground | The static auth background |
max-w-150 content column | 600 px centered column (Opal design system standard width) |
BeaconProvider | Owns the beacon position store |
OpalNarrativeMarkPoseProvider | Owns the brand-mark pose / animation state |
OpalNarrativeMarkSlot | The actual <OpalNarrativeMark /> painter |
The brand-mark trap
The <OpalNarrativeMark /> placeholders that step components mount are
pure geometry. They reserve layout footprint (a fixed-size box where the
mark should go) and register their position into a beacon store. They do
not paint anything by themselves.
If you mount the placeholder without BeaconProvider + OpalNarrativeMarkSlot
in a parent, the slot stays empty and the Today mark never renders. The
layout looks right; the mark just doesn't appear.
This is exactly what happened to LoginShell and friends pre-shell — they
were one-off auth shells that mounted their background directly without the
beacon plumbing. The result was three auth pages with blank spots where the
brand mark should be.
The shell brings the missing pieces in one place.
When to use it
For any new auth-adjacent page: mount it via OpalShaderShell. Mounting
the shared background directly is wrong (you'd have to re-import all the
beacon plumbing). Wrapping a step component in a custom shell is also wrong.
// In a new page.tsx under (auth)/ or (oauth)/
import { OpalShaderShell } from '@/components/app-auth/opal-shader-shell'
import { MyNewAuthStep } from '@/components/app-auth/my-new-auth-step'
export default function Page() {
return (
<OpalShaderShell>
<MyNewAuthStep />
</OpalShaderShell>
)
}Test mocks
Unit tests for auth / OAuth pages stub @todayai-labs/opal-ui with passthrough
fragments for BeaconProvider, OpalNarrativeMarkPoseProvider,
OpalNarrativeMarkSlot, plus OpalLoadingOrbs testid stubs. The mock setup is
in per-test vi.mock(...) calls.
When you add a new Opal primitive that gets mounted by an auth-page shell, extend the mocks or the test file fails with:
Error: No "<Name>" export is defined on the "@todayai-labs/opal-ui" mockThat's a familiar error — it just means a real-component import is hitting the mock surface that hasn't grown to cover it yet.
Related
- Auth interaction architecture — what the three surfaces do in the OAuth dance
- Better-Auth quirks — the client-side request injection that fires from these pages
- Workspace → Design system — what
@todayai-labs/opal-uiexports and how the placeholders relate to the rest of the design system
Better-Auth quirks
The `oauthProviderClient` plugin auto-injects `oauth_query` into non-GET request bodies. What that means in practice and the workaround for calls that shouldn't progress the OAuth flow.
Cross-platform headers
How web and Electron identify Linux, macOS, and Windows requests with X-Client-Platform and X-App-Version, including the Linux cloud rollout boundary.