portable-agent-layer 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -60,10 +60,13 @@ Run with **Node**, not Bun — Playwright's `chromium.launch()` hangs under Bun
60
60
  │ ├── layout.tsx # font wiring (Inter + Source Serif 4)
61
61
  │ ├── page.tsx # the report's layout — edit freely
62
62
  │ └── globals.css # design tokens via @theme + custom CSS
63
- ├── components/ # 9 ported components — edit if you need new shapes
63
+ ├── components/ # report primitives — edit if you need new shapes
64
64
  │ ├── cover-page.tsx
65
+ │ ├── table-of-contents.tsx
65
66
  │ ├── section.tsx
66
67
  │ ├── exhibit.tsx
68
+ │ ├── stat-grid.tsx
69
+ │ ├── comparison-table.tsx
67
70
  │ ├── finding-card.tsx
68
71
  │ ├── recommendation-card.tsx
69
72
  │ ├── severity-badge.tsx
@@ -83,8 +86,11 @@ Run with **Node**, not Bun — Playwright's `chromium.launch()` hangs under Bun
83
86
  ## Component Cheatsheet
84
87
 
85
88
  - `<CoverPage clientName reportTitle reportDate classification consultancyName preTitle />` — full-bleed cover
86
- - `<Section title>`top-level section with bottom-rule heading
89
+ - `<TableOfContents items={[{id, title}, …]} title?>` linked TOC, anchors to section IDs
90
+ - `<Section id title>` — top-level section with bottom-rule heading; `id` enables TOC links
87
91
  - `<Exhibit number title source?>` — bordered card for figures, tables, side data
92
+ - `<StatGrid stats={[{value, label, caption?}, …]} />` — large-number grid for executive summary
93
+ - `<ComparisonTable leftLabel rightLabel rows={[{metric, left, right}, …]} metricLabel?>` — current vs. target side-by-side
88
94
  - `<FindingCard finding={f} index={i} />` — driven by `Finding` type, includes severity badge
89
95
  - `<RecommendationCard recommendation={r} index={i} />` — driven by `Recommendation` type, includes priority badge
90
96
  - `<Callout label?>` — left-rule emphasis block (default label "Key Takeaway")
@@ -266,6 +266,121 @@ body {
266
266
  margin-top: 0.25rem;
267
267
  }
268
268
 
269
+ /* Table of Contents */
270
+ .toc {
271
+ page-break-after: always;
272
+ margin-bottom: 3rem;
273
+ }
274
+ .toc h2 {
275
+ font-family: var(--font-heading);
276
+ font-size: 1.75rem;
277
+ font-weight: 600;
278
+ color: var(--color-foreground);
279
+ margin-bottom: 1.5rem;
280
+ padding-bottom: 0.5rem;
281
+ border-bottom: 2px solid var(--color-primary);
282
+ }
283
+ .toc ol {
284
+ list-style: none;
285
+ padding-left: 0;
286
+ margin: 0;
287
+ }
288
+ .toc li {
289
+ margin: 0.75rem 0;
290
+ border-bottom: 1px dotted var(--color-border-emphasis);
291
+ padding-bottom: 0.5rem;
292
+ }
293
+ .toc a {
294
+ display: flex;
295
+ gap: 1rem;
296
+ align-items: baseline;
297
+ color: var(--color-foreground);
298
+ text-decoration: none;
299
+ }
300
+ .toc-number {
301
+ font-family: var(--font-sans);
302
+ font-weight: 600;
303
+ font-size: 0.875rem;
304
+ color: var(--color-primary);
305
+ width: 2rem;
306
+ flex-shrink: 0;
307
+ }
308
+ .toc-title {
309
+ font-family: var(--font-heading);
310
+ font-weight: 500;
311
+ font-size: 1rem;
312
+ }
313
+
314
+ /* Stat grid */
315
+ .stat-grid {
316
+ display: grid;
317
+ gap: 1.5rem;
318
+ margin: 1.5rem 0;
319
+ padding: 1.5rem;
320
+ background: var(--color-background-secondary);
321
+ border-radius: 0.5rem;
322
+ border: 1px solid var(--color-border);
323
+ page-break-inside: avoid;
324
+ }
325
+ .stat {
326
+ text-align: left;
327
+ }
328
+ .stat-value {
329
+ font-family: var(--font-sans);
330
+ font-size: 2.5rem;
331
+ font-weight: 700;
332
+ color: var(--color-primary);
333
+ letter-spacing: -0.02em;
334
+ line-height: 1;
335
+ }
336
+ .stat-label {
337
+ margin-top: 0.5rem;
338
+ font-family: var(--font-sans);
339
+ font-size: 0.8125rem;
340
+ font-weight: 600;
341
+ color: var(--color-foreground);
342
+ }
343
+ .stat-caption {
344
+ margin-top: 0.25rem;
345
+ font-family: var(--font-body);
346
+ font-size: 0.8125rem;
347
+ color: var(--color-muted);
348
+ }
349
+
350
+ /* Comparison table */
351
+ .comparison-table {
352
+ width: 100%;
353
+ border-collapse: collapse;
354
+ margin: 1.5rem 0;
355
+ font-family: var(--font-body);
356
+ font-size: 0.9375rem;
357
+ page-break-inside: avoid;
358
+ }
359
+ .comparison-table th {
360
+ font-family: var(--font-sans);
361
+ font-size: 0.7rem;
362
+ font-weight: 600;
363
+ text-transform: uppercase;
364
+ letter-spacing: 0.08em;
365
+ color: var(--color-primary);
366
+ padding: 0.75rem 1rem;
367
+ border-bottom: 2px solid var(--color-primary);
368
+ text-align: left;
369
+ }
370
+ .comparison-table td {
371
+ padding: 0.75rem 1rem;
372
+ border-bottom: 1px solid var(--color-border-subtle);
373
+ vertical-align: top;
374
+ }
375
+ .comparison-table .metric {
376
+ font-family: var(--font-sans);
377
+ font-weight: 600;
378
+ color: var(--color-foreground);
379
+ }
380
+ .comparison-table tbody tr:last-child td {
381
+ border-bottom: none;
382
+ }
383
+
269
384
  /* Cover page */
270
385
  .cover-page {
271
386
  min-height: 100vh;
@@ -1,13 +1,28 @@
1
1
  import { AlertTriangle, CheckCircle2, Lightbulb, Target } from "lucide-react";
2
2
  import { Callout } from "@/components/callout";
3
+ import { ComparisonTable } from "@/components/comparison-table";
3
4
  import { CoverPage } from "@/components/cover-page";
4
5
  import { Exhibit } from "@/components/exhibit";
5
6
  import { FindingCard } from "@/components/finding-card";
6
7
  import { RecommendationCard } from "@/components/recommendation-card";
7
8
  import { Section } from "@/components/section";
9
+ import { StatGrid } from "@/components/stat-grid";
10
+ import { TableOfContents } from "@/components/table-of-contents";
8
11
  import { Timeline } from "@/components/timeline";
9
12
  import { reportData } from "@/lib/report-data";
10
13
 
14
+ const sections = [
15
+ { id: "executive-summary", title: "Executive Summary" },
16
+ { id: "situation-assessment", title: "Situation Assessment" },
17
+ { id: "key-findings", title: "Key Findings" },
18
+ { id: "risk-analysis", title: "Risk Analysis" },
19
+ { id: "strategic-opportunity", title: "Strategic Opportunity" },
20
+ { id: "recommendations", title: "Strategic Recommendations" },
21
+ { id: "target-state", title: "Target State Vision" },
22
+ { id: "roadmap", title: "Implementation Roadmap" },
23
+ { id: "call-to-action", title: "Call to Action" },
24
+ ];
25
+
11
26
  export default function ReportPage() {
12
27
  const data = reportData;
13
28
 
@@ -23,29 +38,34 @@ export default function ReportPage() {
23
38
  />
24
39
 
25
40
  <div className="report-container">
26
- <Section title="Executive Summary">
41
+ <TableOfContents items={sections} />
42
+
43
+ <Section id="executive-summary" title="Executive Summary">
27
44
  <p className="text-lg mb-6">{data.executiveSummary.context}</p>
28
45
 
29
- <Exhibit number={1} title="Assessment Methodology">
30
- <div className="flex items-start gap-8">
31
- <div>
32
- <p className="text-3xl font-bold text-primary">
33
- {data.executiveSummary.methodology.interviewCount}
34
- </p>
35
- <p className="text-sm text-muted">Interviews Conducted</p>
36
- </div>
37
- <div className="flex-1">
38
- <p className="text-sm font-semibold text-foreground mb-2">
39
- Roles Interviewed:
46
+ <StatGrid
47
+ stats={[
48
+ {
49
+ value: String(data.executiveSummary.methodology.interviewCount),
50
+ label: "Interviews",
51
+ caption: "across leadership and engineering",
52
+ },
53
+ { value: "3×", label: "MTTR", caption: "vs. peer benchmark" },
54
+ {
55
+ value: "71%",
56
+ label: "Concentration",
57
+ caption: "on-call held by 3 engineers",
58
+ },
59
+ ]}
60
+ />
61
+
62
+ <Exhibit number={1} title="Roles Interviewed">
63
+ <div className="grid grid-cols-2 gap-1">
64
+ {data.executiveSummary.methodology.roles.map((role) => (
65
+ <p key={role} className="text-sm text-muted">
66
+ {role}
40
67
  </p>
41
- <div className="grid grid-cols-2 gap-1">
42
- {data.executiveSummary.methodology.roles.map((role) => (
43
- <p key={role} className="text-sm text-muted">
44
- {role}
45
- </p>
46
- ))}
47
- </div>
48
- </div>
68
+ ))}
49
69
  </div>
50
70
  </Exhibit>
51
71
 
@@ -75,7 +95,7 @@ export default function ReportPage() {
75
95
  </ul>
76
96
  </Section>
77
97
 
78
- <Section title="Situation Assessment">
98
+ <Section id="situation-assessment" title="Situation Assessment">
79
99
  <div className="grid gap-6">
80
100
  <div>
81
101
  <h3 className="text-lg font-semibold mb-2 flex items-center gap-2">
@@ -95,7 +115,7 @@ export default function ReportPage() {
95
115
  </div>
96
116
  </Section>
97
117
 
98
- <Section title="Key Findings">
118
+ <Section id="key-findings" title="Key Findings">
99
119
  <p className="text-muted mb-6">
100
120
  Our analysis identified {data.findings.length} significant findings that
101
121
  require attention. Each finding is supported by evidence gathered during our
@@ -108,7 +128,7 @@ export default function ReportPage() {
108
128
  </div>
109
129
  </Section>
110
130
 
111
- <Section title="Risk Analysis">
131
+ <Section id="risk-analysis" title="Risk Analysis">
112
132
  <div className="grid gap-6">
113
133
  <Exhibit number={3} title="Existential Risks">
114
134
  <div className="space-y-3">
@@ -142,7 +162,7 @@ export default function ReportPage() {
142
162
  </div>
143
163
  </Section>
144
164
 
145
- <Section title="Strategic Opportunity">
165
+ <Section id="strategic-opportunity" title="Strategic Opportunity">
146
166
  <Callout label="The Path Forward">{data.strategicOpportunity.goodNews}</Callout>
147
167
 
148
168
  <h3 className="text-lg font-semibold mt-6 mb-3 flex items-center gap-2">
@@ -162,7 +182,7 @@ export default function ReportPage() {
162
182
  </ul>
163
183
  </Section>
164
184
 
165
- <Section title="Strategic Recommendations">
185
+ <Section id="recommendations" title="Strategic Recommendations">
166
186
  <p className="text-muted mb-6">
167
187
  Recommendations are prioritized by urgency and impact.
168
188
  </p>
@@ -173,9 +193,36 @@ export default function ReportPage() {
173
193
  </div>
174
194
  </Section>
175
195
 
176
- <Section title="Target State Vision">
196
+ <Section id="target-state" title="Target State Vision">
177
197
  <p className="text-lg mb-6">{data.targetState.description}</p>
178
198
 
199
+ <ComparisonTable
200
+ leftLabel="Today"
201
+ rightLabel="Target"
202
+ rows={[
203
+ {
204
+ metric: "MTTR",
205
+ left: "3× peer benchmark",
206
+ right: "Within 1.5× peer benchmark",
207
+ },
208
+ {
209
+ metric: "Rollback path",
210
+ left: "Manual, untested",
211
+ right: "Single-command, validated monthly",
212
+ },
213
+ {
214
+ metric: "On-call coverage",
215
+ left: "3 engineers carry 71%",
216
+ right: "8 engineers, max 15% each",
217
+ },
218
+ {
219
+ metric: "Runbook freshness",
220
+ left: "7 of 14 stale (>18 months)",
221
+ right: "Tracked as service-owner KPI",
222
+ },
223
+ ]}
224
+ />
225
+
179
226
  <Exhibit number={5} title="Key Capabilities Enabled">
180
227
  <div className="grid md:grid-cols-3 gap-4">
181
228
  {data.targetState.keyCapabilities.map((capability) => (
@@ -201,7 +248,7 @@ export default function ReportPage() {
201
248
  </ul>
202
249
  </Section>
203
250
 
204
- <Section title="Implementation Roadmap">
251
+ <Section id="roadmap" title="Implementation Roadmap">
205
252
  <p className="text-muted mb-6">
206
253
  The transformation should be executed in phases, with clear milestones and
207
254
  decision points.
@@ -211,7 +258,7 @@ export default function ReportPage() {
211
258
  </Exhibit>
212
259
  </Section>
213
260
 
214
- <Section title="Call to Action">
261
+ <Section id="call-to-action" title="Call to Action">
215
262
  <div className="bg-primary/5 rounded-2xl p-8 border border-primary/20">
216
263
  <h3 className="text-xl font-semibold mb-4">Immediate Next Steps</h3>
217
264
  <ol className="space-y-3 mb-6">
@@ -0,0 +1,40 @@
1
+ interface ComparisonRow {
2
+ metric: string;
3
+ left: string;
4
+ right: string;
5
+ }
6
+
7
+ interface ComparisonTableProps {
8
+ leftLabel: string;
9
+ rightLabel: string;
10
+ rows: ComparisonRow[];
11
+ metricLabel?: string;
12
+ }
13
+
14
+ export function ComparisonTable({
15
+ leftLabel,
16
+ rightLabel,
17
+ rows,
18
+ metricLabel = "Metric",
19
+ }: ComparisonTableProps) {
20
+ return (
21
+ <table className="comparison-table">
22
+ <thead>
23
+ <tr>
24
+ <th>{metricLabel}</th>
25
+ <th>{leftLabel}</th>
26
+ <th>{rightLabel}</th>
27
+ </tr>
28
+ </thead>
29
+ <tbody>
30
+ {rows.map((row) => (
31
+ <tr key={row.metric}>
32
+ <td className="metric">{row.metric}</td>
33
+ <td>{row.left}</td>
34
+ <td>{row.right}</td>
35
+ </tr>
36
+ ))}
37
+ </tbody>
38
+ </table>
39
+ );
40
+ }
@@ -1,14 +1,15 @@
1
1
  import { cn } from "@/lib/utils";
2
2
 
3
3
  interface SectionProps {
4
+ id?: string;
4
5
  title: string;
5
6
  children: React.ReactNode;
6
7
  className?: string;
7
8
  }
8
9
 
9
- export function Section({ title, children, className }: SectionProps) {
10
+ export function Section({ id, title, children, className }: SectionProps) {
10
11
  return (
11
- <section className={cn("report-section", className)}>
12
+ <section id={id} className={cn("report-section", className)}>
12
13
  <h2>{title}</h2>
13
14
  {children}
14
15
  </section>
@@ -0,0 +1,26 @@
1
+ interface Stat {
2
+ value: string;
3
+ label: string;
4
+ caption?: string;
5
+ }
6
+
7
+ interface StatGridProps {
8
+ stats: Stat[];
9
+ }
10
+
11
+ export function StatGrid({ stats }: StatGridProps) {
12
+ return (
13
+ <div
14
+ className="stat-grid"
15
+ style={{ gridTemplateColumns: `repeat(${stats.length}, minmax(0, 1fr))` }}
16
+ >
17
+ {stats.map((s) => (
18
+ <div key={s.label} className="stat">
19
+ <div className="stat-value">{s.value}</div>
20
+ <div className="stat-label">{s.label}</div>
21
+ {s.caption && <div className="stat-caption">{s.caption}</div>}
22
+ </div>
23
+ ))}
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,27 @@
1
+ interface TocItem {
2
+ id: string;
3
+ title: string;
4
+ }
5
+
6
+ interface TableOfContentsProps {
7
+ items: TocItem[];
8
+ title?: string;
9
+ }
10
+
11
+ export function TableOfContents({ items, title = "Contents" }: TableOfContentsProps) {
12
+ return (
13
+ <nav className="toc">
14
+ <h2>{title}</h2>
15
+ <ol>
16
+ {items.map((item, i) => (
17
+ <li key={item.id}>
18
+ <a href={`#${item.id}`}>
19
+ <span className="toc-number">{(i + 1).toString().padStart(2, "0")}</span>
20
+ <span className="toc-title">{item.title}</span>
21
+ </a>
22
+ </li>
23
+ ))}
24
+ </ol>
25
+ </nav>
26
+ );
27
+ }
@@ -266,6 +266,121 @@ body {
266
266
  margin-top: 0.25rem;
267
267
  }
268
268
 
269
+ /* Table of Contents */
270
+ .toc {
271
+ page-break-after: always;
272
+ margin-bottom: 3rem;
273
+ }
274
+ .toc h2 {
275
+ font-family: var(--font-heading);
276
+ font-size: 1.75rem;
277
+ font-weight: 600;
278
+ color: var(--color-foreground);
279
+ margin-bottom: 1.5rem;
280
+ padding-bottom: 0.5rem;
281
+ border-bottom: 2px solid var(--color-primary);
282
+ }
283
+ .toc ol {
284
+ list-style: none;
285
+ padding-left: 0;
286
+ margin: 0;
287
+ }
288
+ .toc li {
289
+ margin: 0.75rem 0;
290
+ border-bottom: 1px dotted var(--color-border-emphasis);
291
+ padding-bottom: 0.5rem;
292
+ }
293
+ .toc a {
294
+ display: flex;
295
+ gap: 1rem;
296
+ align-items: baseline;
297
+ color: var(--color-foreground);
298
+ text-decoration: none;
299
+ }
300
+ .toc-number {
301
+ font-family: var(--font-sans);
302
+ font-weight: 600;
303
+ font-size: 0.875rem;
304
+ color: var(--color-primary);
305
+ width: 2rem;
306
+ flex-shrink: 0;
307
+ }
308
+ .toc-title {
309
+ font-family: var(--font-heading);
310
+ font-weight: 500;
311
+ font-size: 1rem;
312
+ }
313
+
314
+ /* Stat grid */
315
+ .stat-grid {
316
+ display: grid;
317
+ gap: 1.5rem;
318
+ margin: 1.5rem 0;
319
+ padding: 1.5rem;
320
+ background: var(--color-background-secondary);
321
+ border-radius: 0.5rem;
322
+ border: 1px solid var(--color-border);
323
+ page-break-inside: avoid;
324
+ }
325
+ .stat {
326
+ text-align: left;
327
+ }
328
+ .stat-value {
329
+ font-family: var(--font-sans);
330
+ font-size: 2.5rem;
331
+ font-weight: 700;
332
+ color: var(--color-primary);
333
+ letter-spacing: -0.02em;
334
+ line-height: 1;
335
+ }
336
+ .stat-label {
337
+ margin-top: 0.5rem;
338
+ font-family: var(--font-sans);
339
+ font-size: 0.8125rem;
340
+ font-weight: 600;
341
+ color: var(--color-foreground);
342
+ }
343
+ .stat-caption {
344
+ margin-top: 0.25rem;
345
+ font-family: var(--font-body);
346
+ font-size: 0.8125rem;
347
+ color: var(--color-muted);
348
+ }
349
+
350
+ /* Comparison table */
351
+ .comparison-table {
352
+ width: 100%;
353
+ border-collapse: collapse;
354
+ margin: 1.5rem 0;
355
+ font-family: var(--font-body);
356
+ font-size: 0.9375rem;
357
+ page-break-inside: avoid;
358
+ }
359
+ .comparison-table th {
360
+ font-family: var(--font-sans);
361
+ font-size: 0.7rem;
362
+ font-weight: 600;
363
+ text-transform: uppercase;
364
+ letter-spacing: 0.08em;
365
+ color: var(--color-primary);
366
+ padding: 0.75rem 1rem;
367
+ border-bottom: 2px solid var(--color-primary);
368
+ text-align: left;
369
+ }
370
+ .comparison-table td {
371
+ padding: 0.75rem 1rem;
372
+ border-bottom: 1px solid var(--color-border-subtle);
373
+ vertical-align: top;
374
+ }
375
+ .comparison-table .metric {
376
+ font-family: var(--font-sans);
377
+ font-weight: 600;
378
+ color: var(--color-foreground);
379
+ }
380
+ .comparison-table tbody tr:last-child td {
381
+ border-bottom: none;
382
+ }
383
+
269
384
  /* Cover page */
270
385
  .cover-page {
271
386
  min-height: 100vh;
@@ -1,13 +1,28 @@
1
1
  import { AlertTriangle, CheckCircle2, Lightbulb, Target } from "lucide-react";
2
2
  import { Callout } from "@/components/callout";
3
+ import { ComparisonTable } from "@/components/comparison-table";
3
4
  import { CoverPage } from "@/components/cover-page";
4
5
  import { Exhibit } from "@/components/exhibit";
5
6
  import { FindingCard } from "@/components/finding-card";
6
7
  import { RecommendationCard } from "@/components/recommendation-card";
7
8
  import { Section } from "@/components/section";
9
+ import { StatGrid } from "@/components/stat-grid";
10
+ import { TableOfContents } from "@/components/table-of-contents";
8
11
  import { Timeline } from "@/components/timeline";
9
12
  import { reportData } from "@/lib/report-data";
10
13
 
14
+ const sections = [
15
+ { id: "executive-summary", title: "Executive Summary" },
16
+ { id: "situation-assessment", title: "Situation Assessment" },
17
+ { id: "key-findings", title: "Key Findings" },
18
+ { id: "risk-analysis", title: "Risk Analysis" },
19
+ { id: "strategic-opportunity", title: "Strategic Opportunity" },
20
+ { id: "recommendations", title: "Strategic Recommendations" },
21
+ { id: "target-state", title: "Target State Vision" },
22
+ { id: "roadmap", title: "Implementation Roadmap" },
23
+ { id: "call-to-action", title: "Call to Action" },
24
+ ];
25
+
11
26
  export default function ReportPage() {
12
27
  const data = reportData;
13
28
 
@@ -23,29 +38,29 @@ export default function ReportPage() {
23
38
  />
24
39
 
25
40
  <div className="report-container">
26
- <Section title="Executive Summary">
41
+ <TableOfContents items={sections} />
42
+
43
+ <Section id="executive-summary" title="Executive Summary">
27
44
  <p className="text-lg mb-6">{data.executiveSummary.context}</p>
28
45
 
29
- <Exhibit number={1} title="Assessment Methodology">
30
- <div className="flex items-start gap-8">
31
- <div>
32
- <p className="text-3xl font-bold text-primary">
33
- {data.executiveSummary.methodology.interviewCount}
34
- </p>
35
- <p className="text-sm text-muted">Interviews Conducted</p>
36
- </div>
37
- <div className="flex-1">
38
- <p className="text-sm font-semibold text-foreground mb-2">
39
- Roles Interviewed:
46
+ <StatGrid
47
+ stats={[
48
+ {
49
+ value: String(data.executiveSummary.methodology.interviewCount),
50
+ label: "Interviews",
51
+ },
52
+ { value: "—", label: "Headline metric", caption: "replace with your own" },
53
+ { value: "—", label: "Headline metric", caption: "replace with your own" },
54
+ ]}
55
+ />
56
+
57
+ <Exhibit number={1} title="Roles Interviewed">
58
+ <div className="grid grid-cols-2 gap-1">
59
+ {data.executiveSummary.methodology.roles.map((role) => (
60
+ <p key={role} className="text-sm text-muted">
61
+ {role}
40
62
  </p>
41
- <div className="grid grid-cols-2 gap-1">
42
- {data.executiveSummary.methodology.roles.map((role) => (
43
- <p key={role} className="text-sm text-muted">
44
- {role}
45
- </p>
46
- ))}
47
- </div>
48
- </div>
63
+ ))}
49
64
  </div>
50
65
  </Exhibit>
51
66
 
@@ -75,7 +90,7 @@ export default function ReportPage() {
75
90
  </ul>
76
91
  </Section>
77
92
 
78
- <Section title="Situation Assessment">
93
+ <Section id="situation-assessment" title="Situation Assessment">
79
94
  <div className="grid gap-6">
80
95
  <div>
81
96
  <h3 className="text-lg font-semibold mb-2 flex items-center gap-2">
@@ -95,7 +110,7 @@ export default function ReportPage() {
95
110
  </div>
96
111
  </Section>
97
112
 
98
- <Section title="Key Findings">
113
+ <Section id="key-findings" title="Key Findings">
99
114
  <p className="text-muted mb-6">
100
115
  Our analysis identified {data.findings.length} significant findings that
101
116
  require attention. Each finding is supported by evidence gathered during our
@@ -108,7 +123,7 @@ export default function ReportPage() {
108
123
  </div>
109
124
  </Section>
110
125
 
111
- <Section title="Risk Analysis">
126
+ <Section id="risk-analysis" title="Risk Analysis">
112
127
  <div className="grid gap-6">
113
128
  <Exhibit number={3} title="Existential Risks">
114
129
  <div className="space-y-3">
@@ -142,7 +157,7 @@ export default function ReportPage() {
142
157
  </div>
143
158
  </Section>
144
159
 
145
- <Section title="Strategic Opportunity">
160
+ <Section id="strategic-opportunity" title="Strategic Opportunity">
146
161
  <Callout label="The Path Forward">{data.strategicOpportunity.goodNews}</Callout>
147
162
 
148
163
  <h3 className="text-lg font-semibold mt-6 mb-3 flex items-center gap-2">
@@ -162,7 +177,7 @@ export default function ReportPage() {
162
177
  </ul>
163
178
  </Section>
164
179
 
165
- <Section title="Strategic Recommendations">
180
+ <Section id="recommendations" title="Strategic Recommendations">
166
181
  <p className="text-muted mb-6">
167
182
  Recommendations are prioritized by urgency and impact.
168
183
  </p>
@@ -173,9 +188,21 @@ export default function ReportPage() {
173
188
  </div>
174
189
  </Section>
175
190
 
176
- <Section title="Target State Vision">
191
+ <Section id="target-state" title="Target State Vision">
177
192
  <p className="text-lg mb-6">{data.targetState.description}</p>
178
193
 
194
+ <ComparisonTable
195
+ leftLabel="Today"
196
+ rightLabel="Target"
197
+ rows={[
198
+ {
199
+ metric: "Replace with metric",
200
+ left: "Current state value",
201
+ right: "Target state value",
202
+ },
203
+ ]}
204
+ />
205
+
179
206
  <Exhibit number={5} title="Key Capabilities Enabled">
180
207
  <div className="grid md:grid-cols-3 gap-4">
181
208
  {data.targetState.keyCapabilities.map((capability) => (
@@ -201,7 +228,7 @@ export default function ReportPage() {
201
228
  </ul>
202
229
  </Section>
203
230
 
204
- <Section title="Implementation Roadmap">
231
+ <Section id="roadmap" title="Implementation Roadmap">
205
232
  <p className="text-muted mb-6">
206
233
  The transformation should be executed in phases, with clear milestones and
207
234
  decision points.
@@ -211,7 +238,7 @@ export default function ReportPage() {
211
238
  </Exhibit>
212
239
  </Section>
213
240
 
214
- <Section title="Call to Action">
241
+ <Section id="call-to-action" title="Call to Action">
215
242
  <div className="bg-primary/5 rounded-2xl p-8 border border-primary/20">
216
243
  <h3 className="text-xl font-semibold mb-4">Immediate Next Steps</h3>
217
244
  <ol className="space-y-3 mb-6">
@@ -0,0 +1,40 @@
1
+ interface ComparisonRow {
2
+ metric: string;
3
+ left: string;
4
+ right: string;
5
+ }
6
+
7
+ interface ComparisonTableProps {
8
+ leftLabel: string;
9
+ rightLabel: string;
10
+ rows: ComparisonRow[];
11
+ metricLabel?: string;
12
+ }
13
+
14
+ export function ComparisonTable({
15
+ leftLabel,
16
+ rightLabel,
17
+ rows,
18
+ metricLabel = "Metric",
19
+ }: ComparisonTableProps) {
20
+ return (
21
+ <table className="comparison-table">
22
+ <thead>
23
+ <tr>
24
+ <th>{metricLabel}</th>
25
+ <th>{leftLabel}</th>
26
+ <th>{rightLabel}</th>
27
+ </tr>
28
+ </thead>
29
+ <tbody>
30
+ {rows.map((row) => (
31
+ <tr key={row.metric}>
32
+ <td className="metric">{row.metric}</td>
33
+ <td>{row.left}</td>
34
+ <td>{row.right}</td>
35
+ </tr>
36
+ ))}
37
+ </tbody>
38
+ </table>
39
+ );
40
+ }
@@ -1,14 +1,15 @@
1
1
  import { cn } from "@/lib/utils";
2
2
 
3
3
  interface SectionProps {
4
+ id?: string;
4
5
  title: string;
5
6
  children: React.ReactNode;
6
7
  className?: string;
7
8
  }
8
9
 
9
- export function Section({ title, children, className }: SectionProps) {
10
+ export function Section({ id, title, children, className }: SectionProps) {
10
11
  return (
11
- <section className={cn("report-section", className)}>
12
+ <section id={id} className={cn("report-section", className)}>
12
13
  <h2>{title}</h2>
13
14
  {children}
14
15
  </section>
@@ -0,0 +1,26 @@
1
+ interface Stat {
2
+ value: string;
3
+ label: string;
4
+ caption?: string;
5
+ }
6
+
7
+ interface StatGridProps {
8
+ stats: Stat[];
9
+ }
10
+
11
+ export function StatGrid({ stats }: StatGridProps) {
12
+ return (
13
+ <div
14
+ className="stat-grid"
15
+ style={{ gridTemplateColumns: `repeat(${stats.length}, minmax(0, 1fr))` }}
16
+ >
17
+ {stats.map((s) => (
18
+ <div key={s.label} className="stat">
19
+ <div className="stat-value">{s.value}</div>
20
+ <div className="stat-label">{s.label}</div>
21
+ {s.caption && <div className="stat-caption">{s.caption}</div>}
22
+ </div>
23
+ ))}
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,27 @@
1
+ interface TocItem {
2
+ id: string;
3
+ title: string;
4
+ }
5
+
6
+ interface TableOfContentsProps {
7
+ items: TocItem[];
8
+ title?: string;
9
+ }
10
+
11
+ export function TableOfContents({ items, title = "Contents" }: TableOfContentsProps) {
12
+ return (
13
+ <nav className="toc">
14
+ <h2>{title}</h2>
15
+ <ol>
16
+ {items.map((item, i) => (
17
+ <li key={item.id}>
18
+ <a href={`#${item.id}`}>
19
+ <span className="toc-number">{(i + 1).toString().padStart(2, "0")}</span>
20
+ <span className="toc-title">{item.title}</span>
21
+ </a>
22
+ </li>
23
+ ))}
24
+ </ol>
25
+ </nav>
26
+ );
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {