signature: add systems map, command palette and motion primitives
Ship the Signature UX as standalone, SSR-safe pieces: bilingual copy, a dual SVG/list systems map, a Map-backed command palette in the shell, opt-in reveal and metric primitives, and a leak-free decorative canvas. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
75
src/app/shared/motion/reveal.directive.ts
Normal file
75
src/app/shared/motion/reveal.directive.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import {
|
||||
afterNextRender,
|
||||
DestroyRef,
|
||||
Directive,
|
||||
ElementRef,
|
||||
inject,
|
||||
Injector,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { isBrowserPlatform, prefersReducedMotion } from '../../core/platform/browser';
|
||||
|
||||
@Directive({
|
||||
selector: '[appReveal]',
|
||||
})
|
||||
export class RevealDirective {
|
||||
readonly appRevealThreshold = input(0.12);
|
||||
|
||||
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly injector = inject(Injector);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly isBrowser = isBrowserPlatform();
|
||||
private readonly reducedMotion = prefersReducedMotion();
|
||||
private observer: IntersectionObserver | null = null;
|
||||
|
||||
constructor() {
|
||||
if (!this.isBrowser || this.reducedMotion || !this.canObserve()) {
|
||||
this.revealNow();
|
||||
return;
|
||||
}
|
||||
|
||||
afterNextRender(() => this.observe(), { injector: this.injector });
|
||||
this.destroyRef.onDestroy(() => this.disconnect());
|
||||
}
|
||||
|
||||
private canObserve(): boolean {
|
||||
const view = this.document.defaultView;
|
||||
return !!view && typeof view.IntersectionObserver === 'function';
|
||||
}
|
||||
|
||||
private observe(): void {
|
||||
const view = this.document.defaultView;
|
||||
|
||||
if (!view || typeof view.IntersectionObserver !== 'function') {
|
||||
this.revealNow();
|
||||
return;
|
||||
}
|
||||
|
||||
this.host.nativeElement.classList.add('reveal-pending');
|
||||
this.observer = new view.IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries.some((entry) => entry.isIntersecting)) {
|
||||
this.host.nativeElement.classList.remove('reveal-pending');
|
||||
this.host.nativeElement.classList.add('is-revealed');
|
||||
this.disconnect();
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: '0px 0px -8% 0px',
|
||||
threshold: this.appRevealThreshold(),
|
||||
},
|
||||
);
|
||||
this.observer.observe(this.host.nativeElement);
|
||||
}
|
||||
|
||||
private revealNow(): void {
|
||||
this.host.nativeElement.classList.add('is-revealed');
|
||||
}
|
||||
|
||||
private disconnect(): void {
|
||||
this.observer?.disconnect();
|
||||
this.observer = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user