feat: surface availability and operations evidence

This commit is contained in:
2026-09-04 20:34:19 +03:00
parent 3509f5fbdf
commit 55158d490e
13 changed files with 147 additions and 22 deletions

View File

@@ -224,7 +224,7 @@ onUnmounted(() => removePageListeners());
</header>
<main id="portfolio-content" class="portfolio-main" tabindex="-1">
<HeroJourney :identity="content.identity" @explore="navigation.onSectionLinkClick" />
<HeroJourney :identity="content.identity" :email="content.contact.email" @explore="navigation.onSectionLinkClick" />
<PortfolioSection id="profile" :layout="sectionLayouts.profile" :heading="sectionLabel('profile')">
<p>{{ content.profile.introduction }}</p>

View File

@@ -9,14 +9,14 @@ defineProps<{
<template>
<div class="contact-destination">
<p class="contact-destination__copy">Thanks for making the trip. The road's open if you'd like to build something together.</p>
<a class="contact-link" :href="`mailto:${contact.email}`">Start a conversation</a>
<p class="contact-destination__copy">{{ contact.introduction }}</p>
<a class="contact-link" :href="`mailto:${contact.email}`">Email Luca</a>
<p class="contact-destination__email">{{ contact.email }}</p>
<p class="contact-destination__cv">{{ contact.cvMessage }}</p>
</div>
<ul class="contact-social">
<li v-for="link in contact.social" :key="link.service">
<a class="contact-social-link" :href="link.url" target="_blank" rel="noopener noreferrer">
<a class="contact-social-link" :href="link.url" target="_blank" rel="me noopener noreferrer">
<BrandIcon :label="link.service" />
<span>{{ link.service }}</span>
</a>

View File

@@ -3,6 +3,7 @@ import type { PortfolioContent } from '@/content/types';
defineProps<{
identity: Readonly<PortfolioContent['identity']>;
email: string;
}>();
const emit = defineEmits<{
@@ -16,7 +17,11 @@ const emit = defineEmits<{
<h1 id="start-heading" tabindex="-1">{{ identity.name }}</h1>
<p class="hero-journey__role">{{ identity.role }}</p>
<p class="hero-journey__value">{{ identity.valueProposition }}</p>
<a class="hero-journey__action" href="#selected-builds" @click="emit('explore', $event)">Explore work</a>
<p class="hero-journey__availability">{{ identity.availability }}</p>
<div class="hero-journey__actions">
<a class="hero-journey__action hero-journey__action--primary" :href="`mailto:${email}`">Email Luca</a>
<a class="hero-journey__action" href="#selected-builds" @click="emit('explore', $event)">Explore work</a>
</div>
</div>
<p class="print-portfolio-label" aria-hidden="true">Portfolio</p>
</section>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import OperationsProof from './OperationsProof.vue';
import type { InfrastructureCaseStudy } from "@/content/types";
defineProps<{ infrastructure: Readonly<InfrastructureCaseStudy> }>();
@@ -118,5 +119,6 @@ defineProps<{ infrastructure: Readonly<InfrastructureCaseStudy> }>();
>: {{ band.summary }}
</li>
</ul>
<OperationsProof id="nextcloud-migration" :operation="infrastructure.featuredOperation" />
</article>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { FeaturedOperation } from '@/content/types';
defineProps<{ id: string; operation: Readonly<FeaturedOperation> }>();
</script>
<template>
<section class="operations-proof" data-featured-operation :aria-labelledby="`${id}-heading`">
<h4 :id="`${id}-heading`">{{ operation.title }}</h4>
<p>{{ operation.context }}</p>
<ol>
<li v-for="action in operation.actions" :key="action">{{ action }}</li>
</ol>
<p class="operations-proof__outcome"><strong>Outcome:</strong> {{ operation.outcome }}</p>
</section>
</template>

View File

@@ -23,6 +23,19 @@
font-size: var(--step-0);
}
.hero-journey__availability {
max-inline-size: 52ch;
color: var(--text-muted);
font-size: var(--step--1);
}
.hero-journey__actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
align-items: center;
}
.hero-journey__action,
.project-link,
.contact-link,
@@ -43,6 +56,17 @@
text-decoration: none;
}
.hero-journey__action--primary {
padding-inline: 0.9rem;
border: 1px solid var(--cyan);
background: var(--cyan);
color: #041017;
}
.hero-journey__action--primary::after {
content: none;
}
.hero-journey__action::after {
content: "→";
transition: transform var(--transition);
@@ -189,6 +213,50 @@
background: rgb(5 12 23 / 90%);
}
.operations-proof {
display: grid;
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
gap: var(--space-2) var(--space-3);
margin-block-start: var(--space-3);
padding-block-start: var(--space-3);
border-block-start: 2px solid var(--line-strong);
}
.operations-proof > h4,
.operations-proof__outcome {
grid-column: 1 / -1;
margin: 0;
}
.operations-proof > h4 {
color: var(--text);
font-size: var(--step-0);
}
.operations-proof > p:not(.operations-proof__outcome),
.operations-proof > ol {
margin: 0;
}
.operations-proof > ol {
padding-inline-start: 1.2rem;
}
.operations-proof__outcome {
color: var(--text) !important;
}
@media (max-width: 48rem) {
.operations-proof {
grid-template-columns: minmax(0, 1fr);
}
.operations-proof > h4,
.operations-proof__outcome {
grid-column: auto;
}
}
.portfolio-section[data-layout="full-diagram"] figure {
background: rgb(10 24 38 / 72%);
}

View File

@@ -85,6 +85,7 @@
.hero-journey__content {
display: grid;
min-inline-size: 0;
inline-size: min(34rem, 39vw);
gap: 1rem;
padding: clamp(1.4rem, 2.2vw, 2.1rem);

View File

@@ -50,6 +50,20 @@
margin-block-start: 0.4rem;
}
.hero-journey__actions,
.operations-proof {
display: block;
}
.operations-proof {
margin-block-start: 1rem;
padding-block-start: 1rem;
}
.operations-proof > * + * {
margin-block-start: 0.6rem;
}
.print-portfolio-label {
display: block;
margin-block: 0.25rem 1rem;