Port the shared otm-login/otm-menu CSS+markup from @otm/account-panel otm-login-core.ts 0.44.0 (verbatim, kept in sync via header comments) into login.html and the sidebar account menu in index.html/app.js. Accent is the studio's --plum, heading font via the module's --otm-font-display variable. Behaviour is unchanged (OTM-first / "Other ways to sign in" toggle, server-filled __OTM_SSO_URL__, same reasons). The account menu needs to know whether a session came in via OTM to show the badge/"Manage OTM account" link, which the session didn't track before — added a `via: "otm"` claim on the SSO callback's session and exposed it from GET /api/me (additive; password and operator-magic sessions are unaffected and read as not-linked). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014LCQmFvKshyAJsE8C3B5ET
99 lines
6.8 KiB
HTML
99 lines
6.8 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>🌙 Studio Notebook — sign in</title>
|
|
<style>
|
|
/* ported from @otm/account-panel otm-login-core.ts 0.44.0 (OTM_LOGIN_CSS) —
|
|
keep in sync. Verbatim; the studio's brand (accent, heading font) is set
|
|
via the documented --otm-* variables on .otm-login below, not by editing
|
|
these rules. */
|
|
.otm-login{--otm-accent:#b5562a;--otm-accent-ink:#fff;--otm-bg:#faf8f5;--otm-card:#fff;--otm-fg:#1f1b18;--otm-muted:#6b6560;--otm-line:rgba(31,27,24,.12);--otm-radius:20px;--otm-font-display:inherit;
|
|
min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px 16px;background:var(--otm-bg);color:var(--otm-fg);box-sizing:border-box}
|
|
@media (prefers-color-scheme:dark){.otm-login{--otm-bg:#161412;--otm-card:#211e1b;--otm-fg:#f3efe9;--otm-muted:#a39b92;--otm-line:rgba(243,239,233,.12)}}
|
|
.otm-login *{box-sizing:border-box}
|
|
.otm-login__col{width:100%;max-width:26rem}
|
|
.otm-login__brand{text-align:center;margin-bottom:24px}
|
|
.otm-login__mark{width:56px;height:56px;border-radius:999px;background:var(--otm-accent);color:var(--otm-accent-ink);display:inline-flex;align-items:center;justify-content:center;font-weight:700;font-size:20px;letter-spacing:.02em;margin-bottom:12px}
|
|
.otm-login__mark svg,.otm-login__mark img{width:28px;height:28px}
|
|
.otm-login__name{font-family:var(--otm-font-display);font-size:30px;line-height:1.1;letter-spacing:-.01em;font-weight:600;margin:0}
|
|
.otm-login__tagline{color:var(--otm-muted);font-size:14px;margin:6px 0 0}
|
|
.otm-login__card{background:var(--otm-card);border:1px solid var(--otm-line);border-radius:var(--otm-radius);padding:24px;box-shadow:0 4px 24px rgba(0,0,0,.06)}
|
|
.otm-login__btn{display:flex;align-items:center;justify-content:center;width:100%;height:44px;border-radius:12px;border:0;background:var(--otm-accent);color:var(--otm-accent-ink);font:inherit;font-weight:600;font-size:15px;text-decoration:none;cursor:pointer}
|
|
.otm-login__btn:hover{filter:brightness(1.06)}
|
|
.otm-login__btn--outline{background:transparent;color:var(--otm-fg);border:1px solid var(--otm-line)}
|
|
.otm-login__btn--outline:hover{background:rgba(127,127,127,.06);filter:none}
|
|
.otm-login__btn[disabled]{opacity:.55;cursor:default}
|
|
.otm-login__helper{color:var(--otm-muted);font-size:14px;text-align:center;margin:12px 0 0;line-height:1.45}
|
|
.otm-login__error{color:#b3556b;font-size:14px;margin:0 0 12px;text-align:center}
|
|
.otm-login__link{display:inline-block;background:none;border:0;padding:0;font:inherit;font-size:14px;color:var(--otm-muted);text-decoration:underline;text-underline-offset:3px;cursor:pointer}
|
|
.otm-login__link:hover{color:var(--otm-fg)}
|
|
.otm-login__foot{text-align:center;margin-top:16px}
|
|
.otm-login__field{display:block;margin-bottom:12px}
|
|
.otm-login__label{display:block;font-size:13px;color:var(--otm-muted);margin-bottom:6px}
|
|
.otm-login__input{width:100%;height:44px;border-radius:12px;border:1px solid var(--otm-line);background:var(--otm-bg);color:var(--otm-fg);padding:0 14px;font:inherit;font-size:15px}
|
|
.otm-login__input:focus{outline:2px solid color-mix(in srgb,var(--otm-accent) 45%,transparent);outline-offset:1px;border-color:var(--otm-accent)}
|
|
.otm-login__stack>*+*{margin-top:10px}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="otm-login" style="--otm-accent:#8d6b94;--otm-accent-ink:#fff;--otm-font-display:Georgia,'Iowan Old Style',serif">
|
|
<div class="otm-login__col">
|
|
<div class="otm-login__brand">
|
|
<div class="otm-login__mark">🌙</div>
|
|
<h1 class="otm-login__name">Studio Notebook</h1>
|
|
<p class="otm-login__tagline">welcome back to the studio</p>
|
|
</div>
|
|
<div class="otm-login__card">
|
|
<p class="otm-login__error" id="err" role="alert" hidden></p>
|
|
|
|
<!-- Sign in with OTM: one OTM account opens every OTM surface. The server
|
|
fills in the link (it knows this studio's audience); left empty, the
|
|
block stays hidden and the password form shows directly instead. -->
|
|
<div id="otm-step">
|
|
<a class="otm-login__btn" id="otmLink" href="__OTM_SSO_URL__">Sign in with OTM</a>
|
|
<p class="otm-login__helper">Your OTM account — Face ID, Touch ID or a passkey works there.</p>
|
|
<p class="otm-login__foot"><button type="button" class="otm-login__link" id="showOther">Other ways to sign in</button></p>
|
|
</div>
|
|
|
|
<form id="local" class="otm-login__stack">
|
|
<div class="otm-login__field"><label class="otm-login__label">Email</label><input class="otm-login__input" type="email" id="email" autocomplete="username" required></div>
|
|
<div class="otm-login__field"><label class="otm-login__label">Password</label><input class="otm-login__input" type="password" id="password" autocomplete="current-password" required></div>
|
|
<button class="otm-login__btn" type="submit">Sign in</button>
|
|
<p class="otm-login__foot"><button type="button" class="otm-login__link" id="showOtm">Sign in with OTM</button></p>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
// ?reason=… arrives when an OTM "Log in as admin" link didn't take
|
|
const REASONS = { not_enabled: "OTM sign-in isn't set up for this studio yet",
|
|
invalid_token: "that sign-in link isn't valid", link_already_used: "that sign-in link was already used — ask for a fresh one",
|
|
no_admin: "there's no owner account here to sign in as", no_account: "no studio account matches your OTM account yet",
|
|
server_error: "something went wrong — try again" };
|
|
const otmStep = document.getElementById("otm-step");
|
|
const localForm = document.getElementById("local");
|
|
const configured = document.getElementById("otmLink").getAttribute("href").startsWith("https://");
|
|
if (configured) { otmStep.style.display = "block"; localForm.style.display = "none"; }
|
|
else { otmStep.style.display = "none"; localForm.style.display = "block"; }
|
|
document.getElementById("showOther").addEventListener("click", () => {
|
|
otmStep.style.display = "none"; localForm.style.display = "block";
|
|
});
|
|
document.getElementById("showOtm").addEventListener("click", (e) => {
|
|
e.preventDefault(); otmStep.style.display = "block"; localForm.style.display = "none";
|
|
});
|
|
const reason = new URLSearchParams(location.search).get("reason");
|
|
if (typeof REASONS[reason] === "string") { const e = document.getElementById("err"); e.textContent = REASONS[reason]; e.hidden = false; }
|
|
|
|
document.getElementById("local").addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
const r = await fetch("/api/login", { method: "POST", headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ email: email.value, password: password.value }) }).then((x) => x.json());
|
|
if (r.ok) location.href = "/";
|
|
else { const el = document.getElementById("err"); el.textContent = r.error || "sign-in failed"; el.hidden = false; }
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|