Files
Panama/user/agents/skills/ticket/templates/plan.html
T

423 lines
15 KiB
HTML

<!doctype html>
<!--
plan.html template for the ticket skill. Copy this file next to plan.md and fill
every slot marked "slot:". Everything outside a slot stays as it is, so every plan
page looks and behaves the same. The page is self-contained: no network, no
external assets, opens from file://.
Slots, in order:
KEY, TITLE, JIRA_URL, BRANCH, KIND the header line
criteria one <li> per acceptance criterion
questions one <li> per open question, or the "None" line
approach the Approach narrative as <p> blocks
steps one <li> per step, with a badge for (mock) / (deliverable)
risks one <li> per risk or edge case
tests one <li> per test plan item
mocks one <template> per artboard, plus its tab button;
delete the whole <section id="mocks"> and its
nav link when the ticket changes nothing a user sees
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- slot: KEY and TITLE -->
<title>KACP-00000 plan</title>
<style>
:root {
--bg: #f7f7f5;
--surface: #ffffff;
--ink: #1c1c1a;
--muted: #6b6b66;
--line: #e2e2dd;
--accent: #2f5fd0;
--accent-ink: #ffffff;
--warn-bg: #fff6e0;
--warn-line: #e6c36b;
--badge-mock: #e8f0ff;
--badge-deliverable: #eaf7ea;
--mono: ui-monospace, "JetBrains Mono", "Cascadia Mono", Menlo, monospace;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, "Inter", sans-serif;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #151614;
--surface: #1e1f1c;
--ink: #ecebe6;
--muted: #9b9a93;
--line: #2e2f2b;
--accent: #7c9cf0;
--accent-ink: #0f1420;
--warn-bg: #2b2513;
--warn-line: #7a6427;
--badge-mock: #1f2a44;
--badge-deliverable: #1d2f1f;
}
}
:root[data-theme="dark"] {
--bg: #151614;
--surface: #1e1f1c;
--ink: #ecebe6;
--muted: #9b9a93;
--line: #2e2f2b;
--accent: #7c9cf0;
--accent-ink: #0f1420;
--warn-bg: #2b2513;
--warn-line: #7a6427;
--badge-mock: #1f2a44;
--badge-deliverable: #1d2f1f;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font: 16px/1.55 var(--sans);
}
a { color: var(--accent); }
code { font-family: var(--mono); font-size: 0.92em; }
pre {
font-family: var(--mono);
font-size: 0.88em;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 6px;
padding: 12px 14px;
overflow-x: auto;
}
.layout {
display: grid;
grid-template-columns: 220px minmax(0, 1fr);
gap: 32px;
max-width: 1180px;
margin: 0 auto;
padding: 24px 16px 64px;
}
nav {
position: sticky;
top: 24px;
align-self: start;
font-size: 0.92em;
}
nav ol { list-style: none; margin: 0; padding: 0; }
nav li { margin: 0 0 6px; }
nav a { color: var(--muted); text-decoration: none; }
nav a:hover, nav a.active { color: var(--ink); }
nav .progress {
margin-top: 18px;
padding-top: 14px;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: 0.9em;
}
header.plan { margin-bottom: 28px; }
header.plan h1 { margin: 0 0 6px; font-size: 1.7em; line-height: 1.2; }
header.plan .meta {
display: flex;
flex-wrap: wrap;
gap: 6px 18px;
color: var(--muted);
font-size: 0.92em;
}
header.plan .meta code { color: var(--ink); }
section { margin-bottom: 36px; }
section h2 {
font-size: 1.15em;
margin: 0 0 12px;
padding-bottom: 6px;
border-bottom: 1px solid var(--line);
}
section p { margin: 0 0 12px; max-width: 72ch; }
section ul { padding-left: 22px; margin: 0; }
section li { margin: 0 0 8px; max-width: 72ch; }
.checklist { list-style: none; padding: 0; }
.checklist li { display: flex; gap: 10px; align-items: baseline; }
.checklist input { margin: 0; flex: none; position: relative; top: 2px; }
.checklist li.done label { color: var(--muted); text-decoration: line-through; }
.badge {
display: inline-block;
font-size: 0.75em;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
padding: 1px 7px;
border-radius: 999px;
margin-left: 6px;
vertical-align: middle;
}
.badge.mock { background: var(--badge-mock); }
.badge.deliverable { background: var(--badge-deliverable); }
.callout {
background: var(--warn-bg);
border: 1px solid var(--warn-line);
border-radius: 6px;
padding: 12px 16px;
}
.callout ul { margin: 0; }
.callout.quiet { background: var(--surface); border-color: var(--line); color: var(--muted); }
/* Mocks: one artboard per <template>, shown in an iframe so the mock's CSS stays its own. */
.mock-bar {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
margin-bottom: 12px;
}
.mock-bar .spacer { flex: 1; }
.mock-bar button {
font: inherit;
font-size: 0.9em;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 6px;
padding: 5px 12px;
cursor: pointer;
}
.mock-bar button[aria-pressed="true"] {
background: var(--accent);
color: var(--accent-ink);
border-color: var(--accent);
}
.mock-stage {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
padding: 16px;
overflow-x: auto;
}
.mock-stage iframe {
display: block;
width: 100%;
max-width: 100%;
height: 480px;
margin: 0 auto;
border: 1px solid var(--line);
border-radius: 6px;
background: #fff;
}
.mock-stage[data-width="tablet"] iframe { width: 820px; }
.mock-stage[data-width="phone"] iframe { width: 390px; }
.mock-note { color: var(--muted); font-size: 0.92em; margin-top: 10px; }
.mock-note:empty { display: none; }
@media (max-width: 820px) {
.layout { grid-template-columns: 1fr; gap: 16px; }
nav { position: static; }
nav ol { display: flex; flex-wrap: wrap; gap: 4px 14px; }
nav .progress { display: none; }
.mock-stage[data-width="tablet"] iframe,
.mock-stage[data-width="phone"] iframe { width: 100%; }
}
</style>
</head>
<body>
<div class="layout">
<nav aria-label="Sections">
<ol>
<li><a href="#criteria">Acceptance criteria</a></li>
<li><a href="#questions">Open questions</a></li>
<li><a href="#approach">Approach</a></li>
<!-- slot: mocks nav link, delete with the section when there are no mocks -->
<li><a href="#mocks">Mocks</a></li>
<li><a href="#steps">Steps</a></li>
<li><a href="#risks">Risks and edge cases</a></li>
<li><a href="#tests">Test plan</a></li>
</ol>
<div class="progress" id="progress"></div>
</nav>
<main>
<header class="plan">
<!-- slot: KEY, TITLE, JIRA_URL, BRANCH, KIND (bounded or architectural) -->
<h1>KACP-00000: Short title of the ticket</h1>
<div class="meta">
<span><a href="https://ksense-tech.atlassian.net/browse/KACP-00000">Open in Jira</a></span>
<span>Branch <code>KACP-00000-Short-Title</code></span>
<span>Classified <code>bounded</code></span>
</div>
</header>
<section id="criteria">
<h2>Acceptance criteria</h2>
<p class="mock-note">Tick a box as you confirm the plan covers it. Ticks stay in this browser only.</p>
<ul class="checklist" data-persist="criteria">
<!-- slot: criteria, one <li> per criterion, restated precisely from the ticket -->
<li><input type="checkbox" id="ac-1"><label for="ac-1">First acceptance criterion</label></li>
</ul>
</section>
<section id="questions">
<h2>Open questions</h2>
<!-- slot: questions. Use class="callout" with one <li> per question that needs an answer
before or during implementation. With none, keep the quiet version below instead. -->
<div class="callout">
<ul>
<li>A question whose answer changes the plan. Name the options and which one the plan assumes.</li>
</ul>
</div>
<!-- <div class="callout quiet">None. Nothing here needs an answer before implementation.</div> -->
</section>
<section id="approach">
<h2>Approach</h2>
<!-- slot: approach, the plan.md narrative as <p> blocks; <code> for file and symbol names -->
<p>What will change, why, which files and modules are involved, and how it fits the patterns already in the codebase.</p>
</section>
<!-- slot: mocks. Keep this section only when a step changes what a user sees.
One <template> per artboard: a <style> block followed by the screen's markup, rendered
in its own iframe so nothing leaks between the mock and this page.
Several artboards when the direction is the user's call, one when it is settled.
The data-note attribute states what the artboard shows that the implementation will not do. -->
<section id="mocks">
<h2>Mocks</h2>
<div class="mock-bar" role="tablist">
<button type="button" data-mock="mock-a" aria-pressed="true">Option A</button>
<button type="button" data-mock="mock-b" aria-pressed="false">Option B</button>
<span class="spacer"></span>
<button type="button" data-width="desktop" aria-pressed="true">Desktop</button>
<button type="button" data-width="tablet" aria-pressed="false">Tablet</button>
<button type="button" data-width="phone" aria-pressed="false">Phone</button>
</div>
<div class="mock-stage" data-width="desktop">
<iframe title="Mock" sandbox="allow-same-origin"></iframe>
<div class="mock-note"></div>
</div>
<template id="mock-a" data-note="Option A keeps the existing layout and adds the new column at the end.">
<style>
body { margin: 0; padding: 24px; font: 14px/1.5 system-ui, sans-serif; color: #1c1c1a; background: #fff; }
</style>
<h1 style="font-size:18px;margin:0 0 12px">Screen name</h1>
<p>Draw the intended screen here with plain HTML and its own style block.</p>
</template>
<template id="mock-b" data-note="Option B moves the action into the row menu.">
<style>
body { margin: 0; padding: 24px; font: 14px/1.5 system-ui, sans-serif; color: #1c1c1a; background: #fff; }
</style>
<h1 style="font-size:18px;margin:0 0 12px">Screen name</h1>
<p>Second option.</p>
</template>
</section>
<section id="steps">
<h2>Steps</h2>
<ul class="checklist" data-persist="steps">
<!-- slot: steps, one <li> per step, roughly one commit each.
Add <span class="badge mock">mock</span> or <span class="badge deliverable">deliverable</span>
after the label text for steps marked that way in plan.md. -->
<li><input type="checkbox" id="step-1"><label for="step-1">First step</label></li>
<li><input type="checkbox" id="step-2"><label for="step-2">A step that builds a mock</label><span class="badge mock">mock</span></li>
</ul>
</section>
<section id="risks">
<h2>Risks and edge cases considered</h2>
<ul>
<!-- slot: risks, one <li> per edge case: the case, then how the plan handles it -->
<li><strong>Edge case.</strong> How the plan handles it.</li>
</ul>
</section>
<section id="tests">
<h2>Test plan</h2>
<ul>
<!-- slot: tests, one <li> per item: which criterion or ticket test case it proves and how -->
<li><strong>Criterion or ticket case.</strong> The suite to run or the test to add, and what it proves.</li>
</ul>
</section>
</main>
</div>
<script>
(() => {
const key = location.pathname;
// Checkbox ticks persist per page in this browser. Storage can be unavailable on file://
// in some browsers, so every access is guarded.
const store = {
get(name) { try { return JSON.parse(localStorage.getItem(key + ':' + name) || '[]'); } catch { return []; } },
set(name, ids) { try { localStorage.setItem(key + ':' + name, JSON.stringify(ids)); } catch {} },
};
const lists = [...document.querySelectorAll('.checklist[data-persist]')];
const progress = document.getElementById('progress');
const paint = () => {
const boxes = lists.flatMap(l => [...l.querySelectorAll('input[type="checkbox"]')]);
boxes.forEach(b => b.closest('li').classList.toggle('done', b.checked));
if (progress) progress.textContent = boxes.filter(b => b.checked).length + ' of ' + boxes.length + ' reviewed';
};
lists.forEach(list => {
const name = list.dataset.persist;
const saved = new Set(store.get(name));
list.querySelectorAll('input[type="checkbox"]').forEach(box => {
box.checked = saved.has(box.id);
box.addEventListener('change', () => {
const ids = [...list.querySelectorAll('input:checked')].map(b => b.id);
store.set(name, ids);
paint();
});
});
});
paint();
// Mock tabs and width toggle. Each <template> is a full HTML document loaded into the iframe.
const mocks = document.getElementById('mocks');
if (mocks) {
const frame = mocks.querySelector('iframe');
const stage = mocks.querySelector('.mock-stage');
const note = mocks.querySelector('.mock-note');
const press = (buttons, active) => buttons.forEach(b => b.setAttribute('aria-pressed', String(b === active)));
const mockButtons = [...mocks.querySelectorAll('button[data-mock]')];
const widthButtons = [...mocks.querySelectorAll('button[data-width]')];
const fit = () => {
try {
const doc = frame.contentDocument;
if (doc && doc.documentElement) frame.style.height = Math.max(320, doc.documentElement.scrollHeight + 2) + 'px';
} catch {}
};
const show = id => {
const tpl = document.getElementById(id);
if (!tpl) return;
frame.srcdoc = tpl.innerHTML;
note.textContent = tpl.dataset.note || '';
press(mockButtons, mockButtons.find(b => b.dataset.mock === id));
};
frame.addEventListener('load', fit);
mockButtons.forEach(b => b.addEventListener('click', () => show(b.dataset.mock)));
widthButtons.forEach(b => b.addEventListener('click', () => {
stage.dataset.width = b.dataset.width;
press(widthButtons, b);
setTimeout(fit, 50);
}));
if (mockButtons[0]) show(mockButtons[0].dataset.mock);
}
// Highlight the section in view in the nav.
const links = [...document.querySelectorAll('nav a[href^="#"]')];
const byId = new Map(links.map(a => [a.getAttribute('href').slice(1), a]));
const observer = new IntersectionObserver(entries => {
entries.forEach(e => {
const a = byId.get(e.target.id);
if (a && e.isIntersecting) { links.forEach(l => l.classList.remove('active')); a.classList.add('active'); }
});
}, { rootMargin: '-10% 0px -70% 0px' });
byId.forEach((_, id) => { const el = document.getElementById(id); if (el) observer.observe(el); });
})();
</script>
</body>
</html>