diff --git a/src/client/admin-clients.ts b/src/client/admin-clients.ts index 24b5306..48488b5 100644 --- a/src/client/admin-clients.ts +++ b/src/client/admin-clients.ts @@ -1,5 +1,7 @@ const token = localStorage.getItem("indiko_session"); -const footer = document.getElementById("footer") as HTMLElement; +import "./ds"; +import type IToast from "./ds/toast"; + const clientsList = document.getElementById("clientsList") as HTMLElement; const createClientBtn = document.getElementById( "createClientBtn", @@ -15,9 +17,6 @@ const addRedirectUriBtn = document.getElementById( const redirectUrisList = document.getElementById( "redirectUrisList", ) as HTMLElement; -import "./ds"; -import type IToast from "./ds/toast"; - const toast = document.getElementById("toast") as unknown as IToast; function showToast(message: string, type: "success" | "error" = "success") { @@ -45,27 +44,6 @@ async function checkAuth() { const data = await response.json(); - footer.innerHTML = `admin • signed in as ${data.username} • sign out - `; - - document - .getElementById("logoutLink") - ?.addEventListener("click", async (e) => { - e.preventDefault(); - try { - await fetch("/auth/logout", { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, - }); - } catch { - // Ignore logout errors - } - localStorage.removeItem("indiko_session"); - window.location.href = "/login"; - }); - if (!data.isAdmin) { window.location.href = "/"; return; @@ -74,7 +52,6 @@ async function checkAuth() { loadClients(); } catch (error) { console.error("Auth check failed:", error); - footer.textContent = "error loading user info"; clientsList.innerHTML = '
Failed to load clients
'; } } diff --git a/src/client/admin-invites.ts b/src/client/admin-invites.ts index c811521..298d753 100644 --- a/src/client/admin-invites.ts +++ b/src/client/admin-invites.ts @@ -1,5 +1,6 @@ +import "./ds"; + const token = localStorage.getItem("indiko_session"); -const footer = document.getElementById("footer") as HTMLElement; const invitesList = document.getElementById("invitesList") as HTMLElement; const createInviteBtn = document.getElementById( "createInviteBtn", @@ -27,28 +28,6 @@ async function checkAuth() { const data = await response.json(); - footer.innerHTML = `admin • signed in as ${data.username} • sign out - `; - - // Handle logout - document - .getElementById("logoutLink") - ?.addEventListener("click", async (e) => { - e.preventDefault(); - try { - await fetch("/auth/logout", { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, - }); - } catch { - // Ignore logout errors - } - localStorage.removeItem("indiko_session"); - window.location.href = "/login"; - }); - // Check if admin if (!data.isAdmin) { window.location.href = "/"; @@ -59,8 +38,7 @@ async function checkAuth() { loadInvites(); } catch (error) { console.error("Auth check failed:", error); - footer.textContent = "error loading user info"; - usersList.innerHTML = '
Failed to load users
'; + invitesList.innerHTML = '
Failed to load invites
'; } } diff --git a/src/client/admin.ts b/src/client/admin.ts index 8be604e..467e342 100644 --- a/src/client/admin.ts +++ b/src/client/admin.ts @@ -1,5 +1,6 @@ +import "./ds"; + const token = localStorage.getItem("indiko_session"); -const footer = document.getElementById("footer") as HTMLElement; const usersList = document.getElementById("usersList") as HTMLElement; let currentUserId: number; @@ -26,28 +27,6 @@ async function checkAuth() { const data = await response.json(); currentUserId = data.id; - footer.innerHTML = `admin • signed in as ${data.username} • sign out - `; - - // Handle logout - document - .getElementById("logoutLink") - ?.addEventListener("click", async (e) => { - e.preventDefault(); - try { - await fetch("/auth/logout", { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, - }); - } catch { - // Ignore logout errors - } - localStorage.removeItem("indiko_session"); - window.location.href = "/login"; - }); - // Check if admin if (!data.isAdmin) { window.location.href = "/"; @@ -58,7 +37,6 @@ async function checkAuth() { loadUsers(); } catch (error) { console.error("Auth check failed:", error); - footer.textContent = "error loading user info"; usersList.innerHTML = '
Failed to load users
'; } } diff --git a/src/client/docs.ts b/src/client/docs.ts index db58e17..df1b826 100644 --- a/src/client/docs.ts +++ b/src/client/docs.ts @@ -1,3 +1,5 @@ +import "./ds"; + // JSON syntax highlighter function highlightJSON(json: string): string { return json diff --git a/src/client/ds/components.css b/src/client/ds/components.css index ca42727..f6828dd 100644 --- a/src/client/ds/components.css +++ b/src/client/ds/components.css @@ -113,6 +113,92 @@ } } +/* i-nav — shared top navigation */ +@scope (i-nav) { + :scope { + display: block; + width: 100%; + } + + .nav { + display: flex; + align-items: center; + gap: var(--space-6); + width: 100%; + max-width: var(--page-width); + margin: 0 auto var(--space-8); + padding-bottom: var(--space-4); + border-bottom: 1px solid var(--paper-dim); + } + + .brand img { + height: 1.75rem; + display: block; + } + + .links { + display: flex; + gap: var(--space-6); + flex: 1; + } + + .links a { + color: var(--paper-dim); + text-decoration: none; + font-size: var(--text-sm); + font-weight: 600; + letter-spacing: 0.05rem; + padding-bottom: var(--space-1); + border-bottom: 2px solid transparent; + transition: color 0.15s; + } + + .links a:hover { + color: var(--paper); + } + + .links a.active { + color: var(--accent); + border-bottom-color: var(--accent); + } + + .who { + display: flex; + align-items: center; + gap: var(--space-4); + font-size: var(--text-sm); + } + + .who .user { + color: var(--paper); + text-decoration: none; + font-weight: 600; + } + + .who .user:hover { + text-decoration: underline; + } + + .who .signout { + background: transparent; + border: 1px solid var(--paper-dim); + padding: 0.3rem 0.75rem; + color: var(--paper-dim); + font-family: var(--font); + font-size: var(--text-xs); + font-weight: 600; + letter-spacing: 0.03rem; + cursor: pointer; + transition: all 0.15s; + } + + .who .signout:hover { + border-color: var(--accent-deep); + color: var(--accent-deep); + background: rgba(160, 70, 104, 0.12); + } +} + /* i-toast */ @scope (i-toast) { .toast { diff --git a/src/client/ds/index.ts b/src/client/ds/index.ts index 038534c..c938a05 100644 --- a/src/client/ds/index.ts +++ b/src/client/ds/index.ts @@ -1,12 +1,14 @@ // Indiko design system — import once per page to register all components. import "./button"; import "./card"; -import "./toast"; +import "./nav"; import "./passkey-row"; import "./scope-list"; +import "./toast"; export { default as IButton } from "./button"; export { default as ICard } from "./card"; +export { default as INav } from "./nav"; export { default as IPasskeyRow } from "./passkey-row"; export { default as IScopeList } from "./scope-list"; export { default as IToast } from "./toast"; diff --git a/src/client/ds/nav.ts b/src/client/ds/nav.ts new file mode 100644 index 0000000..37adbc7 --- /dev/null +++ b/src/client/ds/nav.ts @@ -0,0 +1,95 @@ +import { Elena, html, nothing } from "@elenajs/core"; + +interface SessionInfo { + username: string; + isAdmin: boolean; +} + +/** + * — shared top navigation for authenticated pages. + * + * Attributes: active (dashboard|apps|docs|admin) + * Renders logo, primary links, and the signed-in user chip. Fetches the + * current session itself so every page behaves identically. Stays minimal + * (logo + links) until the session resolves. + * + * Usage: + */ +export default class INav extends Elena(HTMLElement) { + static override tagName = "i-nav"; + static override props = ["active"]; + + active = ""; + + private session: SessionInfo | null = null; + + override async firstUpdated() { + const token = localStorage.getItem("indiko_session"); + if (!token) return; + + try { + const res = await fetch("/api/hello", { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) return; + const data = await res.json(); + this.session = { username: data.username, isAdmin: !!data.isAdmin }; + this.requestUpdate(); + } catch { + // Stay logged-out-looking on error + } + } + + private link(href: string, key: string, label: string) { + const current = this.active === key; + return html`${label}`; + } + + override render() { + const user = this.session; + return html` + + `; + } + + override updated() { + const btn = this.querySelector("#signout"); + if (btn && !btn.dataset.bound) { + btn.dataset.bound = "1"; + btn.addEventListener("click", () => this.signOut()); + } + } + + private async signOut() { + const token = localStorage.getItem("indiko_session"); + try { + await fetch("/auth/logout", { + method: "POST", + headers: { Authorization: `Bearer ${token}` }, + }); + } catch { + // Ignore + } + localStorage.removeItem("indiko_session"); + window.location.href = "/login"; + } +} +INav.define(); diff --git a/src/client/ds/tokens.css b/src/client/ds/tokens.css index 41f9fe0..e88bacb 100644 --- a/src/client/ds/tokens.css +++ b/src/client/ds/tokens.css @@ -10,6 +10,9 @@ --accent-deep: #a04668; --danger: #e57373; + /* Layout — pages override this to keep nav + content aligned */ + --page-width: 56.25rem; + /* Typography */ --font: "Space Grotesk", sans-serif; --text-xs: 0.75rem; diff --git a/src/client/index.ts b/src/client/index.ts index 3fb224c..dda6a4e 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -5,7 +5,7 @@ import type IToast from "./ds/toast"; const token = localStorage.getItem("indiko_session"); -let footer!: HTMLElement; + let welcome!: HTMLElement; let subtitle!: HTMLElement; let recentApps!: HTMLElement; @@ -44,7 +44,6 @@ function $(id: string): HTMLElement { } function init() { - footer = $("footer"); welcome = $("welcome"); subtitle = $("subtitle"); recentApps = $("recentApps"); @@ -138,36 +137,12 @@ async function checkAuth() { welcome.textContent = `welcome, ${data.username}`; subtitle.textContent = "your identity dashboard"; - // Build footer with conditional admin link - const adminLink = data.isAdmin ? ' • admin' : ""; - footer.innerHTML = `signed in as ${data.username} • apps • docs${adminLink} • sign out`; - - // Handle logout - document - .getElementById("logoutLink") - ?.addEventListener("click", async (e) => { - e.preventDefault(); - try { - await fetch("/auth/logout", { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, - }); - } catch { - // Ignore logout errors - } - localStorage.removeItem("indiko_session"); - window.location.href = "/login"; - }); - // Load profile and apps loadProfile(); loadRecentApps(); loadPasskeys(); } catch (error) { console.error("Auth check failed:", error); - footer.textContent = "error loading user info"; } } diff --git a/src/html/admin-clients.html b/src/html/admin-clients.html index b040ff0..b5aa1b1 100644 --- a/src/html/admin-clients.html +++ b/src/html/admin-clients.html @@ -603,14 +603,13 @@ -
- - -
+ + +
@@ -624,10 +623,6 @@ - -
- - diff --git a/src/html/apps.html b/src/html/apps.html index 81c6c17..d3f8a5f 100644 --- a/src/html/apps.html +++ b/src/html/apps.html @@ -29,21 +29,24 @@ header, main { width: 100%; - max-width: 44rem; + max-width: var(--page-width); margin-left: auto; margin-right: auto; } header { - margin-bottom: 2rem; + margin-bottom: 3rem; } h1 { - margin-bottom: 0.25rem; + font-size: 2.5rem; + margin-bottom: 0.5rem; } .subtitle { color: var(--paper-dim); + font-size: 1.125rem; + font-weight: 300; } .apps-list { @@ -134,24 +137,12 @@ padding: 1rem 0; } - .back-link { - margin-top: 2rem; - text-align: center; - font-size: var(--text-sm); - } - - .back-link a { - color: var(--accent); - text-decoration: none; - } - - .back-link a:hover { - text-decoration: underline; - } + +

authorized apps

manage apps that have access to your account

@@ -162,9 +153,6 @@
loading apps...
- diff --git a/src/html/docs.html b/src/html/docs.html index 7a80ab6..fc21d3c 100644 --- a/src/html/docs.html +++ b/src/html/docs.html @@ -21,6 +21,7 @@ +