global topbar (#6)

adds a global topbar to the theme

Reviewed-on: #6
Reviewed-by: xtul <mpiasecki720@protonmail.com>
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
This commit was merged in pull request #6.
This commit is contained in:
2026-06-26 08:27:49 +00:00
committed by archvillainette
parent 52999ae604
commit 2d37f8a1f6
13 changed files with 835 additions and 232 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
## Project ## Project
This repository is `nodebb-theme-westgate`, a NodeBB theme for the Shadows Over Westgate forum. This repository is `nodebb-theme-westgate`, a NodeBB theme for the Shadows Over Westgate forum and website.
It is forked from `nodebb-theme-quickstart` and should remain a small, focused NodeBB child theme rather than a full replacement for NodeBB or Harmony. It is forked from `nodebb-theme-quickstart` and should remain a small, focused NodeBB child theme rather than a full replacement for NodeBB or Harmony.
@@ -39,10 +39,9 @@ drafts, and authentication entry points.
- `theme.json` declares `baseTheme: "nodebb-theme-harmony"`. - `theme.json` declares `baseTheme: "nodebb-theme-harmony"`.
- `theme.scss` imports Harmony first, then focused Westgate partials under - `theme.scss` imports Harmony first, then focused Westgate partials under
`scss/westgate/`. `scss/westgate/`.
- `templates/header.tpl` currently matches the Harmony layout pattern: a - `templates/header.tpl` currently mounts the Westgate topbar inside the
`layout-container`, `partials/sidebar-left.tpl`, `#panel`, and `layout-container` before `#panel`.
`partials/header/brand.tpl`. - `templates/footer.tpl` no longer imports Harmony's right sidebar.
- Harmony's `footer.tpl` imports `partials/sidebar-right.tpl`.
- Harmony's sidebar partials under - Harmony's sidebar partials under
`/home/vicky/Repositories/nodebb-theme-harmony/templates/partials/sidebar/` `/home/vicky/Repositories/nodebb-theme-harmony/templates/partials/sidebar/`
own the live controls for navigation, user menu, search, notifications, chat, own the live controls for navigation, user menu, search, notifications, chat,
-6
View File
@@ -115,11 +115,6 @@ library.defineWidgetAreas = async function (areas) {
template: 'global', template: 'global',
location: 'sidebar-footer', location: 'sidebar-footer',
}, },
{
name: 'Brand Header',
template: 'global',
location: 'brand-header',
},
{ {
name: 'About me (before)', name: 'About me (before)',
template: 'account/profile.tpl', template: 'account/profile.tpl',
@@ -182,4 +177,3 @@ library.filterMiddlewareRenderHeader = async function (hookData) {
hookData.templateData.bootswatchSkinOptions = await meta.css.getSkinSwitcherOptions(hookData.req.uid); hookData.templateData.bootswatchSkinOptions = await meta.css.getSkinSwitcherOptions(hookData.req.uid);
return hookData; return hookData;
}; };
+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 = window.westgateTheme || {};
window.westgateTheme.wrapWikiTables = wrapWestgateWikiTables; window.westgateTheme.wrapWikiTables = wrapWestgateWikiTables;
window.westgateTheme.initTopbar = initWestgateTopbar;
$(document).ready(function () { $(document).ready(function () {
wrapWestgateWikiTables(document); wrapWestgateWikiTables(document);
initWestgateTopbar();
$(window).on('action:ajaxify.end', function () { $(window).on('action:ajaxify.end', function () {
wrapWestgateWikiTables(document); wrapWestgateWikiTables(document);
initWestgateTopbar();
closeWestgateTopbarPanels(getWestgateTopbar(), null);
closeWestgateTopbarDrawer(getWestgateTopbar());
}); });
require(['api'], function (api) { require(['api'], function (api) {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

-187
View File
@@ -1,187 +0,0 @@
.westgate-brand-hero {
position: relative;
overflow: hidden;
margin-top: 0.25rem;
border: 1px solid rgba(194, 163, 90, 0.22);
border-radius: 10px;
padding: 1.2rem 1.05rem;
filter: saturate(1) contrast(1) brightness(1);
background-color: #16121b;
background:
linear-gradient(90deg, rgba(5, 4, 7, 0.82) 0%, rgba(7, 6, 9, 0.72) 22%, rgba(9, 7, 11, 0.48) 34%, rgba(10, 8, 12, 0.18) 43%, rgba(10, 8, 12, 0) 52%),
url("./plugins/nodebb-theme-westgate/images/plum-header-bg.png") center 50% / cover no-repeat;
box-shadow:
inset 0 1px 0 rgba(255, 242, 204, 0.08),
inset 0 0 0 1px rgba(33, 21, 28, 0.5),
0 18px 42px rgba(0, 0, 0, 0.3);
&::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(circle at 62% 100%, rgba(88, 38, 86, 0.26), rgba(88, 38, 86, 0.13) 20%, transparent 44%),
linear-gradient(155deg, transparent 58%, rgba(55, 24, 53, 0.08) 78%, rgba(78, 33, 75, 0.14) 100%);
pointer-events: none;
}
}
.westgate-brand-hero__veil,
.westgate-brand-hero__inner {
position: relative;
z-index: 1;
}
.westgate-brand-hero__veil {
display: none;
}
.westgate-brand-hero__inner {
align-items: center;
justify-content: flex-start;
gap: 0;
min-height: 142px;
width: 100%;
z-index: 2;
}
.westgate-brand-hero__brand {
width: 100%;
flex-wrap: wrap;
min-height: inherit;
padding: 0 0 0 clamp(1rem, 4vw, 3rem);
justify-content: flex-start;
text-align: left;
&:hover,
&:focus-within {
background: transparent;
}
}
.westgate-brand-hero__brand :where([component="brand/anchor"], [component="siteTitle"]) {
color: inherit;
text-decoration: none;
&:hover,
&:focus {
color: inherit;
text-decoration: none;
background: transparent;
box-shadow: none;
outline: none;
}
}
.westgate-brand-hero__title-shell {
width: 100%;
padding: 0;
min-height: inherit;
justify-content: flex-start;
text-align: left;
color: inherit;
position: relative;
z-index: 3;
text-decoration: none;
&:hover,
&:focus {
color: inherit;
text-decoration: none;
background: transparent;
box-shadow: none;
outline: none;
}
}
.westgate-brand-hero__title-block {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.35rem;
text-align: left;
}
.westgate-brand-hero__brand [component="siteTitle"] :where(h1, .fs-6) {
font-family: var(--wg-font-display);
font-size: clamp(1.25rem, 2.1vw, 1.9rem) !important;
line-height: 0.92;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
text-shadow:
0 1px 0 rgba(255, 222, 142, 0.05),
0 2px 0 rgba(52, 37, 15, 0.92),
0 4px 0 rgba(16, 11, 6, 0.96),
0 8px 14px rgba(0, 0, 0, 0.72),
0 14px 26px rgba(0, 0, 0, 0.46),
0 0 16px rgba(48, 10, 26, 0.24);
opacity: 0.82;
mix-blend-mode: screen;
}
.westgate-brand-hero__subtitle {
color: #c2a35a;
font-family: var(--wg-font-display);
font-size: 0.64rem;
font-weight: 600;
letter-spacing: 0.22em;
line-height: 1;
text-transform: uppercase;
text-shadow:
0 1px 0 rgba(0, 0, 0, 0.92),
0 3px 8px rgba(0, 0, 0, 0.34);
}
.westgate-brand-hero__widgets {
position: relative;
z-index: 4;
color: var(--wg-text);
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.74);
width: 100%;
padding-left: 0;
border-left: 0;
text-align: center;
margin-top: 0.5rem;
> :last-child {
margin-bottom: 0;
}
}
@media (max-width: 991.98px) {
.westgate-brand-hero {
margin-top: 0;
padding: 1.05rem 0.9rem;
background-position: center 46%;
}
.westgate-brand-hero__inner {
min-height: 118px;
flex-wrap: wrap;
}
.westgate-brand-hero__brand {
min-height: 0;
padding: 0 0 0 0.65rem;
}
.westgate-brand-hero__title-block {
gap: 0.28rem;
}
.westgate-brand-hero__brand [component="siteTitle"] :where(h1, .fs-6) {
font-size: clamp(1rem, 3.9vw, 1.35rem) !important;
letter-spacing: 0.07em;
}
.westgate-brand-hero__subtitle {
font-size: 0.56rem;
letter-spacing: 0.18em;
}
.westgate-brand-hero__widgets {
border-top: 1px solid rgba(216, 194, 138, 0.12);
padding-top: 0.85rem;
}
}
+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}}}"> <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> <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 }}} <div class="layout-container wg-layout-container d-flex flex-column pb-4 pb-md-0">
<!-- IMPORT partials/mobile-header.tpl --> <!-- IMPORT partials/header/topbar.tpl -->
{{{ end }}}
<div class="layout-container d-flex justify-content-between pb-4 pb-md-0">
<!-- IMPORT partials/sidebar-left.tpl -->
<main id="panel" class="d-flex flex-column gap-3 flex-grow-1 mt-3" style="min-width: 0;"> <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"> <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/warning.tpl -->
<!-- IMPORT partials/noscript/message.tpl --> <!-- IMPORT partials/noscript/message.tpl -->
-26
View File
@@ -1,26 +0,0 @@
{{{ if (brand:logo || (config.showSiteTitle || widgets.brand-header.length)) }}}
<div class="container-lg px-md-4 brand-container">
<div class="westgate-brand-hero">
<div class="westgate-brand-hero__veil"></div>
<div class="d-flex westgate-brand-hero__inner justify-content-start">
{{{ if config.showSiteTitle }}}
<div component="brand/wrapper" class="d-flex align-items-center justify-content-start align-content-stretch westgate-brand-hero__brand">
<a component="siteTitle" class="align-self-stretch align-items-center d-flex justify-content-start westgate-brand-hero__title-shell" href="{{{ if title:url }}}{title:url}{{{ else }}}{relative_path}/{{{ end }}}">
<span class="westgate-brand-hero__title-block">
<h1 class="fs-6 fw-bold text-body mb-0">{config.siteTitle}</h1>
<span class="westgate-brand-hero__subtitle">Forums</span>
</span>
</a>
</div>
{{{ end }}}
{{{ if widgets.brand-header.length }}}
<div data-widget-area="brand-header" class="flex-fill gap-3 align-self-center westgate-brand-hero__widgets">
{{{each widgets.brand-header}}}
{{./html}}
{{{end}}}
</div>
{{{ end }}}
</div>
</div>
</div>
{{{ end }}}
+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 {./textClass}">{./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 class="{./textClass}">{./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>
+127
View File
@@ -0,0 +1,127 @@
'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 assertMissing(relativePath, message) {
const filePath = path.join(__dirname, '..', relativePath);
assert(!fs.existsSync(filePath), message || `${relativePath} should not exist`);
}
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');
const themeHooks = read('lib/theme.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'
);
assertExcludes(
theme,
'@import "./scss/westgate/header";',
'theme.scss should not import the removed brand banner stylesheet'
);
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');
assertIncludes(topbar, '{./textClass}', 'Topbar should honor ACP Navigation text visibility classes');
assertExcludes(topbar, 'href="{relative_path}/admin"', 'Topbar should not hard-code the administrator link');
assertMissing(
'templates/partials/header/brand.tpl',
'Theme should inherit Harmony brand markup instead of shipping the removed Westgate brand banner override'
);
assertMissing(
'scss/westgate/_header.scss',
'Removed brand banner styles should not remain in the theme'
);
assertMissing(
'public/images/plum-header-bg.png',
'Removed brand banner background should not remain in static assets'
);
assertExcludes(
themeHooks,
'brand-header',
'Removed brand banner widget area should not remain registered'
);
+1 -1
View File
@@ -1,7 +1,7 @@
@import "../nodebb-theme-harmony/theme"; @import "../nodebb-theme-harmony/theme";
@import "./scss/westgate/tokens"; @import "./scss/westgate/tokens";
@import "./scss/westgate/wiki-prose"; @import "./scss/westgate/wiki-prose";
@import "./scss/westgate/header"; @import "./scss/westgate/topbar";
@import "./scss/westgate/surfaces"; @import "./scss/westgate/surfaces";
@import "./scss/westgate/categories"; @import "./scss/westgate/categories";
@import "./scss/westgate/topics"; @import "./scss/westgate/topics";