Content-only page transitions + dependency security overrides

- Persistent ShellComponent hosts the router-outlet so header/footer mount once
  (no re-mount flash); pages nest under a shell route. page-shell is now
  content-only; page-layout removed.
- Native withViewTransitions() cross-fades only the routed content (chrome gets
  stable view-transition-names); respects prefers-reduced-motion.
- package.json overrides pin patched transitive dev/build deps: npm audit 16
  (3 high/9 mod/4 low) -> 5 low; shipped app stays at 0 (npm audit --omit=dev).
  No Angular downgrade, no breaking Babel 8 bump. jsdom 28 -> 29.
- README: page-transitions section + honest dependency-security note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-06-25 15:46:42 +02:00
parent f1f4f982a6
commit 4e14152758
9 changed files with 202 additions and 869 deletions

View File

@@ -1,5 +1,5 @@
import { ApplicationConfig, LOCALE_ID, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideRouter, withViewTransitions } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { registerLocaleData } from '@angular/common';
import localeNl from '@angular/common/locales/nl';
@@ -12,7 +12,7 @@ registerLocaleData(localeNl);
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideRouter(routes, withViewTransitions()),
provideHttpClient(withInterceptors([scenarioInterceptor])),
{ provide: LOCALE_ID, useValue: 'nl' },
]

View File

@@ -1,10 +1,17 @@
import { Routes } from '@angular/router';
import { ShellComponent } from './templates/shell/shell.component';
export const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'login' },
{ path: 'login', loadComponent: () => import('./pages/login/login.page').then(m => m.LoginPage) },
{ path: 'dashboard', loadComponent: () => import('./pages/dashboard/dashboard.page').then(m => m.DashboardPage) },
{ path: 'registratie', loadComponent: () => import('./pages/registration-detail/registration-detail.page').then(m => m.RegistrationDetailPage) },
{ path: 'herregistratie', loadComponent: () => import('./pages/herregistratie/herregistratie.page').then(m => m.HerregistratiePage) },
{ path: '**', redirectTo: 'login' },
{
path: '',
component: ShellComponent, // persistent header/footer; only children swap
children: [
{ path: '', pathMatch: 'full', redirectTo: 'login' },
{ path: 'login', loadComponent: () => import('./pages/login/login.page').then(m => m.LoginPage) },
{ path: 'dashboard', loadComponent: () => import('./pages/dashboard/dashboard.page').then(m => m.DashboardPage) },
{ path: 'registratie', loadComponent: () => import('./pages/registration-detail/registration-detail.page').then(m => m.RegistrationDetailPage) },
{ path: 'herregistratie', loadComponent: () => import('./pages/herregistratie/herregistratie.page').then(m => m.HerregistratiePage) },
{ path: '**', redirectTo: 'login' },
],
},
];

View File

@@ -1,28 +1,25 @@
import { Component, input } from '@angular/core';
import { PageLayoutComponent } from '../page-layout/page-layout.component';
import { HeadingComponent } from '../../atoms/heading/heading.component';
import { LinkComponent } from '../../atoms/link/link.component';
/** Template: standard page body inside the chrome — optional back-link, a
heading, optional intro, and projected content. Every content page plugs
into this, so the heading/back-link markup lives in one place. */
/** Template: standard page body — optional back-link, a heading, optional intro,
and projected content. Rendered inside the persistent ShellComponent via the
router outlet, so it owns only the content (not the header/footer chrome). */
@Component({
selector: 'app-page-shell',
imports: [PageLayoutComponent, HeadingComponent, LinkComponent],
imports: [HeadingComponent, LinkComponent],
styles: [':host{display:block}'],
template: `
<app-page-layout>
<div [style.max-width]="width() === 'narrow' ? '32rem' : null">
@if (backLink()) {
<p><app-link [to]="backLink()!">← {{ backLabel() }}</app-link></p>
}
<app-heading [level]="1">{{ heading() }}</app-heading>
@if (intro()) {
<p class="rhc-paragraph" style="margin-bottom:1.5rem">{{ intro() }}</p>
}
<ng-content />
</div>
</app-page-layout>
<div [style.max-width]="width() === 'narrow' ? '32rem' : null">
@if (backLink()) {
<p><app-link [to]="backLink()!">← {{ backLabel() }}</app-link></p>
}
<app-heading [level]="1">{{ heading() }}</app-heading>
@if (intro()) {
<p class="rhc-paragraph" style="margin-bottom:1.5rem">{{ intro() }}</p>
}
<ng-content />
</div>
`,
})
export class PageShellComponent {

View File

@@ -1,27 +1,25 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { SiteHeaderComponent } from '../../organisms/site-header/site-header.component';
import { SiteFooterComponent } from '../../organisms/site-footer/site-footer.component';
/** Template: full-bleed header + footer with a centered content column. Wraps
every page. The colored bars span the viewport; content lines up via the
shared --app-content-max width. */
/** Template: persistent app chrome. Header + footer mount once; only the routed
content inside <router-outlet> changes (and cross-fades see styles.scss). */
@Component({
selector: 'app-page-layout',
imports: [SiteHeaderComponent, SiteFooterComponent],
selector: 'app-shell',
imports: [RouterOutlet, SiteHeaderComponent, SiteFooterComponent],
styles: [':host{display:block}'],
template: `
<a href="#main" class="rhc-skip-link" style="position:absolute;left:-999px">Naar de inhoud</a>
<!-- align-items:stretch overrides the design-system flex-start so the
full-bleed header/footer bars fill the viewport width. -->
<div class="utrecht-page-layout utrecht-page-layout--stretch" style="--app-content-max:64rem;min-height:100vh;align-items:stretch">
<app-site-header />
<main id="main" class="utrecht-page-content" style="flex:1;width:100%;box-sizing:border-box">
<div style="max-width:var(--app-content-max);margin:0 auto;padding:2rem 1.5rem;box-sizing:border-box">
<ng-content />
<router-outlet />
</div>
</main>
<app-site-footer />
</div>
`,
})
export class PageLayoutComponent {}
export class ShellComponent {}

View File

@@ -6,3 +6,21 @@
@import '@rijkshuisstijl-community/components-css/dist/index.css'; /* component classes */
html, body { margin: 0; min-height: 100%; }
/* Route transitions (withViewTransitions): cross-fade the routed CONTENT only.
The chrome gets its own stable view-transition-name so it's lifted out of the
`root` snapshot and stays put while the content fades. */
app-site-header { view-transition-name: site-header; }
app-site-footer { view-transition-name: site-footer; }
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 180ms;
animation-timing-function: ease;
}
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) { animation: none !important; }
}