global topbar #6

Merged
archvillainette merged 6 commits from feat/global-topbar-20260626 into main 2026-06-26 08:27:49 +00:00
7 changed files with 800 additions and 7 deletions
Showing only changes of commit 8cb4ec263f - Show all commits
+113
View File
@@ -145,13 +145,126 @@
});
}
function getWestgateTopbar() {
if (!document || typeof document.querySelector !== 'function') {
return null;
}
return document.querySelector('[data-wg-topbar]');
}
function closeWestgateTopbarPanels(topbar, exceptKey) {
if (!topbar || typeof topbar.querySelectorAll !== 'function') {
return;
}
toArray(topbar.querySelectorAll('[data-wg-panel]')).forEach((panel) => {
if (panel.getAttribute('data-wg-panel') !== exceptKey) {
panel.classList.remove('is-open');
}
});
toArray(topbar.querySelectorAll('[data-wg-menu]')).forEach((trigger) => {
if (trigger.getAttribute('data-wg-menu') !== exceptKey) {
trigger.setAttribute('aria-expanded', 'false');
}
});
}
function closeWestgateTopbarDrawer(topbar) {
if (!topbar) {
return;
}
topbar.classList.remove('is-drawer-open');
const burger = topbar.querySelector('[data-wg-burger]');
if (burger) {
burger.setAttribute('aria-expanded', 'false');
}
}
function initWestgateTopbar() {
const topbar = getWestgateTopbar();
if (!topbar) {
return;
}
if (!window.westgateTheme.topbarEventsBound) {
document.addEventListener('click', (event) => {
const currentTopbar = getWestgateTopbar();
if (!currentTopbar) {
return;
}
const trigger = event.target.closest && event.target.closest('[data-wg-menu]');
if (trigger && currentTopbar.contains(trigger)) {
event.preventDefault();
event.stopPropagation();
const key = trigger.getAttribute('data-wg-menu');
const panel = currentTopbar.querySelector(`[data-wg-panel="${key}"]`);
const shouldOpen = !!panel && !panel.classList.contains('is-open');
closeWestgateTopbarPanels(currentTopbar, shouldOpen ? key : null);
if (panel) {
panel.classList.toggle('is-open', shouldOpen);
}
trigger.setAttribute('aria-expanded', String(shouldOpen));
return;
}
const burger = event.target.closest && event.target.closest('[data-wg-burger]');
if (burger && currentTopbar.contains(burger)) {
event.preventDefault();
event.stopPropagation();
const isOpen = !currentTopbar.classList.contains('is-drawer-open');
currentTopbar.classList.toggle('is-drawer-open', isOpen);
burger.setAttribute('aria-expanded', String(isOpen));
closeWestgateTopbarPanels(currentTopbar, null);
return;
}
if (event.target.closest && event.target.closest('[data-wg-panel]')) {
return;
}
closeWestgateTopbarPanels(currentTopbar, null);
if (!currentTopbar.contains(event.target)) {
closeWestgateTopbarDrawer(currentTopbar);
}
});
document.addEventListener('keydown', (event) => {
if (event.key !== 'Escape') {
return;
}
const currentTopbar = getWestgateTopbar();
closeWestgateTopbarPanels(currentTopbar, null);
closeWestgateTopbarDrawer(currentTopbar);
});
window.westgateTheme.topbarEventsBound = true;
}
$(document)
.off('shown.bs.dropdown.westgateTopbar')
.on('shown.bs.dropdown.westgateTopbar', '.wg-topbar [component="sidebar/search"]', function () {
$(this).find('[component="search/fields"] input[name="query"]').trigger('focus');
});
}
window.westgateTheme = window.westgateTheme || {};
window.westgateTheme.wrapWikiTables = wrapWestgateWikiTables;
window.westgateTheme.initTopbar = initWestgateTopbar;
$(document).ready(function () {
wrapWestgateWikiTables(document);
initWestgateTopbar();
$(window).on('action:ajaxify.end', function () {
wrapWestgateWikiTables(document);
initWestgateTopbar();
closeWestgateTopbarPanels(getWestgateTopbar(), null);
closeWestgateTopbarDrawer(getWestgateTopbar());
});
require(['api'], function (api) {
+418
View File
@@ -0,0 +1,418 @@
.wg-layout-container {
min-height: 100vh;
}
.wg-topbar {
top: 0;
z-index: 1020;
color: var(--wg-text-soft);
background:
linear-gradient(180deg, rgba(255, 241, 196, 0.035), transparent 42%),
linear-gradient(90deg, rgba(42, 18, 34, 0.96), rgba(15, 13, 18, 0.98) 38%, rgba(9, 8, 11, 0.99));
border-bottom: 1px solid rgba(194, 163, 90, 0.2);
box-shadow:
inset 0 1px 0 rgba(255, 244, 221, 0.04),
0 12px 32px rgba(0, 0, 0, 0.34);
}
.wg-topbar__inner {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto auto;
align-items: center;
gap: 0.9rem;
width: min(100%, 1420px);
min-height: 64px;
margin: 0 auto;
padding: 0.55rem clamp(0.85rem, 2vw, 1.5rem);
}
.wg-topbar__brand {
display: inline-flex;
align-items: center;
min-width: 0;
gap: 0.7rem;
color: var(--wg-text);
text-decoration: none;
&:hover,
&:focus {
color: #f3ede4;
text-decoration: none;
}
}
.wg-topbar__brand-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.3rem;
height: 2.3rem;
border: 1px solid rgba(194, 163, 90, 0.36);
border-radius: 50%;
color: var(--wg-gold);
background:
radial-gradient(circle at 38% 24%, rgba(255, 238, 190, 0.2), transparent 30%),
linear-gradient(145deg, rgba(58, 24, 48, 0.7), rgba(10, 9, 13, 0.98));
box-shadow:
inset 0 0 0 1px rgba(9, 8, 11, 0.66),
0 8px 18px rgba(0, 0, 0, 0.35);
font-family: var(--wg-font-display);
font-weight: 700;
}
.wg-topbar__brand-text {
display: flex;
flex-direction: column;
min-width: 0;
line-height: 1;
}
.wg-topbar__brand-name {
max-width: 22ch;
overflow: hidden;
color: var(--wg-text);
font-family: var(--wg-font-display);
font-size: 0.96rem;
font-weight: 600;
letter-spacing: 0.08em;
text-overflow: ellipsis;
text-transform: uppercase;
white-space: nowrap;
}
.wg-topbar__brand-subtitle {
margin-top: 0.28rem;
color: var(--wg-gold-soft);
font-family: var(--wg-font-code);
font-size: 0.62rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.wg-topbar__nav {
min-width: 0;
}
.wg-topbar__nav-list,
.wg-topbar__utility-list {
gap: 0.28rem;
}
.wg-topbar__nav-list {
min-width: 0;
overflow: hidden;
}
.wg-topbar .nav-link,
.wg-topbar__nav-link,
.wg-topbar__forums-toggle {
min-height: 2.35rem;
border: 1px solid transparent;
border-radius: 6px;
padding: 0.42rem 0.62rem;
color: var(--wg-text-soft);
background: transparent;
font-family: var(--wg-font-ui);
font-size: 0.86rem;
line-height: 1.1;
text-decoration: none;
}
.wg-topbar .nav-link:hover,
.wg-topbar .nav-link:focus,
.wg-topbar__nav-link:hover,
.wg-topbar__nav-link:focus,
.wg-topbar__forums-toggle:hover,
.wg-topbar__forums-toggle:focus,
.wg-topbar .nav-item.active > .nav-link,
.wg-topbar .nav-item.active > .navigation-link {
color: #f3ede4;
border-color: rgba(194, 163, 90, 0.2);
background: rgba(194, 163, 90, 0.075);
box-shadow: inset 0 1px 0 rgba(255, 244, 221, 0.035);
}
.wg-topbar__forums {
position: relative;
}
.wg-topbar__forums-toggle {
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.wg-topbar__utilities {
justify-self: end;
}
.wg-topbar__utility-list {
align-items: center;
}
.wg-topbar__utility-list > .nav-item {
position: relative;
}
.wg-topbar__utility-list :where(.nav-text, .visible-open) {
display: none !important;
}
.wg-topbar__utility-list .nav-link {
justify-content: center;
width: 2.35rem;
height: 2.35rem;
padding: 0;
}
.wg-topbar__utility-list [component$="/count"],
.wg-topbar__drawer-actions [component$="/count"],
.wg-topbar [component="navigation/count"] {
background: linear-gradient(to bottom, rgba(142, 52, 56, 0.92), rgba(84, 28, 39, 0.96)) !important;
border: 1px solid rgba(216, 194, 138, 0.26);
color: #fff0dc;
}
.wg-topbar__utility-list .visible-closed {
display: inline-block !important;
}
.wg-topbar__skin .dropend {
margin: 0 !important;
}
.wg-topbar__skin .dropdown-menu,
.wg-topbar__dropdown,
.wg-topbar .search-dropdown,
.wg-topbar .notifications-dropdown,
.wg-topbar .chats-dropdown,
.wg-topbar .drafts-dropdown,
.wg-topbar .user-dropdown,
.wg-topbar__panel {
min-width: min(22rem, calc(100vw - 1.5rem));
max-width: calc(100vw - 1.5rem);
border: 1px solid rgba(194, 163, 90, 0.22) !important;
border-radius: 8px !important;
color: var(--wg-text-soft) !important;
background:
linear-gradient(180deg, rgba(255, 244, 221, 0.028), transparent 36%),
var(--wg-velvet-panel) !important;
box-shadow:
inset 0 1px 0 rgba(255, 244, 221, 0.035),
0 18px 42px rgba(0, 0, 0, 0.42) !important;
}
.wg-topbar .notifications-dropdown,
.wg-topbar .chats-dropdown,
.wg-topbar .drafts-dropdown {
width: min(24rem, calc(100vw - 1.5rem));
}
.wg-topbar .dropdown-item,
.wg-topbar .btn.btn-ghost,
.wg-topbar .btn.btn-light {
color: var(--wg-text-soft) !important;
}
.wg-topbar .dropdown-item:hover,
.wg-topbar .dropdown-item:focus,
.wg-topbar .btn.btn-ghost:hover,
.wg-topbar .btn.btn-ghost:focus,
.wg-topbar .btn.btn-light:hover,
.wg-topbar .btn.btn-light:focus {
color: var(--wg-text) !important;
background: rgba(194, 163, 90, 0.08) !important;
}
.wg-topbar .btn-primary,
.wg-topbar__register {
border-color: rgba(194, 163, 90, 0.34) !important;
color: #1b1208 !important;
background: linear-gradient(to bottom, #d8c28a, var(--wg-gold)) !important;
}
.wg-topbar__login,
.wg-topbar__register {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 2.35rem;
border-radius: 6px;
padding: 0.35rem 0.78rem;
font-family: var(--wg-font-ui);
font-size: 0.86rem;
font-weight: 600;
text-decoration: none;
}
.wg-topbar__login {
color: var(--wg-text-soft);
border: 1px solid rgba(194, 163, 90, 0.2);
background: rgba(194, 163, 90, 0.055);
}
.wg-topbar__login:hover,
.wg-topbar__login:focus {
color: var(--wg-text);
border-color: rgba(194, 163, 90, 0.3);
background: rgba(194, 163, 90, 0.1);
}
.wg-topbar__panel {
position: absolute;
top: calc(100% + 0.45rem);
right: 0;
display: none;
padding: 0.42rem;
}
.wg-topbar__panel.is-open {
display: grid;
gap: 0.2rem;
}
.wg-topbar__panel--forums a {
display: flex;
align-items: center;
min-height: 2.25rem;
border-radius: 6px;
padding: 0.35rem 0.55rem;
color: var(--wg-text-soft);
font-size: 0.9rem;
text-decoration: none;
}
.wg-topbar__panel--forums a:hover,
.wg-topbar__panel--forums a:focus {
color: var(--wg-text);
background: rgba(194, 163, 90, 0.08);
}
.wg-topbar__burger {
display: none;
align-items: center;
justify-content: center;
width: 2.4rem;
height: 2.4rem;
border: 1px solid rgba(194, 163, 90, 0.22);
border-radius: 6px;
color: var(--wg-text-soft);
background: rgba(194, 163, 90, 0.055);
}
.wg-topbar__burger:hover,
.wg-topbar__burger:focus {
color: var(--wg-text);
background: rgba(194, 163, 90, 0.1);
}
.wg-topbar__drawer {
display: none;
border-top: 1px solid rgba(194, 163, 90, 0.12);
background:
linear-gradient(180deg, rgba(42, 18, 34, 0.98), rgba(12, 10, 15, 0.99));
}
.wg-topbar.is-drawer-open .wg-topbar__drawer {
display: block;
}
.wg-topbar__drawer-inner {
display: grid;
gap: 0.8rem;
width: min(100%, 42rem);
margin: 0 auto;
padding: 0.85rem;
}
.wg-topbar__drawer-search {
display: flex;
align-items: center;
gap: 0.55rem;
border: 1px solid rgba(194, 163, 90, 0.18);
border-radius: 8px;
padding: 0.5rem 0.65rem;
background: rgba(9, 8, 11, 0.45);
}
.wg-topbar__drawer-search input {
min-width: 0;
width: 100%;
border: 0;
color: var(--wg-text);
background: transparent;
outline: 0;
}
.wg-topbar__drawer-nav,
.wg-topbar__drawer-actions,
.wg-topbar__drawer-auth {
display: grid;
gap: 0.35rem;
}
.wg-topbar__drawer-link,
.wg-topbar__drawer-actions a {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.55rem;
border: 1px solid rgba(194, 163, 90, 0.12);
border-radius: 7px;
padding: 0.62rem 0.7rem;
color: var(--wg-text-soft);
background: rgba(255, 244, 221, 0.02);
text-decoration: none;
}
.wg-topbar__drawer-link:hover,
.wg-topbar__drawer-link:focus,
.wg-topbar__drawer-actions a:hover,
.wg-topbar__drawer-actions a:focus {
color: var(--wg-text);
border-color: rgba(194, 163, 90, 0.22);
background: rgba(194, 163, 90, 0.08);
}
@media (max-width: 1199.98px) {
.wg-topbar__inner {
grid-template-columns: auto minmax(0, 1fr) auto;
}
.wg-topbar__nav {
display: none;
}
}
@media (max-width: 991.98px) {
.wg-topbar__inner {
grid-template-columns: minmax(0, 1fr) auto;
}
.wg-topbar__brand-name {
max-width: 16ch;
}
.wg-topbar__brand-subtitle,
.wg-topbar__forums,
.wg-topbar__utilities {
display: none;
}
.wg-topbar__burger {
display: inline-flex;
}
}
@media (max-width: 480px) {
.wg-topbar__brand-mark {
width: 2rem;
height: 2rem;
}
.wg-topbar__brand-name {
max-width: 13ch;
font-size: 0.82rem;
letter-spacing: 0.05em;
}
}
+20
View File
@@ -0,0 +1,20 @@
</div><!-- /.container#content -->
</main>
</div>
<div class="fixed-bottom d-lg-none">
<!-- IMPORT partials/topic/navigator-mobile.tpl -->
</div>
{{{ if !isSpider }}}
<div>
<div component="toaster/tray" class="alert-window fixed-bottom mb-5 mb-md-2 me-2 me-md-5 ms-auto" style="width:300px; z-index: 1090;">
<!-- IMPORT partials/reconnect-alert.tpl -->
</div>
</div>
{{{ end }}}
<!-- IMPORT partials/footer/js.tpl -->
</body>
</html>
+2 -7
View File
@@ -25,15 +25,10 @@
<body class="{bodyClass} skin-{{{if bootswatchSkin}}}{bootswatchSkin}{{{else}}}noskin{{{end}}}">
<a class="visually-hidden-focusable position-absolute top-0 start-0 p-3 m-3 bg-body" style="z-index: 1021;" href="#content">[[global:skip-to-content]]</a>
{{{ if config.theme.topMobilebar }}}
<!-- IMPORT partials/mobile-header.tpl -->
{{{ end }}}
<div class="layout-container d-flex justify-content-between pb-4 pb-md-0">
<!-- IMPORT partials/sidebar-left.tpl -->
<div class="layout-container wg-layout-container d-flex flex-column pb-4 pb-md-0">
<!-- IMPORT partials/header/topbar.tpl -->
<main id="panel" class="d-flex flex-column gap-3 flex-grow-1 mt-3" style="min-width: 0;">
<!-- IMPORT partials/header/brand.tpl -->
<div class="container-lg px-md-4 d-flex flex-column gap-3 h-100 mb-5 mb-lg-0" id="content">
<!-- IMPORT partials/noscript/warning.tpl -->
<!-- IMPORT partials/noscript/message.tpl -->
+150
View File
@@ -0,0 +1,150 @@
<header class="wg-topbar sticky-top" data-wg-topbar>
<div class="wg-topbar__inner">
<a class="wg-topbar__brand" href="{relative_path}/" aria-label="{config.siteTitle}">
<span class="wg-topbar__brand-mark" aria-hidden="true">W</span>
<span class="wg-topbar__brand-text">
<span class="wg-topbar__brand-name">{config.siteTitle}</span>
<span class="wg-topbar__brand-subtitle">Shadows Over Westgate</span>
</span>
</a>
<nav class="wg-topbar__nav" aria-label="[[global:navigation]]">
<ul id="main-nav" class="wg-topbar__nav-list list-unstyled d-flex align-items-center mb-0">
{{{ each navigation }}}
{{{ if displayMenuItem(@root, @index) }}}
<li class="nav-item {./class}{{{ if ./dropdown }}} dropdown{{{ end }}}" title="{./title}">
<a class="nav-link navigation-link wg-topbar__nav-link d-flex gap-2 align-items-center {{{ if ./dropdown }}}dropdown-toggle{{{ end }}}" {{{ if ./dropdown }}} href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" {{{ else }}} href="{./route}"{{{ end }}} {{{ if ./id }}}id="{./id}"{{{ end }}}{{{ if ./targetBlank }}} target="_blank"{{{ end }}} {{{ if ./text }}}aria-label="{./text}"{{{ end }}}>
{{{ if ./iconClass }}}
<i class="fa fa-fw {./iconClass}" data-content="{./content}"></i>
{{{ end }}}
{{{ if ./text }}}<span class="nav-text text-truncate">{./text}</span>{{{ end }}}
<span component="navigation/count" class="badge rounded-1 bg-primary {{{ if !./content }}}hidden{{{ end }}}">{./content}</span>
</a>
{{{ if ./dropdown }}}
<ul class="dropdown-menu wg-topbar__dropdown p-1 shadow" role="menu">
{./dropdownContent}
</ul>
{{{ end }}}
</li>
{{{ end }}}
{{{ end }}}
</ul>
</nav>
<div class="wg-topbar__forums dropdown">
<button class="wg-topbar__nav-link wg-topbar__forums-toggle" type="button" data-wg-menu="forums" aria-expanded="false" aria-haspopup="true">
<span>Forums</span>
<i class="fa fa-fw fa-chevron-down" aria-hidden="true"></i>
</button>
<div class="wg-topbar__panel wg-topbar__panel--forums" data-wg-panel="forums" role="menu">
<a href="{relative_path}/categories" role="menuitem">[[pages:categories]]</a>
<a href="{relative_path}/recent" role="menuitem">[[recent:title]]</a>
<a href="{relative_path}/unread" role="menuitem">[[unread:title]]</a>
<a href="{relative_path}/popular" role="menuitem">[[pages:popular]]</a>
<a href="{relative_path}/tags" role="menuitem">[[tags:tags]]</a>
</div>
</div>
<div class="wg-topbar__utilities">
{{{ if config.loggedIn }}}
<ul id="wg-topbar-logged-in-menu" class="wg-topbar__utility-list list-unstyled d-flex align-items-center mb-0">
{{{ if (config.searchEnabled && user.privileges.search:content) }}}
<li component="sidebar/search" class="nav-item search dropdown position-relative" title="[[global:header.search]]" role="menuitem">
<!-- IMPORT partials/sidebar/search.tpl -->
</li>
{{{ end }}}
<li component="notifications" class="nav-item notifications dropdown" title="[[global:header.notifications]]" role="menuitem">
<!-- IMPORT partials/sidebar/notifications.tpl -->
</li>
{{{ if canChat }}}
<li class="nav-item chats dropdown" title="[[global:header.chats]]" role="menuitem">
<!-- IMPORT partials/sidebar/chats.tpl -->
</li>
{{{ end }}}
<li component="sidebar/drafts" class="nav-item drafts dropdown" title="[[global:header.drafts]]" role="menuitem">
<!-- IMPORT partials/sidebar/drafts.tpl -->
</li>
{{{ if !config.disableCustomUserSkins }}}
<li class="nav-item wg-topbar__skin">
<!-- IMPORT partials/skin-switcher.tpl -->
</li>
{{{ end }}}
<li id="user_label" class="nav-item usermenu dropdown" title="{user.username}" role="menuitem">
<!-- IMPORT partials/sidebar/user-menu.tpl -->
</li>
</ul>
{{{ else }}}
<ul id="wg-topbar-logged-out-menu" class="wg-topbar__utility-list wg-topbar__utility-list--guest list-unstyled d-flex align-items-center mb-0">
{{{ if (config.searchEnabled && user.privileges.search:content) }}}
<li component="sidebar/search" class="nav-item search dropdown position-relative" title="[[global:header.search]]" role="menuitem">
<!-- IMPORT partials/sidebar/search.tpl -->
</li>
{{{ end }}}
<li class="nav-item">
<a class="wg-topbar__login" href="{relative_path}/login">[[global:login]]</a>
</li>
{{{ if allowRegistration }}}
<li class="nav-item">
<a class="wg-topbar__register" href="{relative_path}/register">[[global:register]]</a>
</li>
{{{ end }}}
</ul>
{{{ end }}}
</div>
<button class="wg-topbar__burger" type="button" data-wg-burger aria-expanded="false" aria-controls="wg-topbar-drawer" aria-label="[[global:menu]]">
<i class="fa fa-fw fa-bars" aria-hidden="true"></i>
</button>
</div>
<div id="wg-topbar-drawer" class="wg-topbar__drawer">
<div class="wg-topbar__drawer-inner">
{{{ if (config.searchEnabled && user.privileges.search:content) }}}
<form class="wg-topbar__drawer-search" action="{relative_path}/search" method="GET" role="search">
<i class="fa fa-fw fa-search" aria-hidden="true"></i>
<input autocomplete="off" type="text" name="query" placeholder="[[global:search]]" aria-label="[[search:type-to-search]]">
</form>
{{{ end }}}
<nav class="wg-topbar__drawer-nav" aria-label="[[global:navigation]]">
{{{ each navigation }}}
{{{ if displayMenuItem(@root, @index) }}}
<a class="wg-topbar__drawer-link {./class}" href="{./route}" {{{ if ./targetBlank }}}target="_blank"{{{ end }}}>
{{{ if ./iconClass }}}<i class="fa fa-fw {./iconClass}" aria-hidden="true"></i>{{{ end }}}
{{{ if ./text }}}<span>{./text}</span>{{{ end }}}
<span component="navigation/count" class="badge rounded-1 bg-primary {{{ if !./content }}}hidden{{{ end }}}">{./content}</span>
</a>
{{{ end }}}
{{{ end }}}
<a class="wg-topbar__drawer-link" href="{relative_path}/categories"><i class="fa fa-fw fa-layer-group" aria-hidden="true"></i><span>[[pages:categories]]</span></a>
<a class="wg-topbar__drawer-link" href="{relative_path}/recent"><i class="fa fa-fw fa-clock" aria-hidden="true"></i><span>[[recent:title]]</span></a>
<a class="wg-topbar__drawer-link" href="{relative_path}/unread"><i class="fa fa-fw fa-inbox" aria-hidden="true"></i><span>[[unread:title]]</span></a>
<a class="wg-topbar__drawer-link" href="{relative_path}/tags"><i class="fa fa-fw fa-tags" aria-hidden="true"></i><span>[[tags:tags]]</span></a>
</nav>
{{{ if config.loggedIn }}}
<div class="wg-topbar__drawer-actions">
<a href="{relative_path}/notifications"><i class="fa fa-fw fa-bell" aria-hidden="true"></i><span>[[global:header.notifications]]</span><span component="notifications/count" class="badge rounded-1 bg-primary {{{ if !unreadCount.notification }}}hidden{{{ end }}}">{unreadCount.notification}</span></a>
{{{ if canChat }}}
<a href="{relative_path}/user/{user.userslug}/chats{{{ if user.lastRoomId }}}/{user.lastRoomId}{{{ end }}}"><i class="fa fa-fw fa-comment" aria-hidden="true"></i><span>[[global:header.chats]]</span><span component="chat/count" class="badge rounded-1 bg-primary {{{ if !unreadCount.chat }}}hidden{{{ end }}}">{unreadCount.chat}</span></a>
{{{ end }}}
<a href="{relative_path}/user/{user.userslug}"><i class="fa fa-fw fa-user" aria-hidden="true"></i><span>[[user:profile]]</span></a>
<a href="{relative_path}/user/{user.userslug}/settings"><i class="fa fa-fw fa-gear" aria-hidden="true"></i><span>[[user:settings]]</span></a>
</div>
{{{ else }}}
<div class="wg-topbar__drawer-auth">
<a class="wg-topbar__login" href="{relative_path}/login">[[global:login]]</a>
{{{ if allowRegistration }}}
<a class="wg-topbar__register" href="{relative_path}/register">[[global:register]]</a>
{{{ end }}}
</div>
{{{ end }}}
</div>
</div>
</header>
+96
View File
@@ -0,0 +1,96 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
function read(relativePath) {
const filePath = path.join(__dirname, '..', relativePath);
assert(fs.existsSync(filePath), `${relativePath} should exist`);
return fs.readFileSync(filePath, 'utf8');
}
function assertIncludes(haystack, needle, message) {
assert(
haystack.includes(needle),
message || `Expected file to include ${needle}`
);
}
function assertExcludes(haystack, needle, message) {
assert(
!haystack.includes(needle),
message || `Expected file not to include ${needle}`
);
}
const header = read('templates/header.tpl');
const footer = read('templates/footer.tpl');
const topbar = read('templates/partials/header/topbar.tpl');
const theme = read('theme.scss');
const stylesheet = read('scss/westgate/_topbar.scss');
const client = read('public/client.js');
assertIncludes(
header,
'<!-- IMPORT partials/header/topbar.tpl -->',
'Header should mount the Westgate topbar partial'
);
assertExcludes(
header,
'<!-- IMPORT partials/sidebar-left.tpl -->',
"Header should no longer render Harmony's left global sidebar"
);
assertExcludes(
footer,
'<!-- IMPORT partials/sidebar-right.tpl -->',
"Footer should no longer render Harmony's right global sidebar"
);
assertIncludes(
theme,
'@import "./scss/westgate/topbar";',
'theme.scss should import the focused topbar partial'
);
assertIncludes(
stylesheet,
'.wg-topbar',
'Topbar styles should be scoped to .wg-topbar'
);
assertIncludes(
client,
'initWestgateTopbar',
'Client bundle should initialize Westgate topbar behavior'
);
[
'component="sidebar/search"',
'component="notifications"',
'component="sidebar/drafts"',
].forEach((component) => {
assertIncludes(topbar, component, `Topbar should preserve live NodeBB hook ${component}`);
});
[
'<!-- IMPORT partials/sidebar/search.tpl -->',
'<!-- IMPORT partials/sidebar/notifications.tpl -->',
'<!-- IMPORT partials/sidebar/chats.tpl -->',
'<!-- IMPORT partials/sidebar/drafts.tpl -->',
'<!-- IMPORT partials/sidebar/user-menu.tpl -->',
].forEach((templateImport) => {
assertIncludes(topbar, templateImport, `Topbar should reuse Harmony live partial ${templateImport}`);
});
[
'Velessa Thorne',
'Guildmaster Orin',
'A letter left at the Undergate',
'wg-topbar-demo',
"localStorage.setItem('wg-topbar-presence'",
].forEach((fixture) => {
assertExcludes(topbar, fixture, `Topbar should not ship preview fixture ${fixture}`);
});
assertIncludes(topbar, '{{{ if config.loggedIn }}}', 'Topbar should branch on the real logged-in state');
assertIncludes(topbar, '{{{ if canChat }}}', 'Chat controls should remain permission-gated');
assertIncludes(topbar, '{{{ if allowRegistration }}}', 'Registration should remain server-gated');
assertIncludes(topbar, '{{{ each navigation }}}', 'Topbar should render ACP Navigation items');
+1
View File
@@ -2,6 +2,7 @@
@import "./scss/westgate/tokens";
@import "./scss/westgate/wiki-prose";
@import "./scss/westgate/header";
@import "./scss/westgate/topbar";
@import "./scss/westgate/surfaces";
@import "./scss/westgate/categories";
@import "./scss/westgate/topics";