diff --git a/public/client.js b/public/client.js index 4634899..22ba7f3 100644 --- a/public/client.js +++ b/public/client.js @@ -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) { diff --git a/scss/westgate/_topbar.scss b/scss/westgate/_topbar.scss new file mode 100644 index 0000000..7ecec9e --- /dev/null +++ b/scss/westgate/_topbar.scss @@ -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; + } +} diff --git a/templates/footer.tpl b/templates/footer.tpl new file mode 100644 index 0000000..60e0878 --- /dev/null +++ b/templates/footer.tpl @@ -0,0 +1,20 @@ + + + + + +
+ +
+ + {{{ if !isSpider }}} +
+
+ +
+
+ {{{ end }}} + + + + diff --git a/templates/header.tpl b/templates/header.tpl index 412826c..83fe707 100644 --- a/templates/header.tpl +++ b/templates/header.tpl @@ -25,15 +25,10 @@ [[global:skip-to-content]] - {{{ if config.theme.topMobilebar }}} - - {{{ end }}} - -
- +
+
-
diff --git a/templates/partials/header/topbar.tpl b/templates/partials/header/topbar.tpl new file mode 100644 index 0000000..a28b6d9 --- /dev/null +++ b/templates/partials/header/topbar.tpl @@ -0,0 +1,150 @@ +
+
+ + + + {config.siteTitle} + Shadows Over Westgate + + + + + + + +
+ {{{ if config.loggedIn }}} +
    + {{{ if (config.searchEnabled && user.privileges.search:content) }}} + + {{{ end }}} + + + + {{{ if canChat }}} + + {{{ end }}} + + + + {{{ if !config.disableCustomUserSkins }}} + + {{{ end }}} + + +
+ {{{ else }}} +
    + {{{ if (config.searchEnabled && user.privileges.search:content) }}} + + {{{ end }}} + + + {{{ if allowRegistration }}} + + {{{ end }}} +
+ {{{ end }}} +
+ + +
+ +
+
+ {{{ if (config.searchEnabled && user.privileges.search:content) }}} + + {{{ end }}} + + + + {{{ if config.loggedIn }}} + + {{{ else }}} +
+ + {{{ if allowRegistration }}} + [[global:register]] + {{{ end }}} +
+ {{{ end }}} +
+
+
diff --git a/tests/global-topbar-contract.test.js b/tests/global-topbar-contract.test.js new file mode 100644 index 0000000..efe2fb4 --- /dev/null +++ b/tests/global-topbar-contract.test.js @@ -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, + '', + 'Header should mount the Westgate topbar partial' +); +assertExcludes( + header, + '', + "Header should no longer render Harmony's left global sidebar" +); +assertExcludes( + footer, + '', + "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}`); +}); + +[ + '', + '', + '', + '', + '', +].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'); diff --git a/theme.scss b/theme.scss index ee8e390..748907a 100644 --- a/theme.scss +++ b/theme.scss @@ -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";