From 646b39b2003315a27c189709a4ac92b723ec64b6 Mon Sep 17 00:00:00 2001 From: Antonio Ledebuhr Date: Wed, 26 Aug 2026 12:52:17 +0200 Subject: [PATCH] Make the primary nav customer-facing and keep the services submenu readable. Stack and the CV leave the chrome so Contact stays the main action. The desktop submenu now uses an opaque surface token and a stacking context above page content. Co-authored-by: Cursor --- src/app/app.html | 12 ++--------- src/app/app.scss | 21 ++++++++++++------- src/app/app.spec.ts | 19 ++++++++++------- src/app/app.ts | 14 ++----------- src/app/core/content/shell-copy.ts | 3 --- .../navigation/navigation.service.spec.ts | 8 +++++++ src/app/core/navigation/navigation.ts | 4 ---- src/app/submenu.styles.spec.ts | 15 +++++++++++++ src/styles.scss | 1 + 9 files changed, 52 insertions(+), 45 deletions(-) create mode 100644 src/app/submenu.styles.spec.ts diff --git a/src/app/app.html b/src/app/app.html index 53afa24..7591de4 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1,13 +1,12 @@ -
+
- + diff --git a/src/app/app.scss b/src/app/app.scss index b1f2811..bffcff9 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -16,7 +16,7 @@ .site-header { position: relative; top: 0; - z-index: 10; + z-index: 30; padding-block: var(--space-3); } @@ -199,6 +199,7 @@ .primary-nav > li { position: relative; + z-index: 31; } .primary-nav > li > a { @@ -211,20 +212,24 @@ top: 100%; left: 0; min-width: max-content; - z-index: 20; - padding: var(--space-2) var(--space-3); + z-index: 32; + padding: var(--space-3) var(--space-4); gap: var(--space-1); border-radius: var(--radius-md); - background: var(--surface-glass); - backdrop-filter: blur(var(--blur-glass)); - -webkit-backdrop-filter: blur(var(--blur-glass)); + background: var(--color-surface-menu); border: 1px solid var(--surface-glass-border); - box-shadow: var(--shadow-soft); + box-shadow: var(--shadow-raised); } - .primary-nav > li:hover > .submenu, .primary-nav > li:focus-within > .submenu { display: flex; flex-direction: column; } + + @media (hover: hover) and (pointer: fine) { + .primary-nav > li:hover > .submenu { + display: flex; + flex-direction: column; + } + } } diff --git a/src/app/app.spec.ts b/src/app/app.spec.ts index ccebb65..4573d46 100644 --- a/src/app/app.spec.ts +++ b/src/app/app.spec.ts @@ -89,16 +89,19 @@ describe('App', () => { expect(compiled.querySelector('footer')).toBeTruthy(); }); - it('keeps the palette trigger in the header and the dialog outside .site', async () => { + it('renders the terminal dock outside .site and keeps the header free of a CV link', async () => { const fixture = TestBed.createComponent(App); await fixture.whenStable(); const compiled = fixture.nativeElement as HTMLElement; const site = compiled.querySelector('.site'); - const trigger = compiled.querySelector('header .command-palette-trigger'); + const dock = compiled.querySelector('app-terminal-dock'); + const trigger = compiled.querySelector('.terminal-dock-trigger'); expect(site).toBeTruthy(); - expect(trigger).toBeTruthy(); - expect(site?.contains(trigger)).toBe(true); + expect(dock).toBeTruthy(); + expect(site?.contains(dock)).toBe(false); + expect(compiled.querySelector('header a[href="/cv/CV.pdf"]')).toBeNull(); + expect(compiled.querySelector('header .command-palette-trigger')).toBeNull(); (trigger as HTMLButtonElement).click(); fixture.detectChanges(); @@ -106,10 +109,10 @@ describe('App', () => { TestBed.inject(ApplicationRef).tick(); fixture.detectChanges(); - const dialog = compiled.querySelector('[role="dialog"]'); - expect(dialog).toBeTruthy(); - expect(site?.contains(dialog)).toBe(false); - expect(site?.hasAttribute('inert')).toBe(true); + const panel = compiled.querySelector('.terminal-dock-panel'); + expect(panel).toBeTruthy(); + expect(site?.contains(panel)).toBe(false); + expect(site?.hasAttribute('inert')).toBe(false); }); it('keeps the server-rendered nav expanded and collapses after hydration on a narrow viewport', async () => { diff --git a/src/app/app.ts b/src/app/app.ts index f2c039b..98c0eea 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -18,9 +18,7 @@ import { LOCALE_HTML_LANG, otherLocale } from './core/i18n/locale'; import { LocaleService } from './core/i18n/locale.service'; import { NavigationService } from './core/navigation/navigation.service'; import { isBrowserPlatform, viewportMatches } from './core/platform/browser'; -import { CommandPalette } from './shared/command-palette/command-palette'; -import { CommandPaletteTrigger } from './shared/command-palette/command-palette-trigger/command-palette-trigger'; -import { CommandPaletteService } from './shared/command-palette/command-palette.service'; +import { TerminalDock } from './shared/terminal/terminal-dock'; /** Matches `md` in `src/_breakpoints.scss` (48rem). */ export const WIDE_NAV_QUERY = '(min-width: 48rem)'; @@ -28,21 +26,13 @@ export const WIDE_NAV_QUERY = '(min-width: 48rem)'; @Component({ selector: 'app-root', changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - RouterOutlet, - RouterLink, - RouterLinkActive, - DotBackground, - CommandPalette, - CommandPaletteTrigger, - ], + imports: [RouterOutlet, RouterLink, RouterLinkActive, DotBackground, TerminalDock], templateUrl: './app.html', styleUrl: './app.scss', }) export class App { protected readonly navigation = inject(NavigationService); protected readonly localeService = inject(LocaleService); - protected readonly palette = inject(CommandPaletteService); private readonly router = inject(Router); private readonly injector = inject(Injector); private readonly document = inject(DOCUMENT); diff --git a/src/app/core/content/shell-copy.ts b/src/app/core/content/shell-copy.ts index 3bfd4bc..c3054d8 100644 --- a/src/app/core/content/shell-copy.ts +++ b/src/app/core/content/shell-copy.ts @@ -8,7 +8,6 @@ export interface ShellCopy { readonly menuClose: string; readonly languageSwitch: string; readonly otherLocaleName: string; - readonly cvLabel: string; readonly contactCta: string; } @@ -21,7 +20,6 @@ export const SHELL_COPY: Record = { menuClose: 'Menü schließen', languageSwitch: 'Zur englischen Version wechseln', otherLocaleName: 'English', - cvLabel: 'Lebenslauf als PDF', contactCta: 'Kontakt', }, en: { @@ -32,7 +30,6 @@ export const SHELL_COPY: Record = { menuClose: 'Close menu', languageSwitch: 'Switch to the German version', otherLocaleName: 'Deutsch', - cvLabel: 'Curriculum vitae as PDF', contactCta: 'Contact', }, }; diff --git a/src/app/core/navigation/navigation.service.spec.ts b/src/app/core/navigation/navigation.service.spec.ts index ad6f997..41bb37a 100644 --- a/src/app/core/navigation/navigation.service.spec.ts +++ b/src/app/core/navigation/navigation.service.spec.ts @@ -5,6 +5,7 @@ import { routes } from '../../app.routes'; import { SITE_CONTENT } from '../content/content.token'; import { SITE_CONTENT_DATA } from '../content/site-content'; import { LocaleService } from '../i18n/locale.service'; +import { PRIMARY_NAV } from './navigation'; import { NavigationService } from './navigation.service'; describe('NavigationService', () => { @@ -40,4 +41,11 @@ describe('NavigationService', () => { expect(navigation.link('home')).toEqual(['/', 'en']); expect(navigation.link('contact', 'de')).toEqual(['/', 'kontakt']); }); + + it('keeps primary nav to home, services, projects, about and contact', () => { + const ids = PRIMARY_NAV.map((item) => item.routeId); + expect(ids).toEqual(['home', 'services', 'projects', 'about', 'contact']); + expect(ids).not.toContain('stack'); + expect(ids).not.toContain('pitch'); + }); }); diff --git a/src/app/core/navigation/navigation.ts b/src/app/core/navigation/navigation.ts index 91560c0..0561f03 100644 --- a/src/app/core/navigation/navigation.ts +++ b/src/app/core/navigation/navigation.ts @@ -38,10 +38,6 @@ export const PRIMARY_NAV: readonly NavItem[] = [ routeId: 'projects', label: { de: 'Projekte', en: 'Projects' }, }, - { - routeId: 'stack', - label: { de: 'Stack', en: 'Stack' }, - }, { routeId: 'about', label: { de: 'Über mich', en: 'About' }, diff --git a/src/app/submenu.styles.spec.ts b/src/app/submenu.styles.spec.ts new file mode 100644 index 0000000..6ab7768 --- /dev/null +++ b/src/app/submenu.styles.spec.ts @@ -0,0 +1,15 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const APP_SCSS = readFileSync(join(process.cwd(), 'src/app/app.scss'), 'utf8'); + +describe('primary nav submenu surface', () => { + it('uses the opaque menu token and not the glass surface', () => { + const submenu = APP_SCSS.slice(APP_SCSS.indexOf('.primary-nav > li > .submenu')); + + expect(submenu).toMatch(/background:\s*var\(--color-surface-menu\)/); + expect(submenu).not.toMatch(/var\(--surface-glass\)/); + expect(submenu).toMatch(/box-shadow:/); + expect(submenu).toMatch(/border-radius:/); + }); +}); diff --git a/src/styles.scss b/src/styles.scss index 9b09d24..cb018a0 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -5,6 +5,7 @@ --color-surface-raised: #12121a; --color-surface-overlay: #1a1a24; --color-surface-muted: #22222e; + --color-surface-menu: #16161f; /* Color — accent ramp */ --color-accent: #6366f1;