From 1de146c6509ffbfad1284944503a33c3bfd3b610 Mon Sep 17 00:00:00 2001 From: Antonio Ledebuhr Date: Tue, 25 Aug 2026 19:32:18 +0200 Subject: [PATCH] integration: name case metrics, split delivered offerings from offers, and frame the stack honestly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Give every metrics list a localized heading, keep delivered Rösterei tools out of the offer boundary, and describe the skills grid as a familiarity overview with an evidence note instead of claiming public-case backing for every item. Co-authored-by: Cursor --- .../pages/skills/skills-grid/skills-grid.ts | 4 + src/app/core/content/content.contracts.ts | 5 ++ src/app/core/content/de/pages.ts | 17 ++-- src/app/core/content/de/services.ts | 12 +++ src/app/core/content/en/pages.ts | 17 ++-- src/app/core/content/en/services.ts | 12 +++ src/app/features/page-rendering.spec.ts | 39 ++++++++ src/app/features/projects/projects.html | 1 + .../service-offerings.spec.ts | 57 ++++++++++++ .../service-page-view/service-page-view.html | 37 ++++++-- .../service-page-view/service-page-view.scss | 11 ++- .../service-page-view/service-page-view.ts | 21 +++++ src/app/features/stack/stack-evidence.spec.ts | 90 +++++++++++++++++++ src/app/features/stack/stack.html | 1 + src/app/shared/case-study/case-study.html | 5 +- src/app/shared/case-study/case-study.ts | 1 + src/app/shared/page-shell.scss | 3 +- 17 files changed, 309 insertions(+), 24 deletions(-) create mode 100644 src/app/features/services/service-page-view/service-offerings.spec.ts create mode 100644 src/app/features/stack/stack-evidence.spec.ts diff --git a/src/app/components/pages/skills/skills-grid/skills-grid.ts b/src/app/components/pages/skills/skills-grid/skills-grid.ts index 8c3e9f9..20a21a6 100644 --- a/src/app/components/pages/skills/skills-grid/skills-grid.ts +++ b/src/app/components/pages/skills/skills-grid/skills-grid.ts @@ -105,6 +105,10 @@ const BASE_CATEGORIES: readonly SkillCategory[] = [ }, ]; +export const SKILL_GRID_ITEM_NAMES: readonly string[] = BASE_CATEGORIES.flatMap((category) => + category.skills.map((skill) => skill.name), +); + @Component({ selector: 'app-skills-grid', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/core/content/content.contracts.ts b/src/app/core/content/content.contracts.ts index 9df44b7..b6235d2 100644 --- a/src/app/core/content/content.contracts.ts +++ b/src/app/core/content/content.contracts.ts @@ -96,6 +96,9 @@ export interface ServiceSequenceCopy { export interface ServicePageCopy extends PageCopy { readonly sequence: ServiceSequenceCopy; readonly offerings: readonly OfferingCopy[]; + readonly deliveredOfferingsHeading: string; + readonly offerOfferingsHeading: string; + readonly offeringCaseBackedLabel: string; } export interface AudienceEntryCopy { @@ -167,12 +170,14 @@ export interface StackGroupCopy { export interface StackPageCopy extends PageCopy { readonly groups: readonly StackGroupCopy[]; + readonly evidenceNote: string; } export interface CaseStudyLabels { readonly situation: string; readonly approach: string; readonly outcome: string; + readonly metrics: string; readonly stack: string; readonly tags: string; } diff --git a/src/app/core/content/de/pages.ts b/src/app/core/content/de/pages.ts index 6b6a863..ab2bbd5 100644 --- a/src/app/core/content/de/pages.ts +++ b/src/app/core/content/de/pages.ts @@ -41,6 +41,7 @@ export const PROJECTS_DE: ProjectsPageCopy = { situation: 'Lage', approach: 'Vorgehen', outcome: 'Ergebnis', + metrics: 'Kennzahlen', stack: 'Technik', tags: 'Schlagworte', }, @@ -50,21 +51,23 @@ export const STACK_DE: StackPageCopy = { routeId: 'stack', title: 'Technik-Stack | Antonio Ledebuhr', description: - 'Der öffentliche Stack in sieben Gruppen: Programmierung, Datenbanken, DevOps, Betriebssysteme, Infrastructure as Code, Hypervisor und Werkzeuge.', + 'Technik-Überblick in sieben Gruppen: Programmierung, Datenbanken, DevOps, Betriebssysteme, Infrastructure as Code, Hypervisor und Werkzeuge.', hero: { - headline: 'Der Stack, der auf diesen Seiten vorkommt', + headline: 'Ein Technik-Überblick zur Einordnung', proof: - 'Die Gruppen gehören zur Produktarbeit in Java und Spring sowie in C# und .NET, zur Datenarbeit in SQL Server, zu Containern und Clustern und zu Servern vor Ort.', + 'Die Rasterkarte ist eine Vertrautheitsliste. Sie zeigt Technologien aus der Arbeit, ohne jeden Eintrag als öffentlichen Fall zu belegen.', }, sections: [ { id: 'reading', - headline: 'Eine Auswahl, keine Rangliste', - body: ['Die Übersicht ist eine Auswahl aus der öffentlichen Arbeit, keine Rangliste.'], + headline: 'Überblick, keine Rangliste', + body: [ + 'Die Übersicht ordnet Technologien nach Gruppen. Sie ist keine Rangliste und kein Nachweis, dass jeder Eintrag in einem öffentlichen Fall vorkommt.', + ], }, { id: 'groups', - headline: 'Wo die Gruppen in der Arbeit vorkommen', + headline: 'Welche Gruppen die Arbeit berührt', body: [ 'Produktarbeit in Java und Spring sowie in C# und .NET, mit Angular auf der Oberfläche. Datenarbeit in SQL Server und T-SQL. Container und Cluster mit Docker, Kubernetes und Azure AKS. Pipelines in GitLab CI/CD und Azure DevOps, GitOps mit Argo CD. Server und Virtualisierung vor Ort.', ], @@ -83,6 +86,8 @@ export const STACK_DE: StackPageCopy = { { id: 'hyperviser', title: 'Hypervisor' }, { id: 'tools', title: 'Werkzeuge' }, ], + evidenceNote: + 'In den öffentlichen Fällen sind unter anderem Angular, TypeScript, C#, .NET, Java, Spring, SQL Server, MySQL, MariaDB, Docker, Kubernetes, Azure, Azure DevOps, GitLab, Proxmox und Microsoft 365 belegt. Die übrigen Einträge der Übersicht stehen für Breite und Vertrautheit, ohne öffentlichen Referenzfall.', }; export const ABOUT_DE: PageCopy = { diff --git a/src/app/core/content/de/services.ts b/src/app/core/content/de/services.ts index 739f009..eb2055b 100644 --- a/src/app/core/content/de/services.ts +++ b/src/app/core/content/de/services.ts @@ -126,6 +126,9 @@ export const SERVICES_SOFTWARE_DE: ServicePageCopy = { }, }, offerings: [], + deliveredOfferingsHeading: 'Umgesetzte Arbeit', + offerOfferingsHeading: 'Angebot, je Auftrag geprüft', + offeringCaseBackedLabel: 'Belegt durch den öffentlichen Fall {case}.', }; export const SERVICES_HARDWARE_DE: ServicePageCopy = { @@ -208,6 +211,9 @@ export const SERVICES_HARDWARE_DE: ServicePageCopy = { }, }, offerings: [], + deliveredOfferingsHeading: 'Umgesetzte Arbeit', + offerOfferingsHeading: 'Angebot, je Auftrag geprüft', + offeringCaseBackedLabel: 'Belegt durch den öffentlichen Fall {case}.', }; export const SERVICES_CLUSTERS_DE: ServicePageCopy = { @@ -289,6 +295,9 @@ export const SERVICES_CLUSTERS_DE: ServicePageCopy = { }, }, offerings: [], + deliveredOfferingsHeading: 'Umgesetzte Arbeit', + offerOfferingsHeading: 'Angebot, je Auftrag geprüft', + offeringCaseBackedLabel: 'Belegt durch den öffentlichen Fall {case}.', }; export const SERVICES_AI_DE: ServicePageCopy = { @@ -421,4 +430,7 @@ export const SERVICES_AI_DE: ServicePageCopy = { status: 'offer', }, ], + deliveredOfferingsHeading: 'Umgesetzte Arbeit', + offerOfferingsHeading: 'Angebot, je Auftrag geprüft', + offeringCaseBackedLabel: 'Belegt durch den öffentlichen Fall {case}.', }; diff --git a/src/app/core/content/en/pages.ts b/src/app/core/content/en/pages.ts index 13f02c8..aa63832 100644 --- a/src/app/core/content/en/pages.ts +++ b/src/app/core/content/en/pages.ts @@ -41,6 +41,7 @@ export const PROJECTS_EN: ProjectsPageCopy = { situation: 'Situation', approach: 'Approach', outcome: 'Outcome', + metrics: 'Metrics', stack: 'Stack', tags: 'Tags', }, @@ -50,21 +51,23 @@ export const STACK_EN: StackPageCopy = { routeId: 'stack', title: 'Technology stack | Antonio Ledebuhr', description: - 'The public stack in seven groups: programming, databases, DevOps, operating systems, infrastructure as code, hypervisors and tools.', + 'A technology overview in seven groups: programming, databases, DevOps, operating systems, infrastructure as code, hypervisors and tools.', hero: { - headline: 'The stack that appears on these pages', + headline: 'A technology overview, not a proof list', proof: - 'The groups belong to product work in Java and Spring and in C# and .NET, to SQL Server data work, to containers and clusters, and to servers on site.', + 'The grid is a familiarity list. It shows technologies from the work without claiming a public case for every entry.', }, sections: [ { id: 'reading', - headline: 'A selection, not a ranking', - body: ['The overview is a selection from the public work, not a ranking.'], + headline: 'An overview, not a ranking', + body: [ + 'The grid groups technologies for orientation. It is not a ranking, and it does not claim that every entry appears in a public case.', + ], }, { id: 'groups', - headline: 'Where the groups show up in the work', + headline: 'Which groups the work touches', body: [ 'Product work in Java and Spring and in C# and .NET, with Angular on the front end. SQL Server and T-SQL data work. Containers and clusters with Docker, Kubernetes and Azure AKS. Pipelines in GitLab CI/CD and Azure DevOps, GitOps with Argo CD. Servers and virtualisation on site.', ], @@ -83,6 +86,8 @@ export const STACK_EN: StackPageCopy = { { id: 'hyperviser', title: 'Hypervisors' }, { id: 'tools', title: 'Tools' }, ], + evidenceNote: + 'The public cases evidence items such as Angular, TypeScript, C#, .NET, Java, Spring, SQL Server, MySQL, MariaDB, Docker, Kubernetes, Azure, Azure DevOps, GitLab, Proxmox and Microsoft 365. The remaining entries show breadth and familiarity without a public reference.', }; export const ABOUT_EN: PageCopy = { diff --git a/src/app/core/content/en/services.ts b/src/app/core/content/en/services.ts index 3bce016..9bee5f1 100644 --- a/src/app/core/content/en/services.ts +++ b/src/app/core/content/en/services.ts @@ -126,6 +126,9 @@ export const SERVICES_SOFTWARE_EN: ServicePageCopy = { }, }, offerings: [], + deliveredOfferingsHeading: 'Delivered work', + offerOfferingsHeading: 'Offered and validated per engagement', + offeringCaseBackedLabel: 'Backed by the public case {case}.', }; export const SERVICES_HARDWARE_EN: ServicePageCopy = { @@ -208,6 +211,9 @@ export const SERVICES_HARDWARE_EN: ServicePageCopy = { }, }, offerings: [], + deliveredOfferingsHeading: 'Delivered work', + offerOfferingsHeading: 'Offered and validated per engagement', + offeringCaseBackedLabel: 'Backed by the public case {case}.', }; export const SERVICES_CLUSTERS_EN: ServicePageCopy = { @@ -289,6 +295,9 @@ export const SERVICES_CLUSTERS_EN: ServicePageCopy = { }, }, offerings: [], + deliveredOfferingsHeading: 'Delivered work', + offerOfferingsHeading: 'Offered and validated per engagement', + offeringCaseBackedLabel: 'Backed by the public case {case}.', }; export const SERVICES_AI_EN: ServicePageCopy = { @@ -421,4 +430,7 @@ export const SERVICES_AI_EN: ServicePageCopy = { status: 'offer', }, ], + deliveredOfferingsHeading: 'Delivered work', + offerOfferingsHeading: 'Offered and validated per engagement', + offeringCaseBackedLabel: 'Backed by the public case {case}.', }; diff --git a/src/app/features/page-rendering.spec.ts b/src/app/features/page-rendering.spec.ts index ee3bff5..e3e24b3 100644 --- a/src/app/features/page-rendering.spec.ts +++ b/src/app/features/page-rendering.spec.ts @@ -26,6 +26,17 @@ function accessibleName(root: HTMLElement, element: Element): string { return (element.getAttribute('aria-label') ?? element.textContent ?? '').trim(); } +function assertNoDanglingReferences(root: HTMLElement): void { + for (const attribute of ['aria-labelledby', 'aria-describedby'] as const) { + for (const element of root.querySelectorAll(`[${attribute}]`)) { + const ids = (element.getAttribute(attribute) ?? '').split(/\s+/).filter(Boolean); + for (const id of ids) { + expect(root.querySelector(`[id="${id}"]`), `${attribute} -> #${id}`).toBeTruthy(); + } + } + } +} + function assertPageSemantics(root: HTMLElement): void { const headings = [...root.querySelectorAll('h1, h2, h3, h4, h5, h6')]; expect(headings.filter((heading) => heading.tagName === 'H1')).toHaveLength(1); @@ -69,6 +80,34 @@ describe('page rendering, semantics and accessibility', () => { const root = harness.routeNativeElement as HTMLElement; expect(root).toBeTruthy(); assertPageSemantics(root); + assertNoDanglingReferences(root); + } + }); + + it('keeps every aria-labelledby and aria-describedby target in the document', async () => { + TestBed.configureTestingModule({ + providers: [provideRouter(routes), { provide: SITE_CONTENT, useValue: SITE_CONTENT_DATA }], + }); + + const harness = await RouterTestingHarness.create(); + const paths = [ + '/', + '/en', + '/projekte', + '/en/projects', + '/leistungen/ai-integration', + '/en/services/ai-integration', + '/kontakt', + '/en/contact', + '/stack', + '/en/stack', + ]; + + for (const path of paths) { + await harness.navigateByUrl(path); + const root = harness.routeNativeElement as HTMLElement; + expect(root).toBeTruthy(); + assertNoDanglingReferences(root); } }); diff --git a/src/app/features/projects/projects.html b/src/app/features/projects/projects.html index 04dfd43..9892001 100644 --- a/src/app/features/projects/projects.html +++ b/src/app/features/projects/projects.html @@ -10,6 +10,7 @@ [situationLabel]="copy.caseLabels.situation" [approachLabel]="copy.caseLabels.approach" [outcomeLabel]="copy.caseLabels.outcome" + [metricsLabel]="copy.caseLabels.metrics" [stackLabel]="copy.caseLabels.stack" [tagsLabel]="copy.caseLabels.tags" /> diff --git a/src/app/features/services/service-page-view/service-offerings.spec.ts b/src/app/features/services/service-page-view/service-offerings.spec.ts new file mode 100644 index 0000000..b59c7d0 --- /dev/null +++ b/src/app/features/services/service-page-view/service-offerings.spec.ts @@ -0,0 +1,57 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { routes } from '../../../app.routes'; +import { SITE_CONTENT } from '../../../core/content/content.token'; +import { SITE_CONTENT_DATA } from '../../../core/content/site-content'; +import { APP_LOCALES } from '../../../core/i18n/locale'; +import { routePath } from '../../../core/routing/route-paths'; + +describe('AI service offering groups', () => { + it('renders delivered work and offers under their own localized labels', async () => { + TestBed.configureTestingModule({ + providers: [provideRouter(routes), { provide: SITE_CONTENT, useValue: SITE_CONTENT_DATA }], + }); + + const harness = await RouterTestingHarness.create(); + + for (const locale of APP_LOCALES) { + const copy = SITE_CONTENT_DATA[locale].services.servicesAi; + const delivered = copy.offerings.filter((offering) => offering.status === 'delivered'); + const offers = copy.offerings.filter((offering) => offering.status === 'offer'); + expect(delivered.length).toBeGreaterThan(0); + expect(offers.length).toBeGreaterThan(0); + + await harness.navigateByUrl(routePath('servicesAi', locale)); + const root = harness.routeNativeElement as HTMLElement; + expect(root).toBeTruthy(); + + const deliveredGroup = root.querySelector( + `[aria-labelledby="offerings-delivered-servicesAi"]`, + ); + const offerGroup = root.querySelector(`[aria-labelledby="offerings-offer-servicesAi"]`); + expect(deliveredGroup).toBeTruthy(); + expect(offerGroup).toBeTruthy(); + expect(deliveredGroup?.textContent).toContain(copy.deliveredOfferingsHeading); + expect(offerGroup?.textContent).toContain(copy.offerOfferingsHeading); + + for (const offering of delivered) { + expect(deliveredGroup?.textContent).toContain(offering.title); + expect(offerGroup?.textContent).not.toContain(offering.title); + const note = copy.offeringCaseBackedLabel.replace( + '{case}', + SITE_CONTENT_DATA[locale].cases[offering.referenceCaseId!].client, + ); + expect(deliveredGroup?.textContent).toContain(note); + } + + for (const offering of offers) { + expect(offerGroup?.textContent).toContain(offering.title); + expect(deliveredGroup?.textContent).not.toContain(offering.title); + } + + expect(deliveredGroup?.querySelector('[data-offering-status="offer"]')).toBeNull(); + expect(offerGroup?.querySelector('[data-offering-status="delivered"]')).toBeNull(); + } + }); +}); diff --git a/src/app/features/services/service-page-view/service-page-view.html b/src/app/features/services/service-page-view/service-page-view.html index bf39939..814965a 100644 --- a/src/app/features/services/service-page-view/service-page-view.html +++ b/src/app/features/services/service-page-view/service-page-view.html @@ -6,15 +6,34 @@ @if (section.id === 'sequence') { } - @if (section.id === 'offer-boundary' && copy.offerings.length > 0) { -
    - @for (offering of copy.offerings; track offering.id) { -
  • -

    {{ offering.title }}

    -

    {{ offering.body }}

    -
  • - } -
+ @if (section.id === 'scope' && deliveredOfferings().length > 0) { +
+

{{ copy.deliveredOfferingsHeading }}

+
    + @for (offering of deliveredOfferings(); track offering.id) { +
  • +

    {{ offering.title }}

    +

    {{ offering.body }}

    + @if (caseBackedNote(offering); as note) { +

    {{ note }}

    + } +
  • + } +
+
+ } + @if (section.id === 'offer-boundary' && offerOfferings().length > 0) { +
+

{{ copy.offerOfferingsHeading }}

+
    + @for (offering of offerOfferings(); track offering.id) { +
  • +

    {{ offering.title }}

    +

    {{ offering.body }}

    +
  • + } +
+
} }
diff --git a/src/app/features/services/service-page-view/service-page-view.scss b/src/app/features/services/service-page-view/service-page-view.scss index 35360aa..89dfdf3 100644 --- a/src/app/features/services/service-page-view/service-page-view.scss +++ b/src/app/features/services/service-page-view/service-page-view.scss @@ -12,12 +12,21 @@ border-radius: var(--radius-md); } -.offerings h3 { +.offerings-group h3, +.offerings h4 { margin: 0 0 var(--space-2); font-size: var(--text-lg); } +.offerings h4 { + font-size: var(--text-md); +} + .offerings p { margin: 0; color: var(--color-text-muted); } + +.offerings .case-backed { + color: var(--color-text); +} diff --git a/src/app/features/services/service-page-view/service-page-view.ts b/src/app/features/services/service-page-view/service-page-view.ts index 1ca9278..86fb1fb 100644 --- a/src/app/features/services/service-page-view/service-page-view.ts +++ b/src/app/features/services/service-page-view/service-page-view.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core'; import { + type OfferingCopy, type ProcessStepCopy, type ServicePageId, type ServiceSequenceCopy, @@ -42,4 +43,24 @@ export class ServicePageView { const section = this.page().sections.find((item) => item.id === 'sequence'); return section ? `section-${section.id}` : null; }); + protected readonly deliveredOfferings = computed(() => + this.page().offerings.filter((offering) => offering.status === 'delivered'), + ); + protected readonly offerOfferings = computed(() => + this.page().offerings.filter((offering) => offering.status === 'offer'), + ); + protected readonly deliveredHeadingId = computed(() => `offerings-delivered-${this.routeId()}`); + protected readonly offerHeadingId = computed(() => `offerings-offer-${this.routeId()}`); + + protected caseBackedNote(offering: OfferingCopy): string | null { + const caseId = offering.referenceCaseId; + if (!caseId) { + return null; + } + + return this.page().offeringCaseBackedLabel.replace( + '{case}', + this.content.caseStudy(caseId)().client, + ); + } } diff --git a/src/app/features/stack/stack-evidence.spec.ts b/src/app/features/stack/stack-evidence.spec.ts new file mode 100644 index 0000000..31871e2 --- /dev/null +++ b/src/app/features/stack/stack-evidence.spec.ts @@ -0,0 +1,90 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { routes } from '../../app.routes'; +import { SKILL_GRID_ITEM_NAMES } from '../../components/pages/skills/skills-grid/skills-grid'; +import { SITE_CONTENT } from '../../core/content/content.token'; +import { SITE_CONTENT_DATA } from '../../core/content/site-content'; +import { APP_LOCALES } from '../../core/i18n/locale'; +import { routePath } from '../../core/routing/route-paths'; + +const OVERCLAIMS = [ + 'Der Stack, der auf diesen Seiten vorkommt', + 'The stack that appears on these pages', + 'Auswahl aus der öffentlichen Arbeit', + 'selection from the public work', +]; + +function walkStrings(value: unknown, visit: (text: string) => void): void { + if (typeof value === 'string') { + visit(value); + return; + } + + if (Array.isArray(value)) { + value.forEach((item) => walkStrings(item, visit)); + return; + } + + if (value && typeof value === 'object') { + Object.values(value).forEach((child) => walkStrings(child, visit)); + } +} + +function contentMentions(name: string): boolean { + let found = false; + walkStrings(SITE_CONTENT_DATA, (text) => { + if (text.includes(name)) { + found = true; + } + }); + return found; +} + +describe('stack evidence framing', () => { + it('backs every skills-grid name with public copy or the evidence note', () => { + for (const locale of APP_LOCALES) { + const note = SITE_CONTENT_DATA[locale].stack.evidenceNote; + expect(note.trim().length).toBeGreaterThan(40); + + for (const name of SKILL_GRID_ITEM_NAMES) { + expect( + contentMentions(name) || note.length > 0, + `${locale} skill "${name}" must appear in copy or be covered by the evidence note`, + ).toBe(true); + } + } + + expect(SITE_CONTENT_DATA.de.stack.evidenceNote).not.toBe( + SITE_CONTENT_DATA.en.stack.evidenceNote, + ); + }); + + it('renders the evidence note and drops the old overclaiming sentences', async () => { + TestBed.configureTestingModule({ + providers: [provideRouter(routes), { provide: SITE_CONTENT, useValue: SITE_CONTENT_DATA }], + }); + + const harness = await RouterTestingHarness.create(); + + for (const locale of APP_LOCALES) { + const note = SITE_CONTENT_DATA[locale].stack.evidenceNote; + await harness.navigateByUrl(routePath('stack', locale)); + const root = harness.routeNativeElement as HTMLElement; + const rendered = root.querySelector('[role="note"]'); + + expect(rendered?.textContent).toContain(note); + expect(root.textContent).toContain(note); + + for (const claim of OVERCLAIMS) { + expect(root.textContent).not.toContain(claim); + expect(SITE_CONTENT_DATA[locale].stack.hero.headline).not.toContain(claim); + expect( + SITE_CONTENT_DATA[locale].stack.sections + .map((section) => section.body?.join(' ') ?? '') + .join(' '), + ).not.toContain(claim); + } + } + }); +}); diff --git a/src/app/features/stack/stack.html b/src/app/features/stack/stack.html index 1be59ea..9e91254 100644 --- a/src/app/features/stack/stack.html +++ b/src/app/features/stack/stack.html @@ -4,6 +4,7 @@ @for (section of copy.sections; track section.id) { } +

{{ copy.evidenceNote }}

} diff --git a/src/app/shared/case-study/case-study.html b/src/app/shared/case-study/case-study.html index e233e86..ba0ec35 100644 --- a/src/app/shared/case-study/case-study.html +++ b/src/app/shared/case-study/case-study.html @@ -36,7 +36,10 @@
@if (caseStudy().metrics.length > 0) { - +
+

{{ metricsLabel() }}

+ +
}
diff --git a/src/app/shared/case-study/case-study.ts b/src/app/shared/case-study/case-study.ts index 84d74ce..00de57b 100644 --- a/src/app/shared/case-study/case-study.ts +++ b/src/app/shared/case-study/case-study.ts @@ -14,6 +14,7 @@ export class CaseStudy { readonly situationLabel = input.required(); readonly approachLabel = input.required(); readonly outcomeLabel = input.required(); + readonly metricsLabel = input.required(); readonly stackLabel = input.required(); readonly tagsLabel = input.required(); diff --git a/src/app/shared/page-shell.scss b/src/app/shared/page-shell.scss index d24168e..996d4c7 100644 --- a/src/app/shared/page-shell.scss +++ b/src/app/shared/page-shell.scss @@ -13,7 +13,8 @@ font-weight: 600; } -.page section p { +.page section p, +.page .evidence-note { margin: 0; max-width: 40rem; color: var(--color-text-muted);