diff --git a/app/lib/version.mjs b/app/lib/version.mjs index 76abbf1..35d5e18 100644 --- a/app/lib/version.mjs +++ b/app/lib/version.mjs @@ -1 +1 @@ -export const APP_VERSION = "0.5.2"; +export const APP_VERSION = "0.6.0"; diff --git a/app/package.json b/app/package.json index c80dc56..59ec03f 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "bms-backbone", - "version": "0.5.2", + "version": "0.6.0", "private": true, "type": "module", "scripts": { diff --git a/app/public/app.js b/app/public/app.js index f4709c8..36e6d26 100644 --- a/app/public/app.js +++ b/app/public/app.js @@ -873,10 +873,56 @@ async function ideas() { })); } -document.getElementById("signout")?.addEventListener("click", async () => { - await api("/api/logout", {}); - location.href = "/login"; -}); +// Account menu in the sidebar — plain-HTML port of @otm/account-panel's +// OtmAccountMenu (otm-login.tsx 0.44.0): initials avatar, OTM badge + "Manage +// OTM account" when the session came in via Sign in with OTM, Sign out +// always. No passwordHref — the studio has no local "change password" flow. +function initials(name, fallback = "?") { + const parts = String(name ?? "").trim().split(/\s+/).filter(Boolean); + if (!parts.length) return fallback; + return parts.slice(0, 2).map((p) => p[0].toUpperCase()).join(""); +} +async function buildAccountMenu() { + const nav = document.getElementById("account-nav"); + if (!nav) return; + // fetch() directly, not the shared `get` helper — that's defined further + // down this file and this runs immediately at script load. + const me = await fetch("/api/me").then((r) => r.json()).catch(() => ({})); + const email = me.email || ""; + const linked = me.via === "otm"; + nav.innerHTML = ` +
+ + + ${esc(email)} + ${linked ? `OTM` : ""} + + +
`; + document.getElementById("signout")?.addEventListener("click", async () => { + await api("/api/logout", {}); + location.href = "/login"; + }); + const menu = nav.querySelector("details.otm-menu"); + if (menu) { + document.addEventListener("mousedown", (e) => { if (menu.open && !menu.contains(e.target)) menu.open = false; }); + document.addEventListener("keydown", (e) => { if (e.key === "Escape" && menu.open) menu.open = false; }); + } +} +buildAccountMenu(); // ── version + one-tap update (OTM self-update) ─────────────────── const foot = $("#foot"); diff --git a/app/public/index.html b/app/public/index.html index 2e14450..2545f0a 100644 --- a/app/public/index.html +++ b/app/public/index.html @@ -85,6 +85,27 @@ .matrow input:last-of-type { width: 80px; } footer { text-align: center; color: #b1a0ae; font-size: 13px; margin-top: 32px; font-style: italic; font-family: Georgia, serif; } footer .chip { font-family: "Avenir Next", "Nunito", sans-serif; font-style: normal; margin-left: 8px; } + + /* ported from @otm/account-panel otm-login-core.ts 0.44.0 (OTM_LOGIN_CSS, + the .otm-badge/.otm-menu rules) — keep in sync, verbatim except + .otm-menu__panel's `right:0` → `left:0` (the trigger sits in a 212px + left sidebar; opening to the right would clip off the page edge). The + studio's accent (--plum) is set inline on .otm-menu in app.js, not here. */ + .otm-badge{display:inline-block;border-radius:5px;padding:1px 5px;font-size:9px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;background:color-mix(in srgb,var(--otm-accent,#b5562a) 16%,transparent);color:var(--otm-accent,#b5562a);vertical-align:middle} + .otm-menu{position:relative;display:inline-block;--otm-accent:#b5562a;--otm-accent-ink:#fff;--otm-card:#fff;--otm-fg:#1f1b18;--otm-muted:#6b6560;--otm-line:rgba(31,27,24,.12)} + @media (prefers-color-scheme:dark){.otm-menu{--otm-card:#211e1b;--otm-fg:#f3efe9;--otm-muted:#a39b92;--otm-line:rgba(243,239,233,.12)}} + .otm-menu>summary{list-style:none;display:inline-flex;align-items:center;gap:8px;padding:4px 8px;border-radius:10px;cursor:pointer;color:inherit;font:inherit} + .otm-menu>summary::-webkit-details-marker{display:none} + .otm-menu>summary:hover{background:rgba(127,127,127,.1)} + .otm-menu__avatar{width:26px;height:26px;border-radius:999px;background:var(--otm-accent);color:var(--otm-accent-ink);display:inline-flex;align-items:center;justify-content:center;font-size:10px;font-weight:700} + .otm-menu__name{font-size:14px} + @media (max-width:640px){.otm-menu__name{display:none}} + .otm-menu__panel{position:absolute;left:0;top:calc(100% + 6px);min-width:15rem;background:var(--otm-card);color:var(--otm-fg);border:1px solid var(--otm-line);border-radius:14px;padding:6px;box-shadow:0 10px 30px rgba(0,0,0,.18);z-index:50} + .otm-menu__who{padding:8px 10px 10px;font-size:12px;color:var(--otm-muted);border-bottom:1px solid var(--otm-line);margin-bottom:6px} + .otm-menu__who strong{display:flex;align-items:center;gap:6px;color:var(--otm-fg);font-weight:500;margin-bottom:2px} + .otm-menu__item{display:flex;align-items:center;gap:10px;width:100%;padding:8px 10px;border-radius:9px;border:0;background:none;color:inherit;font:inherit;font-size:14px;text-align:left;text-decoration:none;cursor:pointer} + .otm-menu__item:hover{background:rgba(127,127,127,.1)} + .otm-menu__item svg{width:16px;height:16px;opacity:.75;flex:none} @@ -102,7 +123,7 @@ - +
diff --git a/app/public/login.html b/app/public/login.html index 03594f9..f94a9d9 100644 --- a/app/public/login.html +++ b/app/public/login.html @@ -5,57 +5,65 @@ 🌙 Studio Notebook — sign in -
-
-

🌙 Studio Notebook

-
welcome back to the studio
-
-
-
- - -
- Sign in with OTM -
Your OTM account — Face ID, Touch ID or a passkey works there.
- + diff --git a/app/server.mjs b/app/server.mjs index d9c0f1c..17bd273 100644 --- a/app/server.mjs +++ b/app/server.mjs @@ -229,9 +229,9 @@ const routes = { if (!user) user = await db.user.findFirst({ where: { role: p.role }, orderBy: { id: "asc" } }); if (!user) return fail("no_account"); const next = p.next && p.next.startsWith("/") && !p.next.startsWith("//") ? p.next : "/"; - return { _setSession: { email: user.email, exp: Date.now() + SESSION_DAYS * 86400e3 }, _redirect: next }; + return { _setSession: { email: user.email, via: "otm", exp: Date.now() + SESSION_DAYS * 86400e3 }, _redirect: next }; }, - "GET /api/me": async (_b, _q, session) => ({ email: session.email, operator: session.operator ?? null }), + "GET /api/me": async (_b, _q, session) => ({ email: session.email, operator: session.operator ?? null, via: session.via ?? null }), "GET /api/summary": async () => ({ todos: (await db.todo.findMany({ orderBy: { id: "desc" }, take: 60 })).map(sTodo),