Files
homepage_relaunch/src/components/home/Sectors.astro
T
2026-10-08 10:36:09 +02:00

207 lines
8.4 KiB
Plaintext

---
import { Picture } from 'astro:assets';
import fiveG from '../../assets/images/sector-5g.jpg';
import substation from '../../assets/images/sector-substation.jpg';
import controlRoom from '../../assets/images/sector-ot.jpg';
import products from '../../assets/images/sector-device-line.jpg';
import datacentre from '../../assets/images/sector-datacentre.jpg';
import { t, type Lang } from '../../i18n/ui';
interface Props { lang: Lang }
const { lang } = Astro.props;
const ui = t(lang);
const images = [fiveG, substation, controlRoom, products, datacentre];
/* Where each picture keeps its subject when cropped. */
const focus = ['center', 'center', 'center', 'center', 'center'];
const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], focus: focus[i], id: `sector-${i + 1}` }));
---
<section class="section sectors" id="sectors" aria-labelledby="sectors-title">
<div class="container sectors__grid" data-sectors>
<div class="sectors__text">
<h2 id="sectors-title" class="h2">{ui.sectors.title}</h2>
<div class="sectors__tabs" role="tablist" aria-orientation="vertical" aria-labelledby="sectors-title">
{items.map((item, i) => (
<div class="sectors__tab" data-tab>
<h3 class="sectors__heading">
<button
class="sectors__button"
type="button"
role="tab"
id={`${item.id}-tab`}
aria-controls={`${item.id}-panel`}
aria-selected={i === 0 ? 'true' : 'false'}
tabindex={i === 0 ? undefined : -1}
>{item.title}</button>
</h3>
<div class="sectors__panel" role="tabpanel" id={`${item.id}-panel`} aria-labelledby={`${item.id}-tab`} tabindex="0" data-open={i === 0 ? '' : undefined}>
<div class="sectors__panel-inner">
<p class="muted">{item.body}</p>
<Picture
src={item.image}
alt={item.imageAlt}
formats={['avif']}
fallbackFormat="webp"
widths={[480, 768, 1024, 1440, 1920]}
sizes="(min-width: 64rem) 60vw, 100vw"
loading="lazy"
decoding="async"
class="sectors__img img-doc"
style={`object-position: ${item.focus}`}
pictureAttributes={{ class: 'sectors__picture' }}
/>
</div>
</div>
</div>
))}
</div>
</div>
<div class="sectors__media" aria-hidden="true">
{items.map((item, i) => (
<Picture
src={item.image}
alt=""
formats={['avif']}
fallbackFormat="webp"
widths={[480, 768, 1024, 1440, 1920]}
sizes="(min-width: 64rem) 60vw, 100vw"
loading="lazy"
decoding="async"
class="sectors__img img-doc"
style={`object-position: ${item.focus}`}
pictureAttributes={{ class: 'sectors__picture sectors__picture--stage', 'data-stage-item': '', 'data-active': i === 0 ? '' : undefined }}
/>
))}
</div>
</div>
</section>
<style>
.sectors { border-top: var(--hairline) solid var(--line); }
.sectors__grid {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
column-gap: var(--gap);
row-gap: var(--space-7);
}
.sectors__text {
grid-column: 1 / -1;
display: flex;
flex-direction: column;
gap: var(--space-8);
}
/* The tabs are rows between hairlines. The selected row's rule turns ink: that is the marker. */
.sectors__tabs {
margin-top: auto;
border-bottom: var(--hairline) solid var(--line);
}
.sectors__tab {
position: relative;
border-top: var(--hairline) solid var(--line);
}
.sectors__tab::before {
content: '';
position: absolute;
left: 0;
right: 0;
top: calc(-1 * var(--hairline));
height: var(--hairline);
background: var(--ink);
transform: scaleX(0);
transform-origin: left;
transition: transform var(--dur-slow) var(--ease);
}
.sectors__tab:has([aria-selected='true'])::before { transform: none; }
.sectors__heading { font-size: inherit; font-weight: inherit; }
/* The whole row is the button, so the target is as wide as the column. */
.sectors__button {
display: block;
width: 100%;
padding-block: var(--space-5) var(--space-3);
text-align: left;
font-size: var(--step-h3);
line-height: var(--leading-h3);
letter-spacing: var(--track-h3);
font-weight: var(--weight-medium);
color: var(--ink);
transition: color var(--dur-fast) var(--ease);
}
.sectors__button[aria-selected='false'] { color: var(--ink-muted); }
.sectors__button[aria-selected='false']:hover { color: var(--ink); }
.sectors__panel-inner { display: grid; gap: var(--space-5); }
/* The button keeps a small step below its title, so an open body sits close under it and the
focus ring never crosses the text. The rest of the row padding is the panel's margin: below
the body when open, in place of it when closed, so every row is symmetric. */
.sectors__panel { margin-bottom: var(--space-5); }
.sectors__panel p { max-width: 28em; }
:global(.sectors__picture) { display: block; width: 100%; }
.sectors__img, :global(.sectors__img) { width: 100%; height: auto; aspect-ratio: 4 / 3; object-fit: cover; }
/* The stage image on the right exists for wide screens. On narrow screens each panel carries its own image. */
.sectors__media { display: none; }
/* With scripting, one panel at a time; without, everything is readable. The first panel is
open in the markup, so nothing expands on load and the page below it does not move. */
:global(html.js) .sectors__panel {
display: grid;
grid-template-rows: 0fr;
overflow: hidden;
transition:
grid-template-rows var(--dur-slow) var(--ease),
margin var(--dur-slow) var(--ease);
}
:global(html.js) .sectors__panel-inner { min-height: 0; }
:global(html.js) .sectors__panel[data-open] { grid-template-rows: 1fr; }
/* A closed panel is hidden, not only collapsed, so it does not take focus while invisible.
Visibility switches at the end of the collapse and at the start of the expand. */
:global(html.js) .sectors__panel:not([data-open]) {
margin-bottom: calc(var(--space-5) - var(--space-3));
visibility: hidden;
transition:
grid-template-rows var(--dur-slow) var(--ease),
margin var(--dur-slow) var(--ease),
visibility 0s linear var(--dur-slow);
}
@media (min-width: 64rem) {
.sectors__text { grid-column: 1 / span 5; }
/* The image runs past the container to the right edge of the window: the page gutter, plus
half of whatever the window is wider than the container. The section clips, so a classic
scrollbar (counted in 100vw) cannot cause a horizontal scroll. */
.sectors { overflow-x: clip; }
/* The image box is at least 5:4 (never taller than 45rem), and stretches with the row when
the list beside it is taller, so its top and bottom always meet the heading and the last rule. */
.sectors__media {
grid-column: 7 / span 6;
display: block;
position: relative;
margin-right: calc(-1 * (max(var(--gutter), var(--safe-right)) + max(0px, (100vw - var(--container)) / 2)));
}
/* The minimum: a spacer as tall as 5:4 of the width, capped at 45rem. A grid item stretches
to its row, so a taller list beside it makes the box taller; the pictures fill the box. */
.sectors__media::before { content: ''; display: block; padding-top: min(80%, 45rem); }
.sectors__media :global(.sectors__img) { height: 100%; aspect-ratio: auto; }
.sectors__media :global(.sectors__picture--stage) {
position: absolute;
inset: 0;
opacity: 0;
transition: opacity var(--dur-slow) var(--ease);
}
.sectors__media :global(.sectors__picture--stage[data-active]) { opacity: 1; }
/* Inside the panels the image is redundant on wide screens. */
.sectors__panel :global(.sectors__picture) { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.sectors__panel,
:global(html.js) .sectors__panel,
:global(html.js) .sectors__panel:not([data-open]),
.sectors__tab::before,
.sectors__media :global(.sectors__picture--stage) { transition: none; }
}
</style>
<script>
import { initTabs } from '../../scripts/tabs';
const root = document.querySelector<HTMLElement>('[data-sectors]');
if (root) initTabs(root);
</script>