import { useState } from "react";
import { CheckCircle2, KeyRound, Mail } from "lucide-react";
import { Alert, AlertDescription, AlertTitle, Skeleton } from "@godxjp/ui/feedback";
import {
Avatar,
AvatarFallback,
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
EmptyState,
ListRow,
} from "@godxjp/ui/data-display";
import {
Checkbox,
Field,
FormField,
Input,
PasswordInput,
PasswordStrength,
} from "@godxjp/ui/data-entry";
import { Button, Reveal } from "@godxjp/ui/general";
import {
AuthDivider,
AuthFooter,
AuthIdentity,
AuthShell,
AuthStack,
Flex,
} from "@godxjp/ui/layout";
import { AppSettingPicker } from "@godxjp/ui/navigation";
/**
* AuthShell preset="registration" — the canonical sign-up measure.
*
* The preset owns the whole page geometry: a 360px (22.5rem) form measure at 1440/1024, a 15px
* inline gutter at 390 (card x=15, width=360 — the same page rhythm as preset="login", so
* sign-in → sign-up never jumps on a phone), the block-start inset, and the FOOTER CLEARANCE the
* legal/consent footer keeps at the end of a long scroll. There is no page-local width, inset,
* media query or colour anywhere in this file.
*
* WHY THIS PRESET IS START-ALIGNED: a registration card is the tallest surface in the
* hosted-identity set — name · email · password · confirm · strength meter · consent · submit ·
* provider row. A vertically CENTRED tall card overflows ABOVE the scroll origin on a short
* viewport, which puts its first field permanently out of reach. Start-aligned, the block-start
* inset is ordinary page padding the user simply scrolls past.
*
* Two things in this page are COMPOSITIONS, not framework components (Gate 0 / rule #46) —
* they are the formally documented public replacements #256 asked for:
*
* · SOCIAL / PROVIDER ACTIONS ("SocialLinks") — an `AuthDivider` + a `Flex direction="col"` of
* real `Button variant="outline"`. It owns no behaviour: each provider is a plain action the
* app wires to its own OAuth start URL. A framework component here would have to invent the
* provider list, its order and its consent semantics — product decisions the package must not
* make. `disabled` and `loading` are the Button's own props; no new API.
*
* · ORGANIZATION CHOICE LIST — `Card` + `CardContent flush` + a `
` of `ListRow as="li"`,
* the same composition documented on the context-selection page. Its loading / empty / error /
* denied states are shown here because #256 asked for them explicitly: Skeleton rows,
* `EmptyState`, and an `Alert` — all existing exports, no consumer CSS.
*
* `?state=standalone|one-line|wrapped|pending-email` drives the package visual script
* (scripts/auth-shell-registration-visual.mjs): the first three vary the identity copy length and
* the last one swaps the card to the pending-email confirmation — all four must keep the same
* card anchor. Verify at 1440x900 · 1024x900 · 390x844.
*/
type Stage = "form" | "pending-email";
type ListState = "ready" | "loading" | "empty" | "error" | "denied";
const PROVIDERS = [
{ id: "google", label: "Google で登録", icon: Mail },
{ id: "passkey", label: "パスキーで登録", icon: KeyRound },
];
const ORGANIZATIONS = [
{ id: "acme", name: "Acme 株式会社", role: "招待 · 管理者として参加", initials: "AC" },
{ id: "godx-labs", name: "GoDX Labs", role: "招待 · メンバーとして参加", initials: "GL" },
];
export default function Demo() {
const urlState = new URLSearchParams(window.location.search).get("state") ?? "one-line";
const [stage, setStage] = useState(
urlState === "pending-email" ? "pending-email" : "form",
);
const [password, setPassword] = useState("");
const [confirm, setConfirm] = useState("");
const [consent, setConsent] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [listState, setListState] = useState("ready");
const requester =
urlState === "standalone"
? undefined
: urlState === "wrapped"
? "すでにアカウントをお持ちの場合はこちらからサインインしてください(組織の管理者から招待を受けている場合も同じです)"
: stage === "form"
? "すでにアカウントをお持ちの場合はサインインしてください"
: "satoshi@example.com 宛に確認リンクを送信しました";
// The one real validation this page owns — a confirmation that does not match is the classic
// registration error, and it must be announced, not just coloured (WCAG 1.4.1).
const mismatch = confirm.length > 0 && confirm !== password;
return (
// NO `brand` bar — deliberately, and it is load-bearing for the geometry, not a styling
// preference. The canonical hosted-identity screens (SCR-001 Login, SCR-002 Registration) put
// the mark INSIDE the column as `AuthIdentity`, above the card, and pass no top brand bar.
// Pass one here and the whole column is pushed down by the bar's height, so every offset read
// off this page would be wrong and the preset's canonical card anchor would silently miss.
}
/>
}
>
{stage === "form" ? (
登録情報
すべての項目が必須です。パスワードは 12 文字以上を推奨します。
setPassword(event.target.value)}
/>
{/* The strength meter is a real primitive — never a hand-rolled coloured bar. */}
setConfirm(event.target.value)}
/>
setConsent(next === true)}
/>
{/* ── SOCIAL / PROVIDER ACTIONS — the documented "SocialLinks" composition.
A divider + real outline Buttons. No new component: the package must not
invent which providers a product offers, in what order, or what consent
they imply. `disabled` / `loading` are the Button's own props. ── */}
{PROVIDERS.map((provider) => {
const Icon = provider.icon;
return (
);
})}
) : (
/* ── PENDING-EMAIL state — carried by the SAME preset, no geometry change. ── */
確認メールを送信しました
メール内のリンクを開くと登録が完了します。リンクの有効期限は 24 時間です。
satoshi@example.com に送信しました
受信箱に届かない場合は迷惑メールフォルダをご確認ください。
)}
{/* ── ORGANIZATION CHOICE LIST — the documented public composition, with every state #256
asked for. Card + CardContent flush +
of ListRow as="li"; the states are
Skeleton / EmptyState / Alert. Nothing here is a new component and nothing needs
consumer CSS. ── */}
参加する組織招待されている組織があれば、登録と同時に参加できます。
{listState === "ready" && (