Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions nx/blocks/nav/nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand All @@ -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 = `<span>${brandLink.innerHTML}</span>`;
brandLink.classList.add('nx-nav-brand');
brandLink.insertAdjacentHTML('afterbegin', '<svg class="icon"><use href="#spectrum-ExperienceCloud"/></svg>');
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';
Expand Down
4 changes: 4 additions & 0 deletions nx/img/logos/adobe-branding.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
149 changes: 136 additions & 13 deletions nx2/blocks/nav/nav.css
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
Expand Down Expand Up @@ -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);
}

}
135 changes: 122 additions & 13 deletions nx2/blocks/nav/nav.js
Original file line number Diff line number Diff line change
@@ -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`
<svg class="crumb-chevron" width="10" height="10" viewBox="0 0 10 10" fill="none"
xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path fill="currentColor" d="M7.48254 4.40625L3.85949 0.783199C3.53137 0.455079 3.00011 0.455079 2.67199 0.783199C2.34387 1.11132 2.34387 1.64258 2.67199 1.9707L5.70129 5L2.67199 8.0293C2.34387 8.35742 2.34387 8.88868 2.67199 9.2168C2.83605 9.38086 3.0509 9.46289 3.26574 9.46289C3.48058 9.46289 3.69543 9.38086 3.85949 9.2168L7.48254 5.59375C7.81066 5.26563 7.81066 4.73437 7.48254 4.40625Z" />
</svg>
`;

class NXNav extends LitElement {
details = new HashController(this);

static properties = {
path: { attribute: false },
_brand: { state: true },
_actions: { state: true },
_homeIcon: { state: true },
};

connectedCallback() {
Expand All @@ -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();
Expand All @@ -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;
Expand All @@ -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`
<nav class="workspace-breadcrumb" aria-label="Location">
<ol>
${segments.flatMap((raw, i) => {
const isLast = i === segments.length - 1;
const label = formatBreadcrumbLabel(raw, isLast);
const hashHref = `#/${segments.slice(0, i + 1).join('/')}`;
const items = [];
if (i > 0) {
items.push(html`
<li class="crumb-separator" aria-hidden="true">${BREADCRUMB_CHEVRON}</li>
`);
}
items.push(html`
<li class="crumb">
${isLast
? html`<span class="crumb-label current" aria-current="page">${label}</span>`
: html`<a class="crumb-label" href="${hashHref}">${label}</a>`}
</li>
`);
return items;
})}
</ol>
</nav>
`;
}

render() {
return html`
<div class="brand-area">
<button
type="button"
class="nav-home-btn"
aria-label="Home"
aria-expanded=${document.body.classList.contains('sidenav-collapsed') ? 'false' : 'true'}
@click=${this._onHomeClick}
>
${this._homeIcon ?? nothing}
</button>
${this._brand}
${this._renderWorkspaceBreadcrumb()}
</div>
<div class="action-area">
${this._actions}
Expand Down
Loading