Files
ai-for-dummies/src/components/blocks/WorktreeMap.astro
T
Marcos Paulo 6bfae2033e refactor(styles): resolve token gaps and expand typography scale
Extended the typography `--step-*` scale to cover the ad-hoc pixel values
used across components (10px to 48px). Added overlay tokens `--white-14`,
`--white-23`, `--white-25`, `--white-31`.

Canonicalized one-off legacy color hex values in `ReviewDetail`,
`ChangeLens`, `PreviewPane`, `RulesInteractive`, and `SkillPackageExplorer`
to map to the core semantic palette (`--deep`, `--ink`, `--line`, `--paper`,
`--muted`).

Replaced ad-hoc max-width media query boundaries (520px, 530px, 600px, 620px)
with the closest approved named tokens (`560px`, `800px`, `1100px`).
2026-09-05 22:53:56 +00:00

218 lines
5.4 KiB
TypeScript

---
// WorktreeMap — the repository-topology diagram with the SVG trunk-and-branches
// behind four `tree-node` buttons.
//
// Static shell: the SVG paths render server-side; the nodes are buttons with
// the `data-tree` hook asserted by `scripts/verify.mjs`. The `root` node and
// the `initial` branch are marked selected.
type Localized = { en: string; pt: string };
interface Branch {
/** The `data-tree` hook, e.g. "ui", "tests", "docs". */
id: string;
/** Uppercase label, e.g. "UI AGENT". */
label: string | Localized;
/** Strong line, e.g. "agent/ui". */
strong: string;
/** Status small, e.g. "3 files · working". */
small: string | Localized;
/** CSS modifier so each branch picks up its tone. */
tone: 'ui' | 'tests' | 'docs' | string;
}
interface Props {
branches: Branch[];
initial?: string;
rootLabel?: string | Localized;
rootSmall?: string | Localized;
}
const { branches, initial = 'main', rootLabel = 'ROOT', rootSmall = '● clean' } = Astro.props;
---
<div class="tree-stage" role="tree" aria-label="Repository worktree topology">
<svg viewBox="0 0 760 330" preserveAspectRatio="none" aria-hidden="true">
<path class="tree-edge trunk" d="M380 48 V118"></path>
<path class="tree-edge" d="M380 118 C380 170 110 150 110 224"></path>
<path class="tree-edge" d="M380 118 V224"></path>
<path class="tree-edge" d="M380 118 C380 170 650 150 650 224"></path>
</svg>
<button
class:list={['tree-node', 'root', { active: initial === 'main' }]}
data-tree="main"
role="treeitem"
aria-selected={initial === 'main'}
>
<span>
{
typeof rootLabel === 'string' ? (
rootLabel
) : (
<>
<span data-language-content="en">{rootLabel.en}</span>
<span data-language-content="pt" hidden>
{rootLabel.pt}
</span>
</>
)
}
</span>
<strong>main</strong>
<small>
{
typeof rootSmall === 'string' ? (
rootSmall
) : (
<>
<span data-language-content="en">{rootSmall.en}</span>
<span data-language-content="pt" hidden>
{rootSmall.pt}
</span>
</>
)
}
</small>
</button>
{
branches.map((branch) => (
<button
class:list={['tree-node', 'branch', branch.tone, { active: branch.id === initial }]}
data-tree={branch.id}
role="treeitem"
aria-selected={branch.id === initial}
>
<>
<span>
{typeof branch.label === 'string' ? (
branch.label
) : (
<>
<span data-language-content="en">{branch.label.en}</span>
<span data-language-content="pt" hidden>
{branch.label.pt}
</span>
</>
)}
</span>
<strong>{branch.strong}</strong>
<small>
{typeof branch.small === 'string' ? (
branch.small
) : (
<>
<span data-language-content="en">{branch.small.en}</span>
<span data-language-content="pt" hidden>
{branch.small.pt}
</span>
</>
)}
</small>
</>
</button>
))
}
</div>
<style>
.tree-stage {
position: relative;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
padding-top: 90px;
}
/* SVG trunk-and-branches behind the buttons. */
.tree-stage svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.tree-edge {
fill: none;
stroke: var(--gold);
stroke-width: 2;
}
/* Root sits at the top centre, spanning both columns. */
.tree-node.root {
grid-column: 1 / -1;
justify-self: center;
width: 180px;
}
.tree-node {
display: grid;
align-content: start;
gap: 12px;
min-height: 135px;
padding: 20px;
color: var(--paper);
background: var(--ink);
border: 1px solid var(--muted);
text-align: left;
cursor: pointer;
}
.tree-node.root {
background: var(--gold);
color: var(--ink);
}
.tree-node span {
color: var(--accent);
/* token-gap: source uses 9px; no --step-* covers 9px; owner design-system-keeper */
font:
500 9px 'DM Mono',
monospace;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.tree-node strong {
/* token-gap: source uses 14px with default weight; no --step-* covers 14px; owner design-system-keeper */
font:
14px 'DM Mono',
monospace;
}
.tree-node small {
margin-top: 0;
color: var(--muted);
font-size: var(--step-0);
}
.tree-node.root small {
color: var(--muted);
}
.tree-node.active {
box-shadow: inset 4px 0 0 var(--gold);
}
.tree-node:focus-visible {
outline: 3px solid var(--gold);
outline-offset: -3px;
}
/* Branch tone variants — left/right placement stays on the grid columns. */
.tree-node.branch.ui {
grid-column: 1;
}
.tree-node.branch.tests {
grid-column: 1 / -1;
justify-self: center;
}
.tree-node.branch.docs {
grid-column: 2;
}
@media (max-width: 560px) {
.tree-stage {
grid-template-columns: 1fr;
padding-top: 90px;
}
.tree-node.branch.ui,
.tree-node.branch.tests,
.tree-node.branch.docs {
grid-column: 1;
justify-self: stretch;
}
}
</style>