diff --git a/nx/blocks/nav/nav.js b/nx/blocks/nav/nav.js index 01e84e81..34905439 100644 --- a/nx/blocks/nav/nav.js +++ b/nx/blocks/nav/nav.js @@ -5,10 +5,17 @@ import getSvg from '../../utils/svg.js'; const { nxBase } = getConfig(); const ICONS = [ - `${nxBase}/img/logos/aec.svg`, `${nxBase}/img/icons/S2IconHelp20N-icon.svg`, ]; +async function loadBrandLogoSvg() { + const resp = await fetch(`${nxBase}/img/logos/adobe-branding.svg`); + if (!resp.ok) return null; + const text = await resp.text(); + const doc = new DOMParser().parseFromString(text, 'image/svg+xml'); + return doc.querySelector('svg'); +} + function getDefaultPath() { const { origin } = new URL(import.meta.url); return `${origin}/fragments/nx-nav`; @@ -34,11 +41,17 @@ class Nav extends HTMLElement { await loadArea(doc.body); const sections = doc.querySelectorAll('body > .section'); - // Grab the first link as it will be the main branding const brandLink = doc.querySelector('a'); - brandLink.innerHTML = `${brandLink.innerHTML}`; brandLink.classList.add('nx-nav-brand'); - brandLink.insertAdjacentHTML('afterbegin', ''); + brandLink.setAttribute('aria-label', 'Home'); + brandLink.textContent = ''; + const brandSvg = await loadBrandLogoSvg(); + if (brandSvg) { + const svg = brandSvg.cloneNode(true); + svg.classList.add('icon'); + svg.setAttribute('aria-hidden', 'true'); + brandLink.append(svg); + } const inner = document.createElement('div'); inner.className = 'nx-nav-inner'; diff --git a/nx/img/logos/adobe-branding.svg b/nx/img/logos/adobe-branding.svg new file mode 100644 index 00000000..c6c5a461 --- /dev/null +++ b/nx/img/logos/adobe-branding.svg @@ -0,0 +1,4 @@ + diff --git a/nx2/blocks/nav/nav.css b/nx2/blocks/nav/nav.css index e74ec930..3c5dfa88 100644 --- a/nx2/blocks/nav/nav.css +++ b/nx2/blocks/nav/nav.css @@ -16,21 +16,9 @@ } } -.brand-area { - a { - display: block; - height: 100%; - color: inherit; - text-decoration: none; - } - - svg { - height: 100%; - } -} - .action-area { display: none; + flex-shrink: 0; ul { height: 100%; @@ -84,3 +72,138 @@ display: unset; } } + +.brand-area { + display: flex; + align-items: center; + min-width: 0; + flex: 1; + gap: 0; + + a { + display: flex; + align-items: center; + height: 100%; + color: inherit; + text-decoration: none; + } + + /* Logo link — 12px to breadcrumb */ + > a { + flex-shrink: 0; + margin-inline-end: 12px; + } + + a > svg { + width: 24px; + height: 24px; + max-width: 24px; + max-height: 24px; + flex-shrink: 0; + display: block; + box-sizing: border-box; + } + + .nav-home-btn { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + box-sizing: border-box; + width: 32px; + height: 32px; + padding: 0; + margin: 0; + margin-inline: 16px 12px; + border: none; + border-radius: var(--s2-corner-radius-500); + background: transparent; + color: var(--s2-gray-800); + cursor: pointer; + } + + .nav-home-btn:hover { + background-color: var(--s2-gray-100); + } + + .nav-home-btn:focus-visible { + outline: 2px solid var(--s2-blue-800); + outline-offset: 2px; + } + + .nav-home-btn svg { + display: block; + } +} + +.workspace-breadcrumb { + min-width: 0; + overflow: hidden; + + /* Typography: Component M, Regular (S2 component tokens) */ + + /* Color: content / neutral-subdued — default --s2-gray-700, hover --s2-gray-800 */ + ol { + display: flex; + align-items: center; + flex-wrap: nowrap; + list-style: none; + margin: 0; + padding: 0; + gap: 0; + font-family: var(--s2-font-family); + font-size: var(--s2-component-m-regular-font-size); + line-height: var(--s2-component-m-regular-line-height); + font-weight: var(--s2-component-m-regular-font-weight); + color: var(--s2-gray-700); + } + + .crumb { + display: flex; + align-items: center; + min-width: 0; + } + + .crumb-separator { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-inline: 4px 6px; + list-style: none; + + /* design #505050 — matches --s2-gray-700 in light; token in dark */ + color: var(--s2-gray-700); + pointer-events: none; + user-select: none; + } + + .crumb-chevron { + display: block; + transform: translateY(1px); + } + + .crumb-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; + font-weight: inherit; + color: var(--s2-gray-700); + text-decoration: none; + } + + a.crumb-label:hover { + color: var(--s2-gray-800); + } + + .crumb-label.current { + cursor: default; + color: var(--s2-gray-800); + font-weight: var(--s2-component-m-bold-font-weight); + } + +} diff --git a/nx2/blocks/nav/nav.js b/nx2/blocks/nav/nav.js index f019970f..7d9830d3 100644 --- a/nx2/blocks/nav/nav.js +++ b/nx2/blocks/nav/nav.js @@ -1,19 +1,65 @@ -import { LitElement, html } from 'da-lit'; +import { LitElement, html, nothing } from 'da-lit'; import { getMetadata } from '../../scripts/nx.js'; -import { loadStyle } from '../../utils/utils.js'; +import { loadStyle, HashController } from '../../utils/utils.js'; import { loadFragment } from '../fragment/fragment.js'; -import { loadHrefSvg } from '../../utils/svg.js'; +import { loadHrefSvg, ICONS_BASE } from '../../utils/svg.js'; + +const HOME_ICON_HREF = `${ICONS_BASE}S2_Icon_Home_20_N.svg`; const DEFAULT_NAV_PATH = '/nx/fragments/nav'; const style = await loadStyle(import.meta.url); +/** Resolve against this module so ?nx=local fetches from the Nexter dev origin, not da.live. */ +function getBrandLogoHref() { + return new URL('../../../nx/img/logos/adobe-branding.svg', import.meta.url).href; +} + +function normalizeBrandSvg(svg) { + if (!svg) return; + svg.setAttribute('width', '24'); + svg.setAttribute('height', '24'); + svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); +} + +/** Hash segments after `#/`: org, site, then path parts → Folder > Folder > Page */ +function workspaceBreadcrumbSegments(state) { + if (!state?.org) return []; + const segments = [state.org]; + if (state.site) segments.push(state.site); + if (state.path) { + segments.push(...state.path.split('/').filter(Boolean)); + } + return segments; +} + +function formatBreadcrumbLabel(raw, isLast) { + try { + const decoded = decodeURIComponent(raw); + if (isLast && decoded.endsWith('.html')) return decoded.slice(0, -5); + return decoded; + } catch { + return raw; + } +} + +/** Chevron between breadcrumb segments; fill uses currentColor (see `.crumb-separator`). */ +const BREADCRUMB_CHEVRON = html` + +`; + class NXNav extends LitElement { + details = new HashController(this); + static properties = { path: { attribute: false }, _brand: { state: true }, _actions: { state: true }, + _homeIcon: { state: true }, }; connectedCallback() { @@ -22,6 +68,16 @@ class NXNav extends LitElement { this.loadNav(); } + async firstUpdated() { + const svg = await loadHrefSvg(HOME_ICON_HREF); + if (svg) { + svg.setAttribute('width', '20'); + svg.setAttribute('height', '20'); + svg.setAttribute('aria-hidden', 'true'); + this._homeIcon = svg; + } + } + change(props) { if (props.has('path') && this.path) { this.loadNav(); @@ -37,19 +93,18 @@ class NXNav extends LitElement { } async decorateBrand(brandSection) { - // The first link will always be at least an icon const brandLink = brandSection.querySelector('a'); if (!brandLink) return null; - const { href, textContent } = brandLink; - - // Attempt to find a lockup svg - const hasLockup = href.includes('.svg'); - if (hasLockup) { - brandLink.setAttribute('aria-label', textContent); - brandLink.textContent = ''; - const lockup = await loadHrefSvg(href); - brandLink.append(lockup); + + brandLink.setAttribute('aria-label', 'Adobe'); + brandLink.textContent = ''; + + const graphic = await loadHrefSvg(getBrandLogoHref()); + if (graphic) { + normalizeBrandSvg(graphic); + brandLink.append(graphic); } + brandLink.href = '/'; return brandLink; @@ -73,10 +128,64 @@ class NXNav extends LitElement { return getMetadata('nav-path') || this.path || DEFAULT_NAV_PATH; } + async _onHomeClick() { + const { setPanelsGrid } = await import('../../utils/panel.js'); + if (document.body.classList.contains('sidenav-collapsed')) { + document.body.classList.remove('sidenav-collapsed'); + sessionStorage.setItem('nx-sidenav-visible', 'true'); + } else { + document.body.classList.add('sidenav-collapsed'); + sessionStorage.removeItem('nx-sidenav-visible'); + } + setPanelsGrid(); + this.requestUpdate(); + } + + _renderWorkspaceBreadcrumb() { + const segments = workspaceBreadcrumbSegments(this.details.value); + if (segments.length < 2) return nothing; + + return html` + + `; + } + render() { return html`