Auth blocks
Sign-in, sign-up, and password flows with the validation and OAuth patterns users expect.
Centred card
The workhorse sign-in UI: a centred card with OAuth, a trust footer, and a password reveal driven by one client signal. Markup only, with no server behind it, so it does not sign anyone in.
-
components
-
arden
-
centred_card
-
-
templates
-
pages
-
<c-lbr.signals id="centred-card-ui" $reveal="false" />
<main class="relative isolate overflow-hidden min-h-screen flex items-center justify-center px-4 py-12 bg-base-100">
<div class="pointer-events-none absolute inset-x-0 top-0 h-[30rem] -z-10"
style="background:
radial-gradient(52% 68% at 18% 0%, #0f766e 0%, transparent 68%),
radial-gradient(48% 62% at 78% 4%, #1d4ed8 0%, transparent 70%);
opacity: 0.10;
-webkit-mask-image: linear-gradient(black, transparent);
mask-image: linear-gradient(black, transparent);"
aria-hidden="true"></div>
<div class="w-full max-w-md flex flex-col gap-6">
<c-centred-card.mark />
<c-centred-card.card />
<c-centred-card.assurance />
</div>
</main>
<div class="flex flex-col items-center gap-3 border-t border-base-300 pt-5">
<div class="flex flex-wrap items-center justify-center gap-x-4 gap-y-2 text-xs text-base-content/50">
<span class="flex items-center gap-1.5">
<c-lbi n="rmx.shield-check" w="14" h="14" />
SOC 2 Type II · Audited February 2026
</span>
<span class="flex items-center gap-1.5">
<c-lbi n="rmx.lock-password" w="14" h="14" />
SSO and SCIM on Scale
</span>
</div>
<p class="text-xs text-base-content/40 text-center">
2,400+ finance and ops teams close their quarter on Arden.
</p>
</div>
<c-lb.card border class="bg-base-100 shadow-sm">
<c-lb.card.body class="gap-5 p-7">
<div class="flex flex-col gap-1.5">
<h1 class="text-2xl font-semibold tracking-tight">Sign in to your workspace</h1>
<p class="text-sm text-base-content/60">
Arden reconciled 18,402 invoices for your team overnight. Pick up the numbers
where they landed.
</p>
</div>
<c-centred-card.oauth />
<c-lb.divider class="my-0 text-xs text-base-content/40">or with your email</c-lb.divider>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label for="cc-email" class="label label-text text-sm font-medium">Work email</label>
<c-lb.input
id="cc-email"
type="email"
name="email"
class="w-full"
autocomplete="username"
placeholder="[email protected]" />
</div>
<c-centred-card.password />
<div class="flex items-center justify-between">
<label class="flex items-center gap-2 text-sm text-base-content/70 cursor-pointer">
<c-lb.checkbox id="cc-remember" size="sm" name="remember" />
Keep me signed in for 30 days
</label>
<a href="#" class="link link-hover text-sm text-base-content/60">Forgot password?</a>
</div>
<c-lb.button id="cc-submit" type="button" variant="primary" modifier="block">
Sign in
</c-lb.button>
</div>
<p class="text-center text-sm text-base-content/60">
New to Arden?
<a href="#" class="link link-primary">Start a 14-day trial</a>
</p>
</c-lb.card.body>
</c-lb.card>
<div class="flex flex-col items-center gap-3">
<c-arden.mark size="sm" />
<div class="flex items-center gap-2">
<span class="text-lg font-semibold tracking-tight">Arden</span>
<c-lb.badge style="soft" size="sm">Revenue ops</c-lb.badge>
</div>
</div>
<div class="grid grid-cols-3 gap-2">
<c-lb.button type="button" btnStyle="outline" icon="rmx.google" size="sm">Google</c-lb.button>
<c-lb.button type="button" btnStyle="outline" icon="rmx.apple" size="sm">Apple</c-lb.button>
<c-lb.button type="button" btnStyle="outline" icon="rmx.github" size="sm">GitHub</c-lb.button>
</div>
<div class="flex flex-col gap-1">
<label for="cc-password" class="label label-text text-sm font-medium">Password</label>
<div class="relative">
<c-lb.input
id="cc-password"
type="password"
name="password"
class="w-full pr-11"
autocomplete="current-password"
placeholder="••••••••"
data-attr:type="$reveal ? 'text' : 'password'" />
<button
id="cc-reveal"
type="button"
class="absolute inset-y-0 right-0 flex items-center px-3 text-base-content/50 hover:text-base-content"
aria-label="Show password"
data-attr:aria-label="$reveal ? 'Hide password' : 'Show password'"
data-on:click="$reveal = !$reveal">
<span data-show="!$reveal">
<c-lbi n="rmx.eye" w="18" h="18" />
</span>
<span data-show="$reveal" style="display: none">
<c-lbi n="rmx.eye-off" w="18" h="18" />
</span>
</button>
</div>
</div>
{% load lb_tags %}
<c-vars
size="sm"
:tile="{'xs':'w-6 h-6 rounded-md','sm':'w-8 h-8 rounded-lg','md':'w-10 h-10 rounded-xl','lg':'w-12 h-12 rounded-xl'}"
:icon="{'xs':'w-4 h-4','sm':'w-5 h-5','md':'w-6 h-6','lg':'w-7 h-7'}"
/>
<span class="inline-flex shrink-0 items-center justify-center bg-primary text-primary-content {{ tile|get_dict_item:size }}" {{ attrs }}>
<svg viewBox="0 0 48 48" class="{{ icon|get_dict_item:size }}" fill="none" aria-hidden="true">
<path d="M12 36 24 12l12 24" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M17 27h14" stroke="currentColor" stroke-width="5" stroke-linecap="round" />
</svg>
</span>
<c-vars active="overview" drawerId="" />
<div class="flex h-full flex-col bg-base-100 border-r border-base-300 transition-[width] is-drawer-close:w-16 is-drawer-open:w-60">
{% if drawerId %}
<c-lb.drawer.toggle
for="{{ drawerId }}"
class="btn btn-square btn-sm fixed top-16 z-50 hidden -translate-x-1/2 border border-base-300 bg-base-100 shadow-sm transition-[left] duration-200 lg:flex is-drawer-close:left-16 is-drawer-open:left-60"
aria-label="Toggle sidebar">
<c-lbi n="rmx.sidebar-unfold" w="18" h="18" />
</c-lb.drawer.toggle>
{% endif %}
<div class="flex h-16 items-center gap-3 px-4">
<c-arden.mark size="xs" />
<span class="font-semibold tracking-tight is-drawer-close:hidden">Arden</span>
</div>
<nav class="grow px-2 py-3">
<ul class="menu gap-1 p-0">
<li><a href="#" class="{% if active == 'overview' %}active {% endif %}is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Overview"><c-lbi n="rmx.pie-chart-2" w="18" h="18" class="shrink-0" /><span class="is-drawer-close:hidden">Overview</span></a></li>
<li><a href="#" class="{% if active == 'customers' %}active {% endif %}is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Customers"><c-lbi n="rmx.group-2" w="18" h="18" class="shrink-0" /><span class="is-drawer-close:hidden">Customers</span></a></li>
<li><a href="#" class="{% if active == 'activity' %}active {% endif %}is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Activity"><c-lbi n="rmx.pulse" w="18" h="18" class="shrink-0" /><span class="is-drawer-close:hidden">Activity</span></a></li>
<li><a href="#" class="{% if active == 'reports' %}active {% endif %}is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Reports"><c-lbi n="rmx.bar-chart-2" w="18" h="18" class="shrink-0" /><span class="is-drawer-close:hidden">Reports</span></a></li>
</ul>
</nav>
<div class="border-t border-base-300 p-2">
<a href="#" class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-base-content/60 hover:bg-base-200 is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Settings">
<c-lbi n="rmx.settings-3" w="18" h="18" class="shrink-0" />
<span class="is-drawer-close:hidden">Settings</span>
</a>
</div>
</div>
Learn from this block
-
The card needs exactly one piece of client state — whether the password is showing. It is declared here in prop mode: no schema, no view, no store.
-
Clicking the eye toggles the signal. That is the whole interaction — no handler to register, no request on the wire, and nothing for a server to do.
-
The field is rendered as a password input by the server, so it is correct before any JavaScript runs. The signal only ever swaps the type attribute, which is the smallest change that does the job.
-
A centred card is the easiest layout to leave bare. The OAuth row, the trust line and the audit footer are what turn it into a page — and every colour on it comes from a theme token, so it recolours rather than breaking.
Magic link
Passwordless sign-in UI: the form becomes a 'check your inbox' state from one client signal. Markup only, with no server behind it, so it does not send a link or sign anyone in.
-
components
-
arden
-
magic_link
-
-
templates
-
pages
-
<c-lbr.signals id="magic-link-ui" $email="" $sent="false" />
<main class="relative isolate overflow-hidden min-h-screen flex items-center px-4 py-12 lg:px-10 bg-base-100">
<div class="pointer-events-none absolute inset-x-0 top-0 h-[32rem] -z-10"
style="background:
radial-gradient(46% 62% at 18% 0%, #1d4ed8 0%, transparent 68%),
radial-gradient(44% 58% at 54% 0%, #4f46e5 0%, transparent 68%),
radial-gradient(40% 56% at 86% 8%, #0f766e 0%, transparent 70%);
opacity: 0.10;
-webkit-mask-image: linear-gradient(black, transparent);
mask-image: linear-gradient(black, transparent);"
aria-hidden="true"></div>
<div class="container mx-auto max-w-5xl grid items-center gap-12 lg:grid-cols-2">
<div class="w-full max-w-md mx-auto lg:mx-0">
<c-magic-link.form />
<c-magic-link.sent />
</div>
<c-magic-link.aside />
</div>
</main>
<div id="magic-aside" class="hidden lg:flex flex-col gap-8">
<div class="flex flex-col gap-4">
<p class="text-xs uppercase tracking-wider text-base-content/40">How it works</p>
<c-magic-link.step n="1" title="Give us your work email">
The one on your Arden workspace. Nothing else is asked for.
</c-magic-link.step>
<c-magic-link.step n="2" title="Open the link we send">
It lands in under a minute and drops you straight into the workspace.
</c-magic-link.step>
<c-magic-link.step n="3" title="That's it — you're in">
No password is set, so there is none to rotate, reset or breach.
</c-magic-link.step>
</div>
<c-lb.card class="bg-base-200/60 border border-base-300">
<c-lb.card.body class="gap-2 p-5">
<div class="flex items-center gap-2">
<c-lbi n="rmx.shield-check" w="18" h="18" class="text-primary" />
<h2 class="text-sm font-semibold">Nothing to phish</h2>
</div>
<p id="magic-security" class="text-sm text-base-content/60">
Arden stores no password for link-only accounts. Links are single-use and
expire after 10 minutes, and signing in on a new device tells the workspace
owner within the hour.
</p>
</c-lb.card.body>
</c-lb.card>
<p class="text-xs text-base-content/40">
61% of Arden workspaces have turned passwords off entirely.
</p>
</div>
<c-lb.card id="magic-form" border class="bg-base-100 shadow-sm" data-show="!$sent">
<c-lb.card.body class="gap-5 p-7">
<div class="flex items-center gap-2">
<c-arden.mark size="xs" />
<span class="text-base font-semibold tracking-tight">Arden</span>
</div>
<div class="flex flex-col gap-1.5">
<h1 class="text-2xl font-semibold tracking-tight">Sign in without a password</h1>
<p class="text-sm text-base-content/60">
Give us the email on your workspace and we will send a one-time link.
Nothing to remember, nothing to leak.
</p>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label for="magic-email" class="label label-text text-sm font-medium">Work email</label>
<c-lb.input
data-lbb-start
id="magic-email"
type="email"
name="email"
class="w-full"
autocomplete="username"
placeholder="[email protected]"
bind="$email" />
</div>
<c-lb.button
id="magic-send"
type="button"
variant="primary"
modifier="block"
icon="rmx.mail-send"
data-attr:disabled="!$email"
data-on:click="$sent = true">
Email me a sign-in link
</c-lb.button>
</div>
<c-lb.divider class="my-0 text-xs text-base-content/40">or continue with</c-lb.divider>
<c-magic-link.oauth />
<p class="text-center text-sm text-base-content/60">
Prefer a password?
<a href="#" class="link link-primary">Use your workspace credentials</a>
</p>
</c-lb.card.body>
</c-lb.card>
<div class="grid grid-cols-3 gap-2">
<c-lb.button type="button" btnStyle="outline" icon="rmx.google" size="sm">Google</c-lb.button>
<c-lb.button type="button" btnStyle="outline" icon="rmx.apple" size="sm">Apple</c-lb.button>
<c-lb.button type="button" btnStyle="outline" icon="rmx.github" size="sm">GitHub</c-lb.button>
</div>
<c-lb.card id="magic-sent" border class="bg-base-100 shadow-sm" data-show="$sent" style="display: none">
<c-lb.card.body class="gap-5 p-7">
<div class="flex items-center gap-2">
<c-arden.mark size="xs" />
<span class="text-base font-semibold tracking-tight">Arden</span>
</div>
<div class="flex flex-col items-start gap-4">
<div class="rounded-xl bg-primary/10 p-3 text-primary">
<c-lbi n="rmx.mail-check" w="24" h="24" />
</div>
<div class="flex flex-col gap-1.5">
<h1 class="text-2xl font-semibold tracking-tight">Check your inbox</h1>
<p class="text-sm text-base-content/60">
We sent a sign-in link to
<span id="magic-recipient"
class="font-medium text-base-content/80"
data-text="$email">your work email</span>.
Open it on this device and you are in.
</p>
</div>
</div>
<c-lb.alert alertStyle="soft" icon="rmx.time">
<span class="text-sm">The link works once and expires after 10 minutes.</span>
</c-lb.alert>
<div class="flex flex-col gap-2">
<c-lb.button
id="magic-resend"
type="button"
btnStyle="outline"
modifier="block"
icon="rmx.refresh"
behavior="disabled">
Send it again in 60 seconds
</c-lb.button>
<c-lb.button
id="magic-back"
type="button"
btnStyle="ghost"
modifier="block"
size="sm"
data-on:click="$sent = false">
Use a different email
</c-lb.button>
</div>
<p class="text-center text-xs text-base-content/50">
Nothing yet? It usually lands within a minute — check spam before trying again.
</p>
</c-lb.card.body>
</c-lb.card>
<c-vars n="" title="" />
<div class="flex items-start gap-3">
<span class="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full
bg-primary/10 text-xs font-semibold tabular-nums text-primary">{{ n }}</span>
<div class="flex flex-col gap-0.5">
<div class="text-sm font-medium">{{ title }}</div>
<p class="text-sm text-base-content/60">{{ slot }}</p>
</div>
</div>
{% load lb_tags %}
<c-vars
size="sm"
:tile="{'xs':'w-6 h-6 rounded-md','sm':'w-8 h-8 rounded-lg','md':'w-10 h-10 rounded-xl','lg':'w-12 h-12 rounded-xl'}"
:icon="{'xs':'w-4 h-4','sm':'w-5 h-5','md':'w-6 h-6','lg':'w-7 h-7'}"
/>
<span class="inline-flex shrink-0 items-center justify-center bg-primary text-primary-content {{ tile|get_dict_item:size }}" {{ attrs }}>
<svg viewBox="0 0 48 48" class="{{ icon|get_dict_item:size }}" fill="none" aria-hidden="true">
<path d="M12 36 24 12l12 24" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M17 27h14" stroke="currentColor" stroke-width="5" stroke-linecap="round" />
</svg>
</span>
<c-vars active="overview" drawerId="" />
<div class="flex h-full flex-col bg-base-100 border-r border-base-300 transition-[width] is-drawer-close:w-16 is-drawer-open:w-60">
{% if drawerId %}
<c-lb.drawer.toggle
for="{{ drawerId }}"
class="btn btn-square btn-sm fixed top-16 z-50 hidden -translate-x-1/2 border border-base-300 bg-base-100 shadow-sm transition-[left] duration-200 lg:flex is-drawer-close:left-16 is-drawer-open:left-60"
aria-label="Toggle sidebar">
<c-lbi n="rmx.sidebar-unfold" w="18" h="18" />
</c-lb.drawer.toggle>
{% endif %}
<div class="flex h-16 items-center gap-3 px-4">
<c-arden.mark size="xs" />
<span class="font-semibold tracking-tight is-drawer-close:hidden">Arden</span>
</div>
<nav class="grow px-2 py-3">
<ul class="menu gap-1 p-0">
<li><a href="#" class="{% if active == 'overview' %}active {% endif %}is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Overview"><c-lbi n="rmx.pie-chart-2" w="18" h="18" class="shrink-0" /><span class="is-drawer-close:hidden">Overview</span></a></li>
<li><a href="#" class="{% if active == 'customers' %}active {% endif %}is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Customers"><c-lbi n="rmx.group-2" w="18" h="18" class="shrink-0" /><span class="is-drawer-close:hidden">Customers</span></a></li>
<li><a href="#" class="{% if active == 'activity' %}active {% endif %}is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Activity"><c-lbi n="rmx.pulse" w="18" h="18" class="shrink-0" /><span class="is-drawer-close:hidden">Activity</span></a></li>
<li><a href="#" class="{% if active == 'reports' %}active {% endif %}is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Reports"><c-lbi n="rmx.bar-chart-2" w="18" h="18" class="shrink-0" /><span class="is-drawer-close:hidden">Reports</span></a></li>
</ul>
</nav>
<div class="border-t border-base-300 p-2">
<a href="#" class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-base-content/60 hover:bg-base-200 is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Settings">
<c-lbi n="rmx.settings-3" w="18" h="18" class="shrink-0" />
<span class="is-drawer-close:hidden">Settings</span>
</a>
</div>
</div>
Learn from this block
-
`email` and `sent`. That is the entire state of a passwordless sign-in, and it lives in the browser.
-
One prop. No change handler, no listener, no framework state to wire up — the field simply is the signal, which is what lets the next screen greet the user by the address they typed.
-
The click flips one boolean. Open the network tab first: the page changes state and the wire stays silent.
-
Both states are rendered by the server and the signal decides which one is on screen. The sent card reads the address straight out of `$email`, so it is personal without ever having asked a server who is signing in.
-
Passwordless sign-in asks the visitor to trust something unfamiliar, so the block answers the question rather than leaving it. The steps and the security note are the surrounding matter — and they cost nothing at runtime.
Split brand
UI demo of sign-in and sign-up on a split brand layout, showing live per-field validation over an ordinary Django view. It does not authenticate anyone: no password is checked, no credential is stored, and no session is started. Replace the views with django.contrib.auth before using it for real.
"""Split brand sign-in and sign-up: a UI demo, not an authentication system.
These views authenticate nobody. Read this before wiring them into a project.
`submit` looks up an LbMember by email and declares the sign-in successful. The
password is length-checked and then never compared against anything. There is
nothing to compare it to: LbMember is a demo model with no password field and
no relationship to django.contrib.auth. `sign_up_submit` creates a member with
no credential at all, and no session is ever started.
The block exists to demonstrate live per-field validation over an ordinary
Django view. Do not put it in front of anything real. A working sign-in has to
go through django.contrib.auth: use `authenticate()` and `login()` here, back
them with a real user model whose password is set via `set_password()`, and
guard whatever the user reaches next with `login_required`.
"""
from django.db import IntegrityError
from django.utils import timezone
from labb.contrib.blocks import lm, render_page
from labb.signals import Bool, Signals, Str
LbMember = lm("LbMember")
LbWorkspace = lm("LbWorkspace")
SIGN_IN = "lb/auth/split-brand/pages/sign_in.html"
SIGN_UP = "lb/auth/split-brand/pages/sign_up.html"
MIN_PASSWORD = 8
class AuthSignals(Signals):
name = Str(path="form.name", default="")
email = Str(path="form.email", default="")
password = Str(path="form.password", default="")
submitted = Bool(path="submitted", default=False)
def _validate(s, strict=False, with_name=False):
"""Gentle while typing, strict on submit.
One validator serves both sign-in and sign-up — the only difference is
whether a name is expected, which is what makes a single view able to back
both pages.
"""
errors = {}
if with_name:
if s.name:
if len(s.name.strip()) < 2:
errors["name"] = "Enter your full name."
elif strict:
errors["name"] = "Name is required."
if s.email:
if "@" not in s.email or "." not in s.email.split("@")[-1]:
errors["email"] = "Enter a valid email address."
elif strict:
errors["email"] = "Email is required."
if s.password:
if len(s.password) < MIN_PASSWORD:
errors["password"] = (
f"At least {MIN_PASSWORD} characters ({len(s.password)} so far)."
)
elif strict:
errors["password"] = "Password is required."
return errors
def _ctx(signals, errors=None, submitted=False, member=None, failed=""):
return {
"signals": signals,
"errors": errors or {},
"submitted": submitted,
"member": member,
"failed": failed,
}
def _page(request, template, ctx, title):
return render_page(request, template, ctx, title=title)
# --- sign in ---------------------------------------------------------------
def index(request):
return _page(request, SIGN_IN, _ctx(AuthSignals(request)), "Sign in to Arden")
def validate(request):
s = AuthSignals(request)
return _page(request, SIGN_IN, _ctx(s, errors=_validate(s)), "Sign in to Arden")
def submit(request):
if request.method != "POST":
return index(request)
s = AuthSignals(request)
errors = _validate(s, strict=True)
if errors:
return _page(request, SIGN_IN, _ctx(s, errors=errors), "Sign in to Arden")
member = LbMember.objects.filter(email=s.email).select_related("workspace").first()
if member is None:
return _page(
request,
SIGN_IN,
_ctx(s, failed="No Arden account found for that email."),
"Sign in to Arden",
)
# DEMO: a matching email is the whole check. No password, no session.
s.submitted = True
return _page(
request, SIGN_IN, _ctx(s, submitted=True, member=member), "Sign in to Arden"
)
# --- sign up ---------------------------------------------------------------
def sign_up(request):
return _page(
request, SIGN_UP, _ctx(AuthSignals(request)), "Create an Arden account"
)
def sign_up_validate(request):
s = AuthSignals(request)
return _page(
request,
SIGN_UP,
_ctx(s, errors=_validate(s, with_name=True)),
"Create an Arden account",
)
def sign_up_submit(request):
if request.method != "POST":
return sign_up(request)
s = AuthSignals(request)
errors = _validate(s, strict=True, with_name=True)
if errors:
return _page(
request, SIGN_UP, _ctx(s, errors=errors), "Create an Arden account"
)
if LbMember.objects.filter(email=s.email).exists():
return _page(
request,
SIGN_UP,
_ctx(s, failed="That email is already on an Arden workspace."),
"Create an Arden account",
)
workspace = LbWorkspace.objects.first()
if workspace is None:
return _page(
request,
SIGN_UP,
_ctx(
s,
failed="There is no Arden workspace yet. Load the demo fixtures first.",
),
"Create an Arden account",
)
# DEMO: LbMember has no password field, so the typed password is discarded.
try:
member = LbMember.objects.create(
workspace=workspace,
name=s.name.strip(),
email=s.email,
title="",
role="member",
status="invited",
joined_on=timezone.now().date(),
last_active_at=timezone.now(),
)
except IntegrityError:
return _page(
request,
SIGN_UP,
_ctx(s, failed="That email is already on an Arden workspace."),
"Create an Arden account",
)
s.submitted = True
return _page(
request,
SIGN_UP,
_ctx(s, submitted=True, member=member),
"Create an Arden account",
)
from django.urls import path
from . import views
app_name = "block_auth_split_brand"
urlpatterns = [
path("", views.index, name="index"),
path("validate/", views.validate, name="validate"),
path("submit/", views.submit, name="submit"),
path("sign-up/", views.sign_up, name="sign_up"),
path("sign-up/validate/", views.sign_up_validate, name="sign_up_validate"),
path("sign-up/submit/", views.sign_up_submit, name="sign_up_submit"),
]
<c-lbr.signals id="auth-signals" :schema=signals />
<c-split-brand.shell>
<c-slot name="brand">
<c-split-brand.brand
headline="Know your revenue."
sub="Every customer, invoice and renewal in one place." />
</c-slot>
{% if submitted %}
<c-split-brand.success :member=member />
{% else %}
<c-split-brand.form
title="Welcome back to Arden"
lede="Sign in with your workspace credentials."
action="block_auth_split_brand:submit"
validateTo="block_auth_split_brand:validate"
cta="Sign in"
:signals=signals
:errors=errors
:failed=failed />
{% endif %}
</c-split-brand.shell>
<c-lbr.signals id="auth-signals" :schema=signals />
<c-split-brand.shell>
<c-slot name="brand">
<c-split-brand.brand
headline="Start knowing your revenue."
sub="Set up your workspace in a couple of minutes." />
</c-slot>
{% if submitted %}
<c-split-brand.success :member=member joined />
{% else %}
<c-split-brand.form
title="Create your Arden account"
lede="Fourteen days free. No card required."
action="block_auth_split_brand:sign_up_submit"
validateTo="block_auth_split_brand:sign_up_validate"
cta="Create account"
withName
:signals=signals
:errors=errors
:failed=failed />
{% endif %}
</c-split-brand.shell>
<c-vars headline="" sub="" />
<div>
<div class="flex items-center gap-3">
<c-arden.mark size="sm" />
<span class="text-lg font-semibold tracking-tight">Arden</span>
</div>
<h1 class="mt-10 text-4xl font-semibold tracking-tight text-balance">{{ headline }}</h1>
<p class="mt-3 text-base-content/60 max-w-sm">{{ sub }}</p>
</div>
<c-lb.card class="bg-base-100/70 backdrop-blur border border-base-300 shadow-sm">
<c-lb.card.body class="p-5 gap-4">
<p class="text-sm leading-relaxed">
“We stopped guessing at churn. Arden paid for itself in a quarter — the
first renewal it flagged covered the year.”
</p>
<div class="flex items-center gap-3">
<c-lb.avatar placeholder initials="MC" bgColor="secondary" size="sm" />
<div class="text-xs">
<div class="font-medium">Mira Chen</div>
<div class="text-base-content/50">Founder, AtlasForge</div>
</div>
</div>
</c-lb.card.body>
</c-lb.card>
<div>
<p class="text-xs uppercase tracking-wider text-base-content/40">
Trusted by 2,400+ finance and ops teams
</p>
<div class="mt-3 flex flex-wrap gap-x-5 gap-y-2 text-sm font-medium text-base-content/50">
<span>Kite & Bell</span>
<span>Northwind Co</span>
<span>Verity Labs</span>
<span>Ironvale Group</span>
</div>
</div>
<c-vars label="" hint="" error="" required validateTo="" on="input__debounce.600ms" />
<c-lbr.post :to=validateTo tag="div" on="{{ on }}" before="if ($submitted) return">
<label class="label label-text text-sm font-medium mb-1">
{{ label }}{% if required %} <span class="text-error">*</span>{% endif %}
{% if hint %}<span class="text-base-content/40 font-normal">{{ hint }}</span>{% endif %}
</label>
{{ slot }}
{% if error %}
<p class="text-error text-xs mt-1">{{ error }}</p>
{% endif %}
</c-lbr.post>
<c-vars title="" lede="" action="" validateTo="" cta="Continue" withName :signals="{}" :errors="{}" failed="" />
<div class="flex flex-col gap-5">
<div>
<h2 class="text-2xl font-semibold tracking-tight">{{ title }}</h2>
<p class="text-base-content/60 text-sm mt-1">{{ lede }}</p>
</div>
{% if failed %}
<c-lb.alert variant="error" alertStyle="soft" icon="rmx.error-warning">
{{ failed }}
</c-lb.alert>
{% endif %}
<c-lbr.post :to=action before="$submitted = true" class="flex flex-col gap-5">
{% if withName %}
<c-split-brand.field label="Full name" required :error=errors.name :validateTo=validateTo>
<c-lb.input data-lbb-start placeholder="Mira Chen" class="w-full" autocomplete="name" :bind=signals.fields.name />
</c-split-brand.field>
{% endif %}
<c-split-brand.field label="Work email" required :error=errors.email :validateTo=validateTo>
<c-lb.input type="email" placeholder="[email protected]" class="w-full"
autocomplete="username" :bind=signals.fields.email />
</c-split-brand.field>
<c-split-brand.field label="Password" hint="(8+ characters)" required :error=errors.password :validateTo=validateTo>
<c-lb.input type="password" placeholder="••••••••" class="w-full"
autocomplete="{% if withName %}new-password{% else %}current-password{% endif %}"
:bind=signals.fields.password />
</c-split-brand.field>
<c-lb.button type="submit" variant="primary" modifier="block">{{ cta }}</c-lb.button>
</c-lbr.post>
<c-lb.divider class="text-xs text-base-content/40">or continue with</c-lb.divider>
<c-split-brand.oauth />
<p class="text-center text-sm text-base-content/60">
{% if withName %}
Already on Arden?
<a href="{% url 'block_auth_split_brand:index' %}" class="link link-primary">Sign in</a>
{% else %}
New to Arden?
<a href="{% url 'block_auth_split_brand:sign_up' %}" class="link link-primary">Create an account</a>
{% endif %}
</p>
</div>
<div class="grid grid-cols-3 gap-2">
<c-lb.button btnStyle="outline" icon="rmx.google" size="sm">Google</c-lb.button>
<c-lb.button btnStyle="outline" icon="rmx.apple" size="sm">Apple</c-lb.button>
<c-lb.button btnStyle="outline" icon="rmx.github" size="sm">GitHub</c-lb.button>
</div>
<c-vars brand="" />
<main class="min-h-screen grid lg:grid-cols-2">
<aside class="relative isolate overflow-hidden hidden lg:flex flex-col justify-between p-12
bg-base-200">
<div class="pointer-events-none absolute inset-0 -z-10"
style="background:
radial-gradient(60% 52% at 14% 2%, #0f766e 0%, transparent 70%),
radial-gradient(54% 48% at 92% 22%, #0369a1 0%, transparent 68%),
radial-gradient(64% 52% at 48% 108%, #ca8a04 0%, transparent 72%);
opacity: 0.14;"
aria-hidden="true"></div>
{{ brand }}
</aside>
<section class="flex items-center justify-center p-6 sm:p-12 bg-base-100">
<div class="w-full max-w-sm">
{{ slot }}
</div>
</section>
</main>
<c-vars :member="None" joined />
<div class="flex flex-col gap-5 text-center">
<div class="flex justify-center">
<c-lb.avatar placeholder icon="rmx.check" bgColor="success" size="lg" rounded="full" />
</div>
<div>
<h2 class="text-2xl font-semibold tracking-tight">
{% if joined %}Welcome to Arden, {{ member.name }}.{% else %}Welcome back, {{ member.name }}.{% endif %}
</h2>
<p class="text-base-content/60 text-sm mt-1">
{% if joined %}
We've sent an invite to {{ member.email }} to finish setting up.
{% else %}
Signed in to {{ member.workspace.name }}.
{% endif %}
</p>
</div>
<c-lb.card class="border border-base-300">
<c-lb.card.body class="p-4 gap-2 text-left text-sm">
<div class="flex justify-between">
<span class="text-base-content/50">Workspace</span>
<span class="font-medium">{{ member.workspace.name }}</span>
</div>
<div class="flex justify-between">
<span class="text-base-content/50">Email</span>
<span class="font-medium">{{ member.email }}</span>
</div>
<div class="flex justify-between">
<span class="text-base-content/50">Role</span>
<span class="font-medium capitalize">{{ member.role }}</span>
</div>
</c-lb.card.body>
</c-lb.card>
</div>
{% load lb_tags %}
<c-vars
size="sm"
:tile="{'xs':'w-6 h-6 rounded-md','sm':'w-8 h-8 rounded-lg','md':'w-10 h-10 rounded-xl','lg':'w-12 h-12 rounded-xl'}"
:icon="{'xs':'w-4 h-4','sm':'w-5 h-5','md':'w-6 h-6','lg':'w-7 h-7'}"
/>
<span class="inline-flex shrink-0 items-center justify-center bg-primary text-primary-content {{ tile|get_dict_item:size }}" {{ attrs }}>
<svg viewBox="0 0 48 48" class="{{ icon|get_dict_item:size }}" fill="none" aria-hidden="true">
<path d="M12 36 24 12l12 24" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M17 27h14" stroke="currentColor" stroke-width="5" stroke-linecap="round" />
</svg>
</span>
<c-vars active="overview" drawerId="" />
<div class="flex h-full flex-col bg-base-100 border-r border-base-300 transition-[width] is-drawer-close:w-16 is-drawer-open:w-60">
{% if drawerId %}
<c-lb.drawer.toggle
for="{{ drawerId }}"
class="btn btn-square btn-sm fixed top-16 z-50 hidden -translate-x-1/2 border border-base-300 bg-base-100 shadow-sm transition-[left] duration-200 lg:flex is-drawer-close:left-16 is-drawer-open:left-60"
aria-label="Toggle sidebar">
<c-lbi n="rmx.sidebar-unfold" w="18" h="18" />
</c-lb.drawer.toggle>
{% endif %}
<div class="flex h-16 items-center gap-3 px-4">
<c-arden.mark size="xs" />
<span class="font-semibold tracking-tight is-drawer-close:hidden">Arden</span>
</div>
<nav class="grow px-2 py-3">
<ul class="menu gap-1 p-0">
<li><a href="#" class="{% if active == 'overview' %}active {% endif %}is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Overview"><c-lbi n="rmx.pie-chart-2" w="18" h="18" class="shrink-0" /><span class="is-drawer-close:hidden">Overview</span></a></li>
<li><a href="#" class="{% if active == 'customers' %}active {% endif %}is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Customers"><c-lbi n="rmx.group-2" w="18" h="18" class="shrink-0" /><span class="is-drawer-close:hidden">Customers</span></a></li>
<li><a href="#" class="{% if active == 'activity' %}active {% endif %}is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Activity"><c-lbi n="rmx.pulse" w="18" h="18" class="shrink-0" /><span class="is-drawer-close:hidden">Activity</span></a></li>
<li><a href="#" class="{% if active == 'reports' %}active {% endif %}is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Reports"><c-lbi n="rmx.bar-chart-2" w="18" h="18" class="shrink-0" /><span class="is-drawer-close:hidden">Reports</span></a></li>
</ul>
</nav>
<div class="border-t border-base-300 p-2">
<a href="#" class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-base-content/60 hover:bg-base-200 is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Settings">
<c-lbi n="rmx.settings-3" w="18" h="18" class="shrink-0" />
<span class="is-drawer-close:hidden">Settings</span>
</a>
</div>
</div>
Learn from this block
-
The page receives one typed schema from the view. Its fields provide the initial form state and the values Django validates on the next request.
-
Bind accepts the schema field descriptor, so the input and validation view use the same signal path. The template does not need a hand-written change handler.
-
A debounced POST sends the current signals to Django and returns the refreshed field. The before expression stops background checks once the user submits the form.
-
Both pages use the same validation helper. The view decides whether a name is required, which keeps the shared fields and error handling in one place.
-
These views are a UI demo and authenticate nobody. Submit looks a member up by email and calls it a success, without ever verifying the password.
Two-column visual
Sign-in UI beside a framed product shot built from components, not a screenshot, with no image asset and no JavaScript. Markup only, with no server behind it, so it does not sign anyone in.
-
components
-
arden
-
visual_signin
-
-
templates
-
pages
-
<main class="min-h-screen flex items-center bg-base-100 px-4 py-12 lg:px-10">
<div class="container mx-auto max-w-6xl grid items-center gap-12 lg:grid-cols-2">
<c-visual-signin.form />
<c-visual-signin.shot />
</div>
</main>
<div class="w-full max-w-sm mx-auto lg:mx-0 flex flex-col gap-6">
<div class="flex items-center gap-2">
<c-arden.mark size="xs" />
<span class="text-base font-semibold tracking-tight">Arden</span>
</div>
<div class="flex flex-col gap-1.5">
<h1 class="text-3xl font-semibold tracking-tight text-balance">
Pick up where the quarter left off
</h1>
<p class="text-sm text-base-content/60">
Four renewals moved while you were away. They are waiting on the other side
of this form.
</p>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label for="tv-email" class="label label-text text-sm font-medium">Work email</label>
<c-lb.input
id="tv-email"
type="email"
name="email"
class="w-full"
autocomplete="username"
placeholder="[email protected]" />
</div>
<div class="flex flex-col gap-1">
<div class="flex items-baseline justify-between">
<label for="tv-password" class="label label-text text-sm font-medium">Password</label>
<a href="#" class="link link-hover text-xs text-base-content/60">Forgot password?</a>
</div>
<c-lb.input
id="tv-password"
type="password"
name="password"
class="w-full"
autocomplete="current-password"
placeholder="••••••••" />
</div>
<c-lb.button id="tv-submit" type="button" variant="primary" modifier="block">
Sign in
</c-lb.button>
</div>
<c-lb.divider class="my-0 text-xs text-base-content/40">or continue with</c-lb.divider>
<c-visual-signin.oauth />
<p class="text-sm text-base-content/60">
No workspace yet?
<a href="#" class="link link-primary">Start a 14-day trial</a>
— no card, no call.
</p>
</div>
<div class="grid grid-cols-3 gap-2">
<c-lb.button type="button" btnStyle="outline" icon="rmx.google" size="sm">Google</c-lb.button>
<c-lb.button type="button" btnStyle="outline" icon="rmx.apple" size="sm">Apple</c-lb.button>
<c-lb.button type="button" btnStyle="outline" icon="rmx.github" size="sm">GitHub</c-lb.button>
</div>
<div class="flex flex-col gap-4 border-t border-base-300 bg-base-100 p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<c-arden.mark size="xs" />
<span class="text-sm font-semibold tracking-tight">Renewals · next 30 days</span>
</div>
<c-lb.badge variant="warning" style="soft" size="sm">4 need a decision</c-lb.badge>
</div>
<div class="grid grid-cols-3 gap-2">
<c-visual-signin.tile label="Up for renewal" value="$186,300" note="9 contracts" />
<c-visual-signin.tile label="Auto-renewing" value="$121,700" note="5 contracts" />
<c-visual-signin.tile label="Needs a call" value="$64,600" note="4 contracts" noteClass="text-warning" />
</div>
<c-lb.table size="sm">
<thead>
<tr>
<th>Customer</th>
<th class="text-right">Value</th>
<th class="text-right">Renews</th>
<th class="text-right">Signal</th>
</tr>
</thead>
<tbody>
<tr>
<td class="font-medium">Ironvale Group</td>
<td class="tabular-nums text-right">$41,200</td>
<td class="tabular-nums text-right text-base-content/60">18 Jul</td>
<td class="text-right">
<c-lb.badge variant="error" style="soft" size="sm">Usage down 34%</c-lb.badge>
</td>
</tr>
<tr>
<td class="font-medium">Verity Labs</td>
<td class="tabular-nums text-right">$23,400</td>
<td class="tabular-nums text-right text-base-content/60">22 Jul</td>
<td class="text-right">
<c-lb.badge variant="warning" style="soft" size="sm">Champion left</c-lb.badge>
</td>
</tr>
<tr>
<td class="font-medium">AtlasForge</td>
<td class="tabular-nums text-right">$58,900</td>
<td class="tabular-nums text-right text-base-content/60">29 Jul</td>
<td class="text-right">
<c-lb.badge variant="success" style="soft" size="sm">Seats +12</c-lb.badge>
</td>
</tr>
<tr>
<td class="font-medium">Kite & Bell</td>
<td class="tabular-nums text-right">$62,800</td>
<td class="tabular-nums text-right text-base-content/60">04 Aug</td>
<td class="text-right">
<c-lb.badge variant="info" style="soft" size="sm">Invoice open</c-lb.badge>
</td>
</tr>
</tbody>
</c-lb.table>
</div>
<div class="hidden lg:flex flex-col gap-8">
<div class="relative isolate">
<div class="pointer-events-none absolute -inset-10 -z-10"
style="background:
radial-gradient(48% 58% at 22% 12%, #0f766e 0%, transparent 70%),
radial-gradient(44% 54% at 76% 22%, #0284c7 0%, transparent 68%),
radial-gradient(46% 54% at 54% 94%, #d97706 0%, transparent 72%);
opacity: 0.14;
filter: blur(24px);"
aria-hidden="true"></div>
<c-lb.mockup-browser
id="tv-shot"
url="app.arden.io/renewals"
class="relative border border-base-300 bg-base-200 shadow-xl">
<c-visual-signin.renewals />
</c-lb.mockup-browser>
</div>
<p id="tv-caption" class="text-sm text-base-content/60 text-center">
This is what loads after you sign in. Last sync 4 minutes ago.
</p>
</div>
<c-vars label="" value="" note="" noteClass="text-base-content/50" />
<div class="rounded-lg border border-base-300 bg-base-100 p-3">
<div class="text-xs text-base-content/50">{{ label }}</div>
<div class="mt-1 text-lg font-semibold tabular-nums tracking-tight">{{ value }}</div>
<div class="text-xs tabular-nums {{ noteClass }}">{{ note }}</div>
</div>
{% load lb_tags %}
<c-vars
size="sm"
:tile="{'xs':'w-6 h-6 rounded-md','sm':'w-8 h-8 rounded-lg','md':'w-10 h-10 rounded-xl','lg':'w-12 h-12 rounded-xl'}"
:icon="{'xs':'w-4 h-4','sm':'w-5 h-5','md':'w-6 h-6','lg':'w-7 h-7'}"
/>
<span class="inline-flex shrink-0 items-center justify-center bg-primary text-primary-content {{ tile|get_dict_item:size }}" {{ attrs }}>
<svg viewBox="0 0 48 48" class="{{ icon|get_dict_item:size }}" fill="none" aria-hidden="true">
<path d="M12 36 24 12l12 24" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M17 27h14" stroke="currentColor" stroke-width="5" stroke-linecap="round" />
</svg>
</span>
<c-vars active="overview" drawerId="" />
<div class="flex h-full flex-col bg-base-100 border-r border-base-300 transition-[width] is-drawer-close:w-16 is-drawer-open:w-60">
{% if drawerId %}
<c-lb.drawer.toggle
for="{{ drawerId }}"
class="btn btn-square btn-sm fixed top-16 z-50 hidden -translate-x-1/2 border border-base-300 bg-base-100 shadow-sm transition-[left] duration-200 lg:flex is-drawer-close:left-16 is-drawer-open:left-60"
aria-label="Toggle sidebar">
<c-lbi n="rmx.sidebar-unfold" w="18" h="18" />
</c-lb.drawer.toggle>
{% endif %}
<div class="flex h-16 items-center gap-3 px-4">
<c-arden.mark size="xs" />
<span class="font-semibold tracking-tight is-drawer-close:hidden">Arden</span>
</div>
<nav class="grow px-2 py-3">
<ul class="menu gap-1 p-0">
<li><a href="#" class="{% if active == 'overview' %}active {% endif %}is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Overview"><c-lbi n="rmx.pie-chart-2" w="18" h="18" class="shrink-0" /><span class="is-drawer-close:hidden">Overview</span></a></li>
<li><a href="#" class="{% if active == 'customers' %}active {% endif %}is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Customers"><c-lbi n="rmx.group-2" w="18" h="18" class="shrink-0" /><span class="is-drawer-close:hidden">Customers</span></a></li>
<li><a href="#" class="{% if active == 'activity' %}active {% endif %}is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Activity"><c-lbi n="rmx.pulse" w="18" h="18" class="shrink-0" /><span class="is-drawer-close:hidden">Activity</span></a></li>
<li><a href="#" class="{% if active == 'reports' %}active {% endif %}is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Reports"><c-lbi n="rmx.bar-chart-2" w="18" h="18" class="shrink-0" /><span class="is-drawer-close:hidden">Reports</span></a></li>
</ul>
</nav>
<div class="border-t border-base-300 p-2">
<a href="#" class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-base-content/60 hover:bg-base-200 is-drawer-close:tooltip is-drawer-close:tooltip-right" data-tip="Settings">
<c-lbi n="rmx.settings-3" w="18" h="18" class="shrink-0" />
<span class="is-drawer-close:hidden">Settings</span>
</a>
</div>
</div>
Learn from this block
-
The whole treatment is server-rendered markup. Nothing here asks for a signal or an action, so nothing is downloaded — a sign-in page has no business shipping a framework.
-
Browser chrome around a real Arden renewals view, built from the same components the app uses. No asset to license, no file to ship, and it stays sharp at any pixel density.
-
The mockup chrome is base tokens — `border-base-300`, `bg-base-200`. A screenshot would be frozen at whatever theme it was taken in; this shot follows the user's.
-
The panel is the surrounding matter. It answers the only question a returning user has — what is waiting for me — before they have typed anything.
Social proof
Sign-in UI under a logo cloud and a rating strip, conversion-shaped and shipping no JavaScript at all. Markup only, with no server behind it, so it does not sign anyone in.