Files
ai-for-dummies/src/components/blocks/FleetDiagram.astro
T
Marcos Paulo cac1115035 feat(blocks): add Localized type support to full-guide blocks
Widens prose props on FleetDiagram, HandoffTable, PhasePanel, RouteTable, SkillPackage, and WorktreeMap to accept {en, pt} as well as string, and conditionally renders language spans. Non-prose props (id, code, etc) were left as strings.
2026-09-05 21:59:29 +00:00

212 lines
5.5 KiB
TypeScript

---
// FleetDiagram — orchestrator card, an arrow, and a 1-up of worker cards.
//
// Static shell: the captain renders literally, the workers render as toggle
// buttons with the `data-worker` hook asserted by `scripts/verify.mjs`. The
// `initial` worker is marked active and pressed.
//
// The source uses `gap:1px` over a coloured parent background to fake
// hairlines between worker cards; the parent background is an off-token seam
// colour marked inline with `token-gap`.
type Localized = { en: string; pt: string };
interface Worker {
/** Used as the `data-worker` hook and the key in the source. */
id: string;
/** Short label rendered uppercase, e.g. "UI". */
label: string | Localized;
/** Body copy describing the worker's remit. */
strong: string | Localized;
/** Path label, e.g. "agent/ui". */
code: string;
}
interface Props {
orchestrator: {
eyebrow: string | Localized;
/** May contain inline `<br>`; rendered with `set:html`. */
title: string | Localized;
code: string;
};
workers: Worker[];
initial?: string;
}
const { orchestrator, workers, initial = workers[0]?.id } = Astro.props;
---
<div class="fleet-grid">
<article class="captain">
<span class="captain-eyebrow">
{
typeof orchestrator.eyebrow === 'string' ? (
orchestrator.eyebrow
) : (
<>
<>
<span data-language-content="en">{orchestrator.eyebrow.en}</span>
<span data-language-content="pt" hidden>
{orchestrator.eyebrow.pt}
</span>
</>
</>
)
}
</span>
{
typeof orchestrator.title === 'string' ? (
<h2 set:html={orchestrator.title} />
) : (
<>
<h2 data-language-content="en" set:html={orchestrator.title.en} />
<h2 data-language-content="pt" hidden set:html={orchestrator.title.pt} />
</>
)
}
<code>{orchestrator.code}</code>
</article>
<div class="arrow" aria-hidden="true"></div>
<div class="workers" role="group" aria-label="Worker agents">
{
workers.map((worker) => (
<button
class:list={['worker-card', { active: worker.id === initial }]}
data-worker={worker.id}
aria-pressed={worker.id === initial}
>
<span>
{typeof worker.label === 'string' ? (
worker.label
) : (
<>
<span data-language-content="en">{worker.label.en}</span>
<span data-language-content="pt" hidden>
{worker.label.pt}
</span>
</>
)}
</span>
<strong>
{typeof worker.strong === 'string' ? (
worker.strong
) : (
<>
<span data-language-content="en">{worker.strong.en}</span>
<span data-language-content="pt" hidden>
{worker.strong.pt}
</span>
</>
)}
</strong>
<code>{worker.code}</code>
</button>
))
}
</div>
</div>
<style>
.fleet-grid {
display: grid;
grid-template-columns: 0.8fr 50px 1.5fr;
gap: 24px;
margin-top: 30px;
color: var(--paper);
background: var(--deep);
}
.captain {
display: grid;
align-content: center;
gap: 16px;
padding: 32px;
}
.captain-eyebrow {
color: var(--gold);
/* token-gap: source uses 10px; no --step-* covers 10px; owner design-system-keeper */
font:
500 10px 'DM Mono',
monospace;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.captain h2 {
margin: 0;
/* token-gap: source uses clamp(24px,3vw,38px); --step-5 is clamp(36px,5vw,65px); owner design-system-keeper */
font-size: clamp(24px, 3vw, 38px);
line-height: 0.98;
letter-spacing: -0.06em;
}
.captain code {
color: var(--gold);
font:
500 var(--step-0) 'DM Mono',
monospace;
}
.arrow {
display: grid;
place-items: center;
color: var(--gold);
/* token-gap: source uses 30px; no --step-* covers 30px; owner design-system-keeper */
font-size: 30px;
}
.workers {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
/* token-gap: source seam colour is #41596b for the gap:1px hairline trick; no token matches; owner design-system-keeper */
background: #41596b;
}
.worker-card {
display: grid;
align-content: space-between;
gap: 20px;
min-height: 180px;
padding: 22px;
color: var(--paper);
background: var(--ink);
border: 0;
text-align: left;
cursor: pointer;
}
.worker-card span {
/* token-gap: source uses #9eabb4; no token matches; owner design-system-keeper */
color: #9eabb4;
/* token-gap: source uses 10px; no --step-* covers 10px; owner design-system-keeper */
font:
500 10px 'DM Mono',
monospace;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.worker-card strong {
/* token-gap: source uses 16px; no --step-* covers 16px; owner design-system-keeper */
font-size: 16px;
line-height: 1.2;
}
.worker-card code {
color: var(--gold);
font:
500 var(--step-0) 'DM Mono',
monospace;
}
.worker-card.active {
box-shadow: inset 4px 0 0 var(--gold);
}
@media (max-width: 800px) {
.fleet-grid {
grid-template-columns: 1fr;
}
.arrow {
transform: rotate(90deg);
min-height: 35px;
}
.workers {
grid-template-columns: 1fr;
}
}
</style>