+ )}
+ {variant === 'page' && {ai.backLabel}}
diff --git a/src/components/ai-section.css b/src/components/ai-section.css
index 75d0811..3b96d14 100644
--- a/src/components/ai-section.css
+++ b/src/components/ai-section.css
@@ -1,26 +1,30 @@
-/* Every other section is prose sitting on the bare background. This one is
- enclosed, and it is the only enclosed thing on the page, because it is the
- only section arguing a position rather than listing facts.
-
- The fill is --panel, which is barely a shade off --bg, so the border does the
- separating: a hairline all round and the accent on the leading edge. The
- emerald-washed grounds separated the surface better but pushed body text to
- 4.39:1, under AA, which is not a trade worth making for a tint. */
+/* The detailed workflow has its own page; home uses the shorter summary. */
.ai .plate {
background: var(--panel);
border: 1px solid var(--line);
border-left: 2px solid var(--acc);
border-radius: 3px;
padding: clamp(24px, 3.4vw, 36px) clamp(20px, 3.4vw, 38px);
- max-width: 72ch;
}
-/* No other section has one, which is half the point. */
+.ai .ai-summary {
+ max-width: 74ch;
+}
+
+.ai .ai-summary p {
+ margin: 0 0 18px;
+ font-size: 17px;
+ line-height: 1.7;
+}
+
+.ai .ai-back {
+ display: inline-block;
+ margin-top: 28px;
+}
+
.ai .eyebrow {
margin: 0 0 20px;
- font-family: var(--font-mono);
- font-size: 10.5px;
- letter-spacing: 0.16em;
+ font-size: 14px;
color: var(--acc);
}
@@ -37,7 +41,7 @@
.ai .prose p + p {
margin: 16px 0 0;
- font-size: 15.5px;
+ font-size: 16px;
line-height: 1.7;
}
@@ -46,11 +50,9 @@
font-weight: 600;
}
-/* He says three rules, so they are numbered: a reader can check the count
- without recounting, and the numbers make the block scannable at a glance. */
.ai .rules-lead {
margin: 20px 0 0;
- font-size: 15.5px;
+ font-size: 16px;
line-height: 1.7;
}
@@ -65,7 +67,7 @@
counter-increment: rule;
position: relative;
padding-left: 30px;
- font-size: 15.5px;
+ font-size: 16px;
line-height: 1.7;
}
diff --git a/src/components/approach.astro b/src/components/approach.astro
index b6df86e..459f25d 100644
--- a/src/components/approach.astro
+++ b/src/components/approach.astro
@@ -17,8 +17,7 @@ const head = copy.sections.approach;
{copy.approach.map((block) => (
-
{block.label}
-
{block.title}
+
{block.title}
))}
diff --git a/src/components/approach.css b/src/components/approach.css
index d86de1d..c49096d 100644
--- a/src/components/approach.css
+++ b/src/components/approach.css
@@ -1,36 +1,20 @@
-/*
- * One column, not a two-by-three grid. Six cards side by side read as a deck of
- * slogans no matter how plainly each one is written; four paragraphs stacked at
- * a reading measure read as somebody talking.
- */
+/* Keep longer prose in a single reading column. */
.approach .grid {
display: grid;
gap: 30px;
max-width: 70ch;
}
-/* A hairline down the left of each one, so they stay four entries rather than
- dissolving into a single wall of text. */
.approach .block {
padding-left: 18px;
border-left: 1px solid var(--line);
}
-.approach .block-label {
- margin: 0 0 8px;
- font-family: var(--font-mono);
- font-size: 11px;
- letter-spacing: 0.04em;
- color: var(--acc);
-}
-
-/* The art block's lilac, the one place a second colour appears here. */
-.approach .block-label.tone-root {
+/* Lilac marks the web-design background. */
+.approach .block-title.tone-root {
color: var(--root);
}
-/* His own titles, and they carry an argument rather than a slogan, so they get
- the line back that mine did not deserve. */
.approach .block-title {
margin: 0 0 9px;
font-size: 17.5px;
@@ -43,13 +27,11 @@
.approach .block-body {
margin: 0;
- font-size: 15.5px;
+ font-size: 16px;
line-height: 1.75;
color: var(--body);
}
-/* The lead opens the sentence instead of sitting above it as a headline, which
- is what turned these into slogans. One per block, and nothing else lifted. */
.approach .block-body b {
color: var(--head);
font-weight: 600;
diff --git a/src/components/contact.astro b/src/components/contact.astro
index 4146fa6..f4492c7 100644
--- a/src/components/contact.astro
+++ b/src/components/contact.astro
@@ -23,6 +23,7 @@ const cvFile = locale === 'it' ? 'andrea-lisi-cv-it.pdf' : 'andrea-lisi-cv.pdf';
diff --git a/src/components/section-title.css b/src/components/section-title.css
index 254dcd6..77e0aff 100644
--- a/src/components/section-title.css
+++ b/src/components/section-title.css
@@ -8,13 +8,6 @@
letter-spacing: -0.01em;
}
-.section-title .num {
- font-family: var(--font-mono);
- font-size: 16px;
- font-weight: 500;
- color: var(--acc);
-}
-
.section-title .rule {
flex: 1 1 40px;
max-width: 260px;
diff --git a/src/components/work.css b/src/components/work.css
index 131d549..b9d7369 100644
--- a/src/components/work.css
+++ b/src/components/work.css
@@ -89,7 +89,7 @@
display: flex;
flex-direction: column;
gap: 11px;
- max-width: 62ch;
+ max-width: 80ch;
}
.work .duty {
@@ -107,7 +107,7 @@
.work .duty p {
margin: 0;
- font-size: 15px;
+ font-size: 16px;
line-height: 1.65;
}
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index ff539e5..fefbc1a 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -7,13 +7,13 @@ export const en: Copy = {
meta: {
title: 'Andrea Lisi · Senior Software Engineer · Frontend Platform',
description:
- 'Senior Software Engineer in Vienna, focused on frontend platforms, monorepos and governance across 16 projects and more than 12 teams.',
+ 'Senior Software Engineer in Vienna. 18+ years building web applications, frontend platforms and developer tools, with a background in web design.',
ogImageAlt: 'Andrea Lisi, Senior Software Engineer in Vienna',
},
aiMeta: {
- title: 'AI · Andrea Lisi · Frontend Platform & Governance',
+ title: 'AI in development · Andrea Lisi',
description:
- 'How I use AI assistants and agents in development: where they help, how I review the result and what checks I apply.',
+ 'How I evaluate AI tools on repository work and use recurring problems to improve documentation, context and shared tooling.',
},
skipLink: 'Skip to content',
@@ -35,24 +35,23 @@ export const en: Copy = {
hero: {
greeting: 'Ciao. My name is',
- tagline: 'Frontend platform and governance.',
- eyebrow: 'SENIOR SOFTWARE ENGINEER · FRONTEND PLATFORM & GOVERNANCE · VIENNA',
+ tagline: 'Senior Software Engineer.',
+ eyebrow: 'Frontend engineering, architecture and developer tools',
stats: [
- { label: 'EXPERIENCE', value: '18+ years' },
- { label: 'FOCUS', value: 'Frontend platform, governance', tone: 'now' },
- { label: 'BASED', value: 'Vienna, remote or hybrid' },
- { label: 'LANGUAGES', value: 'IT · EN · DE in progress' },
- { label: 'BACKGROUND', value: 'Web design & UI', tone: 'root' },
+ { label: 'Based in', value: 'Vienna, Austria' },
+ { label: 'Working', value: 'Remote or hybrid', tone: 'now' },
+ { label: 'Background', value: 'Web design & UI', tone: 'root' },
+ { label: 'Languages', value: 'IT · EN · DE in progress' },
],
intro:
- 'I’m a Senior Software Engineer with over 18 years of experience. Today I work on the frontend platform behind Tosca Cloud, a monorepo containing 16 projects and shared by more than 12 teams. My focus is architecture, dependencies, builds and shared standards.',
- buttons: { github: 'GITHUB', linkedin: 'LINKEDIN', email: 'EMAIL', cv: 'CV.PDF ↓' },
+ 'I’ve been building web software for over 18 years, with deep experience in frontend engineering. I develop applications, shared libraries and developer tools. I work with teams on architecture, performance and integrations, and implement the solutions we agree on.',
+ buttons: { github: 'GitHub', linkedin: 'LinkedIn', email: 'Email me', cv: 'Download CV' },
},
sections: {
approach: { num: '01', title: 'How I work' },
focus: { num: '02', title: 'What I work on' },
- ai: { num: '03', title: 'AI', eyebrow: 'USE, REVIEW AND TESTING' },
+ ai: { num: '03', title: 'AI', eyebrow: 'Repository work' },
experience: { num: '04', title: 'Experience' },
projects: { num: '05', title: 'Personal projects' },
contact: { num: '06', title: 'Get in touch' },
@@ -60,20 +59,17 @@ export const en: Copy = {
approach: [
{
- label: 'Web_Design',
tone: 'root',
- title: 'From web design to frontend engineering',
- body: 'I started by designing and building websites, then focused increasingly on frontend engineering. I have also worked on backend systems and infrastructure. That experience helps me see what changes elsewhere in the system when I work on the frontend.',
+ title: 'An eye for the interface',
+ body: 'I started by designing and building websites. Over the years I’ve also worked on visual identities, print design, backend systems and infrastructure. I work with designers to turn mockups into interfaces that match the design and behave as intended.',
},
{
- label: 'The_Boundaries',
title: 'Rules and automated checks',
- body: 'I often work on problems that affect several teams: module boundaries, dependencies, builds and CI. When a rule matters, I document the reason and add an automated check where I can. That lets the check do the reminding, so I do not have to repeat the same thing to dozens of people one at a time.',
+ body: 'I document shared decisions and automate checks where I can. This applies to dependencies, module boundaries and builds. It also saves me from having the same conversation a dozen times.',
},
{
- label: 'Hands_On',
title: 'I work on the code and alongside the teams',
- body: 'I continue to implement, debug and work on shared engineering tools. Every day I work with developers across the teams, from junior to senior, and I regularly speak with their managers. When a decision affects several products, I also work with design, security and sales to understand what is needed and apply it consistently.',
+ body: 'I write code and debug with developers across the teams, from junior to senior. I regularly work with their managers and with design, security and sales to understand requirements and agree on changes that affect several products.',
},
],
@@ -84,52 +80,81 @@ export const en: Copy = {
'Monorepo architecture',
'Micro-frontends',
'Module boundaries',
- 'Dependency governance',
+ 'Shared libraries',
'Technical RFCs',
+ 'ADRs',
+ 'OpenTelemetry',
+ 'JWT & HTTP interceptors',
],
},
{
- title: 'Frontend & integrations',
+ title: 'Web development',
items: [
'React',
'TypeScript',
'Vue',
+ 'Zustand',
'GraphQL',
'Node.js',
- 'Frontend Okta integration',
- 'JWT handling',
- 'OpenTelemetry',
+ 'JavaScript',
+ 'HTML & CSS',
+ ],
+ },
+ {
+ title: 'Build systems & monorepos',
+ items: ['Nx', 'pnpm workspaces', 'Vite', 'Webpack', 'Build caching', 'Dependency boundaries'],
+ },
+ {
+ title: 'Testing & delivery',
+ items: [
+ 'Vitest',
+ 'Jest',
+ 'Playwright',
+ 'GitHub Actions',
+ 'Azure Pipelines',
+ 'Repository administration',
],
},
{
- title: 'Build & delivery',
+ title: 'Design & UI engineering',
items: [
- 'CI architecture',
- 'Pipeline automation',
- 'Build caching & performance',
- 'Quality gates',
- 'Automated testing',
+ 'Figma',
+ 'Adobe Creative Cloud',
+ 'Design systems',
+ 'Design tokens',
+ 'Storybook',
+ 'Accessibility',
],
},
{
- title: 'Developer experience',
- items: ['Shared tooling', 'Engineering standards', 'Documentation', 'AI tooling evaluation'],
+ title: 'Performance & developer experience',
+ items: [
+ 'Bundle analysis',
+ 'Code splitting',
+ 'Browser & CDN caching',
+ 'Shared tooling',
+ 'Documentation',
+ 'AI tool evaluation',
+ ],
},
],
ai: {
heading: 'How I use AI in development',
+ summary:
+ 'I manage the adoption of AI tools in the repository. I evaluate Claude, Cursor and other tools on real tasks and define rules and context for their use across teams. Recurring problems help me identify where documentation and shared tooling need improvement.',
+ moreLabel: 'Read about my workflow',
paragraphs: [
- 'I use AI assistants and agents to analyse a project’s code, prepare changes and reduce repetitive work. I use them only when I can understand what they did and check that it works.',
- 'I test them on real work to see where they lose context in the codebase. I use that to update the repository’s documentation, rules and tooling.',
+ 'I manage how AI tools are adopted in the repository and in the teams’ development work. I use and evaluate Claude, Cursor and other assistants to analyse code, prepare changes and reduce repetitive work. I define usage rules and maintain the repository context available to them.',
+ 'I pay attention to recurring mistakes: losing context between modules, overlooking an existing implementation or suggesting changes outside the task. I investigate what is missing from the instructions, documentation or shared tooling before trying again.',
],
- rulesLead: 'In practice:',
+ rulesLead: 'What I look at:',
rules: [
- 'First I check whether the change is needed. An agent can suggest rebuilding something that already exists in the project.',
- 'I review the result like any other contribution. I read the diff, run the tests and check that it respects the project’s boundaries.',
- 'I need to be able to explain and maintain every change. If I cannot, I do not use it.',
+ 'The task and its context. I check what the repository already provides and which modules the change should touch. An agent proposing duplicate functionality is a useful sign that it has missed part of that context.',
+ 'The recurring problem. If the same mistake keeps coming back, I check whether an instruction is unclear, documentation is missing or a shared tool needs changing.',
+ 'The resulting code. I review the diff, run tests and check the integration with the project. I keep changes I can explain and maintain; a plausible answer alone does not tell me whether the software works.',
],
- backLabel: 'ANDREA LISI',
+ backLabel: 'Back to my work',
},
work: [
@@ -142,11 +167,9 @@ export const en: Copy = {
tags: ['Micro-frontends', '16 projects', '12+ teams', 'Quality gates'],
current: true,
paragraphs: [
- 'I work on the frontend platform behind Tosca Cloud, a micro-frontend monorepo containing 16 projects and shared by more than 12 teams.',
- 'I work on the monorepo’s architecture and governance, particularly module boundaries, dependency rules and shared frontend standards. I also review RFCs that affect the shared frontend.',
- 'I administer the Tosca Cloud repository on GitHub and automate its pipelines, including on Azure. With the platform architect, I work on build caching and performance, CI structure and quality gates.',
- 'I implement and maintain integrations shared across multiple frontend applications. These include observability with OpenTelemetry and the application-side authentication flow after the Okta login, including JWT handling and HTTP interceptors. When an issue appears across several projects, I debug it at platform level.',
- 'I regularly evaluate AI tools, including Claude and Cursor, on real repository work. If they keep stumbling over the same parts of the codebase, I improve its documentation, rules or shared tooling.',
+ 'Together with the architect on my team, I am a point of contact for technical initiatives that span the entire monorepo. I work with developers across disciplines, engineering managers, principal engineers from other platforms and product stakeholders to assess needs, dependencies and the impact on teams. Through these discussions, we define shared solutions and support their adoption.',
+ 'My work includes both technical decisions and implementation: I develop and maintain shared resources, fix cross-project issues and build the infrastructure needed for migrations. I coordinate the integration of new design system versions with the teams and carry out updates myself when needed. When inconsistencies appear between products, I address them in shared configurations and components.',
+ 'I also work on performance, build optimization and modernizing development tools. I document decisions in ADRs, define monorepo rules and help teams apply them. Product features remain with their teams, where I can also contribute; my ongoing responsibility is the platform they share.',
],
},
{
@@ -171,9 +194,9 @@ export const en: Copy = {
context: 'Luxury e-commerce',
tags: ['ASP', 'jQuery', 'React', 'Webpack'],
paragraphs: [
- 'Modernized a luxury e-commerce platform while it was in production. Gradually replaced ASP and jQuery with React and a new Webpack setup.',
- 'Continued to build new features during the migration without taking the shop offline.',
- 'Worked on both the storefront and the internal CMS used for orders and day-to-day shop operations.',
+ 'I modernized a luxury e-commerce platform while it was in production, gradually replacing ASP and jQuery with React and a new Webpack setup.',
+ 'I continued to build new features during the migration without taking the shop offline.',
+ 'I worked on both the storefront and the internal CMS used for orders and day-to-day shop operations.',
],
},
{
@@ -185,9 +208,9 @@ export const en: Copy = {
tags: ['Design', 'Web', 'Backend', 'Infrastructure'],
paragraphs: [
'I joined as a designer. Over the years I also worked on web development, backend systems and infrastructure installed at client sites.',
- 'Designed websites, interfaces, visual identities and print material from scratch for client projects.',
- 'Built and maintained both frontend and backend applications.',
- 'Installed servers, networks and firewalls on site for client companies, including manufacturers in Tuscany’s leather and fashion district.',
+ 'I designed websites, interfaces, visual identities and print material from scratch for client projects.',
+ 'I built and maintained both frontend and backend applications.',
+ 'I installed servers, networks and firewalls on site for client companies, including manufacturers in Tuscany’s leather and fashion district.',
],
},
],
@@ -195,18 +218,30 @@ export const en: Copy = {
projects: [
{
name: 'Lumina',
- kind: 'FANTASY MUD',
+ kind: 'Game development & web design',
description:
- 'A fantasy MUD I built myself: a Node.js/MongoDB backend, a React client and Cloudflare infrastructure.',
+ 'A multiplayer fantasy game built around text. I develop the Node.js/MongoDB backend, React client and Cloudflare infrastructure.',
tags: ['Node.js', 'MongoDB', 'React', 'Cloudflare'],
+ href: 'https://luminamud.com/',
+ linkLabel: 'Explore Lumina',
+ detail:
+ 'A personal project that brings together interface design, application code and the systems behind the game.',
+ imageAlt:
+ 'Lumina’s public website: a fantasy landscape with gold typography and navigation into the game’s world.',
+ caption: 'The project’s website. The game is in development.',
},
{
name: 'lockhound',
- kind: 'WINDOWS CLI',
+ kind: 'Open-source developer tool',
description:
- 'Zero-dependency CLI for Windows that finds and terminates processes locking a folder, using the Windows Restart Manager API.',
+ 'A Windows CLI that identifies the processes keeping a folder locked. It uses the Windows Restart Manager API and has no package dependencies.',
tags: ['Windows', 'Restart Manager API', 'Developer tooling'],
command: 'npx lockhound',
+ href: 'https://github.com/karedas/lockhound',
+ linkLabel: 'Read the code on GitHub',
+ detail:
+ 'Run it to inspect the locks. Terminating a process is a separate, explicit action, so you can first see what is holding the folder open.',
+ caption: 'Example usage. Inspect a folder without terminating any process.',
},
],
@@ -217,13 +252,14 @@ export const en: Copy = {
nowBadge: 'CURRENT',
contact: {
- invite:
- 'Hey! If you want to talk about frontend platforms, developer experience or simply compare notes, send me a message.',
- inviteMore: 'It doesn’t need to be formal. A quick message is fine too.',
+ invite: 'Hey! Have a project, a technical role or something you’d like to talk about?',
+ inviteMore:
+ 'I enjoy building software and working through technical problems with other developers. A quick message is a good place to start.',
cv: 'CV (PDF)',
github: 'GitHub',
linkedin: 'LinkedIn',
email: 'Email',
instagram: 'Instagram',
+ writeLabel: 'Email me',
},
};
diff --git a/src/i18n/it.ts b/src/i18n/it.ts
index 8f85b3b..63655bf 100644
--- a/src/i18n/it.ts
+++ b/src/i18n/it.ts
@@ -8,13 +8,13 @@ export const it: Copy = {
meta: {
title: 'Andrea Lisi · Senior Software Engineer · Frontend Platform',
description:
- 'Senior Software Engineer a Vienna, concentrato su piattaforme frontend, monorepo e governance per 16 progetti e più di 12 team.',
+ 'Senior Software Engineer a Vienna. Oltre 18 anni tra applicazioni web, piattaforme frontend e strumenti per sviluppatori, con un percorso nel web design.',
ogImageAlt: 'Andrea Lisi, Senior Software Engineer a Vienna',
},
aiMeta: {
- title: 'AI · Andrea Lisi · Frontend Platform & Governance',
+ title: 'AI nello sviluppo · Andrea Lisi',
description:
- 'Come uso assistenti AI e agenti nello sviluppo: quando servono, come rivedo il risultato e quali controlli applico.',
+ 'Come valuto gli strumenti AI su attività reali e uso i problemi ricorrenti per migliorare documentazione, contesto e strumenti del repository.',
},
skipLink: 'Vai al contenuto',
@@ -36,24 +36,23 @@ export const it: Copy = {
hero: {
greeting: 'Ciao. Mi chiamo',
- tagline: 'Piattaforma frontend e governance.',
- eyebrow: 'SENIOR SOFTWARE ENGINEER · FRONTEND PLATFORM & GOVERNANCE · VIENNA',
+ tagline: 'Senior Software Engineer.',
+ eyebrow: 'Sviluppo frontend, architettura e strumenti per sviluppatori',
stats: [
- { label: 'ESPERIENZA', value: '18+ anni' },
- { label: 'FOCUS', value: 'Piattaforma frontend, governance', tone: 'now' },
- { label: 'BASE', value: 'Vienna, remoto o ibrido' },
- { label: 'LINGUE', value: 'IT · EN · DE in corso' },
- { label: 'PERCORSO', value: 'Web design e UI', tone: 'root' },
+ { label: 'Dove vivo', value: 'Vienna, Austria' },
+ { label: 'Come lavoro', value: 'Remoto o ibrido', tone: 'now' },
+ { label: 'Percorso', value: 'Web design e UI', tone: 'root' },
+ { label: 'Lingue', value: 'IT · EN · DE in corso' },
],
intro:
- 'Sono un Senior Software Engineer con più di 18 anni di esperienza. Oggi lavoro sulla piattaforma frontend di Tosca Cloud, una monorepo che raccoglie 16 progetti ed è condivisa da più di 12 team. Mi occupo soprattutto di architettura, dipendenze, build e standard condivisi.',
- buttons: { github: 'GITHUB', linkedin: 'LINKEDIN', email: 'EMAIL', cv: 'CV.PDF ↓' },
+ 'Sviluppo software per il web da oltre 18 anni, con una forte esperienza nel frontend. Realizzo applicazioni, librerie condivise e strumenti per sviluppatori. Lavoro con i team su architettura, performance e integrazioni, dalle decisioni tecniche al codice.',
+ buttons: { github: 'GitHub', linkedin: 'LinkedIn', email: 'Scrivimi', cv: 'Scarica il CV' },
},
sections: {
approach: { num: '01', title: 'Come lavoro' },
focus: { num: '02', title: 'Su cosa lavoro' },
- ai: { num: '03', title: 'AI', eyebrow: 'USO, REVISIONE E TEST' },
+ ai: { num: '03', title: 'AI', eyebrow: 'Lavoro nel repository' },
experience: { num: '04', title: 'Esperienza' },
projects: { num: '05', title: 'Progetti personali' },
contact: { num: '06', title: 'Restiamo in contatto' },
@@ -61,20 +60,17 @@ export const it: Copy = {
approach: [
{
- label: 'Web_Design',
tone: 'root',
- title: 'Dal web design al frontend',
- body: 'Ho iniziato progettando e sviluppando siti web, poi mi sono concentrato sempre di più sul frontend. Ho lavorato anche su sistemi backend e infrastruttura. Questa esperienza mi aiuta a vedere cosa cambia nel resto del sistema quando intervengo sul frontend.',
+ title: 'Un occhio all’interfaccia',
+ body: 'Ho iniziato progettando e sviluppando siti web. Negli anni ho lavorato anche su identità visive, grafica per la stampa, sistemi backend e infrastruttura. Collaboro con i designer per tradurre i mockup in interfacce fedeli al progetto e con il comportamento previsto.',
},
{
- label: 'The_Boundaries',
title: 'Regole e controlli automatici',
- body: 'Lavoro spesso su problemi che coinvolgono più team: confini tra moduli, dipendenze, build e CI. Quando una regola è importante, ne documento il motivo e, se possibile, aggiungo un controllo automatico. Così è il controllo a ricordare la regola, e io evito di ripeterla a decine di persone, una alla volta.',
+ body: 'Documento le decisioni condivise e, quando posso, automatizzo i controlli. Vale per dipendenze, confini tra moduli e build. Mi evita anche di ripetere la stessa conversazione con una decina di persone, una alla volta.',
},
{
- label: 'Hands_On',
title: 'Lavoro sul codice e con i team',
- body: 'Continuo a implementare, fare debugging e lavorare sugli strumenti condivisi. Collaboro ogni giorno con gli sviluppatori dei team, dagli junior ai senior, e mi confronto spesso con i loro manager. Quando una decisione coinvolge più prodotti, lavoro anche con design, security e sales per capire cosa serve e applicarlo in modo coerente.',
+ body: 'Sviluppo e faccio debugging con gli sviluppatori dei team, dagli junior ai senior. Mi confronto regolarmente con i loro manager e con design, security e sales per capire le richieste e concordare le modifiche che coinvolgono più prodotti.',
},
],
@@ -85,38 +81,66 @@ export const it: Copy = {
'Architettura di monorepo',
'Micro-frontend',
'Confini tra moduli',
- 'Governance delle dipendenze',
+ 'Librerie condivise',
'RFC tecnici',
+ 'ADR',
+ 'OpenTelemetry',
+ 'JWT e interceptor HTTP',
],
},
{
- title: 'Frontend e integrazioni',
+ title: 'Sviluppo web',
items: [
'React',
'TypeScript',
'Vue',
+ 'Zustand',
'GraphQL',
'Node.js',
- 'Integrazione Okta lato frontend',
- 'Gestione JWT',
- 'OpenTelemetry',
+ 'JavaScript',
+ 'HTML e CSS',
+ ],
+ },
+ {
+ title: 'Build system e monorepo',
+ items: [
+ 'Nx',
+ 'pnpm workspaces',
+ 'Vite',
+ 'Webpack',
+ 'Cache delle build',
+ 'Confini delle dipendenze',
+ ],
+ },
+ {
+ title: 'Test e rilascio',
+ items: [
+ 'Vitest',
+ 'Jest',
+ 'Playwright',
+ 'GitHub Actions',
+ 'Azure Pipelines',
+ 'Amministrazione repository',
],
},
{
- title: 'Build e rilascio',
+ title: 'Design e sviluppo UI',
items: [
- 'Architettura CI',
- 'Automazione delle pipeline',
- 'Cache e performance delle build',
- 'Controlli di qualità',
- 'Test automatici',
+ 'Figma',
+ 'Adobe Creative Cloud',
+ 'Design system',
+ 'Design token',
+ 'Storybook',
+ 'Accessibilità',
],
},
{
- title: 'Developer experience',
+ title: 'Performance e developer experience',
items: [
+ 'Analisi dei bundle',
+ 'Code splitting',
+ 'Cache browser e CDN',
'Strumenti condivisi',
- 'Standard tecnici',
'Documentazione',
'Valutazione di strumenti AI',
],
@@ -125,17 +149,20 @@ export const it: Copy = {
ai: {
heading: 'Come uso l’AI nello sviluppo',
+ summary:
+ 'Mi occupo dell’adozione degli strumenti AI nel repository. Valuto Claude, Cursor e altri strumenti su attività reali e definisco regole e contesto per usarli nel lavoro dei team. I problemi ricorrenti mi aiutano a capire dove intervenire su documentazione e strumenti condivisi.',
+ moreLabel: 'Come li uso nel lavoro quotidiano',
paragraphs: [
- 'Uso assistenti AI e agenti per analizzare il codice di un progetto, preparare modifiche e ridurre il lavoro ripetitivo. Li uso solo se posso capire cosa hanno fatto e verificare che funzioni.',
- 'Li provo su attività reali per capire dove perdono il contesto della codebase. In base a quello aggiorno documentazione, regole e strumenti del repository.',
+ 'Mi occupo di come gli strumenti AI vengono adottati nel repository e nel lavoro dei team. Uso e valuto Claude, Cursor e altri assistenti per analizzare il codice, preparare modifiche e ridurre il lavoro ripetitivo. Definisco le regole d’uso e curo il contesto del repository a loro disposizione.',
+ 'Guardo soprattutto gli errori che si ripetono: perdere il contesto tra moduli, ignorare un’implementazione già presente o proporre modifiche fuori tema. Prima di riprovare, cerco cosa manca nelle istruzioni, nella documentazione o negli strumenti condivisi.',
],
- rulesLead: 'In pratica:',
+ rulesLead: 'Cosa verifico:',
rules: [
- 'Prima verifico che la modifica serva. Un agente può suggerire di ricostruire qualcosa che nel progetto esiste già.',
- 'Rivedo il risultato come qualsiasi altro contributo. Leggo il diff, eseguo i test e controllo che rispetti i confini del progetto.',
- 'Devo poter spiegare e mantenere ogni modifica. Se non ci riesco, non la uso.',
+ 'L’attività e il suo contesto. Controllo cosa offre già il repository e quali moduli devono essere modificati. Se un agente propone una funzionalità duplicata, è un segnale che gli manca una parte di quel contesto.',
+ 'Il problema ricorrente. Se lo stesso errore continua a ripresentarsi, verifico se un’istruzione è poco chiara, manca della documentazione o serve modificare uno strumento condiviso.',
+ 'Il codice prodotto. Leggo il diff, eseguo i test e verifico l’integrazione nel progetto. Tengo le modifiche che so spiegare e mantenere: una risposta plausibile, da sola, non mi dice se il software funziona.',
],
- backLabel: 'ANDREA LISI',
+ backLabel: 'Torna al mio lavoro',
},
work: [
@@ -148,11 +175,9 @@ export const it: Copy = {
tags: ['Microfrontend', '16 progetti', '12+ team', 'Quality gates'],
current: true,
paragraphs: [
- 'Lavoro sulla piattaforma frontend di Tosca Cloud, una monorepo a micro-frontend che raccoglie 16 progetti ed è condivisa da più di 12 team.',
- 'Lavoro sull’architettura e sulla governance della monorepo, in particolare su confini tra moduli, regole sulle dipendenze e standard frontend condivisi. Rivedo anche gli RFC che coinvolgono il frontend condiviso.',
- 'Amministro la repository di Tosca Cloud su GitHub e automatizzo le pipeline, anche su Azure. Con il platform architect lavoro sulla cache e sulle performance delle build, sulla struttura della CI e sui quality gate.',
- 'Implemento e mantengo integrazioni condivise tra più applicazioni frontend. Tra queste ci sono l’osservabilità con OpenTelemetry e la parte applicativa dell’autenticazione dopo il login Okta, compresa la gestione dei JWT e degli interceptor HTTP. Quando un problema compare in più progetti, lo indago a livello di piattaforma.',
- 'Valuto regolarmente strumenti AI, tra cui Claude e Cursor, su attività reali nel repository. Se inciampano sempre negli stessi punti, intervengo sulla documentazione, sulle regole o sugli strumenti condivisi.',
+ 'Insieme all’architetto del mio team sono un riferimento per le iniziative tecniche che coinvolgono l’intera monorepo. Mi confronto con sviluppatori di diversi ambiti, engineering manager, principal engineer di altre piattaforme e referenti di prodotto per valutare esigenze, dipendenze e impatto sui team. Da questo confronto definiamo le soluzioni condivise e ne accompagniamo l’adozione.',
+ 'Il mio lavoro comprende sia le decisioni tecniche sia l’implementazione: sviluppo e mantengo risorse condivise, correggo problemi trasversali e preparo l’infrastruttura necessaria alle migrazioni. Seguo l’integrazione delle nuove versioni del design system, coordinando gli aggiornamenti con i team e intervenendo direttamente quando serve. Quando emergono differenze tra prodotti, lavoro sulle configurazioni e sui componenti comuni per correggerle.',
+ 'Mi occupo inoltre di performance, ottimizzazione delle build e modernizzazione degli strumenti di sviluppo. Documento le scelte negli ADR, definisco le regole della monorepo e supporto i team nell’applicarle. Le feature restano ai team di prodotto, ai quali posso contribuire; la mia responsabilità continuativa è la piattaforma che condividono.',
],
},
{
@@ -201,18 +226,30 @@ export const it: Copy = {
projects: [
{
name: 'Lumina',
- kind: 'FANTASY MUD',
+ kind: 'Sviluppo di giochi e web design',
description:
- 'Un MUD fantasy che ho sviluppato interamente: backend Node.js/MongoDB, client React e infrastruttura Cloudflare.',
+ 'Un gioco fantasy multiplayer basato sul testo. Sviluppo il backend Node.js/MongoDB, il client React e l’infrastruttura Cloudflare.',
tags: ['Node.js', 'MongoDB', 'React', 'Cloudflare'],
+ href: 'https://luminamud.com/',
+ linkLabel: 'Esplora Lumina',
+ detail:
+ 'Un progetto personale in cui lavoro sia al design delle interfacce sia al codice e ai sistemi che fanno funzionare il gioco.',
+ imageAlt:
+ 'Il sito pubblico di Lumina: un paesaggio fantasy con titoli dorati e la navigazione per esplorare il mondo del gioco.',
+ caption: 'Il sito del progetto. Il gioco è in sviluppo.',
},
{
name: 'lockhound',
- kind: 'WINDOWS CLI',
+ kind: 'Strumento open source per sviluppatori',
description:
- 'CLI per Windows senza dipendenze che trova e termina i processi che tengono bloccata una cartella, usando la Windows Restart Manager API.',
+ 'Una CLI per Windows che identifica i processi che tengono bloccata una cartella. Usa la Windows Restart Manager API e non richiede dipendenze aggiuntive.',
tags: ['Windows', 'Restart Manager API', 'Developer tooling'],
command: 'npx lockhound',
+ href: 'https://github.com/karedas/lockhound',
+ linkLabel: 'Leggi il codice su GitHub',
+ detail:
+ 'Il comando mostra quali processi bloccano la cartella. Terminarli richiede un’azione separata ed esplicita, così puoi prima vedere cosa sta succedendo.',
+ caption: 'Esempio d’uso. Ispeziona una cartella senza terminare alcun processo.',
},
],
@@ -223,9 +260,10 @@ export const it: Copy = {
nowBadge: 'IN CORSO',
contact: {
- invite:
- 'Hey! Se ti va di parlare di piattaforme frontend, developer experience o semplicemente di scambiarci due idee, scrivimi.',
- inviteMore: 'Non serve una proposta formale: anche un messaggio al volo va benissimo.',
+ invite: 'Hey! Hai un progetto, un ruolo tecnico o qualcosa di cui ti va di parlare?',
+ inviteMore:
+ 'Mi piace sviluppare software e ragionare sui problemi tecnici con altri sviluppatori. Per cominciare basta un messaggio al volo.',
+ writeLabel: 'Scrivimi',
cv: 'CV (PDF)',
github: 'GitHub',
linkedin: 'LinkedIn',
diff --git a/src/i18n/types.ts b/src/i18n/types.ts
index 2d19fd9..45a7e71 100644
--- a/src/i18n/types.ts
+++ b/src/i18n/types.ts
@@ -9,11 +9,7 @@ export type SectionHead = {
title: string;
};
-/**
- * Only the AI section carries a label. It is the one enclosed block on the page,
- * and the label is part of what sets it apart, so the field belongs to that
- * section alone rather than sitting unused on the other three.
- */
+/** Label used on the detailed AI page. */
export type LabelledSectionHead = SectionHead & {
eyebrow: string;
};
@@ -31,17 +27,7 @@ export type Stat = {
};
export type ApproachBlock = {
- /**
- * Short mono tag above the paragraph, written as an identifier rather than as
- * a phrase. Deliberately English in both locales: it reads as code, not
- * prose, and Italian does not snake_case gracefully.
- */
- label: string;
- /**
- * A claim with a reason behind it, not a slogan. Titles were dropped once
- * because mine were aphorisms over one-liners; these are his own, and they
- * carry an argument, so they earn the line back.
- */
+ /** Heading describing a concrete part of the author's work. */
title: string;
/** HTML: opens with a lead and carries no other emphasis. */
body: string;
@@ -59,6 +45,11 @@ export type ProjectEntry = {
description: string;
tags: string[];
command?: string;
+ href: string;
+ linkLabel: string;
+ detail: string;
+ imageAlt?: string;
+ caption: string;
};
export type WorkEntry = {
@@ -131,6 +122,8 @@ export type Copy = {
ai: {
heading: string;
+ summary: string;
+ moreLabel: string;
/** HTML: the opening paragraphs, before the rules. */
paragraphs: string[];
/** The line that hands over to the list. */
@@ -162,5 +155,6 @@ export type Copy = {
linkedin: string;
email: string;
instagram: string;
+ writeLabel: string;
};
};
diff --git a/src/layouts/layout.astro b/src/layouts/layout.astro
index 3faeee5..24bd385 100644
--- a/src/layouts/layout.astro
+++ b/src/layouts/layout.astro
@@ -20,13 +20,8 @@ const fontHref =
'https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap';
---
-
+
-
@@ -39,10 +34,6 @@ const fontHref =
- {/* The deep page repeats the home section word for word. It exists so the
- stance can be handed over as its own URL, not to be found in search, so
- it stays out of the index rather than competing with the home page. */}
- {page === 'ai' && }
{LOCALES.map((code) => (
diff --git a/src/styles/global.css b/src/styles/global.css
index 8c99f7c..eb40a86 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -32,8 +32,16 @@ a:hover {
color: var(--acc);
}
+.text-link {
+ color: var(--acc);
+ text-decoration: underline;
+ text-underline-offset: 5px;
+ text-decoration-thickness: 1px;
+ line-height: 1.7;
+}
+
::selection {
- background: rgba(255, 131, 84, 0.22);
+ background: var(--acc-soft);
color: var(--head);
}
diff --git a/src/styles/motion.css b/src/styles/motion.css
index 0b6e4db..ba3f745 100644
--- a/src/styles/motion.css
+++ b/src/styles/motion.css
@@ -1,78 +1,12 @@
-/* =========================================================
- Motion. Short, once, on first viewport entry.
- Logic: src/scripts/reveal.ts
- ========================================================= */
-
-@keyframes alf {
- from {
- opacity: 0;
- transform: translateY(16px);
- }
- to {
- opacity: 1;
- transform: none;
- }
-}
-
-@keyframes gdraw {
- to {
- stroke-dashoffset: 0;
- }
-}
-
-@keyframes gin {
- to {
- opacity: 1;
- }
-}
-
-@keyframes gout {
- to {
- opacity: 0;
- visibility: hidden;
- }
-}
-
-/* Hero pieces enter on load, staggered by an inline delay. */
-.enter {
- animation: alf 0.6s ease both;
-}
-
-/* Everything below the fold enters when it first scrolls in. */
-[data-reveal] {
- opacity: 0;
- transform: translateY(14px);
- transition:
- opacity 0.7s ease,
- transform 0.7s ease;
-}
-
-[data-reveal].is-in,
-html.no-js [data-reveal] {
- opacity: 1;
- transform: none;
-}
-
-/* The !important here is the point, not an oversight: this block has to beat
- every animation and transition on the page whatever their specificity or
- source order, including the inline stagger delays on the hero. Take them out
- and the reduced-motion promise stops holding, which the e2e suite checks. */
-/* biome-ignore-start lint/complexity/noImportantStyles: an accessibility reset has to win the cascade outright */
+/* Content is immediately visible, with or without JavaScript.
+ Motion is limited to interaction feedback and the current-role indicator. */
+/* biome-ignore-start lint/complexity/noImportantStyles: the accessibility reset must win the cascade */
@media (prefers-reduced-motion: reduce) {
- .loader {
- display: none !important;
- }
-
*,
*::before,
*::after {
animation: none !important;
transition: none !important;
}
-
- [data-reveal] {
- opacity: 1 !important;
- transform: none !important;
- }
}
-/* biome-ignore-end lint/complexity/noImportantStyles: an accessibility reset has to win the cascade outright */
+/* biome-ignore-end lint/complexity/noImportantStyles: the accessibility reset must win the cascade */
diff --git a/src/styles/tokens.css b/src/styles/tokens.css
index d507b67..96fc453 100644
--- a/src/styles/tokens.css
+++ b/src/styles/tokens.css
@@ -21,11 +21,11 @@
--line: #233554;
--head: #ccd6f6;
- --body: #8892b0;
+ --body: #a5b2cc;
/* The design shipped #5f6c8c here, which gives 3.36:1 on --bg and fails AA
for the dates, the chips and the meta lines it is used on. Lifted to the
nearest tone in the same hue that clears 4.5:1. */
- --dim: #7683a3;
+ --dim: #92a1bd;
/* Emerald, deliberately greener than the #64ffda mint that every navy
portfolio uses: hue 161 against its 166, and darker, so it reads as
diff --git a/tests/e2e/homepage.spec.ts b/tests/e2e/homepage.spec.ts
index 1d8a1d5..06a635b 100644
--- a/tests/e2e/homepage.spec.ts
+++ b/tests/e2e/homepage.spec.ts
@@ -84,15 +84,14 @@ for (const locale of ['en', 'it'] as const) {
}
});
- test('renders the ways-of-working blocks as tagged prose', async ({ page }) => {
+ test('renders ways of working with headings and a clear opening sentence', async ({ page }) => {
await page.goto(home);
const blocks = page.locator('.approach .block');
await expect(blocks).toHaveCount(copy.approach.length);
for (const [i, block] of copy.approach.entries()) {
const el = blocks.nth(i);
- await expect(el.locator('.block-label')).toHaveText(block.label);
- await expect(el.locator('.block-title')).toHaveText(block.title);
+ await expect(el.getByRole('heading', { level: 3 })).toHaveText(block.title);
// The lead opens the paragraph, and it is the only emphasis in it.
await expect(el.locator('.block-body b')).toHaveCount(1);
}
@@ -102,23 +101,14 @@ for (const locale of ['en', 'it'] as const) {
expect(copy.approach.length).toBe(3);
});
- test('sets the AI section apart as the one enclosed block', async ({ page }) => {
+ test('summarizes AI work and links to the detailed workflow', async ({ page }) => {
await page.goto(home);
- const opening = page.locator('.ai .prose > p:not(.rules-lead)');
- await expect(opening).toHaveCount(copy.ai.paragraphs.length);
- await expect(page.locator('.ai .prose .lede')).toContainText(
- plain(copy.ai.paragraphs[0] ?? '').slice(0, 40),
- );
- await expect(page.locator('.ai .rules-lead')).toHaveText(copy.ai.rulesLead);
- // He says three rules, so there had better be three.
- await expect(page.locator('.ai .rules > li')).toHaveCount(copy.ai.rules.length);
- expect(copy.ai.rules.length).toBe(3);
- await expect(page.locator('.ai .plate .eyebrow')).toHaveText(copy.sections.ai.eyebrow);
-
- // Being the only enclosed surface is what distinguishes it, so a second
- // one appearing anywhere would take that away.
- await expect(page.locator('.plate')).toHaveCount(1);
+ await expect(page.locator('.ai-summary p')).toHaveText(copy.ai.summary);
+ await expect(page.locator('.ai .rules')).toHaveCount(0);
+ await page.locator('.ai').getByRole('link', { name: copy.ai.moreLabel }).click();
+ await expect(page).toHaveURL(new RegExp(`${ai}/?$`));
+ await expect(page.locator('.ai .prose .lede')).toHaveText(plain(copy.ai.paragraphs[0] ?? ''));
// The home page itself must stay indexable.
await expect(page.locator('meta[name=robots]')).toHaveCount(0);
});
@@ -132,6 +122,18 @@ for (const locale of ['en', 'it'] as const) {
await expect(page.locator('.projects .project')).toHaveCount(copy.projects.length);
await expect(page.locator('.projects').getByText('lockhound', { exact: true })).toBeVisible();
await expect(page.locator('.projects code')).toHaveText('npx lockhound');
+ for (const project of copy.projects) {
+ await expect(page.getByRole('link', { name: project.linkLabel })).toHaveAttribute(
+ 'href',
+ project.href,
+ );
+ }
+ const preview = page.locator('.project-preview img');
+ await preview.scrollIntoViewIfNeeded();
+ await expect(preview).toBeVisible();
+ await expect
+ .poll(() => preview.evaluate((img: HTMLImageElement) => img.naturalWidth))
+ .toBeGreaterThan(0);
});
test('lists four jobs, each with bullets and chips', async ({ page }) => {
@@ -149,7 +151,7 @@ for (const locale of ['en', 'it'] as const) {
}
});
- test('closes on an invitation, with the address behind the mail icon', async ({ page }) => {
+ test('closes on an invitation and a labelled email action', async ({ page }) => {
await page.goto(home);
await expect(page.locator('.contact .invite')).toHaveText(copy.contact.invite);
@@ -157,7 +159,7 @@ for (const locale of ['en', 'it'] as const) {
// The address is a link, not printed text: no duplication with the icon.
await expect(page.locator('.contact').getByText('lisandr84@gmail.com')).toHaveCount(0);
await expect(
- page.locator('.contact').getByRole('link', { name: copy.contact.email }),
+ page.locator('.contact').getByRole('link', { name: copy.contact.writeLabel }),
).toHaveAttribute('href', 'mailto:lisandr84@gmail.com');
});
@@ -191,20 +193,22 @@ for (const locale of ['en', 'it'] as const) {
}
});
- /*
- * The hero now opens with the scale of the current platform instead of a
- * general claim. The focus and work sections then add the technical detail.
- */
- test('opens with platform scale and adds architecture detail later', async ({ page }) => {
+ test('uses the official title and keeps employer detail out of the personal bio', async ({
+ page,
+ }) => {
await page.goto(home);
- const architecture = /micro-?frontend|monorepo/i;
-
- await expect(page.locator('.hero')).toContainText('16');
- await expect(page.locator('.hero')).toContainText('12');
- await expect(page.locator('.hero').getByText(architecture)).toHaveCount(1);
- await expect(page.locator('.approach').getByText(architecture)).toHaveCount(0);
- await expect(page.locator('.focus').getByText(architecture).first()).toBeVisible();
- await expect(page.locator('.work').getByText(architecture).first()).toBeVisible();
+ await expect(page.locator('.hero .tagline')).toHaveText('Senior Software Engineer.');
+ await expect(page.locator('.hero')).not.toContainText(/Tricentis|Tosca Cloud|Staff Engineer/);
+ await expect(page.locator('.work .job-title').first()).toContainText(
+ 'Senior Software Engineer',
+ );
+ await expect(page.locator('#examples')).toHaveCount(0);
+ await expect(
+ page
+ .locator('.work')
+ .getByText(/monorepo/i)
+ .first(),
+ ).toBeVisible();
});
test('spends its three colours only where each one means something', async ({ page }) => {
@@ -219,7 +223,7 @@ for (const locale of ['en', 'it'] as const) {
// Lilac: the art foundation. The school, the block about drawing, the
// job where the drawing was the job.
await expect(page.locator('.hero .fact-v.tone-root')).toHaveCount(1);
- await expect(page.locator('.approach .block-label.tone-root')).toHaveCount(1);
+ await expect(page.locator('.approach .block-title.tone-root')).toHaveCount(1);
await expect(page.locator('.work .chip.root')).toHaveCount(1);
const tones = await page.evaluate(() => {
@@ -258,13 +262,14 @@ for (const locale of ['en', 'it'] as const) {
await context.close();
});
- test('drops the loader under prefers-reduced-motion', async ({ browser }) => {
+ test('has no loader and respects reduced motion', async ({ browser }) => {
const context = await browser.newContext({ reducedMotion: 'reduce' });
const page = await context.newPage();
await page.goto(home);
- await expect(page.locator('.loader')).toBeHidden();
+ await expect(page.locator('.loader')).toHaveCount(0);
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
+ await expect(page.locator('.work .pulse')).toHaveCSS('animation-name', 'none');
await context.close();
});
@@ -278,6 +283,9 @@ for (const locale of ['en', 'it'] as const) {
() => document.documentElement.scrollWidth > window.innerWidth + 1,
);
expect(overflows).toBe(false);
+ const cv = await page.locator('.hero .cv').boundingBox();
+ expect(cv).not.toBeNull();
+ expect((cv?.y ?? 1000) + (cv?.height ?? 0)).toBeLessThanOrEqual(812);
});
test('passes axe accessibility checks (no critical/serious violations)', async ({ page }) => {
@@ -287,15 +295,18 @@ for (const locale of ['en', 'it'] as const) {
});
test.describe(`the AI deep link (${locale})`, () => {
- test('carries the same prose on its own page', async ({ page }) => {
+ test('carries the detailed workflow on its own indexable page', async ({ page }) => {
await page.goto(ai);
await expect(page.locator('html')).toHaveAttribute('lang', copy.htmlLang);
await expect(page.getByRole('heading', { level: 1 })).toContainText(copy.ai.heading);
await expect(page.locator('.ai .rules > li')).toHaveCount(copy.ai.rules.length);
- // Word for word the home section, so it stays out of the index and exists
- // only as a URL that can be handed to someone.
- await expect(page.locator('meta[name=robots]')).toHaveAttribute('content', /noindex/);
+ await expect(page.locator('meta[name=robots]')).toHaveCount(0);
+ await expect(page.locator('.ai .ai-summary')).toHaveCount(0);
+ await expect(page.getByRole('link', { name: copy.ai.backLabel })).toHaveAttribute(
+ 'href',
+ `${home}#ai`,
+ );
});
test('keeps its own description under the truncation limit', async ({ page }) => {
diff --git a/tests/unit/i18n.test.ts b/tests/unit/i18n.test.ts
index 0316fa7..b7b69c3 100644
--- a/tests/unit/i18n.test.ts
+++ b/tests/unit/i18n.test.ts
@@ -93,10 +93,26 @@ describe('the work history', () => {
});
describe('focus and personal projects', () => {
- it('keeps four focus groups and includes lockhound in both languages', () => {
+ it('keeps tool names in capabilities without turning recent tasks into achievements', () => {
for (const locale of LOCALES) {
const copy = getCopy(locale);
- expect(copy.focus).toHaveLength(4);
+ const currentRole = copy.work[0]?.paragraphs.join(' ') ?? '';
+ expect(currentRole).toContain('ADR');
+ expect(currentRole).toContain('design system');
+ expect(currentRole).not.toContain('Playwright');
+ expect(copy.focus.flatMap((group) => group.items)).toContain('Playwright');
+ expect(copy.focus.flatMap((group) => group.items)).toContain('Zustand');
+ }
+ });
+
+ it('groups the skills and links each personal project in both languages', () => {
+ for (const locale of LOCALES) {
+ const copy = getCopy(locale);
+ expect(copy.focus).toHaveLength(6);
+ for (const project of copy.projects) {
+ expect(new URL(project.href).protocol).toBe('https:');
+ expect(project.linkLabel.length).toBeGreaterThan(0);
+ }
expect(copy.projects.some((project) => project.name === 'lockhound')).toBe(true);
}
});