<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/webp" href="{{ brand_logo_data_uri|safe }}">
<title>{{ title }} · SDD</title>
{% include "html/partials/design-system.html" %}
<style>
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background-color: var(--bg);
background-image:
linear-gradient(rgba(28, 30, 36, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(28, 30, 36, 0.025) 1px, transparent 1px);
background-size: 42px 42px;
background-attachment: fixed;
color: var(--ink);
font-family: var(--font-body);
font-size: 16px;
line-height: 1.68;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
a { color: var(--accent-strong); text-underline-offset: 3px; }
a:hover { text-decoration-thickness: 2px; }
:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 3px;
border-radius: 5px;
}
.skip-link {
position: fixed;
left: 18px;
top: 10px;
z-index: 100;
transform: translateY(-160%);
background: var(--panel);
color: var(--ink);
border: 1px solid var(--line);
border-radius: 999px;
padding: 9px 14px;
font-weight: 650;
box-shadow: var(--shadow);
}
.skip-link:focus { transform: translateY(0); }
.page-header {
position: sticky;
top: 0;
z-index: 40;
min-width: 0;
background: color-mix(in srgb, var(--bg) 92%, transparent);
backdrop-filter: blur(16px) saturate(1.35);
border-bottom: 1px solid var(--line);
}
.header-inner {
display: flex;
align-items: center;
gap: 10px;
height: 60px;
max-width: 1200px;
margin: 0 auto;
padding: 0 26px;
min-width: 0;
}
.brand-lockup {
display: inline-flex;
min-width: 0;
align-items: center;
color: var(--ink);
text-decoration: none;
}
.brand-logo {
width: 76px;
height: 41px;
display: block;
flex: none;
object-fit: contain;
object-position: left center;
filter: drop-shadow(0 4px 9px rgba(28, 30, 36, 0.12));
}
.page-header .stage-badge {
display: inline-flex;
align-items: center;
min-height: 30px;
margin-left: 8px;
border: 1px solid color-mix(in srgb, var(--accent) 34%, var(--line));
border-radius: 5px;
padding: 5px 12px;
background: var(--accent-soft);
color: var(--accent-strong);
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}
.header-title {
min-width: 0;
margin-left: auto;
color: var(--muted);
font-size: 12.5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.page-wrap {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 26px;
display: grid;
grid-template-columns: 216px minmax(0, 1fr);
gap: 46px;
align-items: start;
min-height: calc(100vh - 60px);
}
.page-nav {
position: sticky;
top: 88px;
max-height: calc(100vh - 108px);
overflow-y: auto;
padding: 38px 0 44px;
scrollbar-width: thin;
}
.page-main {
min-width: 0;
max-width: 890px;
padding: 38px 0 96px;
}
h1, h2, h3, h4 {
color: var(--ink);
line-height: 1.16;
font-weight: 650;
letter-spacing: -0.022em;
}
h1 { margin: 0 0 20px; font-size: clamp(2rem, 4vw, 2.65rem); letter-spacing: -0.035em; }
h2 {
margin: 54px 0 16px;
padding-bottom: 10px;
border-bottom: 1px solid var(--line);
font-size: 1.36rem;
}
h3 { margin: 34px 0 11px; font-size: 1.08rem; }
h4 { margin: 25px 0 8px; font-size: 0.96rem; color: var(--ink-soft); }
h2[id], h3[id], h4[id] { scroll-margin-top: calc(var(--header-height) + 24px); }
.heading-anchor {
display: inline-grid;
width: 1.05em;
height: 1.05em;
place-items: center;
margin-left: -1.05em;
color: var(--faint);
font-family: var(--font-mono);
font-size: .72em;
text-decoration: none;
opacity: 0;
transform: translateX(-4px);
transition: color var(--motion-fast) ease, opacity var(--motion-fast) ease, transform var(--motion-fast) ease;
}
.heading-anchor::before { content: "#"; }
h2[id]:hover > .heading-anchor,
h3[id]:hover > .heading-anchor,
h4[id]:hover > .heading-anchor,
.heading-anchor:focus-visible {
color: var(--accent-strong);
opacity: 1;
transform: translateX(0);
}
p { margin: 0 0 15px; }
strong { font-weight: 650; color: var(--ink); }
code {
background: var(--code-bg);
border: 1px solid var(--code-bd);
border-radius: 5px;
color: var(--accent-strong);
padding: 2px 6px;
font-family: var(--font-mono);
font-size: 0.86em;
overflow-wrap: anywhere;
}
pre {
position: relative;
overflow-x: auto;
margin: 0 0 20px;
border: 0;
border: 1px solid #27242c;
border-radius: 9px;
background: var(--terminal);
color: #e6e6e6;
padding: 18px 20px;
font-family: var(--font-mono);
font-size: 0.82rem;
line-height: 1.72;
}
pre code { background: none; border: 0; color: inherit; padding: 0; }
.code-block {
overflow: hidden;
margin: 18px 0 26px;
border: 1px solid #3a3d51;
border-radius: 8px;
background: #292d3e;
box-shadow: 0 18px 42px rgba(28, 30, 36, .12);
}
.code-block-toolbar {
display: flex;
min-height: 42px;
align-items: center;
justify-content: space-between;
gap: 16px;
border-bottom: 1px solid #3a3d51;
background: #232635;
padding: 7px 9px 7px 14px;
}
.code-block-language {
display: inline-flex;
align-items: center;
gap: 9px;
color: #a6accd;
font-family: var(--font-mono);
font-size: .59rem;
font-weight: 650;
letter-spacing: .1em;
text-transform: uppercase;
}
.code-block-language::before {
content: "";
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent);
box-shadow: 13px 0 0 #6f6976, 26px 0 0 #403c45;
margin-right: 24px;
}
.code-copy-button {
min-height: 30px;
border: 1px solid #45495f;
border-radius: 5px;
background: transparent;
color: #d9dbea;
padding: 5px 9px;
font-family: var(--font-mono);
font-size: .58rem;
cursor: pointer;
}
.code-copy-button:hover,
.code-copy-button:focus-visible {
border-color: var(--accent);
color: #fff;
}
.code-copy-button[data-copied="true"] {
border-color: var(--success);
color: #9be2a8;
}
.code-block pre {
max-height: 520px;
margin: 0;
border: 0;
border-radius: 0;
background: transparent;
padding: 15px 0 18px;
}
.code-block pre code {
display: grid;
min-width: max-content;
counter-reset: code-line;
}
.code-line {
position: relative;
display: block;
min-height: 1.72em;
padding: 0 20px 0 52px;
counter-increment: code-line;
white-space: pre;
}
.code-line::before {
content: counter(code-line);
position: absolute;
left: 16px;
width: 22px;
color: #676e95;
text-align: right;
}
.code-line:hover { background: rgba(235, 160, 110, .055); }
.tok-comment { color: #676e95; font-style: italic; }
.tok-keyword { color: #c792ea; font-style: italic; }
.tok-string { color: #c3e88d; }
.tok-number { color: #f78c6c; }
.tok-type, .tok-function { color: #82aaff; }
.tok-property { color: #ffcb6b; }
.tok-operator { color: #89ddff; }
ul, ol { margin: 0 0 17px; padding-left: 23px; }
li { margin: 0 0 7px; padding-left: 2px; }
li::marker { color: var(--muted); }
.living-check-item {
list-style: none;
margin-left: -23px;
padding: 0;
}
.living-check-item label,
.task-check-control {
display: grid;
grid-template-columns: 20px minmax(0, 1fr);
gap: 12px;
align-items: center;
min-height: 44px;
padding: 9px 12px;
border-radius: var(--radius-control);
cursor: pointer;
transition: background-color var(--motion-fast) ease, color var(--motion-fast) ease;
}
.living-check-item label:hover,
.task-check-control:hover {
background: color-mix(in srgb, var(--accent-soft) 55%, transparent);
}
[data-living-check] {
width: 20px;
height: 20px;
margin: 0;
accent-color: var(--accent);
cursor: pointer;
}
[data-living-check]:checked + .living-check-label,
[data-living-check]:checked ~ .living-check-label {
color: var(--muted);
text-decoration: line-through;
text-decoration-thickness: 1px;
}
.task-check-control {
grid-template-columns: 20px auto minmax(0, 1fr);
width: fit-content;
margin: 10px 0 14px;
color: var(--muted);
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.02em;
}
.task-check-id { color: var(--accent-strong); font-weight: 650; }
.living-progress {
display: grid;
grid-template-columns: auto 1fr;
gap: 6px 14px;
align-items: center;
margin: 0 0 22px;
color: var(--ink-soft);
font-family: var(--font-mono);
font-size: 0.75rem;
}
.living-progress-note {
justify-self: end;
color: var(--muted);
font-family: var(--font-sans);
font-size: 0.72rem;
}
.living-progress-track {
grid-column: 1 / -1;
height: 3px;
overflow: hidden;
border-radius: 999px;
background: var(--surface-strong);
}
.living-progress-value {
display: block;
width: 0;
height: 100%;
background: var(--accent);
transition: width 240ms cubic-bezier(.2, .7, .2, 1);
}
blockquote {
margin: 0 0 18px;
border: 0;
border-left: 3px solid var(--accent);
border-radius: 0 9px 9px 0;
background: var(--surface-soft);
color: var(--ink-soft);
padding: 15px 18px;
}
hr { margin: 40px 0; border: 0; border-top: 1px solid var(--line); }
.muted { color: var(--muted); font-size: 0.9rem; }
.panel {
min-width: 0;
margin: 0 0 18px;
border: 1px solid var(--line);
border-radius: 10px;
background: var(--panel);
padding: 21px 23px;
}
.panel h2:first-child, .panel h3:first-child { margin-top: 0; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 14px;
margin: 0 0 18px;
}
.card {
min-width: 0;
border: 0;
border: 1px solid var(--line);
border-radius: 10px;
background: var(--panel);
padding: 18px;
box-shadow: var(--shadow);
}
.metric {
display: block;
color: var(--accent-strong);
font-size: 2rem;
font-weight: 650;
letter-spacing: -0.04em;
line-height: 1;
}
.label { color: var(--muted); font-size: 0.82rem; }
.badge-row { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 0; }
.badge {
display: inline-flex;
align-items: center;
min-height: 28px;
border: 1px solid color-mix(in srgb, var(--accent) 42%, var(--line));
border-radius: 5px;
background: var(--accent-soft);
color: var(--accent-strong);
padding: 4px 11px;
font-size: 0.82rem;
font-weight: 600;
}
.risk { color: var(--risk); font-weight: 600; }
.warn { color: var(--warn); }
.data-table-wrap {
max-width: 100%;
overflow-x: auto;
margin: 0 0 20px;
border: 1px solid var(--line);
border-radius: var(--radius-table);
background: color-mix(in srgb, var(--panel) 92%, transparent);
scrollbar-width: thin;
}
table {
width: 100%;
min-width: 100%;
margin: 0;
border-collapse: collapse;
font-size: 0.86rem;
}
th {
border-bottom: 1px solid var(--line);
background: color-mix(in srgb, var(--accent) 10%, var(--surface-soft));
color: var(--muted);
padding: 12px 15px;
text-align: left;
font-size: 0.68rem;
font-weight: 650;
letter-spacing: 0.08em;
text-transform: uppercase;
white-space: nowrap;
}
td {
border-bottom: 1px solid var(--line-soft);
color: var(--ink-soft);
padding: 13px 15px;
text-align: left;
vertical-align: top;
line-height: 1.55;
}
tr:last-child td { border-bottom: 0; }
td:first-child { color: var(--ink); }
tbody tr {
transition: background-color var(--motion-fast) ease;
}
tbody tr:hover {
background: color-mix(in srgb, var(--accent-soft) 34%, transparent);
}
details { margin: 0 0 13px; }
summary {
min-height: 44px;
cursor: pointer;
list-style: none;
color: var(--accent-strong);
padding: 10px 0;
font-weight: 600;
user-select: none;
}
summary::-webkit-details-marker { display: none; }
summary::before { content: "+"; display: inline-block; width: 20px; font-family: var(--font-mono); }
details[open] summary::before { content: "−"; }
.mermaid-wrap { margin: 18px 0; }
.mermaid-fallback { display: block; }
.mermaid-diagram { display: none; }
.toc-label {
display: block;
padding: 0 0 12px;
color: var(--muted);
font-size: 0.66rem;
font-weight: 650;
letter-spacing: 0.13em;
text-transform: uppercase;
}
.toc-list { margin: 0; padding: 0; list-style: none; }
.toc-list li { margin: 0; padding: 0; }
.toc-list a {
display: block;
border: 0;
border-radius: 4px;
color: var(--muted);
padding: 6px 8px 6px 12px;
font-size: 0.8rem;
line-height: 1.45;
text-decoration: none;
transition: color 150ms ease, border-color 150ms ease, transform 150ms ease;
}
.toc-list a:hover,
.toc-list a:focus,
.toc-list a[aria-current="location"] {
color: var(--ink);
transform: translateX(2px);
}
.toc-list a[aria-current="location"] {
background: var(--accent-soft);
font-weight: 650;
}
.toc-l2 {
position: relative;
padding-left: 29px !important;
font-size: 0.76rem !important;
}
.toc-l2::before {
content: "";
position: absolute;
left: 18px;
top: 50%;
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--line);
transform: translateY(-50%);
}
.toc-l2[aria-current="location"]::before { background: var(--accent); }
.toc-l2[aria-current="location"]::after {
content: "ativo";
float: right;
color: var(--accent-strong);
font-family: var(--font-mono);
font-size: 0.58rem;
font-weight: 650;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.toc-l3 { padding-left: 36px !important; font-size: 0.72rem !important; }
@media (max-width: 900px) {
.header-title { display: none; }
.page-wrap { grid-template-columns: 1fr; gap: 0; padding: 0 22px; }
.page-nav { display: none; }
.page-main { max-width: none; padding: 30px 0 80px; }
.data-table-wrap[data-columns="wide"] table { min-width: 720px; }
}
@media (max-width: 520px) {
body { font-size: 16px; }
.header-inner { height: 58px; padding: 0 16px; }
.brand-logo { width: 68px; height: 30px; }
.page-header .stage-badge {
min-width: 0;
max-width: 54vw;
overflow: hidden;
text-overflow: ellipsis;
}
.page-wrap { padding: 0 16px; }
.page-main { padding-top: 24px; }
h1 { font-size: clamp(2rem, 10vw, 2.55rem); }
h2 { margin-top: 46px; }
.panel { padding: 18px; border-radius: 9px; }
}
@media (prefers-reduced-motion: no-preference) {
.page-main { animation: quiet-enter 360ms cubic-bezier(.2, .7, .2, 1) both; }
@keyframes quiet-enter {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
}
}
@media (prefers-contrast: more) {
:root { --line: currentColor; }
.panel, .card { border: 2px solid currentColor; box-shadow: none; }
}
@media print {
:root {
--bg: #ffffff;
--panel: #ffffff;
--surface-soft: #f5f0ea;
--surface-strong: #eee7df;
--ink: #1e1712;
--ink-soft: #3f352e;
--muted: #655d57;
--line: #cfc4b9;
--line-soft: #e2d8ce;
}
body { background: #ffffff; background-image: none; color: #1e1712; font-size: 11pt; }
.skip-link, .page-nav { display: none !important; }
.page-header { position: static; backdrop-filter: none; }
.header-inner { height: auto; padding: 0 0 10px; border-bottom: 1px solid #999999; }
.header-title { display: block; }
.page-wrap { display: block; max-width: none; min-height: auto; padding: 0; }
.page-main { max-width: none; padding: 16px 0 0; animation: none; }
.panel, .card { break-inside: avoid; box-shadow: none; }
a { color: inherit; text-decoration: underline; }
pre, table { break-inside: avoid; }
.data-table-wrap { overflow: visible; border-color: #cfc4b9; }
.living-progress-note { display: none; }
.code-copy-button { display: none; }
}
</style>
{% block head_extra %}{% endblock %}
</head>
<body>
<a class="skip-link" href="#main-content">Pular para o conteúdo</a>
<header class="page-header">
<div class="header-inner">
<a class="brand-lockup" href="#main-content" aria-label="SDD">
<img class="brand-logo" src="{{ brand_logo_data_uri|safe }}" width="814" height="439" alt="SDD">
</a>
<span class="stage-badge">{{ stage }}</span>
<span class="header-title">{{ title }}</span>
</div>
</header>
<div class="page-wrap">
<nav class="page-nav" aria-label="Índice">
{% include "html/partials/toc.html" %}
</nav>
<main class="page-main" id="main-content">
{% block content %}{% endblock %}
</main>
</div>
<script>
(() => {
const languageNames = {
bash: "Shell", shell: "Shell", sh: "Shell",
js: "JavaScript", javascript: "JavaScript",
ts: "TypeScript", typescript: "TypeScript",
rs: "Rust", rust: "Rust",
py: "Python", python: "Python",
yml: "YAML", yaml: "YAML", json: "JSON",
sql: "SQL", swift: "Swift", http: "HTTP", toml: "TOML",
};
const copySource = async (value, button) => {
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;
button.dataset.copied = "true";
button.textContent = "Copiado";
window.setTimeout(() => {
button.dataset.copied = "false";
button.textContent = "Copiar";
}, 1400);
};
const languageKeywords = {
swift: ["actor", "associatedtype", "async", "await", "case", "class", "enum", "extension", "func", "guard", "import", "in", "init", "let", "protocol", "public", "return", "self", "struct", "switch", "throws", "var", "where"],
rust: ["as", "async", "await", "const", "crate", "dyn", "else", "enum", "fn", "for", "impl", "in", "let", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "struct", "trait", "type", "use", "where"],
js: ["async", "await", "break", "case", "catch", "class", "const", "continue", "default", "else", "export", "extends", "false", "finally", "for", "from", "function", "if", "import", "in", "instanceof", "let", "new", "null", "of", "return", "static", "switch", "this", "throw", "true", "try", "typeof", "undefined", "var", "while"],
ts: ["as", "async", "await", "class", "const", "declare", "else", "enum", "export", "extends", "false", "for", "from", "function", "if", "implements", "import", "in", "interface", "keyof", "let", "new", "null", "private", "protected", "public", "readonly", "return", "static", "this", "true", "type", "typeof", "undefined"],
python: ["and", "as", "async", "await", "class", "def", "elif", "else", "except", "False", "finally", "for", "from", "if", "import", "in", "is", "lambda", "None", "not", "or", "pass", "raise", "return", "True", "try", "while", "with", "yield"],
sql: ["alter", "and", "as", "by", "case", "create", "delete", "desc", "distinct", "drop", "else", "end", "from", "group", "having", "in", "index", "insert", "into", "join", "limit", "not", "null", "on", "or", "order", "select", "set", "table", "then", "union", "unique", "update", "values", "when", "where"],
shell: ["case", "do", "done", "elif", "else", "esac", "export", "fi", "for", "function", "if", "in", "then", "while"],
};
const codeDialect = (language) => {
if (["javascript", "jsx"].includes(language)) return "js";
if (["typescript", "tsx"].includes(language)) return "ts";
if (["py"].includes(language)) return "python";
if (["rs"].includes(language)) return "rust";
if (["bash", "sh"].includes(language)) return "shell";
return language;
};
const appendToken = (parent, value, kind = "") => {
const token = document.createElement(kind ? "span" : "span");
if (kind) token.className = `tok-${kind}`;
token.textContent = value;
parent.append(token);
};
const highlightLine = (row, line, language) => {
const dialect = codeDialect(language);
const keywords = new Set(languageKeywords[dialect] || []);
const commentMarker = ["python", "shell", "yaml", "yml"].includes(dialect) ? "#" : "//";
const pattern = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b\d+(?:\.\d+)?\b|\b[A-Za-z_$][\w$]*\b|\/\/.*$|#.*$|--.*$|=>|->|::|===|!==|==|!=|<=|>=|&&|\|\||[{}()[\].,:;=+\-*/<>!?&|])/g;
let cursor = 0;
let match;
while ((match = pattern.exec(line))) {
if (match.index > cursor) appendToken(row, line.slice(cursor, match.index));
const value = match[0];
let kind = "";
if (value.startsWith(commentMarker) || (dialect === "sql" && value.startsWith("--"))) kind = "comment";
else if (/^["'`]/.test(value)) kind = "string";
else if (/^\d/.test(value)) kind = "number";
else if (keywords.has(value) || keywords.has(value.toLowerCase())) kind = "keyword";
else if (/^[A-Z][\w$]*$/.test(value)) kind = "type";
else if (/^[A-Za-z_$][\w$]*$/.test(value) && line.slice(pattern.lastIndex).trimStart().startsWith("(")) kind = "function";
else if (/^[A-Za-z_$][\w$]*$/.test(value) && line.slice(0, match.index).trimEnd().endsWith(".")) kind = "property";
else if (/^[{}()[\].,:;=+\-*/<>!?&|]+$/.test(value) || ["=>", "->", "::"].includes(value)) kind = "operator";
appendToken(row, value, kind);
cursor = pattern.lastIndex;
if (kind === "comment") {
if (cursor < line.length) appendToken(row, line.slice(cursor), "comment");
cursor = line.length;
break;
}
}
if (cursor < line.length) appendToken(row, line.slice(cursor));
if (!line.length) appendToken(row, " ");
};
document.querySelectorAll("pre:not(.mermaid-fallback)").forEach((pre) => {
if (pre.closest(".code-block")) return;
const code = pre.querySelector("code");
if (!code) return;
const original = code.textContent.replace(/\n$/, "");
const languageClass = [...code.classList].find((name) => name.startsWith("language-"));
const language = languageClass ? languageClass.slice(9).toLowerCase() : "text";
const shell = document.createElement("section");
shell.className = "code-block";
shell.dataset.codeBlock = "";
shell.dataset.language = language;
const toolbar = document.createElement("header");
toolbar.className = "code-block-toolbar";
const label = document.createElement("span");
label.className = "code-block-language";
label.textContent = languageNames[language] || language || "Código";
const copy = document.createElement("button");
copy.className = "code-copy-button";
copy.type = "button";
copy.textContent = "Copiar";
copy.setAttribute("aria-label", `Copiar trecho de código ${label.textContent}`);
copy.addEventListener("click", () => copySource(original, copy));
toolbar.append(label, copy);
code.replaceChildren();
original.split("\n").forEach((line) => {
const row = document.createElement("span");
row.className = "code-line";
highlightLine(row, line, language);
code.append(row);
});
pre.before(shell);
shell.append(toolbar, pre);
});
document.querySelectorAll("table").forEach((table) => {
if (table.closest(".source-table-wrap, .data-table-wrap")) return;
const wrapper = document.createElement("div");
wrapper.className = "data-table-wrap";
const columns = table.querySelectorAll("thead th").length;
wrapper.dataset.columns = columns >= 4 ? "wide" : "compact";
wrapper.tabIndex = 0;
wrapper.setAttribute("role", "region");
wrapper.setAttribute("aria-label", "Tabela rolável horizontalmente");
table.before(wrapper);
wrapper.append(table);
});
document.querySelectorAll("section[id] > h2:not([id]), section[id] > h3:not([id])").forEach((heading) => {
const section = heading.parentElement;
heading.id = section.id;
section.removeAttribute("id");
});
const headingIds = new Map();
document.querySelectorAll("h2[id], h3[id], h4[id]").forEach((heading) => {
const originalId = heading.id;
const occurrence = (headingIds.get(originalId) || 0) + 1;
headingIds.set(originalId, occurrence);
if (occurrence > 1) heading.id = `${originalId}-${occurrence}`;
if (heading.querySelector(".heading-anchor")) return;
const headingLabel = heading.textContent.trim();
const anchor = document.createElement("a");
anchor.className = "heading-anchor";
anchor.href = `#${heading.id}`;
anchor.setAttribute("aria-label", `Link direto para ${headingLabel}`);
anchor.title = "Copiar link desta seção";
heading.prepend(anchor);
heading.setAttribute("aria-label", headingLabel);
});
const scope = location.pathname.replace(/\/[^/]*$/, "") || document.title;
const storageKey = `sdd-living-state:v1:${scope}`;
let state = {};
try {
state = JSON.parse(localStorage.getItem(storageKey) || "{}");
} catch (_) {
state = {};
}
const taskPattern = /^(T-\d+|TSK-\d+)\b/i;
document.querySelectorAll(".doc-content h3, .doc-content h4").forEach((heading) => {
const match = heading.textContent.trim().match(taskPattern);
if (!match || heading.nextElementSibling?.classList.contains("task-check-control")) return;
const taskId = match[1].toUpperCase();
const control = document.createElement("label");
control.className = "task-check-control";
control.innerHTML = `
<input type="checkbox" data-living-check="task-${taskId}" data-task-check="${taskId}">
<span class="task-check-id">${taskId}</span>
<span class="living-check-label">acompanhar conclusão</span>
`;
heading.insertAdjacentElement("afterend", control);
});
const checks = [...document.querySelectorAll("[data-living-check]")];
const synchronize = (key, checked, source) => {
checks.forEach((item) => {
if (item !== source && item.dataset.livingCheck === key) item.checked = checked;
});
};
checks.forEach((check) => {
const key = check.dataset.livingCheck;
if (Object.prototype.hasOwnProperty.call(state, key)) {
check.checked = Boolean(state[key]);
}
synchronize(key, check.checked, check);
});
const progressHosts = [...document.querySelectorAll(".doc-content")];
if (!progressHosts.length) {
const fallback = document.querySelector(".page-main");
if (fallback) progressHosts.push(fallback);
}
const progressBlocks = progressHosts
.filter((host) => host.querySelector("[data-living-check]"))
.map((host) => {
const block = document.createElement("div");
block.className = "living-progress";
block.innerHTML = `
<span class="living-progress-label">0 de 0 concluídos</span>
<span class="living-progress-note">Acompanhamento local · não altera checkpoints</span>
<span class="living-progress-track" aria-hidden="true"><span class="living-progress-value"></span></span>
`;
host.insertAdjacentElement("afterbegin", block);
return { host, block };
});
const refreshProgress = () => {
progressBlocks.forEach(({ host, block }) => {
const scoped = [...host.querySelectorAll("[data-living-check]")];
const complete = scoped.filter((item) => item.checked).length;
const total = scoped.length;
block.querySelector(".living-progress-label").textContent =
`${complete} de ${total} concluídos`;
block.querySelector(".living-progress-value").style.width =
`${total ? (complete / total) * 100 : 0}%`;
});
};
checks.forEach((check) => {
check.addEventListener("change", () => {
const key = check.dataset.livingCheck;
state[key] = check.checked;
synchronize(key, check.checked, check);
try {
localStorage.setItem(storageKey, JSON.stringify(state));
} catch (_) {
}
refreshProgress();
document.dispatchEvent(new CustomEvent("sdd:living-check-change", {
detail: { key, checked: check.checked },
}));
});
});
refreshProgress();
document.dispatchEvent(new CustomEvent("sdd:living-ready", {
detail: { storageKey },
}));
})();
</script>
</body>
</html>