mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
fix(ui): rail/footer cleanup — drop admin btn, real username, logout menu, collapse Manage
Mock-review batch (4 items): - Footer: drop the redundant Admin button — Manage already surfaces every admin tab, so only the theme toggle relocates from the retired header. - Footer: the user chip now shows the real logged-in user. whoami returns user_id but _storePermissions only persisted permissions, so the chip was stuck on the "account" placeholder; it is now stored as ts.username and the chip repaints once whoami lands (Tier-1 render hook). - Footer: Log out moves into a click-menu on the user chip (reuses the .tab-menu popup chrome; the item clicks the hidden #logout-btn so auth.js stays the single owner of logout and its in-flight-refresh race guards). - Manage: groups start collapsed instead of auto-expanding the first one — the rail is a discovery map, not a wall of open links. Verified end-to-end in the real console shell (headless): chip is a button showing the user, no admin button in the footer, the menu opens with Log out which invokes logout, outside-click/Escape close it, no JS errors.
This commit is contained in:
@@ -713,6 +713,15 @@ function _storePermissions(data) {
|
||||
} else {
|
||||
sessionStorage.removeItem("turnstone_permissions");
|
||||
}
|
||||
// Surface the authenticated identity for the rail footer's user chip.
|
||||
// whoami returns user_id (the username); the shell reads it via ts.username
|
||||
// and repaints once it lands. Cleared in lockstep with permissions so the
|
||||
// chip never shows a stale user after logout / revocation.
|
||||
if (data && data.user_id) {
|
||||
sessionStorage.setItem("ts.username", data.user_id);
|
||||
} else {
|
||||
sessionStorage.removeItem("ts.username");
|
||||
}
|
||||
}
|
||||
|
||||
function _setBusy(busy, label) {
|
||||
|
||||
@@ -331,21 +331,21 @@ export function mountManage(root, paneManager) {
|
||||
root.replaceChildren();
|
||||
|
||||
// If the Admin pane is already open (e.g. restored by PaneManager.rehydrate),
|
||||
// seed the rail to its current tab + expand the owning group; otherwise the
|
||||
// first group expands (mock) and nothing is marked until the user clicks.
|
||||
// seed the rail to its current tab + expand the owning group; otherwise every
|
||||
// group starts collapsed and nothing is marked until the user clicks.
|
||||
const adminOpen =
|
||||
paneManager && paneManager.hasPane && paneManager.hasPane("admin");
|
||||
const activeTab = adminOpen && TS.getActiveTab ? TS.getActiveTab() : null;
|
||||
|
||||
const rowByTab = new Map(); // tab -> its row <button>, for active-state sync
|
||||
|
||||
ia.forEach((group, gi) => {
|
||||
ia.forEach((group) => {
|
||||
const tabs = group.tabs.filter((t) => allowed(t.tab));
|
||||
if (!tabs.length) return; // every tab in the group is gated away, drop it
|
||||
|
||||
const expanded = activeTab
|
||||
? group.tabs.some((t) => t.tab === activeTab) // group owning the active tab
|
||||
: gi === 0; // default: first group open (mock)
|
||||
: false; // default: every group collapsed — Manage is a discovery map, not a wall of open links
|
||||
|
||||
const grp = document.createElement("div");
|
||||
grp.className = "grp";
|
||||
|
||||
@@ -395,7 +395,8 @@
|
||||
border-color: var(--accent-dim);
|
||||
color: var(--ink);
|
||||
}
|
||||
/* logout starts hidden (app.js toggles it on auth) — keep that behaviour */
|
||||
/* The user chip is a menu button (click → Log out); reset native button
|
||||
chrome so it reads as the rail's identity line, not a form control. */
|
||||
.user-chip {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
@@ -403,6 +404,17 @@
|
||||
gap: 7px;
|
||||
color: var(--ink-3);
|
||||
font-size: 12px;
|
||||
padding: 4px 6px;
|
||||
margin-right: -6px;
|
||||
border: none;
|
||||
border-radius: var(--r-sm);
|
||||
background: none;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.user-chip:hover {
|
||||
background: var(--panel-2);
|
||||
color: var(--ink);
|
||||
}
|
||||
.avatar {
|
||||
width: 24px;
|
||||
|
||||
@@ -237,18 +237,38 @@ async function mountShell() {
|
||||
shell.connSlot.replaceWith(statusBarEl);
|
||||
}
|
||||
|
||||
// Relocate the header controls into the rail footer (onclick + ids preserved),
|
||||
// then retire the now-empty header. Order: theme · admin · logout · user.
|
||||
for (const id of ["theme-toggle", "admin-btn", "logout-btn"]) {
|
||||
const btn = document.getElementById(id);
|
||||
if (btn) shell.foot.append(btn);
|
||||
}
|
||||
const userChip = make("span", "user-chip");
|
||||
userChip.append(make("span", "avatar", initialsFor(caps)));
|
||||
const nameEl = make("span", null, displayNameFor(caps));
|
||||
userChip.append(nameEl);
|
||||
// Relocate ONLY the theme toggle into the rail footer (id + onclick
|
||||
// preserved), then retire the now-empty header. The Admin button is dropped
|
||||
// — Manage already surfaces every admin tab, so a separate footer button is
|
||||
// redundant. Logout moves into the user menu (the #logout-btn stays in the
|
||||
// hidden header for its wired onclick + auth.js race-guards; the menu clicks it).
|
||||
const themeBtn = document.getElementById("theme-toggle");
|
||||
if (themeBtn) shell.foot.append(themeBtn);
|
||||
|
||||
// User chip = a menu button: click opens a small popup with Log out. Name +
|
||||
// avatar come from the whoami identity; the chip is built before whoami lands,
|
||||
// so it starts at the "account" placeholder and refreshUser() repaints it once
|
||||
// the real username arrives (see the Tier-1 hook below).
|
||||
const userChip = make("button", "user-chip");
|
||||
userChip.type = "button";
|
||||
userChip.setAttribute("aria-haspopup", "menu");
|
||||
userChip.setAttribute("aria-expanded", "false");
|
||||
const avatarEl = make("span", "avatar", initialsFor(caps));
|
||||
const userNameEl = make("span", "user-name", displayNameFor(caps));
|
||||
userChip.append(avatarEl, userNameEl);
|
||||
userChip.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
toggleUserMenu(userChip);
|
||||
});
|
||||
shell.foot.append(userChip);
|
||||
if (headerEl) headerEl.style.display = "none";
|
||||
const refreshUser = () => {
|
||||
const nm = displayNameFor(caps);
|
||||
if (userNameEl.textContent !== nm) {
|
||||
userNameEl.textContent = nm;
|
||||
avatarEl.textContent = initialsFor(caps);
|
||||
}
|
||||
};
|
||||
|
||||
// ----- PaneManager: one new spine -----
|
||||
const pm = new PaneManager({
|
||||
@@ -497,7 +517,10 @@ async function mountShell() {
|
||||
// rail agree and the glyph never sits stale at an open-time placeholder. One
|
||||
// Tier-1 writer for the tab glyph; the pane's Tier-2 stream drives its body.
|
||||
if (window.TS_APP && typeof window.TS_APP.onRender === "function") {
|
||||
window.TS_APP.onRender(() => paintConvTabGlyphs(pm));
|
||||
window.TS_APP.onRender(() => {
|
||||
paintConvTabGlyphs(pm);
|
||||
refreshUser();
|
||||
});
|
||||
}
|
||||
|
||||
// Hand off to the legacy boot (login + Tier-1 stream) now that the shell and
|
||||
@@ -511,6 +534,79 @@ async function mountShell() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Footer user menu (Log out lives here) ---------------------------------
|
||||
// A small popup anchored to the rail-footer user chip. Reuses the .tab-menu
|
||||
// popup chrome; the one item clicks the hidden #logout-btn so auth.js stays the
|
||||
// single owner of logout (incl. its in-flight-refresh race guards).
|
||||
let _userMenuCleanup = null;
|
||||
|
||||
function closeUserMenu() {
|
||||
if (_userMenuCleanup) _userMenuCleanup();
|
||||
}
|
||||
|
||||
function toggleUserMenu(chip) {
|
||||
if (_userMenuCleanup) {
|
||||
closeUserMenu();
|
||||
return;
|
||||
}
|
||||
const menu = document.createElement("div");
|
||||
menu.className = "tab-menu user-menu";
|
||||
menu.setAttribute("role", "menu");
|
||||
menu.setAttribute("aria-label", "Account");
|
||||
const item = document.createElement("button");
|
||||
item.type = "button";
|
||||
item.className = "tab-menu-item destructive";
|
||||
item.setAttribute("role", "menuitem");
|
||||
const label = document.createElement("span");
|
||||
label.className = "tab-menu-label";
|
||||
label.textContent = "Log out";
|
||||
item.append(label);
|
||||
item.addEventListener("click", () => {
|
||||
closeUserMenu();
|
||||
const lb = document.getElementById("logout-btn");
|
||||
if (lb) lb.click();
|
||||
});
|
||||
menu.append(item);
|
||||
document.body.append(menu);
|
||||
|
||||
// Fixed-positioned, popping UP from the chip (the footer sits at the viewport
|
||||
// bottom); flip down only if there is no room above.
|
||||
const ar = chip.getBoundingClientRect();
|
||||
const mr = menu.getBoundingClientRect();
|
||||
let x = ar.left;
|
||||
if (x + mr.width > window.innerWidth) x = window.innerWidth - mr.width - 4;
|
||||
if (x < 4) x = 4;
|
||||
let y = ar.top - mr.height - 4;
|
||||
if (y < 4) y = ar.bottom + 4;
|
||||
menu.style.left = x + "px";
|
||||
menu.style.top = y + "px";
|
||||
chip.setAttribute("aria-expanded", "true");
|
||||
|
||||
const onDown = (e) => {
|
||||
if (!menu.contains(e.target) && !chip.contains(e.target)) closeUserMenu();
|
||||
};
|
||||
const onKey = (e) => {
|
||||
if (e.key === "Escape") {
|
||||
closeUserMenu();
|
||||
chip.focus();
|
||||
}
|
||||
};
|
||||
_userMenuCleanup = () => {
|
||||
document.removeEventListener("mousedown", onDown);
|
||||
document.removeEventListener("keydown", onKey);
|
||||
menu.remove();
|
||||
chip.setAttribute("aria-expanded", "false");
|
||||
_userMenuCleanup = null;
|
||||
};
|
||||
// Defer the listener attach so the click that opened the menu does not
|
||||
// immediately close it.
|
||||
setTimeout(() => {
|
||||
document.addEventListener("mousedown", onDown);
|
||||
document.addEventListener("keydown", onKey);
|
||||
}, 0);
|
||||
item.focus();
|
||||
}
|
||||
|
||||
function displayNameFor(caps) {
|
||||
// sessionStorage is populated by auth.js after whoami; fall back gracefully.
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user