cac1115035
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.
204 lines
4.9 KiB
TypeScript
204 lines
4.9 KiB
TypeScript
---
|
|
// PhasePanel — the three-step "Click a phase / see the handoff" block.
|
|
//
|
|
// Static shell: the tab buttons and the panel markup are server-rendered.
|
|
// The `initial` phase's content is shown by default; the other tabs still
|
|
// carry the `data-phase` hook so the interactive island (task 15) can swap
|
|
// the panel on click.
|
|
//
|
|
// Every `data-phase` value is asserted by `scripts/verify.mjs`.
|
|
|
|
type Localized = { en: string; pt: string };
|
|
|
|
export type PhaseId = 'plan' | 'build' | 'review';
|
|
|
|
interface Phase {
|
|
id: PhaseId;
|
|
/** Two-letter label rendered in the tab, e.g. "PLAN". */
|
|
label: string | Localized;
|
|
/** Numeric prefix, e.g. "01". */
|
|
number: string;
|
|
title: string | Localized;
|
|
body: string | Localized;
|
|
/** Headline + small caption shown above the panel title. */
|
|
meta: { deliverable: string | Localized; gate: string | Localized };
|
|
/** Code-line evidence shown at the bottom of the panel. */
|
|
evidence: string;
|
|
}
|
|
|
|
interface Props {
|
|
phases: Phase[];
|
|
/** Initial active phase. Defaults to the first entry. */
|
|
initial?: PhaseId;
|
|
}
|
|
|
|
const { phases, initial = phases[0]?.id ?? 'plan' } = Astro.props;
|
|
const active = phases.find((phase) => phase.id === initial) ?? phases[0];
|
|
---
|
|
|
|
<div class="phase-tabs" role="tablist" aria-label="Workflow phases">
|
|
{
|
|
phases.map((phase) => (
|
|
<button
|
|
class:list={['phase-tab', { active: phase.id === initial }]}
|
|
data-phase={phase.id}
|
|
role="tab"
|
|
aria-selected={phase.id === initial}
|
|
>
|
|
<b>{phase.number}</b>{' '}
|
|
{typeof phase.label === 'string' ? (
|
|
phase.label
|
|
) : (
|
|
<>
|
|
<span data-language-content="en">{phase.label.en}</span>
|
|
<span data-language-content="pt" hidden>
|
|
{phase.label.pt}
|
|
</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
))
|
|
}
|
|
</div>
|
|
|
|
<article class="phase-panel" id="phase-panel" aria-live="polite">
|
|
<div class="phase-meta">
|
|
<span>
|
|
{
|
|
typeof active.meta.deliverable === 'string' ? (
|
|
active.meta.deliverable
|
|
) : (
|
|
<>
|
|
<>
|
|
<span data-language-content="en">{active.meta.deliverable.en}</span>
|
|
<span data-language-content="pt" hidden>
|
|
{active.meta.deliverable.pt}
|
|
</span>
|
|
</>
|
|
</>
|
|
)
|
|
}
|
|
</span>
|
|
<small>
|
|
{
|
|
typeof active.meta.gate === 'string' ? (
|
|
active.meta.gate
|
|
) : (
|
|
<>
|
|
<>
|
|
<span data-language-content="en">{active.meta.gate.en}</span>
|
|
<span data-language-content="pt" hidden>
|
|
{active.meta.gate.pt}
|
|
</span>
|
|
</>
|
|
</>
|
|
)
|
|
}
|
|
</small>
|
|
</div>
|
|
<h3>
|
|
{
|
|
typeof active.title === 'string' ? (
|
|
active.title
|
|
) : (
|
|
<>
|
|
<>
|
|
<span data-language-content="en">{active.title.en}</span>
|
|
<span data-language-content="pt" hidden>
|
|
{active.title.pt}
|
|
</span>
|
|
</>
|
|
</>
|
|
)
|
|
}
|
|
</h3>
|
|
<p>
|
|
{
|
|
typeof active.body === 'string' ? (
|
|
active.body
|
|
) : (
|
|
<>
|
|
<>
|
|
<span data-language-content="en">{active.body.en}</span>
|
|
<span data-language-content="pt" hidden>
|
|
{active.body.pt}
|
|
</span>
|
|
</>
|
|
</>
|
|
)
|
|
}
|
|
</p>
|
|
<code>{active.evidence}</code>
|
|
</article>
|
|
|
|
<style>
|
|
.phase-tabs {
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
|
|
.phase-tab {
|
|
display: grid;
|
|
grid-template-columns: 42px 1fr;
|
|
gap: 10px;
|
|
padding: 15px;
|
|
border: 1px solid var(--line);
|
|
color: var(--ink);
|
|
background: transparent;
|
|
/* token-gap: source uses 10px; no --step-* covers 10px; owner design-system-keeper */
|
|
font:
|
|
500 10px 'DM Mono',
|
|
monospace;
|
|
letter-spacing: 0.07em;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
}
|
|
.phase-tab b {
|
|
color: var(--accent);
|
|
}
|
|
.phase-tab.active {
|
|
color: var(--paper);
|
|
border-color: var(--ink);
|
|
background: var(--ink);
|
|
}
|
|
|
|
.phase-panel {
|
|
min-height: 260px;
|
|
padding: 30px;
|
|
color: var(--paper);
|
|
background: var(--accent);
|
|
}
|
|
.phase-meta {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 15px;
|
|
color: var(--gold);
|
|
/* token-gap: source uses 10px; no --step-* covers 10px; owner design-system-keeper */
|
|
font:
|
|
500 10px 'DM Mono',
|
|
monospace;
|
|
}
|
|
.phase-meta small {
|
|
color: var(--paper);
|
|
opacity: 0.7;
|
|
}
|
|
.phase-panel h3 {
|
|
margin: 44px 0 12px;
|
|
/* 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: 1.05;
|
|
}
|
|
.phase-panel p {
|
|
line-height: 1.6;
|
|
opacity: 0.82;
|
|
}
|
|
.phase-panel code {
|
|
display: block;
|
|
margin-top: 26px;
|
|
color: var(--gold);
|
|
font:
|
|
500 var(--step-0) 'DM Mono',
|
|
monospace;
|
|
}
|
|
</style>
|