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:
60
src/app/core/content/signature-copy.spec.ts
Normal file
60
src/app/core/content/signature-copy.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { APP_LOCALES } from '../i18n/locale';
|
||||
import { COMMAND_IDS } from '../../shared/command-palette/commands';
|
||||
import { SIGNATURE_COPY } from './signature-copy';
|
||||
|
||||
function leafPaths(value: unknown, prefix = ''): string[] {
|
||||
if (typeof value === 'string') {
|
||||
return [prefix];
|
||||
}
|
||||
|
||||
if (value !== null && typeof value === 'object') {
|
||||
return Object.keys(value).flatMap((key) => {
|
||||
const next = prefix.length > 0 ? `${prefix}.${key}` : key;
|
||||
return leafPaths((value as Record<string, unknown>)[key], next);
|
||||
});
|
||||
}
|
||||
|
||||
return [prefix];
|
||||
}
|
||||
|
||||
function collectStrings(value: unknown): string[] {
|
||||
if (typeof value === 'string') {
|
||||
return [value];
|
||||
}
|
||||
|
||||
if (value !== null && typeof value === 'object') {
|
||||
return Object.values(value).flatMap((entry) => collectStrings(entry));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
describe('SIGNATURE_COPY', () => {
|
||||
it('exposes the same key structure in both locales', () => {
|
||||
const [first, ...rest] = APP_LOCALES.map((locale) => leafPaths(SIGNATURE_COPY[locale]));
|
||||
|
||||
for (const keys of rest) {
|
||||
expect(keys).toEqual(first);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps every string non-empty', () => {
|
||||
for (const locale of APP_LOCALES) {
|
||||
for (const value of collectStrings(SIGNATURE_COPY[locale])) {
|
||||
expect(value.trim().length).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('describes every CommandId in both locales', () => {
|
||||
for (const locale of APP_LOCALES) {
|
||||
const descriptions = SIGNATURE_COPY[locale].palette.commandDescriptions;
|
||||
|
||||
expect(Object.keys(descriptions).sort()).toEqual([...COMMAND_IDS].sort());
|
||||
|
||||
for (const id of COMMAND_IDS) {
|
||||
expect(descriptions[id].trim().length).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
150
src/app/core/content/signature-copy.ts
Normal file
150
src/app/core/content/signature-copy.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { type AppLocale } from '../i18n/locale';
|
||||
import { type CommandId } from '../../shared/command-palette/commands';
|
||||
|
||||
export interface SignatureCopy {
|
||||
readonly palette: {
|
||||
readonly triggerLabel: string;
|
||||
readonly shortcutHint: string;
|
||||
readonly dialogTitle: string;
|
||||
readonly dialogDescription: string;
|
||||
readonly inputLabel: string;
|
||||
readonly inputPlaceholder: string;
|
||||
readonly closeLabel: string;
|
||||
readonly suggestionsLabel: string;
|
||||
readonly outputLabel: string;
|
||||
readonly emptySuggestions: string;
|
||||
readonly unknownCommand: string;
|
||||
readonly helpIntro: string;
|
||||
readonly clearedMessage: string;
|
||||
readonly cvOpened: string;
|
||||
readonly navigating: string;
|
||||
readonly commandDescriptions: Record<CommandId, string>;
|
||||
readonly responses: {
|
||||
readonly brew: string;
|
||||
readonly ignite: string;
|
||||
readonly rev: string;
|
||||
};
|
||||
};
|
||||
readonly systemsMap: {
|
||||
readonly heading: string;
|
||||
readonly intro: string;
|
||||
readonly diagramDescription: string;
|
||||
readonly listHeading: string;
|
||||
readonly legendHeading: string;
|
||||
readonly relationshipLabel: string;
|
||||
readonly legend: {
|
||||
readonly ai: string;
|
||||
readonly cluster: string;
|
||||
readonly hardware: string;
|
||||
readonly software: string;
|
||||
readonly project: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const SIGNATURE_COPY: Record<AppLocale, SignatureCopy> = {
|
||||
de: {
|
||||
palette: {
|
||||
triggerLabel: 'Befehle öffnen',
|
||||
shortcutHint: 'Strg+K',
|
||||
dialogTitle: 'Befehle',
|
||||
dialogDescription:
|
||||
'Zur Navigation oder zu einer kurzen Rückmeldung. Es wird kein Code ausgeführt.',
|
||||
inputLabel: 'Befehl',
|
||||
inputPlaceholder: 'Befehl eingeben',
|
||||
closeLabel: 'Schließen',
|
||||
suggestionsLabel: 'Vorschläge',
|
||||
outputLabel: 'Ausgabe',
|
||||
emptySuggestions: 'Keine passenden Befehle.',
|
||||
unknownCommand: 'Unbekannter Befehl: {command}',
|
||||
helpIntro: 'Verfügbare Befehle:',
|
||||
clearedMessage: 'Ausgabe geleert.',
|
||||
cvOpened: 'Lebenslauf in einem neuen Tab geöffnet.',
|
||||
navigating: 'Wechsel zu {target}.',
|
||||
commandDescriptions: {
|
||||
help: 'Listet die verfügbaren Befehle.',
|
||||
projects: 'Öffnet die Projektübersicht.',
|
||||
servicesAi: 'Öffnet die Seite zur KI-Integration.',
|
||||
cv: 'Öffnet den Lebenslauf als PDF.',
|
||||
contact: 'Öffnet die Kontaktseite.',
|
||||
brew: 'Eine kurze, spielerische Rückmeldung.',
|
||||
ignite: 'Eine kurze, spielerische Rückmeldung.',
|
||||
rev: 'Eine kurze, spielerische Rückmeldung.',
|
||||
clear: 'Leert die Ausgabe.',
|
||||
close: 'Schließt die Befehlsübersicht.',
|
||||
},
|
||||
responses: {
|
||||
brew: 'Frisch aufgebrüht. Automatisierung, die auch vor dem ersten Kaffee läuft.',
|
||||
ignite: 'Zündung frei. Die Systeme laufen warm.',
|
||||
rev: 'Drehzahl steigt, der Content bleibt trotzdem ruhig.',
|
||||
},
|
||||
},
|
||||
systemsMap: {
|
||||
heading: 'Systemkarte',
|
||||
intro:
|
||||
'Wie die technischen Schichten zusammenhängen — von Hardware bis zu den Projektfeldern.',
|
||||
diagramDescription: 'Diagramm der technischen Schichten und ihrer Verbindungen.',
|
||||
listHeading: 'Knoten als Liste',
|
||||
legendHeading: 'Legende',
|
||||
relationshipLabel: 'Verbunden mit {targets}.',
|
||||
legend: {
|
||||
ai: 'KI-Integration',
|
||||
cluster: 'Cluster',
|
||||
hardware: 'Hardware und Netz',
|
||||
software: 'Software',
|
||||
project: 'Projektfeld',
|
||||
},
|
||||
},
|
||||
},
|
||||
en: {
|
||||
palette: {
|
||||
triggerLabel: 'Open commands',
|
||||
shortcutHint: 'Ctrl+K',
|
||||
dialogTitle: 'Commands',
|
||||
dialogDescription: 'Navigate or get a short acknowledgement. No code is executed.',
|
||||
inputLabel: 'Command',
|
||||
inputPlaceholder: 'Type a command',
|
||||
closeLabel: 'Close',
|
||||
suggestionsLabel: 'Suggestions',
|
||||
outputLabel: 'Output',
|
||||
emptySuggestions: 'No matching commands.',
|
||||
unknownCommand: 'Unknown command: {command}',
|
||||
helpIntro: 'Available commands:',
|
||||
clearedMessage: 'Output cleared.',
|
||||
cvOpened: 'Opened the CV in a new tab.',
|
||||
navigating: 'Going to {target}.',
|
||||
commandDescriptions: {
|
||||
help: 'Lists the available commands.',
|
||||
projects: 'Opens the projects overview.',
|
||||
servicesAi: 'Opens the AI integration page.',
|
||||
cv: 'Opens the CV as a PDF.',
|
||||
contact: 'Opens the contact page.',
|
||||
brew: 'A short playful acknowledgement.',
|
||||
ignite: 'A short playful acknowledgement.',
|
||||
rev: 'A short playful acknowledgement.',
|
||||
clear: 'Clears the output.',
|
||||
close: 'Closes the command palette.',
|
||||
},
|
||||
responses: {
|
||||
brew: 'Freshly brewed. Automation that runs before the first coffee.',
|
||||
ignite: 'Ignition on. Systems are warming up.',
|
||||
rev: 'Revs climbing, the content stays calm.',
|
||||
},
|
||||
},
|
||||
systemsMap: {
|
||||
heading: 'Systems map',
|
||||
intro: 'How the technical layers connect — from hardware through to the project fields.',
|
||||
diagramDescription: 'Diagram of the technical layers and their connections.',
|
||||
listHeading: 'Nodes as a list',
|
||||
legendHeading: 'Legend',
|
||||
relationshipLabel: 'Connected to {targets}.',
|
||||
legend: {
|
||||
ai: 'AI integration',
|
||||
cluster: 'Cluster',
|
||||
hardware: 'Hardware and network',
|
||||
software: 'Software',
|
||||
project: 'Project field',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user