{% extends "html/base.html" %}
{% block head_extra %}
<style>
.planning-hero {
position: relative;
display: grid;
min-width: 0;
grid-template-columns: minmax(0, 1.42fr) minmax(270px, 0.58fr);
gap: 46px;
align-items: end;
padding: 40px 0 58px;
border-bottom: 1px solid var(--line);
}
.planning-hero > *,
.mode-panel,
.signal-card,
.executive-section {
min-width: 0;
}
.planning-kicker {
display: inline-flex;
align-items: center;
min-height: 30px;
margin: 0 0 24px;
gap: 8px;
border: 1px solid color-mix(in srgb, var(--accent) 38%, var(--line));
border-radius: 5px;
background: var(--panel);
color: var(--muted);
padding: 6px 11px;
font-family: var(--font-mono);
font-size: 0.67rem;
font-weight: 650;
letter-spacing: 0.13em;
text-transform: uppercase;
}
.planning-kicker::before {
content: "";
width: 7px;
height: 7px;
flex: none;
border-radius: 50%;
background: var(--success);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--success) 16%, transparent);
}
.planning-title {
max-width: 22ch;
margin: 0 0 12px;
font-size: clamp(2.35rem, 4.5vw, 3.55rem);
line-height: 1.07;
letter-spacing: -0.045em;
overflow-wrap: break-word;
}
.planning-slug {
display: flex;
min-width: 0;
max-width: 76ch;
align-items: baseline;
gap: 9px;
margin: 0 0 20px;
color: var(--muted);
font-family: var(--font-mono);
font-size: .67rem;
}
.planning-slug span {
flex: none;
color: var(--accent-strong);
font-weight: 650;
letter-spacing: .08em;
text-transform: uppercase;
}
.planning-slug code {
min-width: 0;
overflow-wrap: anywhere;
color: inherit;
font-size: inherit;
}
.planning-lede {
max-width: 62ch;
margin: 0;
color: var(--ink-soft);
font-size: clamp(1rem, 1.6vw, 1.12rem);
line-height: 1.68;
}
.mode-switch {
display: grid;
width: min(100%, 620px);
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
margin: 30px 0 0;
border: 1px solid var(--line);
border-radius: 11px;
background: var(--surface-soft);
padding: 6px;
}
.mode-switch.is-delivery {
width: min(100%, 760px);
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.mode-switch a {
display: grid;
min-width: 0;
grid-template-columns: 36px minmax(0, 1fr);
align-items: center;
gap: 11px;
min-height: 62px;
border: 0;
border-radius: 7px;
color: var(--muted);
padding: 8px 12px;
text-decoration: none;
transition: background 180ms ease, color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
}
.mode-symbol {
position: relative;
width: 36px;
height: 36px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--panel);
color: var(--accent-strong);
}
.mode-symbol::before,
.mode-symbol::after {
content: "";
position: absolute;
}
.mode-symbol[data-symbol="overview"]::before {
inset: 8px;
background:
linear-gradient(currentColor 0 0) 0 0 / 7px 7px no-repeat,
linear-gradient(currentColor 0 0) 100% 0 / 7px 7px no-repeat,
linear-gradient(currentColor 0 0) 0 100% / 7px 7px no-repeat,
linear-gradient(currentColor 0 0) 100% 100% / 7px 7px no-repeat;
}
.mode-symbol[data-symbol="documents"]::before {
inset: 8px 10px;
border: 1.5px solid currentColor;
border-radius: 2px;
}
.mode-symbol[data-symbol="documents"]::after {
left: 14px;
right: 14px;
top: 14px;
height: 1.5px;
background: currentColor;
box-shadow: 0 5px 0 currentColor, 0 10px 0 currentColor;
}
.mode-symbol[data-symbol="timeline"]::before {
left: 10px;
right: 10px;
top: 17px;
height: 1.5px;
background: currentColor;
}
.mode-symbol[data-symbol="timeline"]::after {
left: 8px;
top: 14px;
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
box-shadow: 8px 0 0 currentColor, 16px 0 0 currentColor;
}
.mode-copy {
display: grid;
min-width: 0;
gap: 2px;
}
.mode-copy strong {
font-size: 0.82rem;
font-weight: 680;
}
.mode-copy span {
overflow: hidden;
color: inherit;
font-size: 0.67rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.mode-switch a:first-child,
.mode-switch a[aria-selected="true"],
body:has(#modo-executivo:target) .mode-switch a[href="#modo-executivo"],
body:has(#modo-lifecycle:target) .mode-switch a[href="#modo-lifecycle"],
body:has(#modo-documentos:target) .mode-switch a[href="#modo-documentos"] {
background: var(--accent);
color: var(--on-accent);
box-shadow: 0 8px 24px color-mix(in srgb, var(--accent) 24%, transparent);
}
body:has(#modo-documentos:target) .mode-switch a[href="#modo-executivo"] {
background: transparent;
color: var(--muted);
box-shadow: none;
}
body:has(#modo-lifecycle:target) .mode-switch a[href="#modo-executivo"] {
background: transparent;
color: var(--muted);
box-shadow: none;
}
.mode-switch a:hover,
.mode-switch a:focus-visible {
color: var(--accent-strong);
transform: translateY(-1px);
}
.mode-switch a[aria-selected="true"] .mode-symbol {
border-color: color-mix(in srgb, var(--on-accent) 18%, transparent);
background: color-mix(in srgb, var(--panel) 42%, transparent);
color: var(--on-accent);
}
.freshness {
display: grid;
gap: 9px;
margin: 0;
border: 0;
border-radius: 0;
background: transparent;
padding: 18px 0;
color: var(--muted);
font-size: 0.78rem;
line-height: 1.5;
}
.freshness-status {
display: flex;
align-items: center;
gap: 9px;
margin-bottom: 2px;
color: var(--success-strong);
font-size: 0.86rem;
font-weight: 650;
}
.freshness-status::before {
content: "";
width: 8px;
height: 8px;
flex: none;
border-radius: 50%;
background: var(--success);
box-shadow: 0 0 0 5px color-mix(in srgb, var(--success) 15%, transparent);
}
.freshness code { font-size: 0.74rem; }
.fingerprint-code {
display: block;
max-height: 3.5em;
margin-top: 2px;
overflow: hidden;
overflow-wrap: anywhere;
}
.mode-panel {
scroll-margin-top: 82px;
padding: 86px 0 12px;
}
.mode-panel:focus-visible {
outline: none;
}
.mode-panel:focus-visible > .section-heading {
outline: 2px solid var(--focus);
outline-offset: 8px;
border-radius: 3px;
}
.section-heading {
display: grid;
grid-template-columns: 64px minmax(0, 1fr);
margin-bottom: 42px;
}
.section-number {
padding-top: 7px;
color: var(--faint);
font-family: var(--font-mono);
font-size: 0.76rem;
font-weight: 600;
letter-spacing: 0.06em;
}
.section-eyebrow {
display: block;
margin-bottom: 9px;
color: var(--accent-strong);
font-size: 0.68rem;
font-weight: 650;
letter-spacing: 0.13em;
text-transform: uppercase;
}
.section-heading h2 {
max-width: 19ch;
margin: 0 0 11px;
border: 0;
padding: 0;
font-size: clamp(1.75rem, 3.3vw, 2.35rem);
}
.section-heading p {
max-width: 68ch;
margin: 0;
color: var(--ink-soft);
}
.readiness-rail {
margin: 0 0 48px;
}
.readiness-label {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 13px;
color: var(--muted);
font-family: var(--font-mono);
font-size: 0.64rem;
font-weight: 600;
letter-spacing: 0.13em;
text-transform: uppercase;
}
.readiness-label::after {
content: "";
height: 1px;
flex: 1;
background: var(--line);
}
.readiness-track {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
}
.readiness-stage {
position: relative;
min-width: 0;
min-height: 118px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 9px;
background: var(--panel);
padding: 18px 16px 14px;
transition: transform 170ms ease, border-color 170ms ease, box-shadow 170ms ease;
}
.readiness-stage::before {
content: "";
position: absolute;
inset: 0 0 auto;
height: 3px;
background: var(--accent);
}
.readiness-stage[data-state="approved"]::before,
.readiness-stage[data-state="recorded"]::before,
.readiness-stage[data-state="current"]::before {
background: var(--success);
}
.readiness-stage[data-state="skipped"]::before,
.readiness-stage[data-state="optional"]::before {
background: var(--warning-signal);
}
.readiness-stage:hover {
transform: translateY(-3px);
border-color: color-mix(in srgb, var(--accent) 48%, var(--line));
box-shadow: var(--shadow);
}
.readiness-stage-label {
display: block;
margin-bottom: 7px;
color: var(--ink);
font-size: 0.9rem;
font-weight: 650;
}
.readiness-stage-file {
display: block;
overflow: hidden;
color: var(--muted);
font-family: var(--font-mono);
font-size: 0.66rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.readiness-stage-state {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 19px;
color: var(--accent-strong);
font-family: var(--font-mono);
font-size: 0.62rem;
font-weight: 650;
letter-spacing: 0.09em;
text-transform: uppercase;
}
.readiness-stage-state::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.readiness-stage[data-state="approved"] .readiness-stage-state,
.readiness-stage[data-state="recorded"] .readiness-stage-state,
.readiness-stage[data-state="current"] .readiness-stage-state {
color: var(--success-strong);
}
.readiness-stage[data-state="skipped"] .readiness-stage-state,
.readiness-stage[data-state="optional"] .readiness-stage-state {
color: var(--warn);
}
.readiness-progress {
position: relative;
height: 2px;
margin: 12px 3px 0;
overflow: hidden;
border-radius: 2px;
background: var(--line);
}
.readiness-progress::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg, var(--accent), var(--success));
}
.signal-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin: 0 0 66px;
padding-top: 22px;
border-top: 1px solid var(--line);
}
.signal-card {
position: relative;
min-height: 154px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 9px;
background: color-mix(in srgb, var(--panel) 88%, transparent);
padding: 19px;
box-shadow: 0 10px 28px rgba(30, 20, 10, 0.035);
transition: border-color 180ms ease, transform 180ms ease, box-shadow 180ms ease;
}
.signal-card::before {
content: "";
position: absolute;
inset: 0 0 auto;
height: 3px;
background: var(--accent);
transform: scaleX(.26);
transform-origin: left;
transition: transform 320ms cubic-bezier(.2, .75, .2, 1);
}
.signal-card:hover {
border-color: color-mix(in srgb, var(--accent) 52%, var(--line));
box-shadow: 0 16px 36px rgba(30, 20, 10, 0.065);
transform: translateY(-2px);
}
.signal-card:hover::before {
transform: scaleX(1);
}
.signal-card .label {
display: block;
margin-top: 11px;
color: var(--ink);
font-size: 0.72rem;
font-weight: 650;
letter-spacing: .04em;
text-transform: uppercase;
}
.signal-card .muted {
margin: 6px 0 0;
font-size: 0.72rem;
line-height: 1.52;
}
.signal-value {
display: block;
overflow-wrap: anywhere;
color: var(--accent-strong);
font-family: var(--font-mono);
font-size: clamp(1.15rem, 2.1vw, 1.55rem);
font-weight: 650;
letter-spacing: -0.025em;
line-height: 1.1;
}
.signal-card:first-child .signal-value { font-family: var(--font-body); }
.signal-card[data-tone="warn"] .signal-value { color: var(--warn); }
.signal-card[data-tone="risk"] .signal-value { color: var(--risk); }
.signal-card[data-tone="ok"] .signal-value { color: var(--success-strong); }
.signal-card[data-tone="ok"]::before { background: var(--success); }
.executive-grid {
display: grid;
min-width: 0;
grid-template-columns: minmax(0, 1fr);
margin: 0 0 62px;
border-top: 1px solid var(--line);
}
.executive-section {
display: grid;
grid-template-columns: 64px minmax(0, 1fr);
margin: 0;
border-bottom: 1px solid var(--line);
background: transparent;
padding: 27px 0 29px;
}
.executive-section > div { min-width: 0; max-width: 100%; }
.rail-index {
color: var(--accent-strong);
font-family: var(--font-mono);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.06em;
}
.executive-section[data-tone="warn"] .rail-index { color: var(--warn); }
.executive-section[data-tone="ok"] .rail-index { color: var(--success-strong); }
.executive-section h3 {
margin: 0 0 3px;
font-size: 1.04rem;
}
.executive-section-source {
display: block;
margin-bottom: 13px;
font-size: 0.72rem;
}
.executive-section p:last-child,
.executive-section ul:last-child { margin-bottom: 0; }
.executive-section h3 + p,
.executive-section h3 + ul { margin-top: 9px; }
.provenance {
margin: 0;
border: 0;
border-radius: 0;
background: transparent;
padding: 12px 0 0;
}
.provenance h3 { margin: 0 0 5px; }
.provenance-lede { margin-bottom: 18px; }
.source-table-wrap {
max-width: 100%;
overflow-x: auto;
border: 1px solid var(--line);
border-radius: var(--radius-table);
background: var(--panel);
scrollbar-width: thin;
}
.source-table {
min-width: 640px;
margin: 0;
font-size: 0.82rem;
}
.source-table code { overflow-wrap: anywhere; }
.documents-stack {
display: grid;
min-width: 0;
grid-template-columns: minmax(0, 1fr);
border-top: 1px solid var(--line);
}
.doc-section {
scroll-margin-top: 144px;
min-width: 0;
max-width: 100%;
margin: 0;
border: 0;
border-bottom: 1px solid var(--line);
border-radius: 0;
background: transparent;
padding: 38px 0 48px;
}
.doc-section[aria-disabled="true"] {
margin-top: 22px;
border: 1px dashed var(--line);
border-radius: 9px;
background: var(--surface-soft);
padding: 24px;
}
.doc-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
margin-bottom: 26px;
padding-bottom: 16px;
border-bottom: 1px solid var(--line);
}
.doc-heading h3 {
margin: 0;
font-size: 1.36rem;
}
.doc-heading .muted {
max-width: 62ch;
margin: 0;
text-align: right;
font-size: 0.73rem;
}
.doc-content h1 { margin-top: 0; font-size: clamp(1.75rem, 3.2vw, 2.25rem); }
.doc-content { min-width: 0; max-width: 100%; }
.doc-content h2 { margin-top: 50px; }
.doc-content h1[id],
.doc-content h2[id],
.doc-content h3[id],
.doc-content h4[id] { scroll-margin-top: 144px; }
.doc-content table { min-width: 100%; }
.artifact-console {
position: sticky;
top: 60px;
z-index: 16;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
height: 60px;
margin: -18px 0 34px;
overflow: hidden;
border: 0;
border-bottom: 1px solid var(--line);
border-radius: 0;
background: color-mix(in srgb, var(--bg) 96%, var(--panel));
padding: 0;
backdrop-filter: blur(18px) saturate(1.25);
}
.artifact-console-head {
display: grid;
align-content: center;
align-self: stretch;
gap: 2px;
min-width: 112px;
border-right: 1px solid var(--line);
padding: 0 13px 0 3px;
}
.artifact-console-label {
color: var(--muted);
font-family: var(--font-mono);
font-size: .66rem;
font-weight: 650;
letter-spacing: .12em;
text-transform: uppercase;
}
.artifact-counter {
color: var(--accent-strong);
font-family: var(--font-mono);
font-size: .7rem;
}
.artifact-tabs {
display: flex;
min-width: 0;
height: 60px;
gap: 2px;
overflow-x: auto;
padding: 4px;
scroll-behavior: smooth;
scroll-padding-inline: 40%;
scroll-snap-type: x proximity;
scrollbar-width: thin;
}
.artifact-tab {
position: relative;
display: grid;
min-width: 0;
min-height: 52px;
flex: 0 0 128px;
grid-template-columns: 24px minmax(0, 1fr);
align-items: center;
gap: 10px;
overflow: hidden;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--muted);
padding: 5px 7px;
scroll-snap-align: center;
text-align: left;
cursor: pointer;
transition: border-color 180ms ease, background 180ms ease, color 180ms ease, transform 180ms ease;
}
.artifact-tab::after {
content: "";
position: absolute;
inset: auto 7px 2px;
height: 2px;
border-radius: 999px;
background: var(--accent);
transform: scaleX(0);
transform-origin: left;
transition: transform 220ms ease;
}
.artifact-tab:hover {
background: var(--panel);
color: var(--ink);
transform: translateY(-1px);
}
.artifact-tab[aria-selected="true"] {
background: var(--accent-soft);
color: var(--ink);
}
.artifact-tab[aria-selected="true"]::after { transform: scaleX(1); }
.artifact-tab-index {
position: relative;
display: grid;
width: 24px;
height: 24px;
place-items: center;
border: 1px solid var(--line);
border-radius: 5px;
background: var(--panel);
color: var(--accent-strong);
font-family: var(--font-mono);
font-size: .66rem;
font-weight: 650;
transition: border-color 220ms ease, color 220ms ease, transform 260ms var(--motion-curve);
}
.artifact-tab[aria-selected="true"] .artifact-tab-index {
border-color: var(--accent);
color: var(--accent-strong);
transform: translateY(-1px) scale(1.04);
}
.artifact-tab.is-centering .artifact-tab-index {
animation: artifact-index-focus 520ms var(--motion-curve) both;
}
@keyframes artifact-index-focus {
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 0%, transparent); }
42% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent) 22%, transparent); }
100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 0%, transparent); }
}
.artifact-tab-copy {
display: grid;
min-width: 0;
gap: 2px;
}
.artifact-tab-copy strong {
overflow: hidden;
font-size: .7rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.artifact-tab-copy span {
overflow: hidden;
font-family: var(--font-mono);
font-size: .55rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.artifact-tab-state {
color: var(--success-strong);
text-transform: uppercase;
}
.doc-section[hidden],
.mode-panel[hidden] { display: none; }
.doc-section.is-entering,
.mode-panel.is-entering {
animation: panel-reveal 360ms cubic-bezier(.2, .72, .2, 1) both;
}
.lifecycle-track {
--lifecycle-progress: 0%;
position: relative;
display: grid;
gap: 0;
width: min(100%, 1080px);
margin: 0 auto 52px;
isolation: isolate;
}
.lifecycle-track::before,
.lifecycle-track::after {
content: "";
position: absolute;
z-index: -1;
left: 71px;
top: 18px;
width: 2px;
border-radius: 999px;
}
.lifecycle-track::before {
bottom: 18px;
background: var(--line);
}
.lifecycle-track::after {
height: var(--lifecycle-progress);
max-height: calc(100% - 36px);
background: linear-gradient(180deg, var(--accent), var(--success));
transition: height 720ms var(--motion-curve);
}
.lifecycle-step {
position: relative;
display: grid;
grid-template-columns: 44px 24px minmax(0, 1fr) 104px;
gap: 16px;
align-items: center;
min-height: 88px;
padding: 10px 0;
opacity: 0;
transform: translateY(10px);
}
.lifecycle-index {
justify-self: end;
color: var(--muted);
font-family: var(--font-mono);
font-size: .68rem;
}
.lifecycle-node {
position: relative;
z-index: 1;
width: 14px;
height: 14px;
justify-self: center;
border: 4px solid var(--bg);
border-radius: 50%;
background: var(--line);
box-shadow: 0 0 0 1px var(--line);
}
.lifecycle-step[data-tone="ok"] .lifecycle-node {
background: var(--success);
box-shadow: 0 0 0 1px var(--success);
}
.lifecycle-step[data-tone="warn"] .lifecycle-node {
background: var(--warning-signal);
box-shadow: 0 0 0 1px var(--warning-signal);
}
.lifecycle-step.is-current .lifecycle-node {
animation: lifecycle-pulse 1.8s ease-out infinite;
}
.lifecycle-copy {
display: grid;
gap: 5px;
min-width: 0;
padding: 8px 0;
}
.lifecycle-copy strong { font-size: .94rem; }
.lifecycle-copy span { color: var(--muted); font-size: .76rem; }
.lifecycle-path {
display: inline-flex;
width: fit-content;
max-width: 100%;
overflow: hidden;
border-radius: 4px;
background: var(--surface-soft);
color: var(--muted);
padding: 4px 7px;
font-family: var(--font-mono);
font-size: .67rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.lifecycle-state {
min-width: 92px;
justify-self: end;
text-align: center;
border-radius: 999px;
background: var(--surface-soft);
color: var(--muted);
padding: 5px 9px;
font-family: var(--font-mono);
font-size: .61rem;
letter-spacing: .06em;
text-transform: uppercase;
}
.lifecycle-step[data-tone="ok"] .lifecycle-state {
background: color-mix(in srgb, var(--success) 12%, transparent);
color: var(--success-strong);
}
.lifecycle-track.is-ready .lifecycle-step {
animation: lifecycle-step-enter 480ms var(--motion-curve) both;
animation-delay: calc(var(--step-order, 0) * 46ms);
}
@keyframes lifecycle-step-enter {
to { opacity: 1; transform: translateY(0); }
}
@keyframes lifecycle-pulse {
0%, 100% { box-shadow: 0 0 0 1px var(--success), 0 0 0 0 color-mix(in srgb, var(--success) 0%, transparent); }
45% { box-shadow: 0 0 0 1px var(--success), 0 0 0 8px color-mix(in srgb, var(--success) 18%, transparent); }
}
@keyframes panel-reveal {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.artifact-visual {
min-width: 0;
max-width: 100%;
margin: 0 0 38px;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.artifact-visual-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
border-bottom: 1px solid var(--line);
padding: 19px 0 17px;
}
.artifact-visual-kicker {
display: block;
margin-bottom: 5px;
color: var(--accent-strong);
font-family: var(--font-mono);
font-size: .64rem;
font-weight: 650;
letter-spacing: .11em;
text-transform: uppercase;
}
.artifact-visual-title {
margin: 0;
font-size: 1.02rem;
}
.artifact-visual-title[id] { scroll-margin-top: 250px; }
.artifact-visual-copy {
max-width: 56ch;
margin: 4px 0 0;
color: var(--muted);
font-size: .73rem;
}
.visual-controls {
display: flex;
flex: none;
flex-wrap: wrap;
justify-content: flex-end;
gap: 5px;
}
.visual-control {
min-height: 36px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--panel);
color: var(--muted);
padding: 7px 11px;
font-family: var(--font-mono);
font-size: .65rem;
cursor: pointer;
transition: border-color 160ms ease, color 160ms ease, background 160ms ease;
}
.visual-control:hover,
.visual-control[aria-pressed="true"] {
border-color: var(--accent);
background: var(--accent-soft);
color: var(--accent-strong);
}
.visual-scale {
display: inline-grid;
min-width: 48px;
min-height: 36px;
place-items: center;
color: var(--muted);
font-family: var(--font-mono);
font-size: .62rem;
}
.network-viewport {
position: relative;
min-width: 0;
max-width: 100%;
height: 390px;
min-height: 390px;
overflow: auto;
background-color: color-mix(in srgb, var(--panel) 76%, var(--bg));
background-image: radial-gradient(circle, color-mix(in srgb, var(--ink) 16%, transparent) 1px, transparent 1.25px);
background-position: 0 0;
background-size: 22px 22px;
cursor: grab;
overscroll-behavior: contain;
scrollbar-width: thin;
touch-action: none;
user-select: none;
}
.network-viewport.is-panning {
cursor: grabbing;
}
.network-plane {
position: absolute;
inset: 0 auto auto 0;
transform-origin: 0 0;
transition: transform 180ms cubic-bezier(.2, .72, .2, 1);
}
.network-surface {
position: relative;
min-width: 100%;
min-height: 390px;
}
.network-links {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
pointer-events: none;
}
.network-edge {
fill: none;
stroke: color-mix(in srgb, var(--ink-soft) 42%, var(--line));
stroke-width: 1.25;
stroke-linejoin: round;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
transition: opacity 180ms ease, stroke 180ms ease, stroke-width 180ms ease;
}
.network-edge[data-route="external"] {
stroke: color-mix(in srgb, var(--ink-soft) 28%, var(--line));
stroke-dasharray: 5 5;
}
.network-edge[data-route="loop"] { stroke-dasharray: 4 4; }
.network-edge-flow {
fill: none;
stroke: var(--accent);
stroke-width: 2.4;
stroke-linecap: round;
stroke-dasharray: 2 18;
opacity: .82;
vector-effect: non-scaling-stroke;
pointer-events: none;
animation: data-flow 1.8s linear infinite;
}
.network-edge-flow.is-muted { opacity: .08; }
.network-edge-flow.is-active {
stroke: var(--success-strong);
opacity: 1;
}
.network-surface[data-flow="paused"] .network-edge-flow {
animation-play-state: paused;
opacity: .16;
}
@keyframes data-flow {
to { stroke-dashoffset: -40; }
}
.network-edge.is-muted { opacity: .14; }
.network-edge.is-active {
stroke: var(--success-strong);
stroke-width: 2.8;
}
.network-node {
position: absolute;
width: 188px;
min-height: 82px;
border: 1px solid var(--line);
border-radius: 6px;
background: color-mix(in srgb, var(--panel) 98%, transparent);
color: var(--ink);
padding: 13px 14px;
text-align: left;
box-shadow: none;
cursor: pointer;
transition: border-color 180ms ease, background 180ms ease, opacity 180ms ease, transform 180ms ease;
}
.network-node-port {
position: absolute;
top: 50%;
width: 8px;
height: 8px;
border: 2px solid var(--panel);
border-radius: 50%;
background: var(--ink-soft);
transform: translateY(-50%);
pointer-events: none;
}
.network-node-port.is-input { left: -5px; }
.network-node-port.is-output { right: -5px; }
.network-surface[data-direction="td"] .network-node-port {
top: auto;
left: 50%;
right: auto;
transform: translateX(-50%);
}
.network-surface[data-direction="td"] .network-node-port.is-input { top: -5px; }
.network-surface[data-direction="td"] .network-node-port.is-output { bottom: -5px; }
.network-node[data-kind="terminal"] {
border-radius: 999px;
padding-inline: 24px;
text-align: center;
}
.network-node[data-kind="decision"] {
border: 0;
background: transparent;
padding: 24px 30px;
text-align: center;
}
.network-node[data-kind="decision"]::before,
.network-node[data-kind="decision"]::after {
content: "";
position: absolute;
inset: 0;
z-index: -2;
clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}
.network-node[data-kind="decision"]::before { background: var(--accent); }
.network-node[data-kind="decision"]::after {
inset: 1px;
z-index: -1;
background: var(--panel);
}
.network-node[data-kind="decision"] .network-node-port.is-input { left: 8px; }
.network-node[data-kind="decision"] .network-node-port.is-output { right: 8px; }
.network-node[data-kind="decision"]:focus-visible { outline: none; }
.network-node[data-kind="decision"]:focus-visible::before { background: var(--focus); }
.network-node[data-kind="subroutine"] {
border-inline: 4px double var(--line);
}
.network-node[data-kind="state-start"],
.network-node[data-kind="state-end"] {
display: grid;
width: 72px !important;
min-height: 72px !important;
place-content: center;
border-radius: 50%;
padding: 10px;
text-align: center;
}
.network-node[data-kind="state-start"] {
border-color: var(--ink);
background: var(--ink);
color: var(--panel);
}
.network-node[data-kind="state-start"] .network-node-id,
.network-node[data-kind="state-end"] .network-node-id { display: none; }
.network-node[data-kind="state-start"] .network-node-title { color: var(--panel); }
.network-node[data-kind="state-end"] {
border: 4px double var(--ink-soft);
}
.network-node:hover,
.network-node:focus-visible {
border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 8%, var(--panel));
color: var(--ink);
transform: translateY(-2px);
}
.network-node:hover .network-node-title,
.network-node:focus-visible .network-node-title,
.network-node.is-selected .network-node-title,
.network-node.is-related .network-node-title { color: var(--ink); }
.network-node:hover .network-node-id,
.network-node:focus-visible .network-node-id,
.network-node.is-selected .network-node-id { color: var(--accent-strong); }
.network-node[data-kind="state-start"]:hover,
.network-node[data-kind="state-start"]:focus-visible,
.network-node[data-kind="state-start"].is-selected {
border-color: var(--accent);
background: var(--brand-charcoal);
color: var(--brand-peach);
}
.network-node[data-kind="state-start"]:hover .network-node-title,
.network-node[data-kind="state-start"]:focus-visible .network-node-title,
.network-node[data-kind="state-start"].is-selected .network-node-title {
color: var(--brand-peach);
}
.network-node.is-selected {
border-color: var(--success-strong);
background: color-mix(in srgb, var(--success) 9%, var(--panel));
}
.network-node.is-complete {
border-color: var(--success);
background: color-mix(in srgb, var(--success) 10%, var(--panel));
}
.network-node.is-complete .network-node-id { color: var(--success-strong); }
.network-node-status {
display: inline-flex;
width: fit-content;
align-items: center;
gap: 6px;
margin-top: 10px;
color: var(--muted);
font-family: var(--font-mono);
font-size: .58rem;
}
.network-node-status::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--line);
}
.network-node.is-complete .network-node-status {
color: var(--success-strong);
}
.network-node.is-complete .network-node-status::before {
background: var(--success);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--success) 16%, transparent);
}
.network-edge.is-complete {
stroke: var(--success-strong);
stroke-width: 2.4;
}
.network-edge-flow.is-complete {
stroke: var(--success);
opacity: 1;
}
.network-edge-label-box {
display: grid;
width: 100%;
min-height: 26px;
place-items: center;
border-radius: 4px;
background: color-mix(in srgb, var(--panel) 88%, transparent);
color: var(--muted);
padding: 3px 6px;
font-family: var(--font-mono);
font-size: 10px;
line-height: 1.25;
overflow-wrap: anywhere;
text-align: center;
pointer-events: none;
}
.network-node.is-related { border-color: var(--accent); }
.network-node.is-muted { opacity: .28; }
.network-node-id {
display: block;
margin-bottom: 7px;
color: var(--accent-strong);
font-family: var(--font-mono);
font-size: .63rem;
font-weight: 650;
letter-spacing: .06em;
}
.network-node-title {
display: block;
font-size: .78rem;
font-weight: 680;
line-height: 1.3;
overflow-wrap: anywhere;
word-break: break-word;
}
.network-node-meta {
display: grid;
gap: 4px;
margin: 10px 0 0;
padding: 0;
list-style: none;
}
.network-node-meta li {
display: flex;
justify-content: space-between;
gap: 12px;
margin: 0;
border-top: 1px solid var(--line-soft);
padding: 4px 0 0;
color: var(--muted);
font-family: var(--font-mono);
font-size: .58rem;
}
.network-node[data-kind="store"] {
background: color-mix(in srgb, var(--ink) 4%, var(--panel));
}
.network-node[data-kind="store"] .network-node-id {
margin: -13px -14px 9px;
border-bottom: 1px solid var(--line);
background: color-mix(in srgb, var(--accent) 9%, var(--panel));
padding: 8px 14px;
}
.network-surface[data-variant="branching"] .network-edge-flow {
stroke: var(--accent-strong);
}
.network-surface[data-variant="dependencies"] .network-node {
min-height: 96px;
}
.network-surface[data-variant="architecture"] .network-node {
border-color: color-mix(in srgb, var(--brand-slate) 26%, var(--line));
background: color-mix(in srgb, var(--panel) 94%, var(--brand-peach));
}
.network-surface[data-variant="architecture"] .network-node[data-kind="store"] {
background: color-mix(in srgb, var(--brand-slate) 6%, var(--panel));
}
.network-surface[data-variant="architecture"] .network-edge-flow {
stroke-width: 2.8;
}
.network-surface[data-variant="state-machine"] .network-node:not([data-kind^="state-"]) {
border-radius: 8px;
}
.network-surface.is-ready .network-node {
animation: network-node-enter 420ms var(--motion-curve) both;
animation-delay: calc(var(--node-order, 0) * 45ms);
}
@keyframes network-node-enter {
from { opacity: 0; transform: translateY(10px) scale(.985); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.network-inspector {
min-height: 54px;
border-top: 1px solid var(--line);
background: color-mix(in srgb, var(--surface-soft) 72%, transparent);
padding: 13px 20px;
color: var(--muted);
font-size: .72rem;
}
.network-inspector strong { color: var(--accent-strong); }
.diagram-fullscreen-dialog {
width: 100vw;
max-width: none;
height: 100dvh;
max-height: none;
margin: 0;
border: 0;
background: var(--bg);
color: var(--ink);
padding: 0;
}
.diagram-fullscreen-dialog::backdrop {
background: color-mix(in srgb, var(--brand-charcoal) 82%, transparent);
backdrop-filter: blur(8px);
}
.diagram-fullscreen-shell {
display: grid;
height: 100dvh;
grid-template-rows: 58px minmax(0, 1fr);
}
.diagram-fullscreen-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
border-bottom: 1px solid var(--line);
background: color-mix(in srgb, var(--panel) 94%, transparent);
padding: 0 20px;
}
.diagram-fullscreen-head strong {
font-family: var(--font-mono);
font-size: .69rem;
letter-spacing: .05em;
}
.diagram-fullscreen-body {
min-width: 0;
min-height: 0;
overflow: hidden;
padding: 18px;
}
.diagram-fullscreen-body > .artifact-visual,
.diagram-fullscreen-body > .mermaid-wrap,
.diagram-fullscreen-body > .excalidraw-embed {
display: grid;
height: 100%;
min-height: 0;
grid-template-rows: auto minmax(0, 1fr) auto;
margin: 0;
}
.diagram-fullscreen-body .artifact-visual-head,
.diagram-fullscreen-body .diagram-toolbar,
.diagram-fullscreen-body .excalidraw-toolbar {
padding-top: 0;
}
.diagram-fullscreen-body .network-viewport,
.diagram-fullscreen-body .diagram-viewport,
.diagram-fullscreen-body .sequence-viewport,
.diagram-fullscreen-body .excalidraw-viewport {
width: 100%;
height: 100%;
min-height: 0;
}
.diagram-fullscreen-body .mermaid-diagram {
height: 100%;
min-height: 0;
}
body:has(.diagram-fullscreen-dialog[open]) { overflow: hidden; }
.mermaid-wrap {
min-width: 0;
max-width: 100%;
margin: 26px 0 34px;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.mermaid-wrap[data-diagram-variant="branching"] .diagram-toolbar strong::after {
content: " · decisões";
color: var(--muted);
font-family: var(--font-mono);
font-size: .62rem;
font-weight: 500;
}
.mermaid-wrap[data-diagram-variant="data-flow"] .diagram-toolbar strong::after {
content: " · dados";
color: var(--muted);
font-family: var(--font-mono);
font-size: .62rem;
font-weight: 500;
}
.mermaid-wrap[data-diagram-variant="architecture"] .diagram-toolbar strong::after {
content: " · arquitetura";
color: var(--muted);
font-family: var(--font-mono);
font-size: .62rem;
font-weight: 500;
}
.mermaid-wrap[data-diagram-variant="dependency-tree"] .diagram-toolbar strong::after {
content: " · dependências";
color: var(--muted);
font-family: var(--font-mono);
font-size: .62rem;
font-weight: 500;
}
.companion-asset {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 14px;
align-items: center;
margin: 22px 0 32px;
border-bottom: 1px solid var(--line);
padding: 16px 0;
}
.companion-asset-badge {
border-radius: 4px;
background: var(--accent-soft);
color: var(--accent-strong);
padding: 6px 8px;
font-family: var(--font-mono);
font-size: .61rem;
font-weight: 650;
letter-spacing: .08em;
text-transform: uppercase;
}
.companion-asset-copy {
display: grid;
min-width: 0;
gap: 3px;
}
.companion-asset-copy strong { font-size: .82rem; }
.companion-asset-copy code {
overflow: hidden;
color: var(--muted);
font-size: .65rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.companion-asset-link {
min-height: 36px;
border-radius: 5px;
background: var(--ink);
color: var(--bg);
padding: 8px 11px;
font-family: var(--font-mono);
font-size: .65rem;
text-decoration: none;
}
.excalidraw-embed {
min-width: 0;
margin: 26px 0 38px;
}
.excalidraw-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
border-bottom: 1px solid var(--line);
padding: 12px 0;
}
.excalidraw-toolbar-copy {
display: grid;
gap: 3px;
min-width: 0;
}
.excalidraw-toolbar-copy strong { font-size: .78rem; }
.excalidraw-toolbar-copy code {
overflow: hidden;
color: var(--muted);
font-size: .61rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.excalidraw-actions {
display: flex;
flex: 0 0 auto;
gap: 5px;
}
.excalidraw-viewport {
position: relative;
height: 390px;
min-height: 390px;
overflow: hidden;
background-color: color-mix(in srgb, var(--panel) 76%, var(--bg));
background-image: radial-gradient(circle, color-mix(in srgb, var(--ink) 16%, transparent) 1px, transparent 1.25px);
background-size: 22px 22px;
cursor: grab;
touch-action: none;
}
.excalidraw-viewport.is-panning { cursor: grabbing; }
.excalidraw-svg {
width: 100%;
height: 100%;
overflow: visible;
transform-origin: center;
transition: transform 220ms var(--motion-curve);
}
.excalidraw-element {
vector-effect: non-scaling-stroke;
transition: opacity 180ms ease, filter 180ms ease;
}
.excalidraw-element:hover {
filter: drop-shadow(0 5px 12px color-mix(in srgb, var(--ink) 12%, transparent));
}
.excalidraw-arrow-flow {
fill: none;
stroke: var(--accent);
stroke-width: 3;
stroke-linecap: round;
stroke-dasharray: 3 16;
opacity: .9;
vector-effect: non-scaling-stroke;
animation: data-flow 1.7s linear infinite;
}
.excalidraw-embed[data-flow="paused"] .excalidraw-arrow-flow {
animation-play-state: paused;
opacity: .16;
}
.excalidraw-label {
fill: var(--ink);
font-family: var(--font-sans);
font-size: 18px;
font-weight: 650;
text-anchor: middle;
dominant-baseline: middle;
pointer-events: none;
}
.excalidraw-caption {
border-top: 1px solid var(--line);
color: var(--muted);
padding: 12px 0 0;
font-size: .69rem;
}
.mermaid-wrap.is-enhanced .mermaid-diagram { display: block; }
.mermaid-wrap.is-enhanced .mermaid-fallback { display: none; margin: 0; border-radius: 0 0 11px 11px; }
.mermaid-wrap.is-code .mermaid-diagram { display: none; }
.mermaid-wrap.is-code .mermaid-fallback { display: block; }
.diagram-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
border-bottom: 1px solid var(--line);
padding: 13px 0;
}
.diagram-toolbar strong {
font-size: .76rem;
letter-spacing: .02em;
}
.diagram-toolbar-actions { display: flex; gap: 5px; }
.diagram-viewport { height: 350px; min-height: 350px; }
.diagram-viewport .network-surface { min-height: 350px; }
.diagram-viewport .network-node {
width: 164px;
min-height: 68px;
}
.sequence-viewport {
position: relative;
height: 390px;
min-height: 390px;
overflow: auto;
background: color-mix(in srgb, var(--panel) 92%, var(--bg));
cursor: grab;
overscroll-behavior: contain;
scrollbar-width: thin;
touch-action: none;
user-select: none;
}
.sequence-viewport.is-panning { cursor: grabbing; }
.sequence-surface {
position: relative;
min-width: 100%;
min-height: 390px;
transform-origin: 0 0;
transition: transform var(--motion-fast) var(--motion-curve);
}
.sequence-actor {
position: absolute;
top: 24px;
display: grid;
width: 144px;
min-height: 58px;
place-content: center;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--panel);
padding: 8px 12px;
color: var(--ink);
text-align: center;
}
.sequence-actor span:first-child {
color: var(--accent-strong);
font-family: var(--font-mono);
font-size: .56rem;
letter-spacing: .08em;
text-transform: uppercase;
}
.sequence-actor strong { margin-top: 3px; font-size: .72rem; }
.sequence-lifeline {
stroke: color-mix(in srgb, var(--ink) 22%, transparent);
stroke-width: 1;
stroke-dasharray: 4 4;
vector-effect: non-scaling-stroke;
}
.sequence-message {
fill: none;
stroke: var(--ink-soft);
stroke-width: 1.25;
vector-effect: non-scaling-stroke;
animation: sequence-message-enter 520ms var(--motion-curve) both;
animation-delay: calc(var(--message-order, 0) * 70ms);
}
.sequence-message.is-return { stroke-dasharray: 6 5; }
.sequence-message.is-primary { stroke: var(--accent-strong); stroke-width: 1.8; }
.sequence-message-flow {
fill: none;
stroke: var(--accent);
stroke-width: 2.2;
stroke-linecap: round;
stroke-dasharray: 2 16;
opacity: .8;
vector-effect: non-scaling-stroke;
animation: data-flow 1.7s linear infinite;
}
.sequence-surface[data-flow="paused"] .sequence-message-flow {
animation-play-state: paused;
opacity: .12;
}
.sequence-message-label {
fill: var(--ink-soft);
stroke: var(--panel);
stroke-width: 7px;
paint-order: stroke;
font-family: var(--font-mono);
font-size: 10px;
text-anchor: middle;
}
@keyframes sequence-message-enter {
from { opacity: 0; stroke-dasharray: 1 24; }
to { opacity: 1; }
}
.api-reference {
--api-nav-width: 224px;
display: grid;
min-width: 0;
grid-template-columns: var(--api-nav-width) 8px minmax(0, 1fr);
column-gap: 10px;
row-gap: 0;
margin: 22px 0 54px;
background:
linear-gradient(90deg, color-mix(in srgb, var(--surface-soft) 76%, transparent) 0 var(--api-nav-width), transparent var(--api-nav-width));
padding: 20px 0 0;
}
.api-reference-nav {
position: sticky;
top: 220px;
align-self: start;
padding: 0 10px 0 0;
}
.api-reference-nav-label {
display: block;
margin: 0 0 10px;
color: var(--muted);
font-family: var(--font-mono);
font-size: .62rem;
font-weight: 650;
letter-spacing: .12em;
text-transform: uppercase;
}
.api-reference-search {
width: 100%;
min-height: 38px;
margin: 0 0 12px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--panel);
color: var(--ink);
padding: 8px 10px;
font: 500 .66rem/1.2 var(--font-mono);
}
.api-reference-search::placeholder { color: var(--muted); }
.api-reference-list {
display: grid;
gap: 3px;
}
.api-reference-item {
display: grid;
grid-template-columns: minmax(0, 1fr) 32px;
align-items: center;
border-radius: 5px;
}
.api-reference-item:has(.api-reference-link[aria-current="true"]) {
background: var(--accent-soft);
}
.api-reference-link {
display: grid;
grid-template-columns: 42px minmax(0, 1fr);
gap: 8px;
align-items: center;
min-height: 42px;
border: 0;
border-radius: 5px;
background: transparent;
color: var(--muted);
padding: 6px 8px;
text-align: left;
cursor: pointer;
transition: background 160ms ease, color 160ms ease;
}
.api-reference-item[hidden] { display: none; }
.api-reference-link:hover,
.api-reference-link[aria-current="true"] {
background: color-mix(in srgb, var(--accent) 10%, transparent);
color: var(--ink);
}
.api-reference-link:hover .api-path,
.api-reference-link[aria-current="true"] .api-path {
color: var(--ink);
}
.api-copy-button {
display: inline-grid;
min-width: 30px;
min-height: 30px;
place-items: center;
border: 0;
border-radius: 4px;
background: transparent;
color: var(--muted);
font-family: var(--font-mono);
font-size: .61rem;
cursor: pointer;
transition: background 160ms ease, color 160ms ease, transform 160ms ease;
}
.api-copy-button:hover,
.api-copy-button:focus-visible {
background: var(--panel);
color: var(--accent-strong);
transform: translateY(-1px);
}
.api-copy-button[data-copied="true"] {
color: var(--success-strong);
}
.api-reference-resizer {
position: relative;
min-height: 100%;
cursor: col-resize;
touch-action: none;
}
.api-reference-resizer::before {
content: "";
position: absolute;
inset: 0 auto 0 50%;
width: 1px;
background: var(--line);
transform: translateX(-50%);
transition: width 160ms ease, background 160ms ease;
}
.api-reference-resizer:hover::before,
.api-reference-resizer:focus-visible::before,
.api-reference.is-resizing .api-reference-resizer::before {
width: 3px;
background: var(--accent);
}
.api-method {
color: var(--success-strong);
font-family: var(--font-mono);
font-size: .61rem;
font-weight: 700;
}
.api-method[data-method="POST"],
.api-method[data-method="PUT"],
.api-method[data-method="PATCH"] { color: var(--accent-strong); }
.api-method[data-method="DELETE"] { color: var(--risk); }
.api-path {
min-width: 0;
overflow: visible;
overflow-wrap: anywhere;
font-family: var(--font-mono);
font-size: .64rem;
line-height: 1.35;
text-overflow: clip;
white-space: normal;
}
.api-reference-main {
min-width: 0;
padding: 0 4px 0 8px;
}
.api-operation {
scroll-margin-top: 220px;
border: 0;
border-bottom: 1px solid var(--line);
padding: 0 0 46px;
}
.api-operation + .api-operation { padding-top: 46px; }
.api-operation-head {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 18px;
}
.api-operation-head .api-method {
display: inline-grid;
min-width: 46px;
min-height: 26px;
place-items: center;
border-radius: 4px;
background: color-mix(in srgb, var(--success) 10%, transparent);
}
.api-operation-head .api-path {
min-width: 0;
color: var(--ink);
font-size: .9rem;
font-weight: 650;
white-space: normal;
}
.api-operation-head .api-copy-button {
min-width: auto;
margin-left: auto;
padding: 6px 9px;
}
.api-operation-body {
display: grid;
min-width: 0;
grid-template-columns: minmax(0, 1fr);
gap: 26px;
align-items: start;
}
.api-operation-body.is-single { grid-template-columns: minmax(0, 1fr); }
.api-operation-description,
.api-operation-sample { min-width: 0; }
.api-column-label {
display: block;
margin-bottom: 14px;
color: var(--muted);
font-family: var(--font-mono);
font-size: .58rem;
font-weight: 700;
letter-spacing: .11em;
text-transform: uppercase;
}
.api-operation-description h4,
.api-operation-description strong {
color: var(--ink);
}
.api-operation-description > p,
.api-operation-description > ul {
max-width: 72ch;
}
.api-operation-sample {
position: relative;
top: auto;
width: min(100%, 780px);
margin-top: 4px;
}
.api-operation-sample:empty { display: none; }
.api-operation pre {
max-height: 420px;
margin-top: 16px;
}
.api-operation .code-block { margin-top: 16px; }
.api-operation-sample .code-block {
margin-top: 0;
box-shadow: 0 14px 34px rgba(28, 30, 36, .1);
}
.api-operation .code-block pre { margin-top: 0; }
.api-operation.is-active .api-operation-head .api-path {
color: var(--accent-strong);
}
@media (prefers-reduced-motion: no-preference) {
.network-surface.is-ready .network-edge {
animation: route-reveal 620ms cubic-bezier(.2, .72, .2, 1) both;
}
@keyframes route-reveal {
from { opacity: 0; stroke-dasharray: 1 14; }
to { opacity: 1; stroke-dasharray: 1000 0; }
}
}
.blocked-link {
border-bottom: 1px dotted var(--risk);
color: var(--risk);
cursor: not-allowed;
font-weight: 650;
}
@media (max-width: 820px) {
.planning-hero {
grid-template-columns: 1fr;
gap: 34px;
padding-top: 30px;
}
.freshness { max-width: 620px; }
.api-reference { grid-template-columns: 1fr; gap: 18px; }
.api-reference {
background: transparent;
padding-left: 0;
}
.api-reference-resizer { display: none; }
.api-reference-nav {
position: relative;
top: auto;
min-width: 0;
max-width: 100%;
overflow: hidden;
}
.api-reference-list {
display: flex;
width: 100%;
min-width: 0;
overflow-x: auto;
overscroll-behavior-x: contain;
scrollbar-width: thin;
}
.api-reference-item { flex: 0 0 220px; }
.api-operation-body { grid-template-columns: minmax(0, 1fr); }
.api-operation-sample { position: relative; top: auto; }
.readiness-track { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.signal-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
.planning-title { font-size: clamp(2.3rem, 12vw, 3.1rem); }
.planning-hero { padding-bottom: 50px; }
.mode-switch { width: 100%; grid-template-columns: 1fr; }
.mode-switch.is-delivery { grid-template-columns: 1fr; }
.mode-switch a { padding-inline: 10px; }
.mode-panel { padding-top: 68px; }
.readiness-track { grid-template-columns: 1fr; }
.readiness-stage { min-height: 104px; }
.section-heading,
.executive-section { grid-template-columns: minmax(0, 1fr); gap: 9px; }
.section-number, .rail-index { padding: 0; }
.signal-grid { gap: 22px 18px; margin-bottom: 52px; }
.signal-card .muted { font-size: 0.72rem; }
.provenance { padding: 8px 0 0; }
.doc-heading { display: grid; }
.doc-heading .muted { text-align: left; }
.artifact-console { margin-top: -4px; }
.artifact-console-head {
min-width: 72px;
align-items: flex-start;
padding-inline: 6px;
}
.artifact-console-label { display: none; }
.artifact-tabs {
display: flex;
margin-inline: -1px;
overflow-x: auto;
padding-bottom: 3px;
scroll-snap-type: x mandatory;
scrollbar-width: thin;
}
.artifact-tab {
min-width: 210px;
scroll-snap-align: center;
}
.artifact-visual-head {
display: grid;
padding: 17px;
}
.visual-controls { width: 100%; }
.visual-controls { flex-wrap: wrap; }
.visual-control { flex: 1; min-width: 72px; min-height: 44px; }
.network-viewport { height: min(64vh, 520px); min-height: 430px; }
.diagram-viewport { height: min(60vh, 480px); min-height: 390px; }
.sequence-viewport { height: min(60vh, 480px); min-height: 390px; }
.network-surface { min-height: 430px; }
.network-node { width: 236px; }
.diagram-toolbar { display: grid; align-items: flex-start; }
.diagram-toolbar-actions {
width: 100%;
max-width: 100%;
flex-wrap: wrap;
}
.lifecycle-step {
grid-template-columns: 32px 18px minmax(0, 1fr);
gap: 8px;
min-height: 104px;
}
.lifecycle-track::before,
.lifecycle-track::after { left: 48px; }
.lifecycle-state { grid-column: 3; justify-self: start; }
.companion-asset {
grid-template-columns: auto minmax(0, 1fr);
}
.companion-asset-link { grid-column: 1 / -1; text-align: center; }
.excalidraw-toolbar { align-items: flex-start; }
.excalidraw-actions { flex-wrap: wrap; justify-content: flex-end; }
.excalidraw-viewport { height: 330px; min-height: 330px; }
}
@media (prefers-reduced-motion: reduce) {
.artifact-tabs { scroll-behavior: auto; }
.artifact-tab.is-centering .artifact-tab-index,
.lifecycle-step.is-current .lifecycle-node,
.lifecycle-track.is-ready .lifecycle-step,
.excalidraw-arrow-flow,
.sequence-message,
.sequence-message-flow,
.network-edge-flow { animation: none; }
.lifecycle-step { opacity: 1; transform: none; }
}
@media print {
.planning-hero { display: block; padding: 24px 0 32px; }
.planning-hero-main { margin-bottom: 20px; }
.mode-switch { display: none; }
.mode-panel,
.mode-panel[hidden] { display: block !important; break-before: auto; padding: 24px 0 0; margin: 0; }
.artifact-console { display: none; }
.doc-section,
.doc-section[hidden] { display: block !important; }
.artifact-visual { break-inside: avoid; box-shadow: none; }
.network-viewport { overflow: visible; }
.network-edge-flow { display: none; }
.sequence-message-flow { display: none; }
.visual-controls,
.diagram-toolbar-actions { display: none; }
.api-reference { display: block; }
.api-reference-nav,
.api-reference-resizer { display: none; }
.excalidraw-actions { display: none; }
.code-copy-button { display: none; }
.diagram-fullscreen-dialog { display: none; }
.section-heading { margin-bottom: 24px; }
.readiness-track { grid-template-columns: repeat(4, 1fr); }
.readiness-stage { min-height: 0; padding: 12px; break-inside: avoid; }
.readiness-stage-state { margin-top: 10px; }
.readiness-progress { display: none; }
.signal-grid { grid-template-columns: repeat(4, 1fr); margin-bottom: 34px; }
.signal-value { font-size: 1.28rem; }
.executive-grid { display: block; margin-bottom: 34px; }
.executive-section { break-inside: avoid; }
.provenance, .doc-section { break-inside: avoid; }
.documents-stack { display: block; }
.doc-section { padding: 24px 0; }
}
</style>
{% endblock %}
{% block content %}
<section class="planning-hero" aria-labelledby="planning-title">
<div class="planning-hero-main">
<p class="planning-kicker">{% if is_delivery %}Registro vivo da entrega{% else %}Artefato derivado pré-Execution{% endif %}</p>
<h1 class="planning-title" id="planning-title">{{ title }}</h1>
<p class="planning-slug"><span>Slug</span><code>{{ orchestration_slug }}</code></p>
<p class="planning-lede">{{ generated_summary }}</p>
<nav class="mode-switch{% if is_delivery %} is-delivery{% endif %}" aria-label="Modos de leitura" role="tablist">
<a href="#modo-executivo" id="tab-modo-executivo" role="tab" data-mode-link="modo-executivo" aria-controls="modo-executivo" aria-selected="true">
<span class="mode-symbol" data-symbol="overview" aria-hidden="true"></span>
<span class="mode-copy">
<strong>Visão executiva</strong>
<span>Decisão, riscos e prontidão</span>
</span>
</a>
{% if is_delivery %}
<a href="#modo-lifecycle" id="tab-modo-lifecycle" role="tab" data-mode-link="modo-lifecycle" aria-controls="modo-lifecycle" aria-selected="false">
<span class="mode-symbol" data-symbol="timeline" aria-hidden="true"></span>
<span class="mode-copy">
<strong>Linha do tempo</strong>
<span>Da descoberta à memória</span>
</span>
</a>
{% endif %}
<a href="#modo-documentos" id="tab-modo-documentos" role="tab" data-mode-link="modo-documentos" aria-controls="modo-documentos" aria-selected="false">
<span class="mode-symbol" data-symbol="documents" aria-hidden="true"></span>
<span class="mode-copy">
<strong>Documentos</strong>
<span>{% if is_delivery %}Todos os artefatos do ciclo{% else %}Idea, ADRs, PRD, Tech Spec, Tasks e Refinement{% endif %}</span>
</span>
</a>
</nav>
</div>
<aside class="freshness" aria-label="Proveniência e freshness">
<strong class="freshness-status">{% if is_delivery %}Entrega consolidada{% else %}Planejamento atual{% endif %}</strong>
<span>{{ freshness_summary }}</span>
<span>Gerador: {{ compiler_name }} v{{ generator_version }}</span>
<span>Gerado em <time datetime="{{ generated_at }}">{{ generated_at }}</time></span>
<span>Fingerprint</span>
<code class="fingerprint-code">{{ fingerprint }}</code>
</aside>
</section>
<section class="mode-panel" id="modo-executivo" tabindex="-1" role="tabpanel" aria-labelledby="tab-modo-executivo modo-executivo-title">
<header class="section-heading">
<span class="section-number">01</span>
<div>
<span class="section-eyebrow">Checkpoint</span>
<h2 id="modo-executivo-title">Visão executiva</h2>
<p>Sinais essenciais para decidir se o planejamento está pronto para execução.</p>
</div>
</header>
<section class="readiness-rail" aria-labelledby="readiness-rail-title">
<h3 class="readiness-label" id="readiness-rail-title">Pipeline de fontes</h3>
<div class="readiness-track">
{% for source in source_rows %}
<article class="readiness-stage" data-state="{{ source.state }}">
<strong class="readiness-stage-label">{{ source.label }}</strong>
<span class="readiness-stage-file">{{ source.path }}</span>
<span class="readiness-stage-state">{{ source.state_label }}</span>
</article>
{% endfor %}
</div>
<div class="readiness-progress" aria-hidden="true"></div>
</section>
<div class="signal-grid">
{% for signal in executive_signals %}
<article class="card signal-card" data-tone="{{ signal.tone }}">
<span class="signal-value">{{ signal.value }}</span>
<span class="label">{{ signal.label }}</span>
<p class="muted">{{ signal.description }}</p>
</article>
{% endfor %}
</div>
<div class="executive-grid" aria-label="Compilado executivo do planejamento">
{% for section in executive_sections %}
<article class="executive-section" data-tone="{{ section.tone }}">
<span class="rail-index">{{ section.index }}</span>
<div>
<h3>{{ section.label }}</h3>
<span class="muted executive-section-source">Fonte · {{ section.source }}</span>
{{ section.content_html|safe }}
</div>
</article>
{% endfor %}
</div>
<section class="provenance" aria-labelledby="proveniencia-title">
<h3 id="proveniencia-title">Proveniência</h3>
<p class="muted provenance-lede">Cada fonte abaixo participa do fingerprint atual.</p>
<div class="source-table-wrap" tabindex="0" role="region" aria-label="Tabela de proveniência rolável horizontalmente">
<table class="source-table">
<thead>
<tr>
<th>Etapa</th>
<th>Arquivo</th>
<th>Estado</th>
<th>Revisão</th>
<th>SHA-256</th>
</tr>
</thead>
<tbody>
{% for source in source_rows %}
<tr>
<td>{{ source.label }}</td>
<td><code>{{ source.path }}</code></td>
<td>{{ source.state_label }}</td>
<td>{{ source.revision }}</td>
<td><code>{{ source.sha256_short }}</code></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
</section>
{% if is_delivery %}
<section class="mode-panel" id="modo-lifecycle" tabindex="-1" role="tabpanel" aria-labelledby="tab-modo-lifecycle modo-lifecycle-title">
<header class="section-heading">
<span class="section-number">02</span>
<div>
<span class="section-eyebrow">Ciclo vivo</span>
<h2 id="modo-lifecycle-title">Linha do tempo</h2>
<p>O compilado cresce a cada artefato canônico salvo. Etapas pendentes permanecem visÃveis sem simular conclusão.</p>
</div>
</header>
<div class="lifecycle-track" aria-label="Ciclo de vida da entrega">
{% for step in lifecycle_steps %}
<article class="lifecycle-step" data-tone="{{ step.tone }}" style="--step-order: {{ loop.index0 }}">
<span class="lifecycle-index">{{ step.index }}</span>
<span class="lifecycle-node" aria-hidden="true"></span>
<span class="lifecycle-copy">
<strong>{{ step.label }}</strong>
<span>{{ step.summary }}</span>
<code class="lifecycle-path">{{ step.path }}</code>
</span>
<span class="lifecycle-state">{{ step.state_label }}</span>
</article>
{% endfor %}
</div>
</section>
{% endif %}
<section class="mode-panel" id="modo-documentos" tabindex="-1" role="tabpanel" aria-labelledby="tab-modo-documentos modo-documentos-title">
<header class="section-heading">
<span class="section-number">{% if is_delivery %}03{% else %}02{% endif %}</span>
<div>
<span class="section-eyebrow">Fonte canônica</span>
<h2 id="modo-documentos-title">Documentos</h2>
<p>Conteúdo derivado dos Markdown canônicos. O Markdown e o traceability-map continuam sendo a fonte de verdade.</p>
</div>
</header>
<nav class="artifact-console" aria-label="Navegação entre artefatos">
<div class="artifact-console-head">
<span class="artifact-console-label">{% if is_delivery %}Artefatos do ciclo{% else %}Artefatos do planejamento{% endif %}</span>
<span class="artifact-counter" data-artifact-counter aria-live="polite">01 / {{ sections.len() }}</span>
</div>
<div class="artifact-tabs" role="tablist" aria-label="Artefatos canônicos">
{% for section in sections %}
<button
class="artifact-tab"
id="artifact-tab-{{ section.stage }}"
type="button"
role="tab"
data-artifact-target="stage-{{ section.stage }}"
aria-controls="stage-{{ section.stage }}"
aria-selected="{% if loop.first %}true{% else %}false{% endif %}"
>
<span class="artifact-tab-index" aria-hidden="true">0{{ loop.index }}</span>
<span class="artifact-tab-copy">
<strong>{{ section.label }}</strong>
<span class="artifact-tab-state">{% if section.required %}canônico{% else %}{% if is_delivery %}pendente{% else %}dispensado{% endif %}{% endif %}</span>
</span>
</button>
{% endfor %}
</div>
</nav>
<div class="documents-stack">
{% for section in sections %}
<article
class="doc-section"
id="stage-{{ section.stage }}"
role="tabpanel"
data-artifact-panel
data-stage="{{ section.stage }}"
aria-labelledby="artifact-tab-{{ section.stage }}"
{% if !section.required %}aria-disabled="true"{% endif %}
>
<header class="doc-heading">
<h3>{{ section.label }}</h3>
<p class="muted">{{ section.summary }}</p>
</header>
<div class="doc-content">
{{ section.content_html|safe }}
</div>
</article>
{% endfor %}
</div>
</section>
<script>
(() => {
"use strict";
const q = (selector, root = document) => root.querySelector(selector);
const qa = (selector, root = document) => Array.from(root.querySelectorAll(selector));
const modeLinks = qa("[data-mode-link]");
const modePanels = qa(".mode-panel");
const artifactTabs = qa("[data-artifact-target]");
const artifactPanels = qa("[data-artifact-panel]");
const artifactCounter = q("[data-artifact-counter]");
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
function reveal(element) {
if (!element || reducedMotion.matches) return;
element.classList.remove("is-entering");
requestAnimationFrame(() => element.classList.add("is-entering"));
}
function centerArtifactTab(tab) {
if (!tab) return;
const scroller = tab.closest(".artifact-tabs");
if (!scroller) return;
const target = tab.offsetLeft + tab.offsetWidth / 2 - scroller.clientWidth / 2;
const maximum = Math.max(0, scroller.scrollWidth - scroller.clientWidth);
scroller.scrollTo({
left: Math.min(maximum, Math.max(0, target)),
behavior: reducedMotion.matches ? "auto" : "smooth"
});
if (!reducedMotion.matches) {
tab.classList.remove("is-centering");
requestAnimationFrame(() => {
tab.classList.add("is-centering");
window.setTimeout(() => tab.classList.remove("is-centering"), 560);
});
}
}
function updateToc(hash) {
qa(".toc-list a").forEach((link) => {
if (link.getAttribute("href") === `#${hash}`) {
link.setAttribute("aria-current", "location");
} else {
link.removeAttribute("aria-current");
}
});
}
function activateMode(id, options = {}) {
const panel = document.getElementById(id);
if (!panel) return;
modePanels.forEach((item) => {
const selected = item === panel;
item.hidden = !selected;
item.setAttribute("aria-hidden", String(!selected));
});
modeLinks.forEach((link) => {
link.setAttribute("aria-selected", String(link.dataset.modeLink === id));
link.tabIndex = link.dataset.modeLink === id ? 0 : -1;
});
reveal(panel);
requestAnimationFrame(() => window.dispatchEvent(new Event("planning:layout")));
if (id === "modo-documentos") {
requestAnimationFrame(() => centerArtifactTab(q('.artifact-tab[aria-selected="true"]')));
}
if (options.history !== false) history.replaceState(null, "", `#${id}`);
updateToc(id);
if (options.focus) panel.focus({ preventScroll: true });
if (options.scroll) panel.scrollIntoView({ behavior: reducedMotion.matches ? "auto" : "smooth", block: "start" });
}
function activateArtifact(id, options = {}) {
const panel = document.getElementById(id);
if (!panel) return;
activateMode("modo-documentos", { history: false });
artifactPanels.forEach((item) => {
const selected = item === panel;
item.hidden = !selected;
item.setAttribute("aria-hidden", String(!selected));
});
let activeTab = null;
artifactTabs.forEach((tab, index) => {
const selected = tab.dataset.artifactTarget === id;
tab.setAttribute("aria-selected", String(selected));
tab.tabIndex = selected ? 0 : -1;
if (selected) activeTab = tab;
if (selected && artifactCounter) {
artifactCounter.textContent = `${String(index + 1).padStart(2, "0")} / ${String(artifactTabs.length).padStart(2, "0")}`;
}
});
requestAnimationFrame(() => centerArtifactTab(activeTab));
reveal(panel);
requestAnimationFrame(() => window.dispatchEvent(new Event("planning:layout")));
if (options.history !== false) history.replaceState(null, "", `#${id}`);
updateToc(id);
if (options.focus) panel.focus({ preventScroll: true });
if (options.scroll) panel.scrollIntoView({ behavior: reducedMotion.matches ? "auto" : "smooth", block: "start" });
}
function wireTabs(tabs, activation) {
tabs.forEach((tab, index) => {
tab.addEventListener("keydown", (event) => {
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
event.preventDefault();
let next = index;
if (event.key === "ArrowRight") next = (index + 1) % tabs.length;
if (event.key === "ArrowLeft") next = (index - 1 + tabs.length) % tabs.length;
if (event.key === "Home") next = 0;
if (event.key === "End") next = tabs.length - 1;
tabs[next].focus();
activation(tabs[next]);
});
});
}
modeLinks.forEach((link) => {
link.addEventListener("click", (event) => {
event.preventDefault();
activateMode(link.dataset.modeLink, { history: true, focus: false, scroll: true });
});
});
artifactTabs.forEach((tab) => {
tab.addEventListener("click", () => activateArtifact(tab.dataset.artifactTarget, { history: true, focus: false }));
});
wireTabs(modeLinks, (link) => activateMode(link.dataset.modeLink, { history: true }));
wireTabs(artifactTabs, (tab) => activateArtifact(tab.dataset.artifactTarget, { history: true }));
function enhanceLifecycle() {
const track = q(".lifecycle-track");
if (!track) return;
const steps = qa(".lifecycle-step", track);
const completed = steps
.map((step, index) => ({ step, index }))
.filter(({ step }) => step.dataset.tone === "ok");
const current = completed[completed.length - 1];
if (current) current.step.classList.add("is-current");
const denominator = Math.max(1, steps.length - 1);
const progress = current ? (current.index / denominator) * 100 : 0;
track.style.setProperty("--lifecycle-progress", `${progress}%`);
requestAnimationFrame(() => track.classList.add("is-ready"));
}
enhanceLifecycle();
qa(".toc-list a[href^='#']").forEach((link) => {
link.addEventListener("click", (event) => {
const id = link.getAttribute("href").slice(1);
if (id.startsWith("stage-")) {
event.preventDefault();
activateArtifact(id, { history: true, focus: true, scroll: true });
} else if (id.startsWith("modo-")) {
event.preventDefault();
activateMode(id, { history: true, focus: true, scroll: true });
}
});
});
function createElement(tag, className, text) {
const element = document.createElement(tag);
if (className) element.className = className;
if (text !== undefined) element.textContent = text;
return element;
}
async function copyText(value, button, copiedLabel = "Copiado") {
let copied = false;
try {
await navigator.clipboard.writeText(value);
copied = true;
} catch (_) {
const fallback = document.createElement("textarea");
fallback.value = value;
fallback.setAttribute("readonly", "");
fallback.style.position = "fixed";
fallback.style.opacity = "0";
document.body.append(fallback);
fallback.select();
copied = document.execCommand("copy");
fallback.remove();
}
if (!copied) return false;
const original = button.dataset.label || button.textContent;
button.dataset.label = original;
button.dataset.copied = "true";
button.textContent = copiedLabel;
window.setTimeout(() => {
button.dataset.copied = "false";
button.textContent = original;
}, 1400);
return true;
}
function normalizedText(value) {
return value
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/`/g, "")
.trim()
.toLowerCase();
}
function installFullscreenControl(container, controls, label, onLayout) {
const openButton = createElement("button", "visual-control", "Tela cheia");
openButton.type = "button";
openButton.setAttribute("aria-label", `Abrir ${label} em tela cheia`);
openButton.addEventListener("click", () => {
const placeholder = document.createComment(`sdd-fullscreen:${label}`);
container.before(placeholder);
const dialog = createElement("dialog", "diagram-fullscreen-dialog");
dialog.setAttribute("aria-label", `${label} em tela cheia`);
const shell = createElement("div", "diagram-fullscreen-shell");
const head = createElement("header", "diagram-fullscreen-head");
head.append(createElement("strong", "", label));
const closeButton = createElement("button", "visual-control", "Fechar");
closeButton.type = "button";
closeButton.setAttribute("aria-label", `Fechar ${label} em tela cheia`);
head.append(closeButton);
const body = createElement("div", "diagram-fullscreen-body");
body.append(container);
shell.append(head, body);
dialog.append(shell);
document.body.append(dialog);
container.classList.add("is-fullscreen-visual");
let restored = false;
const restore = () => {
if (restored) return;
restored = true;
placeholder.replaceWith(container);
container.classList.remove("is-fullscreen-visual");
dialog.remove();
requestAnimationFrame(() => {
window.dispatchEvent(new Event("planning:layout"));
onLayout?.();
openButton.focus({ preventScroll: true });
});
};
closeButton.addEventListener("click", () => dialog.close());
dialog.addEventListener("close", restore, { once: true });
dialog.addEventListener("cancel", (event) => {
event.preventDefault();
dialog.close();
});
dialog.showModal();
requestAnimationFrame(() => requestAnimationFrame(() => {
window.dispatchEvent(new Event("planning:layout"));
onLayout?.();
closeButton.focus({ preventScroll: true });
}));
});
controls.append(openButton);
return openButton;
}
function scopeDocumentHeadings() {
artifactPanels.forEach((panel) => {
const stage = panel.dataset.stage;
const seen = new Map();
qa(".doc-content h1[id], .doc-content h2[id], .doc-content h3[id], .doc-content h4[id]", panel)
.forEach((heading) => {
const base = `stage-${stage}-${heading.id}`;
const occurrence = (seen.get(base) || 0) + 1;
seen.set(base, occurrence);
heading.id = occurrence === 1 ? base : `${base}-${occurrence}`;
});
});
}
function dependencyClosure(start, edges) {
const related = new Set([start]);
edges.forEach((edge) => {
if (edge.from === start) related.add(edge.to);
if (edge.to === start) related.add(edge.from);
});
return related;
}
function renderNetwork(viewport, nodes, edges, direction = "LR", inspector = null, options = {}) {
const surface = createElement("div", "network-surface");
const plane = createElement("div", "network-plane");
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const graphId = `network-${Math.random().toString(36).slice(2, 9)}`;
svg.classList.add("network-links");
svg.setAttribute("aria-hidden", "true");
plane.append(svg);
surface.append(plane);
surface.dataset.flow = "active";
surface.dataset.variant = options.variant || "pipeline";
viewport.replaceChildren(surface);
const nodeElements = new Map();
nodes.forEach((node, index) => {
const button = createElement("button", "network-node");
button.type = "button";
button.dataset.nodeId = node.id;
button.dataset.kind = node.kind || "step";
button.style.setProperty("--node-order", index);
button.setAttribute("aria-label", `${node.id}: ${node.label}`);
button.append(
createElement("span", "network-node-port is-input"),
createElement("span", "network-node-id", node.tag || node.id),
createElement("span", "network-node-title", node.label)
);
if (node.meta?.length) {
const meta = createElement("ul", "network-node-meta");
node.meta.forEach((field) => {
const item = createElement("li");
if (typeof field === "string") {
item.append(createElement("span", "", field));
} else {
item.append(
createElement("span", "", field.name),
createElement("span", "", field.type || "")
);
}
meta.append(item);
});
button.append(meta);
}
if (options.trackCompletion) {
button.append(createElement("span", "network-node-status", "Em acompanhamento"));
} else if (node.status) {
button.append(createElement("span", "network-node-status", node.status));
}
button.append(createElement("span", "network-node-port is-output"));
plane.append(button);
nodeElements.set(node.id, button);
});
let graphDirection = direction;
let scale = options.initialScale || 1;
let baseWidth = 0;
let baseHeight = 0;
let resizeFrame = 0;
function levelsForGraph() {
const order = new Map(nodes.map((node, index) => [node.id, index]));
const levels = new Map(nodes.map((node) => [node.id, 0]));
const indegree = new Map(nodes.map((node) => [node.id, 0]));
const outgoing = new Map(nodes.map((node) => [node.id, []]));
const layoutEdges = edges.filter((edge) => {
if (edge.from === edge.to || !order.has(edge.from) || !order.has(edge.to)) return false;
if (options.variant === "state-machine") {
return order.get(edge.to) > order.get(edge.from);
}
return true;
});
layoutEdges.forEach((edge) => {
outgoing.get(edge.from).push(edge);
indegree.set(edge.to, (indegree.get(edge.to) || 0) + 1);
});
const queue = nodes
.filter((node) => indegree.get(node.id) === 0)
.sort((a, b) => order.get(a.id) - order.get(b.id));
const visited = new Set();
while (queue.length) {
const current = queue.shift();
visited.add(current.id);
outgoing.get(current.id).forEach((edge) => {
levels.set(edge.to, Math.max(levels.get(edge.to) || 0, (levels.get(current.id) || 0) + 1));
indegree.set(edge.to, indegree.get(edge.to) - 1);
if (indegree.get(edge.to) === 0) {
queue.push(nodes.find((node) => node.id === edge.to));
queue.sort((a, b) => order.get(a.id) - order.get(b.id));
}
});
}
if (visited.size !== nodes.length) {
nodes.forEach((node, index) => {
if (visited.has(node.id)) return;
const previous = nodes[index - 1];
levels.set(node.id, previous ? (levels.get(previous.id) || 0) + 1 : 0);
});
}
return levels;
}
function dimensionsFor(node, compact, fallbackWidth) {
const width = node.width || (compact ? Math.min(236, fallbackWidth) : fallbackWidth);
const metaHeight = node.meta?.length ? node.meta.length * 22 : 0;
const lineCapacity = Math.max(12, Math.floor((width - 28) / 7.4));
const labelLines = Math.max(1, Math.ceil(String(node.label || node.id).length / lineCapacity));
const labelHeight = labelLines * 17;
const statusHeight = options.trackCompletion ? 22 : 0;
const naturalHeight = 48 + labelHeight + metaHeight + statusHeight;
return {
width,
height: Math.max(node.height || 0, 82, naturalHeight)
};
}
function updateScale(nextScale, preserveCenter = true) {
const oldWidth = Math.max(1, baseWidth * scale);
const oldHeight = Math.max(1, baseHeight * scale);
const centerX = (viewport.scrollLeft + viewport.clientWidth / 2) / oldWidth;
const centerY = (viewport.scrollTop + viewport.clientHeight / 2) / oldHeight;
scale = Math.min(1.35, Math.max(.55, nextScale));
surface.style.width = `${Math.max(viewport.clientWidth, baseWidth * scale)}px`;
surface.style.height = `${Math.max(viewport.clientHeight, baseHeight * scale)}px`;
plane.style.transform = `scale(${scale})`;
if (preserveCenter) {
viewport.scrollLeft = centerX * baseWidth * scale - viewport.clientWidth / 2;
viewport.scrollTop = centerY * baseHeight * scale - viewport.clientHeight / 2;
}
options.onScale?.(Math.round(scale * 100));
}
function layout() {
const compact = window.innerWidth <= 620;
const vertical = graphDirection === "TD" || compact;
surface.dataset.direction = vertical ? "td" : "lr";
const levels = levelsForGraph();
const positions = new Map();
const groups = new Map();
const fallbackWidth = options.nodeWidth || (graphDirection === "LR" ? 188 : 164);
const originalOrder = new Map(nodes.map((node, index) => [node.id, index]));
nodes.forEach((node) => {
const level = levels.get(node.id) || 0;
if (!groups.has(level)) groups.set(level, []);
groups.get(level).push(node);
});
[...groups.keys()].sort((a, b) => a - b).forEach((level) => {
const group = groups.get(level);
group.sort((a, b) => {
const barycenter = (node) => {
const neighbors = edges
.filter((edge) => edge.to === node.id && (levels.get(edge.from) || 0) < level)
.map((edge) => originalOrder.get(edge.from))
.filter(Number.isFinite);
return neighbors.length
? neighbors.reduce((sum, value) => sum + value, 0) / neighbors.length
: originalOrder.get(node.id);
};
return barycenter(a) - barycenter(b) || originalOrder.get(a.id) - originalOrder.get(b.id);
});
});
const stateGrid = options.variant === "state-machine" && vertical && !compact;
if (stateGrid) {
const startNodes = nodes.filter((node) => node.kind === "state-start");
const endNodes = nodes.filter((node) => node.kind === "state-end");
const stateNodes = nodes.filter((node) => !startNodes.includes(node) && !endNodes.includes(node));
const columns = Math.min(2, Math.max(1, stateNodes.length));
const columnGap = 96;
const rowGap = 116;
const dimensions = new Map(nodes.map((node) => [
node.id,
dimensionsFor(node, false, fallbackWidth)
]));
const cellWidth = Math.max(...stateNodes.map((node) => dimensions.get(node.id).width), fallbackWidth);
const contentWidth = columns * cellWidth + (columns - 1) * columnGap;
baseWidth = Math.max(viewport.clientWidth || 0, contentWidth + 64);
let y = 28;
startNodes.forEach((node) => {
const size = dimensions.get(node.id);
positions.set(node.id, { x: (baseWidth - size.width) / 2, y, ...size });
y += size.height + 68;
});
for (let index = 0; index < stateNodes.length; index += columns) {
const row = stateNodes.slice(index, index + columns);
const rowWidth = row.reduce((total, node, rowIndex) =>
total + dimensions.get(node.id).width + (rowIndex ? columnGap : 0), 0);
const rowHeight = Math.max(...row.map((node) => dimensions.get(node.id).height));
let x = (baseWidth - rowWidth) / 2;
row.forEach((node) => {
const size = dimensions.get(node.id);
positions.set(node.id, { x, y, ...size });
x += size.width + columnGap;
});
y += rowHeight + rowGap;
}
endNodes.forEach((node) => {
const size = dimensions.get(node.id);
positions.set(node.id, { x: (baseWidth - size.width) / 2, y, ...size });
y += size.height + 68;
});
baseHeight = Math.max(430, y + 24);
} else if (vertical && graphDirection === "TD") {
const longestLabel = edges.reduce((length, edge) => Math.max(length, String(edge.label || "").length), 0);
const levelGap = options.levelGap || Math.min(136, Math.max(72, 46 + longestLabel * 2.1));
const groupWidths = new Map();
let widestGroup = 0;
groups.forEach((group, level) => {
const width = group.reduce((total, node, index) => {
const dimensions = dimensionsFor(node, compact, fallbackWidth);
return total + dimensions.width + (index ? 32 : 0);
}, 0);
groupWidths.set(level, width);
widestGroup = Math.max(widestGroup, width);
});
baseWidth = Math.max(viewport.clientWidth || 0, widestGroup + 64);
let y = 32;
[...groups.keys()].sort((a, b) => a - b).forEach((level) => {
const group = groups.get(level);
const maxHeight = Math.max(...group.map((node) =>
dimensionsFor(node, compact, fallbackWidth).height
));
let x = (baseWidth - groupWidths.get(level)) / 2;
group.forEach((node) => {
const dimensions = dimensionsFor(node, compact, fallbackWidth);
positions.set(node.id, { x, y, ...dimensions });
x += dimensions.width + 32;
});
y += maxHeight + levelGap;
});
baseHeight = Math.max(390, y + 32);
} else if (vertical) {
const ordered = [...nodes].sort((a, b) => {
const levelDelta = (levels.get(a.id) || 0) - (levels.get(b.id) || 0);
return levelDelta || nodes.indexOf(a) - nodes.indexOf(b);
});
let y = 32;
let widest = 0;
ordered.forEach((node) => {
const dimensions = dimensionsFor(node, compact, fallbackWidth);
widest = Math.max(widest, dimensions.width);
positions.set(node.id, { x: 32, y, ...dimensions });
y += dimensions.height + 32;
});
baseWidth = Math.max(viewport.clientWidth || 0, widest + 64);
positions.forEach((position) => {
position.x = Math.max(32, (baseWidth - position.width) / 2);
});
baseHeight = Math.max(390, y);
} else {
const groupHeights = new Map();
const groupWidths = new Map();
let maxGroupHeight = 0;
groups.forEach((group, level) => {
const height = group.reduce((total, node, index) => {
const dimensions = dimensionsFor(node, false, fallbackWidth);
return total + dimensions.height + (index ? 32 : 0);
}, 0);
groupHeights.set(level, height);
groupWidths.set(level, Math.max(...group.map((node) =>
dimensionsFor(node, false, fallbackWidth).width
)));
maxGroupHeight = Math.max(maxGroupHeight, height);
});
const levelGap = options.levelGap || 56;
const levelOffsets = new Map();
let cursorX = 32;
[...groups.keys()].sort((a, b) => a - b).forEach((level) => {
levelOffsets.set(level, cursorX);
cursorX += groupWidths.get(level) + levelGap;
});
groups.forEach((group, level) => {
let y = 32 + (maxGroupHeight - groupHeights.get(level)) / 2;
group.forEach((node) => {
const dimensions = dimensionsFor(node, false, fallbackWidth);
positions.set(node.id, { x: levelOffsets.get(level), y, ...dimensions });
y += dimensions.height + 32;
});
});
baseWidth = Math.max(viewport.clientWidth || 0, cursorX + 32 - levelGap);
baseHeight = Math.max(390, maxGroupHeight + 64);
}
const needsExternalRoute = (edge) => {
const from = positions.get(edge.from);
const to = positions.get(edge.to);
if (!from || !to) return false;
if (edge.from === edge.to) return true;
const fromLevel = levels.get(edge.from) || 0;
const toLevel = levels.get(edge.to) || 0;
return toLevel <= fromLevel || toLevel - fromLevel > 1;
};
const externalEdges = edges.filter(needsExternalRoute);
const externalLanes = new Map(externalEdges.map((edge, index) => [edge, index]));
const coreWidth = baseWidth;
const coreHeight = baseHeight;
if (externalEdges.length) {
const reserve = 150 + externalEdges.length * 24;
if (vertical) baseWidth += reserve;
else baseHeight += reserve;
}
plane.style.width = `${baseWidth}px`;
plane.style.height = `${baseHeight}px`;
positions.forEach((position, id) => {
const element = nodeElements.get(id);
if (!element) return;
element.style.left = `${position.x}px`;
element.style.top = `${position.y}px`;
element.style.width = `${position.width}px`;
element.style.minHeight = `${position.height}px`;
});
svg.replaceChildren();
if (options.directed !== false) {
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
marker.id = `${graphId}-arrow`;
marker.setAttribute("markerWidth", "5");
marker.setAttribute("markerHeight", "5");
marker.setAttribute("refX", "4.6");
marker.setAttribute("refY", "2.5");
marker.setAttribute("orient", "auto");
marker.setAttribute("markerUnits", "strokeWidth");
const arrow = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
arrow.setAttribute("points", "0 0, 5 2.5, 0 5");
arrow.setAttribute("fill", "var(--ink-soft)");
marker.append(arrow);
defs.append(marker);
svg.append(defs);
}
svg.setAttribute("viewBox", `0 0 ${baseWidth} ${baseHeight}`);
edges.forEach((edge) => {
const from = positions.get(edge.from);
const to = positions.get(edge.to);
if (!from || !to) return;
let pathData = "";
let labelX = 0;
let labelY = 0;
let route = needsExternalRoute(edge) ? "external" : "direct";
if (edge.from === edge.to) {
route = "loop";
const x = from.x + from.width;
const y1 = from.y + from.height * .34;
const y2 = from.y + from.height * .7;
pathData = `M ${x} ${y1} C ${x + 66} ${y1 - 44}, ${x + 66} ${y2 + 44}, ${x} ${y2}`;
labelX = x + 62;
labelY = from.y - 8;
} else if (vertical) {
const x1 = from.x + from.width / 2;
const y1 = from.y + from.height;
const x2 = to.x + to.width / 2;
const y2 = to.y;
if (route === "external") {
const lane = externalLanes.get(edge) || 0;
const sideX = coreWidth + 46 + lane * 24;
pathData = `M ${x1} ${y1} V ${y1 + 20} H ${sideX} V ${y2 - 20} H ${x2} V ${y2}`;
labelX = sideX - 2;
labelY = (y1 + y2) / 2;
} else {
const middleY = y1 + Math.max(26, (y2 - y1) / 2);
pathData = `M ${x1} ${y1} V ${middleY} H ${x2} V ${y2}`;
labelX = (x1 + x2) / 2;
labelY = middleY - 14;
}
} else {
const x1 = from.x + from.width;
const y1 = from.y + from.height / 2;
const x2 = to.x;
const y2 = to.y + to.height / 2;
if (route === "external") {
const lane = externalLanes.get(edge) || 0;
const sideY = coreHeight + 42 + lane * 24;
pathData = `M ${x1} ${y1} H ${x1 + 20} V ${sideY} H ${x2 - 20} V ${y2} H ${x2}`;
labelX = (x1 + x2) / 2;
labelY = sideY - 16;
} else {
const middleX = x1 + Math.max(28, (x2 - x1) / 2);
pathData = `M ${x1} ${y1} H ${middleX} V ${y2} H ${x2}`;
labelX = middleX;
labelY = (y1 + y2) / 2 - 14;
}
}
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.classList.add("network-edge");
if (edge.kind) path.classList.add(`is-${edge.kind}`);
path.dataset.from = edge.from;
path.dataset.to = edge.to;
path.dataset.route = route;
path.setAttribute("d", pathData);
if (options.directed !== false && edge.directed !== false) {
path.setAttribute("marker-end", `url(#${graphId}-arrow)`);
}
svg.append(path);
if (options.animateFlow !== false) {
const flow = document.createElementNS("http://www.w3.org/2000/svg", "path");
flow.classList.add("network-edge-flow");
if (edge.kind) flow.classList.add(`is-${edge.kind}`);
flow.dataset.from = edge.from;
flow.dataset.to = edge.to;
flow.dataset.route = route;
flow.setAttribute("d", pathData);
svg.append(flow);
}
if (edge.label) {
const label = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
label.setAttribute("x", String(labelX - 110));
label.setAttribute("y", String(labelY - 20));
label.setAttribute("width", "220");
label.setAttribute("height", "54");
const labelBox = document.createElement("div");
labelBox.className = "network-edge-label-box";
labelBox.textContent = edge.label;
label.append(labelBox);
svg.append(label);
}
});
updateScale(scale, false);
surface.classList.add("is-ready");
}
function center() {
viewport.scrollTo({
left: Math.max(0, (baseWidth * scale - viewport.clientWidth) / 2),
top: Math.max(0, (baseHeight * scale - viewport.clientHeight) / 2),
behavior: reducedMotion.matches ? "auto" : "smooth"
});
}
function fit() {
if (!baseWidth || !baseHeight || !viewport.clientWidth) return;
const next = Math.min(
1,
(viewport.clientWidth - 32) / baseWidth,
(viewport.clientHeight - 32) / baseHeight
);
updateScale(next, false);
center();
}
function selectNode(id) {
const related = dependencyClosure(id, edges);
nodeElements.forEach((element, nodeId) => {
element.classList.toggle("is-selected", nodeId === id);
element.classList.toggle("is-related", nodeId !== id && related.has(nodeId));
element.classList.toggle("is-muted", !related.has(nodeId));
});
qa(".network-edge, .network-edge-flow", svg).forEach((path) => {
const active = related.has(path.dataset.from) && related.has(path.dataset.to);
path.classList.toggle("is-active", active);
path.classList.toggle("is-muted", !active);
});
if (inspector) {
if (options.directed === false) {
const related = edges
.filter((edge) => edge.from === id || edge.to === id)
.map((edge) => edge.from === id ? edge.to : edge.from);
inspector.textContent = `${id} · relaciona-se com ${related.join(", ") || "nenhuma outra entidade"}`;
} else {
const incoming = edges.filter((edge) => edge.to === id).map((edge) => edge.from);
const outgoing = edges.filter((edge) => edge.from === id).map((edge) => edge.to);
const parts = [
incoming.length ? `recebe de ${incoming.join(", ")}` : "sem entradas anteriores",
outgoing.length ? `envia para ${outgoing.join(", ")}` : "sem saÃdas posteriores"
];
inspector.textContent = `${id} · ${parts.join(" · ")}`;
}
}
}
let pan = null;
viewport.addEventListener("pointerdown", (event) => {
if (event.button !== 0 || event.target.closest(".network-node")) return;
pan = { x: event.clientX, y: event.clientY, left: viewport.scrollLeft, top: viewport.scrollTop };
viewport.classList.add("is-panning");
viewport.setPointerCapture(event.pointerId);
});
viewport.addEventListener("pointermove", (event) => {
if (!pan) return;
viewport.scrollLeft = pan.left - (event.clientX - pan.x);
viewport.scrollTop = pan.top - (event.clientY - pan.y);
});
const stopPan = (event) => {
if (!pan) return;
pan = null;
viewport.classList.remove("is-panning");
if (viewport.hasPointerCapture(event.pointerId)) viewport.releasePointerCapture(event.pointerId);
};
viewport.addEventListener("pointerup", stopPan);
viewport.addEventListener("pointercancel", stopPan);
viewport.addEventListener("wheel", (event) => {
event.preventDefault();
updateScale(scale + (event.deltaY < 0 ? .08 : -.08));
}, { passive: false });
nodeElements.forEach((element, id) => element.addEventListener("click", () => selectNode(id)));
layout();
window.addEventListener("resize", () => {
cancelAnimationFrame(resizeFrame);
resizeFrame = requestAnimationFrame(layout);
}, { passive: true });
window.addEventListener("planning:layout", layout);
return {
center,
fit,
zoomIn: () => updateScale(scale + .1),
zoomOut: () => updateScale(scale - .1),
toggleOrientation: () => {
graphDirection = graphDirection === "LR" ? "TD" : "LR";
layout();
fit();
return graphDirection;
},
toggleFlow: () => {
if (options.animateFlow === false) return false;
const active = surface.dataset.flow !== "paused";
surface.dataset.flow = active ? "paused" : "active";
return !active;
},
setCompleted: (completedIds) => {
nodeElements.forEach((element, id) => {
const complete = completedIds.has(id);
element.classList.toggle("is-complete", complete);
const status = q(".network-node-status", element);
if (status) status.textContent = complete ? "ConcluÃda" : "Em acompanhamento";
element.setAttribute(
"aria-label",
`${id}: ${nodes.find((node) => node.id === id)?.label || id}. ${complete ? "ConcluÃda" : "Em acompanhamento"}.`
);
});
qa(".network-edge, .network-edge-flow", svg).forEach((path) => {
path.classList.toggle(
"is-complete",
completedIds.has(path.dataset.from) && completedIds.has(path.dataset.to)
);
});
},
reset: () => {
nodeElements.forEach((element) => element.classList.remove("is-selected", "is-related", "is-muted"));
qa(".network-edge, .network-edge-flow", svg).forEach((path) => path.classList.remove("is-active", "is-muted"));
if (inspector) inspector.textContent = options.resetCopy || "Arraste o canvas, selecione um nó ou ajuste o enquadramento.";
}
};
}
function taskIds(text) {
const ids = Array.from(text.matchAll(/\bT-\d+\b/gi), (match) => match[0].toUpperCase());
const range = text.match(/\bT-(\d+)\s+a\s+T-(\d+)\b/i);
if (range) {
const start = Number(range[1]);
const end = Number(range[2]);
for (let value = start; value <= end; value += 1) ids.push(`T-${String(value).padStart(2, "0")}`);
}
return Array.from(new Set(ids));
}
function enhanceTasks() {
const section = q("#stage-tasks");
const content = q(".doc-content", section);
if (!content) return;
const nodes = qa("h3, h4", content).map((heading) => {
const match = heading.textContent.trim().match(/^(T-\d+)\s*(?:—|-|:)\s*(.+)$/i);
return match ? { id: match[1].toUpperCase(), label: match[2].trim() } : null;
}).filter(Boolean);
if (nodes.length < 2) return;
const known = new Set(nodes.map((node) => node.id));
const dependencyHeading = qa("h2, h3", content).find((heading) => normalizedText(heading.textContent) === "dependencias");
const edges = [];
if (dependencyHeading) {
let current = dependencyHeading.nextElementSibling;
while (current && !/^H[12]$/.test(current.tagName)) {
qa("li", current).forEach((item) => {
const ids = taskIds(item.textContent);
const target = ids[0];
ids.slice(1).forEach((source) => {
if (known.has(source) && known.has(target) && source !== target &&
!edges.some((edge) => edge.from === source && edge.to === target)) {
edges.push({ from: source, to: target });
}
});
});
current = current.nextElementSibling;
}
}
const visual = createElement("section", "artifact-visual");
visual.setAttribute("data-task-graph", "");
visual.setAttribute("aria-labelledby", "task-graph-title");
const head = createElement("header", "artifact-visual-head");
const titleGroup = createElement("div");
titleGroup.append(
createElement("span", "artifact-visual-kicker", "Mapa operacional"),
createElement("h4", "artifact-visual-title", "Grafo de dependências")
);
q(".artifact-visual-title", titleGroup).id = "task-graph-title";
titleGroup.append(createElement("p", "artifact-visual-copy", "Arraste o canvas, ajuste a escala e selecione uma tarefa para explorar sua vizinhança."));
const controls = createElement("div", "visual-controls");
head.append(titleGroup, controls);
const viewport = createElement("div", "network-viewport");
viewport.tabIndex = 0;
viewport.setAttribute("role", "region");
viewport.setAttribute("aria-label", "Grafo rolável de dependências entre tarefas");
const inspector = createElement("div", "network-inspector", "Arraste o canvas, selecione um nó ou ajuste o enquadramento.");
inspector.setAttribute("aria-live", "polite");
visual.append(head, viewport, inspector);
content.before(visual);
const scaleLabel = createElement("span", "visual-scale", "90%");
const network = renderNetwork(viewport, nodes, edges, "LR", inspector, {
initialScale: .9,
variant: "dependencies",
nodeWidth: 202,
levelGap: 92,
trackCompletion: true,
resetCopy: "Arraste o canvas, selecione um nó ou ajuste o enquadramento.",
onScale: (value) => { scaleLabel.textContent = `${value}%`; }
});
const fit = createElement("button", "visual-control", "Ajustar");
const zoomOut = createElement("button", "visual-control", "−");
const zoomIn = createElement("button", "visual-control", "+");
const orientation = createElement("button", "visual-control", "Horizontal");
const flow = createElement("button", "visual-control", "Fluxo");
const reset = createElement("button", "visual-control", "Limpar");
[fit, zoomOut, zoomIn, orientation, flow, reset].forEach((button) => { button.type = "button"; });
zoomOut.setAttribute("aria-label", "Reduzir grafo");
zoomIn.setAttribute("aria-label", "Ampliar grafo");
orientation.setAttribute("aria-label", "Alternar orientação do grafo de tarefas");
flow.setAttribute("aria-pressed", "true");
fit.addEventListener("click", network.fit);
zoomOut.addEventListener("click", network.zoomOut);
zoomIn.addEventListener("click", network.zoomIn);
orientation.addEventListener("click", () => {
orientation.textContent = network.toggleOrientation() === "LR" ? "Horizontal" : "Vertical";
});
flow.addEventListener("click", () => flow.setAttribute("aria-pressed", String(network.toggleFlow())));
reset.addEventListener("click", network.reset);
controls.append(fit, zoomOut, scaleLabel, zoomIn, orientation, flow, reset);
installFullscreenControl(visual, controls, "Grafo de dependências das tarefas", network.fit);
const refreshTaskCompletion = () => {
const completed = new Set(nodes
.filter((node) => qa(`[data-task-check="${node.id}"]`).some((check) => check.checked))
.map((node) => node.id));
network.setCompleted(completed);
};
document.addEventListener("sdd:living-ready", refreshTaskCompletion);
document.addEventListener("sdd:living-check-change", (event) => {
if (event.detail?.key?.startsWith("task-")) refreshTaskCompletion();
});
refreshTaskCompletion();
}
function renderSequence(viewport, participants, messages, options = {}) {
const namespace = "http://www.w3.org/2000/svg";
const surface = createElement("div", "sequence-surface");
surface.dataset.flow = "active";
const actorGap = 192;
const baseWidth = Math.max(640, participants.length * actorGap + 96);
const baseHeight = Math.max(390, 120 + messages.length * 64);
surface.style.width = `${baseWidth}px`;
surface.style.height = `${baseHeight}px`;
const centers = new Map();
participants.forEach((participant, index) => {
const center = 72 + index * actorGap + 72;
centers.set(participant.id, center);
const actor = createElement("div", "sequence-actor");
actor.style.left = `${center - 72}px`;
actor.append(
createElement("span", "", participant.id),
createElement("strong", "", participant.label)
);
surface.append(actor);
});
const svg = document.createElementNS(namespace, "svg");
svg.classList.add("network-links");
svg.setAttribute("viewBox", `0 0 ${baseWidth} ${baseHeight}`);
svg.setAttribute("aria-hidden", "true");
const defs = document.createElementNS(namespace, "defs");
const marker = document.createElementNS(namespace, "marker");
const markerId = `sequence-arrow-${Math.random().toString(36).slice(2, 9)}`;
marker.id = markerId;
marker.setAttribute("markerWidth", "8");
marker.setAttribute("markerHeight", "6");
marker.setAttribute("refX", "7");
marker.setAttribute("refY", "3");
marker.setAttribute("orient", "auto");
const polygon = document.createElementNS(namespace, "polygon");
polygon.setAttribute("points", "0 0, 8 3, 0 6");
polygon.setAttribute("fill", "var(--ink-soft)");
marker.append(polygon);
defs.append(marker);
svg.append(defs);
participants.forEach((participant) => {
const line = document.createElementNS(namespace, "line");
const x = centers.get(participant.id);
line.classList.add("sequence-lifeline");
line.setAttribute("x1", x);
line.setAttribute("x2", x);
line.setAttribute("y1", "82");
line.setAttribute("y2", String(baseHeight - 30));
svg.append(line);
});
messages.forEach((message, index) => {
const from = centers.get(message.from);
const to = centers.get(message.to);
if (from === undefined || to === undefined) return;
const y = 122 + index * 64;
let pathData;
let labelX;
if (from === to) {
pathData = `M ${from} ${y} C ${from + 76} ${y}, ${from + 76} ${y + 30}, ${from} ${y + 30}`;
labelX = from + 54;
} else {
pathData = `M ${from} ${y} L ${to} ${y}`;
labelX = (from + to) / 2;
}
const path = document.createElementNS(namespace, "path");
path.classList.add("sequence-message");
if (message.kind === "return") path.classList.add("is-return");
if (index === 0 || message.primary) path.classList.add("is-primary");
path.style.setProperty("--message-order", index);
path.setAttribute("d", pathData);
path.setAttribute("marker-end", `url(#${markerId})`);
const flow = document.createElementNS(namespace, "path");
flow.classList.add("sequence-message-flow");
flow.setAttribute("d", pathData);
const label = document.createElementNS(namespace, "text");
label.classList.add("sequence-message-label");
label.setAttribute("x", labelX);
label.setAttribute("y", String(y - 10));
label.textContent = message.label;
svg.append(path, flow, label);
});
surface.prepend(svg);
viewport.replaceChildren(surface);
let scale = options.initialScale || 1;
let pan = null;
const updateScale = (next) => {
scale = Math.min(1.3, Math.max(.55, next));
surface.style.transform = `scale(${scale})`;
viewport.style.setProperty("--sequence-width", `${baseWidth * scale}px`);
options.onScale?.(Math.round(scale * 100));
};
const center = () => viewport.scrollTo({
left: Math.max(0, (baseWidth * scale - viewport.clientWidth) / 2),
top: 0,
behavior: reducedMotion.matches ? "auto" : "smooth"
});
const fit = () => {
updateScale(Math.min(1, (viewport.clientWidth - 24) / baseWidth));
center();
};
viewport.addEventListener("pointerdown", (event) => {
if (event.button !== 0) return;
pan = { x: event.clientX, y: event.clientY, left: viewport.scrollLeft, top: viewport.scrollTop };
viewport.classList.add("is-panning");
viewport.setPointerCapture(event.pointerId);
});
viewport.addEventListener("pointermove", (event) => {
if (!pan) return;
viewport.scrollLeft = pan.left - (event.clientX - pan.x);
viewport.scrollTop = pan.top - (event.clientY - pan.y);
});
const stopPan = (event) => {
if (!pan) return;
pan = null;
viewport.classList.remove("is-panning");
if (viewport.hasPointerCapture(event.pointerId)) viewport.releasePointerCapture(event.pointerId);
};
viewport.addEventListener("pointerup", stopPan);
viewport.addEventListener("pointercancel", stopPan);
viewport.addEventListener("wheel", (event) => {
event.preventDefault();
updateScale(scale + (event.deltaY < 0 ? .08 : -.08));
}, { passive: false });
updateScale(scale);
return {
center,
fit,
zoomIn: () => updateScale(scale + .1),
zoomOut: () => updateScale(scale - .1),
toggleFlow: () => {
const active = surface.dataset.flow !== "paused";
surface.dataset.flow = active ? "paused" : "active";
return !active;
}
};
}
function parseMermaid(source) {
const lines = source
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("%%"));
const header = lines.shift() || "";
const nodes = new Map();
const edges = [];
const cleanLabel = (value) => value
.replace(/^[\[\(\{]+/, "")
.replace(/[\]\)\}]+$/, "")
.replace(/^["']|["']$/g, "")
.replace(/<br\s*\/?>/gi, " · ")
.replace(/\s+/g, " ")
.trim();
const nodeKind = (raw) => {
const value = raw.trim();
if (value === "[*]") return "state";
if (/\{\s*["']?.+["']?\s*\}/.test(value)) return "decision";
if (/\[\(.+\)\]/.test(value) || /\[\(.*\)\]/.test(value)) return "store";
if (/\[\[.+\]\]/.test(value)) return "subroutine";
if (/\(\[.+\]\)/.test(value) || /\(\(.+\)\)/.test(value)) return "terminal";
return "step";
};
const parseNode = (raw, role = "") => {
const clean = raw.trim().replace(/;$/, "");
if (clean === "[*]") {
const id = role === "from" ? "estado-inicial" : "estado-final";
const node = {
id,
label: role === "from" ? "InÃcio" : "Fim",
kind: role === "from" ? "state-start" : "state-end",
width: 72,
height: 72
};
nodes.set(id, node);
return node;
}
const match = clean.match(/^([A-Za-z0-9_.-]+)(.*)$/);
if (!match) return null;
const explicitLabel = cleanLabel(match[2] || "");
const existing = nodes.get(match[1]);
const hasShape = Boolean((match[2] || "").trim());
const node = {
id: match[1],
label: explicitLabel || existing?.label || match[1],
kind: hasShape ? nodeKind(clean) : existing?.kind || "step"
};
nodes.set(node.id, node);
return node;
};
const flowHeader = header.match(/^(?:flowchart|graph)\s+(LR|RL|TD|TB|BT)$/i);
if (flowHeader) {
lines.forEach((line) => {
const match = line.match(/^(.+?)\s*(-->|---|-.->|==>)\s*(?:\|([^|]+)\|\s*)?(.+?)\s*;?$/);
if (!match) {
parseNode(line);
return;
}
const from = parseNode(match[1], "from");
const to = parseNode(match[4], "to");
if (from && to) edges.push({
from: from.id,
to: to.id,
label: cleanLabel(match[3] || ""),
directed: match[2] !== "---",
kind: match[2] === "-.->" ? "secondary" : ""
});
});
if (nodes.size < 2 || !edges.length) return null;
const values = Array.from(nodes.values());
const outgoing = new Map();
edges.forEach((edge) => outgoing.set(edge.from, (outgoing.get(edge.from) || 0) + 1));
const vocabulary = normalizedText(values.map((node) => `${node.id} ${node.label}`).join(" "));
const hasDecision = values.some((node) => node.kind === "decision");
const hasBranch = Array.from(outgoing.values()).some((count) => count > 1);
const looksArchitectural = /(api|servico|service|banco|database|outbox|fila|queue|frontend|web|worker|normaliz|ingest|evento)/.test(vocabulary);
let variant = flowHeader[1].toUpperCase() === "TD" ? "vertical" : "pipeline";
if (hasBranch) variant = "dependency-tree";
if (looksArchitectural) variant = "architecture";
if (values.some((node) => node.kind === "store")) variant = "data-flow";
if (hasDecision) variant = "branching";
return {
type: "Fluxo",
direction: ["TD", "TB", "BT"].includes(flowHeader[1].toUpperCase()) ? "TD" : "LR",
nodes: values,
edges,
variant
};
}
if (/^sequenceDiagram$/i.test(header)) {
const messages = [];
lines.forEach((line) => {
const participant = line.match(/^(?:participant|actor)\s+([A-Za-z0-9_.-]+)(?:\s+as\s+(.+))?$/i);
if (participant) {
const node = { id: participant[1], label: cleanLabel(participant[2] || participant[1]) };
nodes.set(node.id, node);
return;
}
const message = line.match(/^([A-Za-z0-9_.-]+?)\s*(-->>|->>|-->|->)\s*([A-Za-z0-9_.-]+)\s*:\s*(.+)$/);
if (!message) return;
const from = parseNode(message[1], "from");
const to = parseNode(message[3], "to");
if (from && to) {
const item = {
from: from.id,
to: to.id,
label: cleanLabel(message[4]),
kind: message[2].startsWith("--") ? "return" : "call"
};
messages.push(item);
edges.push({
from: from.id,
to: to.id,
label: item.label,
kind: item.kind
});
}
});
if (nodes.size < 2 || !edges.length) return null;
return {
type: "Sequência",
direction: "LR",
nodes: Array.from(nodes.values()).map((node) => ({ ...node, kind: "actor" })),
edges,
messages,
variant: "sequence"
};
}
if (/^stateDiagram(?:-v2)?$/i.test(header)) {
lines.forEach((line) => {
const transition = line.match(/^(.+?)\s*-->\s*([^:]+?)(?:\s*:\s*(.+))?$/);
if (!transition) return;
const from = parseNode(transition[1], "from");
const to = parseNode(transition[2], "to");
if (from && to) edges.push({
from: from.id,
to: to.id,
label: cleanLabel(transition[3] || "")
});
});
if (nodes.size < 2 || !edges.length) return null;
return {
type: "Estados",
direction: "LR",
nodes: Array.from(nodes.values()).map((node) => ({
...node,
kind: node.kind === "step" ? "state" : node.kind
})),
edges,
variant: "state-machine"
};
}
return null;
}
function enhanceDiagrams() {
qa(".mermaid-wrap").forEach((wrap) => {
const diagram = q(".mermaid-diagram", wrap);
const fallback = q(".mermaid-fallback", wrap);
const parsed = parseMermaid((fallback ? fallback.textContent : diagram.textContent).trim());
if (!parsed) return;
wrap.dataset.diagramType = normalizedText(parsed.type);
wrap.dataset.diagramVariant = parsed.variant || "pipeline";
const toolbar = createElement("div", "diagram-toolbar");
toolbar.append(createElement("strong", "", `Mermaid · ${parsed.type}`));
const actions = createElement("div", "diagram-toolbar-actions");
const fitButton = createElement("button", "visual-control", "Ajustar");
const zoomOut = createElement("button", "visual-control", "−");
const scaleLabel = createElement("span", "visual-scale", "90%");
const zoomIn = createElement("button", "visual-control", "+");
const orientationButton = parsed.variant === "sequence"
? null
: createElement("button", "visual-control", parsed.direction === "LR" ? "Horizontal" : "Vertical");
const flowButton = createElement("button", "visual-control", "Fluxo");
const visualButton = createElement("button", "visual-control", "Visual");
const codeButton = createElement("button", "visual-control", "Código");
[fitButton, zoomOut, zoomIn, orientationButton, flowButton, visualButton, codeButton]
.filter(Boolean)
.forEach((button) => { button.type = "button"; });
zoomOut.setAttribute("aria-label", "Reduzir diagrama");
zoomIn.setAttribute("aria-label", "Ampliar diagrama");
orientationButton?.setAttribute("aria-label", "Alternar orientação do diagrama");
flowButton.setAttribute("aria-pressed", "true");
visualButton.setAttribute("aria-pressed", "true");
codeButton.setAttribute("aria-pressed", "false");
actions.append(fitButton, zoomOut, scaleLabel, zoomIn);
if (orientationButton) actions.append(orientationButton);
actions.append(flowButton, visualButton, codeButton);
toolbar.append(actions);
const viewport = createElement(
"div",
parsed.variant === "sequence"
? "diagram-viewport sequence-viewport"
: "diagram-viewport network-viewport"
);
viewport.tabIndex = 0;
viewport.setAttribute("role", "region");
viewport.setAttribute("aria-label", `Diagrama Mermaid de ${parsed.type.toLowerCase()}, rolável e ampliável`);
diagram.replaceChildren(viewport);
wrap.prepend(toolbar);
const network = parsed.variant === "sequence"
? renderSequence(viewport, parsed.nodes, parsed.messages, {
initialScale: .9,
onScale: (value) => { scaleLabel.textContent = `${value}%`; }
})
: renderNetwork(viewport, parsed.nodes, parsed.edges, parsed.direction, null, {
initialScale: .9,
variant: parsed.variant,
nodeWidth: parsed.variant === "state-machine"
? 210
: parsed.variant === "architecture"
? 200
: parsed.variant === "branching"
? 196
: 184,
levelGap: parsed.variant === "state-machine"
? 96
: ["architecture", "dependency-tree"].includes(parsed.variant)
? 88
: parsed.variant === "branching"
? 82
: 68,
onScale: (value) => { scaleLabel.textContent = `${value}%`; }
});
fitButton.addEventListener("click", network.fit);
zoomOut.addEventListener("click", network.zoomOut);
zoomIn.addEventListener("click", network.zoomIn);
orientationButton?.addEventListener("click", () => {
orientationButton.textContent = network.toggleOrientation() === "LR" ? "Horizontal" : "Vertical";
});
flowButton.addEventListener("click", () => flowButton.setAttribute("aria-pressed", String(network.toggleFlow())));
installFullscreenControl(wrap, actions, `Diagrama Mermaid de ${parsed.type.toLowerCase()}`, network.fit);
wrap.classList.add("is-enhanced");
visualButton.addEventListener("click", () => {
wrap.classList.remove("is-code");
visualButton.setAttribute("aria-pressed", "true");
codeButton.setAttribute("aria-pressed", "false");
});
codeButton.addEventListener("click", () => {
wrap.classList.add("is-code");
visualButton.setAttribute("aria-pressed", "false");
codeButton.setAttribute("aria-pressed", "true");
});
});
}
function enhanceArchitectureOverview() {
const executive = q("#modo-executivo");
const technical = q("#stage-techspec .doc-content");
if (!executive || !technical || q("[data-architecture-overview]", executive)) return;
const candidates = qa(".mermaid-wrap", technical)
.map((wrap) => {
const source = q(".mermaid-fallback", wrap)?.textContent ||
q(".mermaid-diagram", wrap)?.textContent ||
"";
return parseMermaid(source.trim());
})
.filter((diagram) => diagram && diagram.variant !== "sequence" && diagram.nodes.length > 2);
const architecture = candidates.find((diagram) => diagram.variant === "architecture") || candidates[0];
if (!architecture) return;
const isDelivery = Boolean(q("#modo-lifecycle"));
const memoryPanel = q("#stage-memory");
const isComplete = Boolean(memoryPanel && memoryPanel.getAttribute("aria-disabled") !== "true");
const status = !isDelivery ? "Planejado" : isComplete ? "Implementado" : "Em execução";
const nodes = architecture.nodes.slice(0, 9).map((node) => ({ ...node, status }));
const available = new Set(nodes.map((node) => node.id));
const edges = architecture.edges.filter((edge) => available.has(edge.from) && available.has(edge.to));
if (nodes.length < 3 || !edges.length) return;
const visual = createElement("section", "artifact-visual architecture-overview");
visual.setAttribute("data-architecture-overview", "");
visual.setAttribute("aria-labelledby", "architecture-overview-title");
const head = createElement("header", "artifact-visual-head");
const titleGroup = createElement("div");
titleGroup.append(
createElement("span", "artifact-visual-kicker", isDelivery ? "Entrega materializada" : "Plano técnico"),
createElement(
"h3",
"artifact-visual-title",
isDelivery ? "Arquitetura da entrega" : "Arquitetura planejada"
)
);
q(".artifact-visual-title", titleGroup).id = "architecture-overview-title";
const detail = architecture.nodes.length > nodes.length
? `Visão de alto nÃvel com ${nodes.length} de ${architecture.nodes.length} componentes; o diagrama detalhado permanece na Tech Spec.`
: "Componentes e integrações derivados do diagrama canônico da Tech Spec.";
titleGroup.append(createElement("p", "artifact-visual-copy", detail));
const controls = createElement("div", "visual-controls");
head.append(titleGroup, controls);
const viewport = createElement("div", "network-viewport");
viewport.tabIndex = 0;
viewport.setAttribute("role", "region");
viewport.setAttribute("aria-label", "Canvas animado da arquitetura do ciclo");
const inspector = createElement(
"div",
"network-inspector",
`${status} · selecione um componente para inspecionar entradas e saÃdas.`
);
inspector.setAttribute("aria-live", "polite");
visual.append(head, viewport, inspector);
const executiveGrid = q(".executive-grid", executive);
(executiveGrid || q(".provenance", executive))?.after(visual);
const scaleLabel = createElement("span", "visual-scale", "88%");
const network = renderNetwork(viewport, nodes, edges, "LR", inspector, {
initialScale: .88,
variant: "architecture",
nodeWidth: 198,
levelGap: 92,
resetCopy: `${status} · selecione um componente para inspecionar entradas e saÃdas.`,
onScale: (value) => { scaleLabel.textContent = `${value}%`; }
});
const fit = createElement("button", "visual-control", "Ajustar");
const zoomOut = createElement("button", "visual-control", "−");
const zoomIn = createElement("button", "visual-control", "+");
const orientation = createElement("button", "visual-control", "Horizontal");
const flow = createElement("button", "visual-control", "Fluxo");
[fit, zoomOut, zoomIn, orientation, flow].forEach((button) => { button.type = "button"; });
zoomOut.setAttribute("aria-label", "Reduzir arquitetura");
zoomIn.setAttribute("aria-label", "Ampliar arquitetura");
orientation.setAttribute("aria-label", "Alternar orientação da arquitetura");
flow.setAttribute("aria-pressed", "true");
fit.addEventListener("click", network.fit);
zoomOut.addEventListener("click", network.zoomOut);
zoomIn.addEventListener("click", network.zoomIn);
orientation.addEventListener("click", () => {
orientation.textContent = network.toggleOrientation() === "LR" ? "Horizontal" : "Vertical";
});
flow.addEventListener("click", () => flow.setAttribute("aria-pressed", String(network.toggleFlow())));
controls.append(fit, zoomOut, scaleLabel, zoomIn, orientation, flow);
installFullscreenControl(visual, controls, "Arquitetura visual do ciclo", network.fit);
}
function enhanceCompanionAssets() {
qa(".doc-content a[href$='.excalidraw']").forEach((link) => {
const href = link.getAttribute("href");
const payload = qa("script[data-excalidraw-path]", link.closest(".doc-content"))
.find((script) => script.dataset.excalidrawPath === href);
if (!payload) {
const asset = createElement("aside", "companion-asset");
asset.setAttribute("aria-label", "Canvas Excalidraw editável");
asset.append(createElement("span", "companion-asset-badge", "Excalidraw"));
const copy = createElement("span", "companion-asset-copy");
copy.append(createElement("strong", "", "Canvas editável"), createElement("code", "", href));
const action = createElement("a", "companion-asset-link", "Abrir companion");
action.href = href;
asset.append(copy, action);
(link.closest("p") || link).replaceWith(asset);
return;
}
let scene;
try {
scene = JSON.parse(payload.textContent);
} catch (_) {
return;
}
const elements = Array.isArray(scene.elements)
? scene.elements.filter((element) => element && !element.isDeleted)
: [];
if (!elements.length) return;
const svgNamespace = "http://www.w3.org/2000/svg";
const bounds = elements.reduce((result, element) => ({
minX: Math.min(result.minX, Number(element.x) || 0),
minY: Math.min(result.minY, Number(element.y) || 0),
maxX: Math.max(result.maxX, (Number(element.x) || 0) + Math.max(1, Math.abs(Number(element.width) || 0))),
maxY: Math.max(result.maxY, (Number(element.y) || 0) + Math.max(1, Math.abs(Number(element.height) || 0)))
}), { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
const padding = 70;
const baseView = {
x: bounds.minX - padding,
y: bounds.minY - padding,
width: Math.max(320, bounds.maxX - bounds.minX + padding * 2),
height: Math.max(220, bounds.maxY - bounds.minY + padding * 2)
};
let view = { ...baseView };
const embed = createElement("section", "excalidraw-embed");
embed.dataset.flow = "active";
embed.setAttribute("aria-label", "Excalidraw renderizado e navegável");
const toolbar = createElement("header", "excalidraw-toolbar");
const toolbarCopy = createElement("span", "excalidraw-toolbar-copy");
toolbarCopy.append(createElement("strong", "", "Excalidraw · Canvas renderizado"), createElement("code", "", href));
const actions = createElement("span", "excalidraw-actions");
const fitButton = createElement("button", "visual-control", "Ajustar");
const zoomOut = createElement("button", "visual-control", "−");
const zoomIn = createElement("button", "visual-control", "+");
const flowButton = createElement("button", "visual-control", "Fluxo");
const companionLink = createElement("a", "visual-control", "Editar JSON");
[fitButton, zoomOut, zoomIn, flowButton].forEach((button) => { button.type = "button"; });
flowButton.setAttribute("aria-pressed", "true");
companionLink.href = href;
companionLink.setAttribute("download", "");
actions.append(fitButton, zoomOut, zoomIn, flowButton, companionLink);
toolbar.append(toolbarCopy, actions);
const viewport = createElement("div", "excalidraw-viewport");
viewport.tabIndex = 0;
viewport.setAttribute("role", "region");
viewport.setAttribute("aria-label", "Canvas Excalidraw; arraste para navegar e use os controles para ampliar");
const svg = document.createElementNS(svgNamespace, "svg");
svg.classList.add("excalidraw-svg");
svg.setAttribute("role", "img");
svg.setAttribute("aria-label", "Diagrama visual derivado do arquivo Excalidraw");
const defs = document.createElementNS(svgNamespace, "defs");
const marker = document.createElementNS(svgNamespace, "marker");
const markerId = `excalidraw-arrow-${Math.random().toString(36).slice(2)}`;
marker.id = markerId;
marker.setAttribute("viewBox", "0 0 10 10");
marker.setAttribute("refX", "8");
marker.setAttribute("refY", "5");
marker.setAttribute("markerWidth", "7");
marker.setAttribute("markerHeight", "7");
marker.setAttribute("orient", "auto-start-reverse");
const markerPath = document.createElementNS(svgNamespace, "path");
markerPath.setAttribute("d", "M 0 0 L 10 5 L 0 10 z");
markerPath.setAttribute("fill", "context-stroke");
marker.append(markerPath);
defs.append(marker);
svg.append(defs);
const renderedContainerLabels = new Set(
elements.filter((element) => element.type === "text" && element.containerId)
.map((element) => element.containerId)
);
const labelDictionary = {
events: "Eventos",
cases: "Casos operacionais",
workspace: "Workspace operacional",
api: "API",
outbox: "Outbox",
database: "Banco de dados"
};
const createShape = (tag, element) => {
const shape = document.createElementNS(svgNamespace, tag);
shape.classList.add("excalidraw-element");
shape.setAttribute("stroke", element.strokeColor || "currentColor");
shape.setAttribute("stroke-width", String(element.strokeWidth || 2));
shape.setAttribute("fill", element.backgroundColor && element.backgroundColor !== "transparent"
? element.backgroundColor
: "transparent");
shape.setAttribute("opacity", String((element.opacity ?? 100) / 100));
if (element.strokeStyle === "dashed") shape.setAttribute("stroke-dasharray", "10 8");
if (element.strokeStyle === "dotted") shape.setAttribute("stroke-dasharray", "2 7");
return shape;
};
elements.forEach((element) => {
const x = Number(element.x) || 0;
const y = Number(element.y) || 0;
const width = Math.abs(Number(element.width) || 0);
const height = Math.abs(Number(element.height) || 0);
let shape = null;
if (element.type === "rectangle") {
shape = createShape("rect", element);
shape.setAttribute("x", x);
shape.setAttribute("y", y);
shape.setAttribute("width", width);
shape.setAttribute("height", height);
shape.setAttribute("rx", element.roundness ? "12" : "2");
} else if (element.type === "ellipse") {
shape = createShape("ellipse", element);
shape.setAttribute("cx", x + width / 2);
shape.setAttribute("cy", y + height / 2);
shape.setAttribute("rx", Math.max(1, width / 2));
shape.setAttribute("ry", Math.max(1, height / 2));
} else if (element.type === "diamond") {
shape = createShape("polygon", element);
shape.setAttribute("points", `${x + width / 2},${y} ${x + width},${y + height / 2} ${x + width / 2},${y + height} ${x},${y + height / 2}`);
} else if (element.type === "line" || element.type === "arrow") {
const points = Array.isArray(element.points) && element.points.length
? element.points
: [[0, 0], [width, height]];
const pathData = points.map((point, index) =>
`${index ? "L" : "M"} ${x + Number(point[0] || 0)} ${y + Number(point[1] || 0)}`
).join(" ");
shape = createShape("path", element);
shape.setAttribute("d", pathData);
shape.setAttribute("fill", "none");
if (element.type === "arrow" || element.endArrowhead) {
shape.setAttribute("marker-end", `url(#${markerId})`);
const flow = document.createElementNS(svgNamespace, "path");
flow.classList.add("excalidraw-arrow-flow");
flow.setAttribute("d", pathData);
svg.append(flow);
}
} else if (element.type === "text") {
shape = document.createElementNS(svgNamespace, "text");
shape.classList.add("excalidraw-label");
shape.setAttribute("x", x + width / 2);
shape.setAttribute("y", y + Math.max(height / 2, Number(element.fontSize) || 18));
shape.setAttribute("font-size", String(element.fontSize || 18));
shape.textContent = element.text || element.originalText || "";
}
if (shape) svg.append(shape);
if (["rectangle", "ellipse", "diamond"].includes(element.type) &&
!renderedContainerLabels.has(element.id)) {
const label = document.createElementNS(svgNamespace, "text");
label.classList.add("excalidraw-label");
label.setAttribute("x", x + width / 2);
label.setAttribute("y", y + height / 2);
const fallback = labelDictionary[element.id] ||
String(element.id || element.type).replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
label.textContent = fallback;
svg.append(label);
}
});
const applyView = () => {
svg.setAttribute("viewBox", `${view.x} ${view.y} ${view.width} ${view.height}`);
};
const zoom = (factor) => {
const nextWidth = Math.min(baseView.width * 2.4, Math.max(baseView.width * .32, view.width * factor));
const nextHeight = nextWidth * (baseView.height / baseView.width);
view = {
x: view.x + (view.width - nextWidth) / 2,
y: view.y + (view.height - nextHeight) / 2,
width: nextWidth,
height: nextHeight
};
applyView();
};
fitButton.addEventListener("click", () => { view = { ...baseView }; applyView(); });
zoomOut.addEventListener("click", () => zoom(1.14));
zoomIn.addEventListener("click", () => zoom(.86));
flowButton.addEventListener("click", () => {
const active = embed.dataset.flow !== "paused";
embed.dataset.flow = active ? "paused" : "active";
flowButton.setAttribute("aria-pressed", String(!active));
});
installFullscreenControl(embed, actions, "Canvas Excalidraw renderizado", () => {
view = { ...baseView };
applyView();
});
let pan = null;
viewport.addEventListener("pointerdown", (event) => {
if (event.button !== 0) return;
pan = { x: event.clientX, y: event.clientY, view: { ...view } };
viewport.classList.add("is-panning");
viewport.setPointerCapture(event.pointerId);
});
viewport.addEventListener("pointermove", (event) => {
if (!pan) return;
const scaleX = pan.view.width / Math.max(1, viewport.clientWidth);
const scaleY = pan.view.height / Math.max(1, viewport.clientHeight);
view.x = pan.view.x - (event.clientX - pan.x) * scaleX;
view.y = pan.view.y - (event.clientY - pan.y) * scaleY;
applyView();
});
const stopPan = (event) => {
if (!pan) return;
pan = null;
viewport.classList.remove("is-panning");
if (viewport.hasPointerCapture(event.pointerId)) viewport.releasePointerCapture(event.pointerId);
};
viewport.addEventListener("pointerup", stopPan);
viewport.addEventListener("pointercancel", stopPan);
viewport.addEventListener("wheel", (event) => {
event.preventDefault();
zoom(event.deltaY < 0 ? .9 : 1.1);
}, { passive: false });
applyView();
viewport.append(svg);
embed.append(toolbar, viewport, createElement("p", "excalidraw-caption", "Renderização autocontida do companion. Arraste o canvas, aplique zoom ou baixe o JSON para edição."));
const paragraph = link.closest("p");
(paragraph || link).replaceWith(embed);
});
}
function enhanceDataModel() {
const content = q("#stage-techspec .doc-content");
if (!content) return;
const modelHeading = qa("h2", content).find((heading) => normalizedText(heading.textContent) === "modelo de dados");
if (!modelHeading || modelHeading.dataset.enhanced) return;
const entityHeadings = [];
let cursor = modelHeading.nextElementSibling;
while (cursor && cursor.tagName !== "H2") {
if (cursor.tagName === "H3" && /^[a-z][a-z0-9_]+$/i.test(normalizedText(cursor.textContent))) {
entityHeadings.push(cursor);
}
cursor = cursor.nextElementSibling;
}
if (entityHeadings.length < 2) return;
const nodes = entityHeadings.map((heading) => {
const fields = [];
let current = heading.nextElementSibling;
while (current && !["H2", "H3"].includes(current.tagName)) {
const rows = qa("tbody tr", current);
if (rows.length) {
rows.forEach((row) => {
const cells = qa("td", row);
if (cells.length >= 2) fields.push({
name: cells[0].textContent.replace(/`/g, "").trim(),
type: cells[1].textContent.replace(/`/g, "").trim()
});
});
} else {
qa("code", current).forEach((code) => {
const name = code.textContent.trim();
if (/^[a-z][a-z0-9_]+$/i.test(name) && !fields.some((field) => field.name === name)) {
fields.push({ name, type: "" });
}
});
}
current = current.nextElementSibling;
}
const id = normalizedText(heading.textContent);
return {
id,
label: id,
tag: "ENTITY",
kind: "store",
width: 240,
height: Math.max(126, 72 + Math.min(fields.length, 7) * 22),
meta: fields.slice(0, 7)
};
});
const aliases = new Map();
nodes.forEach((node) => {
const tail = node.id.replace(/^.*?_/, "").replace(/ies$/, "y").replace(/s$/, "");
aliases.set(tail, node.id);
const last = node.id.split("_").at(-1).replace(/ies$/, "y").replace(/s$/, "");
aliases.set(last, node.id);
});
const edges = [];
nodes.forEach((node) => {
node.meta.forEach((field) => {
if (!field.name.endsWith("_id")) return;
const prefix = field.name.slice(0, -3).replace(/ies$/, "y").replace(/s$/, "");
const target = aliases.get(prefix);
if (target && target !== node.id && !edges.some((edge) => edge.from === target && edge.to === node.id)) {
edges.push({ from: target, to: node.id });
}
});
});
if (!edges.length) return;
modelHeading.dataset.enhanced = "true";
const visual = createElement("section", "artifact-visual data-model-visual");
visual.setAttribute("data-model-graph", "");
visual.setAttribute("aria-labelledby", "data-model-title");
const head = createElement("header", "artifact-visual-head");
const titleGroup = createElement("div");
titleGroup.append(
createElement("span", "artifact-visual-kicker", "Modelo relacional"),
createElement("h4", "artifact-visual-title", "Entidades e relações detectadas")
);
q(".artifact-visual-title", titleGroup).id = "data-model-title";
titleGroup.append(createElement("p", "artifact-visual-copy", "Campos e relações são derivados das tabelas e chaves estrangeiras descritas no Markdown."));
const controls = createElement("div", "visual-controls");
head.append(titleGroup, controls);
const viewport = createElement("div", "network-viewport");
viewport.tabIndex = 0;
viewport.setAttribute("role", "region");
viewport.setAttribute("aria-label", "Diagrama rolável do modelo de dados");
const inspector = createElement("div", "network-inspector", "Arraste para navegar e selecione uma entidade para inspecionar suas relações.");
inspector.setAttribute("aria-live", "polite");
visual.append(head, viewport, inspector);
modelHeading.after(visual);
const scaleLabel = createElement("span", "visual-scale", "90%");
const network = renderNetwork(viewport, nodes, edges, "LR", inspector, {
nodeWidth: 240,
levelGap: 80,
initialScale: .9,
directed: false,
animateFlow: false,
resetCopy: "Arraste para navegar e selecione uma entidade para inspecionar suas relações.",
onScale: (value) => { scaleLabel.textContent = `${value}%`; }
});
const fit = createElement("button", "visual-control", "Ajustar");
const zoomOut = createElement("button", "visual-control", "−");
const zoomIn = createElement("button", "visual-control", "+");
const orientation = createElement("button", "visual-control", "Horizontal");
[fit, zoomOut, zoomIn, orientation].forEach((button) => { button.type = "button"; });
zoomOut.setAttribute("aria-label", "Reduzir modelo de dados");
zoomIn.setAttribute("aria-label", "Ampliar modelo de dados");
orientation.setAttribute("aria-label", "Alternar orientação do modelo de dados");
fit.addEventListener("click", network.fit);
zoomOut.addEventListener("click", network.zoomOut);
zoomIn.addEventListener("click", network.zoomIn);
orientation.addEventListener("click", () => {
orientation.textContent = network.toggleOrientation() === "LR" ? "Horizontal" : "Vertical";
});
controls.append(fit, zoomOut, scaleLabel, zoomIn, orientation);
installFullscreenControl(visual, controls, "Modelo de dados relacional", network.fit);
}
function enhanceApiReference() {
const content = q("#stage-techspec .doc-content");
if (!content) return;
const apiHeading = qa("h2", content).find((heading) => normalizedText(heading.textContent) === "contratos de api");
if (!apiHeading || apiHeading.dataset.enhanced) return;
const endpointHeadings = [];
let cursor = apiHeading.nextElementSibling;
while (cursor && cursor.tagName !== "H2") {
const match = cursor.tagName === "H3"
? cursor.textContent.replace(/`/g, "").trim().match(/^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s+(.+)$/i)
: null;
if (match) endpointHeadings.push({ heading: cursor, method: match[1].toUpperCase(), path: match[2] });
cursor = cursor.nextElementSibling;
}
if (!endpointHeadings.length) return;
apiHeading.dataset.enhanced = "true";
const reference = createElement("section", "api-reference");
reference.setAttribute("aria-label", "Referência interativa dos contratos REST");
const nav = createElement("nav", "api-reference-nav");
nav.setAttribute("aria-label", "Endpoints REST");
nav.append(createElement("span", "api-reference-nav-label", "Endpoints"));
const search = createElement("input", "api-reference-search");
search.type = "search";
search.placeholder = "Buscar método ou path";
search.setAttribute("aria-label", "Buscar endpoint");
nav.append(search);
const list = createElement("div", "api-reference-list");
nav.append(list);
const resizer = createElement("div", "api-reference-resizer");
resizer.tabIndex = 0;
resizer.setAttribute("role", "separator");
resizer.setAttribute("aria-orientation", "vertical");
resizer.setAttribute("aria-label", "Redimensionar navegação dos endpoints");
resizer.setAttribute("aria-valuemin", "160");
resizer.setAttribute("aria-valuemax", "420");
const main = createElement("div", "api-reference-main");
reference.append(nav, resizer, main);
apiHeading.after(reference);
const setNavWidth = (value, persist = true) => {
const maximum = Math.min(420, Math.max(220, reference.clientWidth * .42));
const width = Math.round(Math.min(maximum, Math.max(160, value)));
reference.style.setProperty("--api-nav-width", `${width}px`);
resizer.setAttribute("aria-valuenow", String(width));
if (persist) {
try { localStorage.setItem("sdd-api-nav-width:v1", String(width)); } catch (_) {}
}
};
try {
const stored = Number(localStorage.getItem("sdd-api-nav-width:v1"));
if (Number.isFinite(stored)) setNavWidth(stored, false);
else setNavWidth(220, false);
} catch (_) {
setNavWidth(220, false);
}
let resizeState = null;
resizer.addEventListener("pointerdown", (event) => {
if (event.button !== 0) return;
resizeState = { startX: event.clientX, width: nav.getBoundingClientRect().width };
reference.classList.add("is-resizing");
resizer.setPointerCapture(event.pointerId);
});
resizer.addEventListener("pointermove", (event) => {
if (!resizeState) return;
setNavWidth(resizeState.width + event.clientX - resizeState.startX, false);
});
const stopResize = (event) => {
if (!resizeState) return;
resizeState = null;
reference.classList.remove("is-resizing");
setNavWidth(nav.getBoundingClientRect().width, true);
if (resizer.hasPointerCapture(event.pointerId)) resizer.releasePointerCapture(event.pointerId);
};
resizer.addEventListener("pointerup", stopResize);
resizer.addEventListener("pointercancel", stopResize);
resizer.addEventListener("keydown", (event) => {
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
event.preventDefault();
const current = nav.getBoundingClientRect().width;
if (event.key === "Home") setNavWidth(160);
else if (event.key === "End") setNavWidth(420);
else setNavWidth(current + (event.key === "ArrowRight" ? 12 : -12));
});
const operations = endpointHeadings.map((endpoint, index) => {
const operation = createElement("section", "api-operation");
operation.id = `api-operation-${index + 1}`;
const head = createElement("header", "api-operation-head");
const method = createElement("span", "api-method", endpoint.method);
method.dataset.method = endpoint.method;
const copyPath = createElement("button", "api-copy-button", "Copiar path");
copyPath.type = "button";
copyPath.setAttribute("aria-label", `Copiar path ${endpoint.path}`);
copyPath.addEventListener("click", () => copyText(endpoint.path, copyPath));
const operationPath = createElement("code", "api-path", endpoint.path);
operationPath.title = endpoint.path;
head.append(method, operationPath, copyPath);
const body = createElement("div", "api-operation-body");
const description = createElement("div", "api-operation-description");
const sample = createElement("div", "api-operation-sample");
let current = endpoint.heading.nextElementSibling;
const nextHeading = endpointHeadings[index + 1]?.heading || null;
while (current && current !== nextHeading && current.tagName !== "H2") {
const next = current.nextElementSibling;
if (current.tagName === "PRE" || current.classList.contains("code-block")) sample.append(current);
else description.append(current);
current = next;
}
endpoint.heading.remove();
if (!sample.children.length) body.classList.add("is-single");
description.prepend(createElement("span", "api-column-label", "Contrato"));
if (sample.children.length) sample.prepend(createElement("span", "api-column-label", "Exemplo e resposta"));
body.append(description, sample);
operation.append(head, body);
main.append(operation);
const item = createElement("div", "api-reference-item");
const link = createElement("button", "api-reference-link");
link.type = "button";
const linkMethod = createElement("span", "api-method", endpoint.method);
linkMethod.dataset.method = endpoint.method;
const navigationPath = createElement("span", "api-path", endpoint.path);
navigationPath.title = endpoint.path;
link.append(linkMethod, navigationPath);
link.setAttribute("aria-controls", operation.id);
link.setAttribute("aria-current", String(index === 0));
link.addEventListener("click", () => {
operation.scrollIntoView({ behavior: reducedMotion.matches ? "auto" : "smooth", block: "start" });
});
const copyLink = createElement("button", "api-copy-button is-compact", "⧉");
copyLink.type = "button";
copyLink.setAttribute("aria-label", `Copiar path ${endpoint.path}`);
copyLink.addEventListener("click", () => copyText(endpoint.path, copyLink, "✓"));
item.append(link, copyLink);
list.append(item);
return { operation, link, item };
});
search.addEventListener("input", () => {
const query = normalizedText(search.value);
operations.forEach(({ item, operation }) => {
const matches = !query || normalizedText(item.textContent).includes(query);
item.hidden = !matches;
operation.hidden = !matches;
});
const firstVisible = operations.find(({ operation }) => !operation.hidden);
if (firstVisible) setActive(firstVisible.operation);
});
const setActive = (active) => {
operations.forEach(({ operation, link }) => {
const selected = operation === active;
operation.classList.toggle("is-active", selected);
link.setAttribute("aria-current", String(selected));
});
};
setActive(operations[0].operation);
if ("IntersectionObserver" in window) {
const observer = new IntersectionObserver((entries) => {
const visible = entries.filter((entry) => entry.isIntersecting).sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
if (visible) setActive(visible.target);
}, { rootMargin: "-25% 0px -60% 0px", threshold: [0, .25, .6] });
operations.forEach(({ operation }) => observer.observe(operation));
}
}
scopeDocumentHeadings();
enhanceTasks();
enhanceDiagrams();
enhanceArchitectureOverview();
enhanceCompanionAssets();
enhanceDataModel();
enhanceApiReference();
artifactPanels.forEach((panel, index) => {
panel.hidden = index !== 0;
panel.setAttribute("aria-hidden", String(index !== 0));
});
function applyLocation() {
const id = location.hash.slice(1);
const target = document.getElementById(id);
const targetPanel = target?.closest("[data-artifact-panel]");
if (targetPanel && target !== targetPanel) {
activateArtifact(targetPanel.id, { history: false });
requestAnimationFrame(() => requestAnimationFrame(() => target.scrollIntoView({
behavior: reducedMotion.matches ? "auto" : "smooth",
block: "start"
})));
} else if (id.startsWith("stage-") && target) {
activateArtifact(id, { history: false, scroll: true });
} else if (id === "modo-documentos") {
activateMode(id, { history: false });
activateArtifact(artifactTabs[0]?.dataset.artifactTarget, { history: false });
} else if (id === "modo-lifecycle" && document.getElementById(id)) {
activateMode(id, { history: false });
} else {
activateMode("modo-executivo", { history: false });
}
}
window.addEventListener("hashchange", applyLocation);
applyLocation();
})();
</script>
{% endblock %}