blob: a4dbd043c16d1254cbf7e376ae0d5fe40e90cb0e [file]
<!--
Licensed to the Apache Software Foundation (ASF) under one or more contributor
license agreements. See the NOTICE file distributed with this work for
additional information regarding copyright ownership. The ASF licenses this
file to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.
-->
<script>
import Icon from "./Icon.svelte";
import { href, link, route } from "../lib/router.js";
import { INFRA_CONTACT } from "../lib/config.js";
let { prefs = null, onLogout } = $props();
const isAdmin = $derived(Boolean(prefs?.credentials?.admin));
const currentAction = $derived($route.action || "preferences");
/** Nav model. `admin: true` entries are hidden from non-admins. */
const menus = $derived([
{
id: "you",
label: "You",
items: [
{
action: "preferences",
icon: "user",
label: "Your account",
desc: "Your link status and the repositories you can write to.",
},
{
action: "verify",
icon: "shield",
label: "Verify your GitHub identity",
desc: "Required for Trusted Publishing.",
},
{
icon: "logout",
label: "Log out",
desc: "End your session and clear your cookie.",
onSelect: onLogout,
},
],
},
{
id: "repos",
label: "Repositories",
items: [
{
action: "newrepo",
icon: "repo-plus",
label: "Create a repository",
desc: "Set up a new repository on GitHub and GitBox.",
},
{
action: "defaultbranch",
icon: "branch",
label: "Change default branch",
desc: "Point a repository at a different default branch.",
},
{
action: "search",
icon: "search",
label: "User search",
desc: "Look up a user's GitBox/GitHub access level.",
admin: true,
},
],
},
{
id: "help",
label: "Help",
items: [
{
action: "faq",
icon: "book",
label: "Frequently asked questions",
desc: "Answers to the questions we get most often.",
},
{
url: INFRA_CONTACT,
icon: "mail",
label: "Contact Infrastructure",
desc: "Ask us anything Boxer could not answer.",
},
],
},
]);
const visibleMenus = $derived(
menus.map((menu) => ({
...menu,
items: menu.items.filter((item) => !item.admin || isAdmin),
})),
);
let openMenu = $state(null);
let mobileOpen = $state(false);
/** The whole bar, including the hamburger, counts as "inside" the menu. */
let barEl;
function toggle(id) {
openMenu = openMenu === id ? null : id;
}
function closeAll() {
openMenu = null;
mobileOpen = false;
}
function onWindowClick(event) {
if (barEl && !barEl.contains(event.target)) closeAll();
}
function onWindowKeydown(event) {
if (event.key === "Escape") closeAll();
}
function select(item, event) {
if (item.onSelect) {
event.preventDefault();
item.onSelect();
}
closeAll();
}
</script>
<svelte:window onclick={onWindowClick} onkeydown={onWindowKeydown} />
<header class="appbar">
<div class="appbar-inner" bind:this={barEl}>
<a class="brand" href={href("")} use:link onclick={closeAll}>
<img src="./images/logo.png" alt="" width="34" height="34" />
<span class="brand-text">
<strong>Boxer</strong>
<em>ASF Infrastructure</em>
</span>
</a>
<button
class="hamburger"
type="button"
aria-expanded={mobileOpen}
aria-controls="primary-nav"
onclick={() => (mobileOpen = !mobileOpen)}
>
<Icon name={mobileOpen ? "close" : "menu"} size={20} />
<span class="sr-only">Menu</span>
</button>
<nav
id="primary-nav"
class="nav"
class:open={mobileOpen}
aria-label="Primary"
>
<a class="nav-link" href="https://gitbox.apache.org/" onclick={closeAll}>
<Icon name="home" size={16} />
GitBox
</a>
{#each visibleMenus as menu (menu.id)}
<div class="menu" class:open={openMenu === menu.id}>
<button
class="nav-link"
class:active={menu.items.some((i) => i.action === currentAction)}
type="button"
aria-expanded={openMenu === menu.id}
onclick={() => toggle(menu.id)}
>
{menu.label}
<span class="caret" class:flip={openMenu === menu.id}>
<Icon name="chevron-down" size={15} />
</span>
</button>
{#if openMenu === menu.id}
<div class="dropdown">
{#each menu.items as item (item.label)}
<a
class="dropdown-item"
class:active={item.action && item.action === currentAction}
href={item.url ?? href(item.action ?? "")}
target={item.url ? "_blank" : undefined}
rel={item.url ? "noopener noreferrer" : undefined}
use:link
onclick={(e) => select(item, e)}
>
<span class="dropdown-icon"><Icon name={item.icon} size={18} /></span>
<span class="dropdown-copy">
<span class="dropdown-title">
{item.label}
{#if item.url}<Icon name="external" size={12} />{/if}
</span>
<span class="dropdown-desc">{item.desc}</span>
</span>
</a>
{/each}
</div>
{/if}
</div>
{/each}
{#if prefs?.credentials?.uid}
<span class="whoami" title={prefs.credentials.fullname || ""}>
{#if prefs.github?.login}
<img
class="whoami-avatar"
src={`https://github.com/${prefs.github.login}.png?size=48`}
alt=""
width="24"
height="24"
loading="lazy"
/>
{:else}
<Icon name="user" size={15} />
{/if}
{prefs.credentials.uid}
{#if isAdmin}<span class="admin-chip">admin</span>{/if}
</span>
{/if}
</nav>
</div>
</header>
<style>
.appbar {
position: sticky;
top: 0;
z-index: 40;
background: var(--shell);
border-bottom: 1px solid rgb(255 255 255 / 8%);
}
.appbar-inner {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
max-width: var(--content-width);
min-height: var(--header-height);
margin: 0 auto;
padding: 0 24px;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
margin-right: 18px;
color: var(--text-on-shell);
text-decoration: none;
}
.brand:hover {
text-decoration: none;
}
.brand img {
border-radius: 7px;
background: rgb(255 255 255 / 6%);
padding: 2px;
}
.brand-text {
display: flex;
flex-direction: column;
line-height: 1.15;
}
.brand-text strong {
font-size: 1.02rem;
font-weight: 650;
letter-spacing: -0.01em;
}
.brand-text em {
font-size: 0.71rem;
font-style: normal;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-on-shell-muted);
}
.nav {
display: flex;
align-items: center;
gap: 2px;
flex: 1;
}
.nav-link {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border: 0;
border-radius: var(--radius-sm);
background: none;
color: var(--text-on-shell-muted);
font: inherit;
font-size: 0.9rem;
font-weight: 520;
cursor: pointer;
white-space: nowrap;
transition: background-color 0.14s ease, color 0.14s ease;
}
.nav-link:hover,
.nav-link[aria-expanded="true"] {
background: rgb(255 255 255 / 8%);
color: var(--text-on-shell);
text-decoration: none;
}
.nav-link.active {
color: var(--text-on-shell);
}
.caret {
display: flex;
transition: transform 0.16s ease;
}
.caret.flip {
transform: rotate(180deg);
}
.menu {
position: relative;
}
.dropdown {
position: absolute;
top: calc(100% + 8px);
left: 0;
z-index: 50;
width: 330px;
padding: 6px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
box-shadow: var(--shadow-lg);
animation: drop 0.13s ease-out;
}
@keyframes drop {
from {
opacity: 0;
transform: translateY(-4px);
}
}
.dropdown-item {
display: flex;
gap: 12px;
padding: 10px;
border-radius: var(--radius-sm);
color: var(--text);
text-decoration: none;
}
.dropdown-item:hover,
.dropdown-item.active {
background: var(--surface-2);
text-decoration: none;
}
.dropdown-icon {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
flex: none;
border-radius: 8px;
background: var(--accent-soft);
color: var(--accent-text);
}
.dropdown-copy {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
}
.dropdown-title {
display: flex;
align-items: center;
gap: 5px;
font-weight: 570;
font-size: 0.9rem;
}
.dropdown-desc {
font-size: 0.8rem;
line-height: 1.4;
color: var(--text-muted);
}
.whoami {
display: inline-flex;
align-items: center;
gap: 7px;
margin-left: auto;
padding: 5px 10px 5px 6px;
border-radius: 999px;
background: rgb(255 255 255 / 7%);
color: var(--text-on-shell);
font-size: 0.83rem;
font-weight: 550;
}
.whoami-avatar {
border-radius: 50%;
}
.admin-chip {
padding: 1px 7px;
border-radius: 999px;
background: var(--accent);
color: #fff;
font-size: 0.66rem;
font-weight: 650;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.hamburger {
display: none;
align-items: center;
justify-content: center;
margin-left: auto;
padding: 8px;
border: 0;
border-radius: var(--radius-sm);
background: rgb(255 255 255 / 8%);
color: var(--text-on-shell);
cursor: pointer;
}
@media (max-width: 860px) {
.appbar-inner {
flex-wrap: wrap;
padding: 0 16px;
}
.hamburger {
display: inline-flex;
}
.nav {
display: none;
flex-basis: 100%;
flex-direction: column;
align-items: stretch;
gap: 2px;
padding: 8px 0 14px;
}
.nav.open {
display: flex;
}
.nav-link {
width: 100%;
padding: 11px 12px;
font-size: 0.95rem;
}
/* Only the section toggles push their caret to the far edge. */
button.nav-link {
justify-content: space-between;
}
.dropdown {
position: static;
width: auto;
margin: 2px 0 6px;
box-shadow: none;
animation: none;
}
.whoami {
margin: 8px 0 0;
align-self: flex-start;
}
}
</style>