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:
214
src/app/shared/systems-map/systems-map.ts
Normal file
214
src/app/shared/systems-map/systems-map.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import { ChangeDetectionStrategy, Component, computed, inject, input, signal } from '@angular/core';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { SIGNATURE_COPY } from '../../core/content/signature-copy';
|
||||
import { type AppLocale } from '../../core/i18n/locale';
|
||||
import { LocaleService } from '../../core/i18n/locale.service';
|
||||
import { NavigationService } from '../../core/navigation/navigation.service';
|
||||
import { routePath } from '../../core/routing/route-paths';
|
||||
import {
|
||||
connectedNodeIds,
|
||||
SYSTEMS_MAP_EDGES,
|
||||
SYSTEMS_MAP_NODE_KINDS,
|
||||
SYSTEMS_MAP_NODES,
|
||||
type SystemsMapNode,
|
||||
type SystemsMapNodeId,
|
||||
} from './systems-map.model';
|
||||
|
||||
let systemsMapInstanceId = 0;
|
||||
|
||||
export interface SystemsMapNodeView {
|
||||
readonly id: SystemsMapNodeId;
|
||||
readonly kind: SystemsMapNode['kind'];
|
||||
readonly routeId: SystemsMapNode['routeId'];
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
readonly label: string;
|
||||
readonly summary: string;
|
||||
readonly href: string;
|
||||
readonly link: unknown[];
|
||||
readonly relationship: string;
|
||||
readonly accessibleName: string;
|
||||
}
|
||||
|
||||
export interface SystemsMapEdgeView {
|
||||
readonly from: SystemsMapNodeId;
|
||||
readonly to: SystemsMapNodeId;
|
||||
readonly x1: number;
|
||||
readonly y1: number;
|
||||
readonly x2: number;
|
||||
readonly y2: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-systems-map',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [RouterLink],
|
||||
templateUrl: './systems-map.html',
|
||||
styleUrl: './systems-map.scss',
|
||||
})
|
||||
export class SystemsMap {
|
||||
readonly heading = input('');
|
||||
readonly intro = input('');
|
||||
readonly headingLevel = input<2 | 3>(2);
|
||||
|
||||
private readonly navigation = inject(NavigationService);
|
||||
private readonly localeService = inject(LocaleService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly instanceId = systemsMapInstanceId++;
|
||||
|
||||
protected readonly headingId = `systems-map-heading-${this.instanceId}`;
|
||||
protected readonly svgTitleId = `systems-map-svg-title-${this.instanceId}`;
|
||||
protected readonly svgDescId = `systems-map-svg-desc-${this.instanceId}`;
|
||||
protected readonly listHeadingId = `systems-map-list-${this.instanceId}`;
|
||||
protected readonly legendHeadingId = `systems-map-legend-${this.instanceId}`;
|
||||
|
||||
protected readonly activeNodeId = signal<SystemsMapNodeId | null>(null);
|
||||
|
||||
protected readonly copy = computed(() => SIGNATURE_COPY[this.localeService.locale()].systemsMap);
|
||||
|
||||
protected readonly headingText = computed(() => {
|
||||
const override = this.heading().trim();
|
||||
return override.length > 0 ? override : this.copy().heading;
|
||||
});
|
||||
|
||||
protected readonly introText = computed(() => {
|
||||
const override = this.intro().trim();
|
||||
return override.length > 0 ? override : this.copy().intro;
|
||||
});
|
||||
|
||||
protected readonly nodes = computed(() => {
|
||||
const locale = this.localeService.locale();
|
||||
const copy = this.copy();
|
||||
|
||||
return SYSTEMS_MAP_NODES.map((node) => this.toNodeView(node, locale, copy.relationshipLabel));
|
||||
});
|
||||
|
||||
protected readonly edges = computed((): readonly SystemsMapEdgeView[] => {
|
||||
const byId = new Map(SYSTEMS_MAP_NODES.map((node) => [node.id, node]));
|
||||
|
||||
return SYSTEMS_MAP_EDGES.map((edge) => {
|
||||
const from = byId.get(edge.from);
|
||||
const to = byId.get(edge.to);
|
||||
|
||||
return {
|
||||
from: edge.from,
|
||||
to: edge.to,
|
||||
x1: from?.x ?? 0,
|
||||
y1: from?.y ?? 0,
|
||||
x2: to?.x ?? 0,
|
||||
y2: to?.y ?? 0,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
protected readonly legendItems = computed(() => {
|
||||
const legend = this.copy().legend;
|
||||
|
||||
return SYSTEMS_MAP_NODE_KINDS.map((kind) => ({
|
||||
kind,
|
||||
label: legend[kind],
|
||||
}));
|
||||
});
|
||||
|
||||
protected readonly activeReadout = computed(() => {
|
||||
const activeId = this.activeNodeId();
|
||||
|
||||
if (!activeId) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const node = this.nodes().find((entry) => entry.id === activeId);
|
||||
return node ? `${node.label}: ${node.relationship}` : '';
|
||||
});
|
||||
|
||||
protected onNodeActivate(event: MouseEvent, node: SystemsMapNodeView): void {
|
||||
if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
void this.router.navigate(this.navigation.link(node.routeId));
|
||||
}
|
||||
|
||||
protected onNodeEnter(id: SystemsMapNodeId): void {
|
||||
this.activeNodeId.set(id);
|
||||
}
|
||||
|
||||
protected onNodeLeave(id: SystemsMapNodeId): void {
|
||||
if (this.activeNodeId() === id) {
|
||||
this.activeNodeId.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
protected isConnectedNode(id: SystemsMapNodeId): boolean {
|
||||
const active = this.activeNodeId();
|
||||
|
||||
if (!active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return active === id || this.neighborSet(active).has(id);
|
||||
}
|
||||
|
||||
protected isDimmedNode(id: SystemsMapNodeId): boolean {
|
||||
const active = this.activeNodeId();
|
||||
|
||||
if (!active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return active !== id && !this.neighborSet(active).has(id);
|
||||
}
|
||||
|
||||
protected isConnectedEdge(edge: SystemsMapEdgeView): boolean {
|
||||
const active = this.activeNodeId();
|
||||
|
||||
if (!active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return edge.from === active || edge.to === active;
|
||||
}
|
||||
|
||||
protected isDimmedEdge(edge: SystemsMapEdgeView): boolean {
|
||||
const active = this.activeNodeId();
|
||||
|
||||
if (!active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return edge.from !== active && edge.to !== active;
|
||||
}
|
||||
|
||||
private toNodeView(
|
||||
node: SystemsMapNode,
|
||||
locale: AppLocale,
|
||||
relationshipLabel: string,
|
||||
): SystemsMapNodeView {
|
||||
const connected = new Set(connectedNodeIds(node.id));
|
||||
const connectedLabels = SYSTEMS_MAP_NODES.filter((entry) => connected.has(entry.id)).map(
|
||||
(entry) => entry.label[locale],
|
||||
);
|
||||
const relationship = relationshipLabel.replace('{targets}', connectedLabels.join(', '));
|
||||
const label = node.label[locale];
|
||||
const summary = node.summary[locale];
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
kind: node.kind,
|
||||
routeId: node.routeId,
|
||||
x: node.x,
|
||||
y: node.y,
|
||||
label,
|
||||
summary,
|
||||
href: routePath(node.routeId, locale),
|
||||
link: this.navigation.link(node.routeId),
|
||||
relationship,
|
||||
accessibleName: `${label}. ${summary} ${relationship}`,
|
||||
};
|
||||
}
|
||||
|
||||
private neighborSet(id: SystemsMapNodeId): ReadonlySet<SystemsMapNodeId> {
|
||||
return new Set(connectedNodeIds(id));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user