207 lines
8.4 KiB
Plaintext
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>
|