@profpowell/vb-project-planning 0.1.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.
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/vb-project-planning.css +1603 -0
- package/dist/vb-project-planning.js +11138 -0
- package/dist/vb-project-planning.min.css +1 -0
- package/dist/vb-project-planning.min.js +3312 -0
- package/package.json +80 -0
- package/src/index.css +14 -0
- package/src/index.js +26 -0
- package/src/lib/bundle-registry.js +156 -0
- package/src/lib/diff-by-key.js +86 -0
- package/src/lib/portrait-url.js +87 -0
- package/src/lib/vb-collection.js +122 -0
- package/src/lib/vb-element.js +96 -0
- package/src/lib/vb-store.js +200 -0
- package/src/lib/vb-view-transition.js +44 -0
- package/src/utils/copy-init.js +287 -0
- package/src/web-components/_ux-base.js +106 -0
- package/src/web-components/adr-wc/README.md +77 -0
- package/src/web-components/adr-wc/api.json +26 -0
- package/src/web-components/adr-wc/logic.js +376 -0
- package/src/web-components/adr-wc/static.html +17 -0
- package/src/web-components/adr-wc/styles.js +338 -0
- package/src/web-components/burndown-chart/api.json +74 -0
- package/src/web-components/burndown-chart/logic.js +198 -0
- package/src/web-components/burndown-chart/static.html +31 -0
- package/src/web-components/burndown-chart/styles.css +59 -0
- package/src/web-components/capacity-plan/_capacity-utils.js +56 -0
- package/src/web-components/capacity-plan/api.json +20 -0
- package/src/web-components/capacity-plan/logic.js +222 -0
- package/src/web-components/capacity-plan/static.html +24 -0
- package/src/web-components/capacity-plan/styles.css +84 -0
- package/src/web-components/empathy-map/api.json +31 -0
- package/src/web-components/empathy-map/logic.js +502 -0
- package/src/web-components/empathy-map/static.html +32 -0
- package/src/web-components/empathy-map/styles.js +534 -0
- package/src/web-components/gantt-chart/README.md +91 -0
- package/src/web-components/gantt-chart/api.json +20 -0
- package/src/web-components/gantt-chart/logic.js +812 -0
- package/src/web-components/gantt-chart/static.html +60 -0
- package/src/web-components/gantt-chart/styles.css +391 -0
- package/src/web-components/impact-effort/api.json +21 -0
- package/src/web-components/impact-effort/logic.js +392 -0
- package/src/web-components/impact-effort/static.html +23 -0
- package/src/web-components/impact-effort/styles.css +193 -0
- package/src/web-components/iron-triangle/_capacity.js +83 -0
- package/src/web-components/iron-triangle/_triangle-geometry.js +322 -0
- package/src/web-components/iron-triangle/api.json +26 -0
- package/src/web-components/iron-triangle/logic.js +474 -0
- package/src/web-components/iron-triangle/static.html +28 -0
- package/src/web-components/iron-triangle/styles.css +158 -0
- package/src/web-components/kanban-board/api.json +24 -0
- package/src/web-components/kanban-board/logic.js +698 -0
- package/src/web-components/kanban-board/static.html +20 -0
- package/src/web-components/kanban-board/styles.css +242 -0
- package/src/web-components/product-roadmap/api.json +118 -0
- package/src/web-components/product-roadmap/logic.js +460 -0
- package/src/web-components/product-roadmap/static.html +44 -0
- package/src/web-components/product-roadmap/styles.css +158 -0
- package/src/web-components/quadrant-grid/api.json +125 -0
- package/src/web-components/quadrant-grid/logic.js +397 -0
- package/src/web-components/quadrant-grid/static.html +49 -0
- package/src/web-components/quadrant-grid/styles.css +184 -0
- package/src/web-components/quality-target/_quality-utils.js +460 -0
- package/src/web-components/quality-target/api.json +30 -0
- package/src/web-components/quality-target/logic.js +516 -0
- package/src/web-components/quality-target/static.html +22 -0
- package/src/web-components/quality-target/styles.css +203 -0
- package/src/web-components/requirement-card/api.json +25 -0
- package/src/web-components/requirement-card/logic.js +166 -0
- package/src/web-components/requirement-card/static.html +36 -0
- package/src/web-components/requirement-card/styles.js +114 -0
- package/src/web-components/review-surface/README.md +178 -0
- package/src/web-components/review-surface/api.json +30 -0
- package/src/web-components/review-surface/logic.js +807 -0
- package/src/web-components/review-surface/static.html +33 -0
- package/src/web-components/review-surface/styles.js +459 -0
- package/src/web-components/risk-register/api.json +47 -0
- package/src/web-components/risk-register/logic.js +180 -0
- package/src/web-components/risk-register/static.html +46 -0
- package/src/web-components/risk-register/styles.css +37 -0
- package/src/web-components/story-map/api.json +22 -0
- package/src/web-components/story-map/logic.js +445 -0
- package/src/web-components/story-map/static.html +35 -0
- package/src/web-components/story-map/styles.css +169 -0
- package/src/web-components/traceability-matrix/api.json +84 -0
- package/src/web-components/traceability-matrix/logic.js +243 -0
- package/src/web-components/traceability-matrix/static.html +41 -0
- package/src/web-components/traceability-matrix/styles.css +94 -0
- package/src/web-components/user-journey/api.json +84 -0
- package/src/web-components/user-journey/logic.js +368 -0
- package/src/web-components/user-journey/static.html +14 -0
- package/src/web-components/user-journey/styles.js +317 -0
- package/src/web-components/user-persona/api.json +35 -0
- package/src/web-components/user-persona/logic.js +502 -0
- package/src/web-components/user-persona/static.html +20 -0
- package/src/web-components/user-persona/styles.js +299 -0
- package/src/web-components/user-story/api.json +35 -0
- package/src/web-components/user-story/logic.js +567 -0
- package/src/web-components/user-story/static.html +27 -0
- package/src/web-components/user-story/styles.js +332 -0
- package/src/web-components/work-item/README.md +85 -0
- package/src/web-components/work-item/api.json +30 -0
- package/src/web-components/work-item/logic.js +414 -0
- package/src/web-components/work-item/static.html +24 -0
- package/src/web-components/work-item/styles.js +376 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "ADR Record Specification"
|
|
3
|
+
component: adr-record
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
status: stable
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# ADR Record
|
|
9
|
+
|
|
10
|
+
MADR-format Architectural Decision Record card with status tracking and cross-referencing.
|
|
11
|
+
|
|
12
|
+
## Purpose
|
|
13
|
+
|
|
14
|
+
Renders architectural decisions in a structured card format following the MADR (Markdown Architectural Decision Records) convention. Each card displays the decision context, the decision itself, and its consequences. ADR cards can be chained together via `supersedes` and `superseded-by` attributes to show decision evolution over time.
|
|
15
|
+
|
|
16
|
+
Part of the **UX Planning Pack**. Works standalone or inside `<kanban-board>`, `<story-map>`, or layout primitives.
|
|
17
|
+
|
|
18
|
+
## Static HTML Form
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<adr-record
|
|
22
|
+
adr-id="ADR-001"
|
|
23
|
+
title="Use PostgreSQL for persistence"
|
|
24
|
+
status="accepted"
|
|
25
|
+
date="2026-01-15"
|
|
26
|
+
superseded-by="ADR-003">
|
|
27
|
+
<p slot="context">We need a relational database for complex queries.</p>
|
|
28
|
+
<p slot="decision">Use PostgreSQL 16 with pgvector extension.</p>
|
|
29
|
+
<ul slot="consequences">
|
|
30
|
+
<li>Good: mature ecosystem, strong SQL support</li>
|
|
31
|
+
<li>Bad: operational overhead vs managed NoSQL</li>
|
|
32
|
+
</ul>
|
|
33
|
+
</adr-record>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Attributes
|
|
37
|
+
|
|
38
|
+
| Attribute | Type | Default | Description |
|
|
39
|
+
|-----------|------|---------|-------------|
|
|
40
|
+
| `adr-id` | string | -- | ADR identifier (also sets HTML `id`) |
|
|
41
|
+
| `title` | string | -- | Decision title |
|
|
42
|
+
| `status` | string | `"proposed"` | `proposed`, `accepted`, `deprecated`, `superseded` |
|
|
43
|
+
| `date` | string | -- | ISO date string |
|
|
44
|
+
| `supersedes` | string | -- | Comma-separated ADR IDs this supersedes |
|
|
45
|
+
| `superseded-by` | string | -- | Comma-separated ADR IDs that supersede this |
|
|
46
|
+
| `detail` | string | `"full"` | `full`, `compact`, `minimal` |
|
|
47
|
+
| `compact` | boolean | -- | Alias for `detail="compact"` |
|
|
48
|
+
| `src` | string | -- | JSON data URL |
|
|
49
|
+
|
|
50
|
+
## Slots
|
|
51
|
+
|
|
52
|
+
| Slot | Description |
|
|
53
|
+
|------|-------------|
|
|
54
|
+
| `context` | Why the decision was needed |
|
|
55
|
+
| `decision` | What was decided |
|
|
56
|
+
| `consequences` | Outcomes (positive and negative) |
|
|
57
|
+
|
|
58
|
+
## Events
|
|
59
|
+
|
|
60
|
+
| Event | Detail |
|
|
61
|
+
|-------|--------|
|
|
62
|
+
| `adr-record:ready` | `{ adrId, title, status }` |
|
|
63
|
+
|
|
64
|
+
## Status Colors
|
|
65
|
+
|
|
66
|
+
| Status | Color | Use Case |
|
|
67
|
+
|--------|-------|----------|
|
|
68
|
+
| `proposed` | Blue (#3b82f6) | Decision under discussion |
|
|
69
|
+
| `accepted` | Green (#22c55e) | Decision approved and active |
|
|
70
|
+
| `deprecated` | Amber (#f59e0b) | Decision no longer recommended |
|
|
71
|
+
| `superseded` | Gray (#6b7280) | Replaced by a newer decision |
|
|
72
|
+
|
|
73
|
+
## Cross-References
|
|
74
|
+
|
|
75
|
+
- `supersedes` links to older ADR elements via fragment anchors
|
|
76
|
+
- `superseded-by` links to newer ADR elements via fragment anchors
|
|
77
|
+
- `adr-id` is auto-set as the HTML `id` for inbound cross-referencing
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schemas/api.schema.json",
|
|
3
|
+
"element": "adr-wc",
|
|
4
|
+
"type": "web-component",
|
|
5
|
+
"description": "MADR-format Architectural Decision Record card with status, context, decision, consequences, and cross-referencing via supersedes chains",
|
|
6
|
+
"htmlvalidate": { "flow": true, "permittedContent": ["@flow"] },
|
|
7
|
+
"attributes": [
|
|
8
|
+
{ "name": "adr-id", "kind": "host-api", "purpose": "config", "type": "string", "description": "ADR identifier (e.g., ADR-001), auto-sets HTML id" },
|
|
9
|
+
{ "name": "status", "kind": "host-api", "purpose": "config", "type": "string", "description": "Status: proposed | accepted | deprecated | superseded" },
|
|
10
|
+
{ "name": "supersedes", "kind": "host-api", "purpose": "config", "type": "string", "description": "Comma-separated ADR IDs this supersedes" },
|
|
11
|
+
{ "name": "superseded-by", "kind": "host-api", "purpose": "config", "type": "string", "description": "Comma-separated ADR IDs that supersede this" },
|
|
12
|
+
{ "name": "detail", "kind": "host-api", "purpose": "config", "type": "string", "description": "Detail level: full | compact | minimal" },
|
|
13
|
+
{ "name": "compact", "kind": "host-api", "purpose": "config", "type": "boolean", "description": "Alias for detail=compact" },
|
|
14
|
+
{ "name": "src", "kind": "host-api", "purpose": "config", "type": "string", "description": "URL to JSON data" }
|
|
15
|
+
],
|
|
16
|
+
"slots": [
|
|
17
|
+
{ "name": "title", "description": "Decision heading (e.g., <h2>)" },
|
|
18
|
+
{ "name": "date", "description": "Decision date (e.g., <time datetime=\"...\">)" },
|
|
19
|
+
{ "name": "context", "description": "Why the decision was needed" },
|
|
20
|
+
{ "name": "decision", "description": "What was decided" },
|
|
21
|
+
{ "name": "consequences", "description": "Outcomes (positive and negative)" }
|
|
22
|
+
],
|
|
23
|
+
"events": [
|
|
24
|
+
{ "name": "adr-wc:ready", "detail": "{ adrId, title, status }", "description": "Fired after render" }
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <adr-wc> — Architectural Decision Record card (MADR format)
|
|
3
|
+
*
|
|
4
|
+
* Renders an ADR with status badge, context, decision, consequences,
|
|
5
|
+
* and chain-linking to other ADRs via supersedes / superseded-by.
|
|
6
|
+
* Part of the UX Planning Pack.
|
|
7
|
+
*
|
|
8
|
+
* Content slots (headings and dates are content, not config):
|
|
9
|
+
* @slot title - Decision heading (e.g., <h2>)
|
|
10
|
+
* @slot date - Decision date (e.g., <time datetime="...">)
|
|
11
|
+
* @slot context - Why the decision was needed
|
|
12
|
+
* @slot decision - What was decided
|
|
13
|
+
* @slot consequences - Outcomes (positive and negative)
|
|
14
|
+
*
|
|
15
|
+
* State/config attributes:
|
|
16
|
+
* @attr {string} adr-id - ADR identifier (e.g., "ADR-001"), auto-sets HTML id
|
|
17
|
+
* @attr {enum} status - proposed | accepted | deprecated | superseded
|
|
18
|
+
* @attr {string} supersedes - Comma-separated ADR IDs this supersedes
|
|
19
|
+
* @attr {string} superseded-by - Comma-separated ADR IDs that supersede this
|
|
20
|
+
* @attr {string} detail - full | compact | minimal
|
|
21
|
+
* @attr {boolean} compact - Alias for detail="compact"
|
|
22
|
+
* @attr {string} src - URL to JSON data
|
|
23
|
+
*
|
|
24
|
+
* @fires adr-wc:ready - After render with { adrId, status }
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* <adr-wc adr-id="ADR-001" status="accepted">
|
|
28
|
+
* <h2 slot="title">Use PostgreSQL for persistence</h2>
|
|
29
|
+
* <time slot="date" datetime="2026-01-15">January 15, 2026</time>
|
|
30
|
+
* <p slot="context">We need a relational database for complex queries.</p>
|
|
31
|
+
* <p slot="decision">Use PostgreSQL 16 with pgvector extension.</p>
|
|
32
|
+
* <ul slot="consequences">
|
|
33
|
+
* <li>Good: mature ecosystem, strong SQL support</li>
|
|
34
|
+
* <li>Bad: operational overhead vs managed NoSQL</li>
|
|
35
|
+
* </ul>
|
|
36
|
+
* </adr-wc>
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { styles } from './styles.js';
|
|
40
|
+
import { registerComponent } from '../../lib/bundle-registry.js';
|
|
41
|
+
import { esc, lucideSvg } from '../_ux-base.js';
|
|
42
|
+
|
|
43
|
+
/* ── Status metadata ──────────────────────────────── */
|
|
44
|
+
|
|
45
|
+
const STATUSES = {
|
|
46
|
+
proposed: { label: 'Proposed', color: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
|
|
47
|
+
accepted: { label: 'Accepted', color: '#22c55e', bg: 'rgba(34, 197, 94, 0.1)' },
|
|
48
|
+
deprecated: { label: 'Deprecated', color: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
|
49
|
+
superseded: { label: 'Superseded', color: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/* ── SVG icon fragments (Lucide) ──────────────────── */
|
|
53
|
+
|
|
54
|
+
const ICONS = {
|
|
55
|
+
calendar: '<rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/>',
|
|
56
|
+
context: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" x2="8" y1="13" y2="13"/><line x1="16" x2="8" y1="17" y2="17"/><line x1="10" x2="8" y1="9" y2="9"/>',
|
|
57
|
+
decision: '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/>',
|
|
58
|
+
consequences: '<path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/><path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/><path d="M7 21h10"/><path d="M12 3v18"/><path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"/>',
|
|
59
|
+
arrowRight: '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/* ── Component ─────────────────────────────────────── */
|
|
63
|
+
|
|
64
|
+
class AdrWc extends HTMLElement {
|
|
65
|
+
static get observedAttributes() {
|
|
66
|
+
return [
|
|
67
|
+
'adr-id', 'status',
|
|
68
|
+
'supersedes', 'superseded-by',
|
|
69
|
+
'detail', 'compact', 'src',
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#slotCache = new Map();
|
|
74
|
+
|
|
75
|
+
constructor() {
|
|
76
|
+
super();
|
|
77
|
+
this.attachShadow({ mode: 'open' });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* ── Slot caching ───────────────────────────────── */
|
|
81
|
+
|
|
82
|
+
#cacheSlotValues() {
|
|
83
|
+
for (const child of [...this.children]) {
|
|
84
|
+
const slotName = child.getAttribute('slot');
|
|
85
|
+
if (slotName) {
|
|
86
|
+
this.#slotCache.set(slotName, child.textContent.trim());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* ── Lifecycle ─────────────────────────────────── */
|
|
92
|
+
|
|
93
|
+
connectedCallback() {
|
|
94
|
+
this.#cacheSlotValues();
|
|
95
|
+
if (this.adrId && !this.id) this.id = this.adrId;
|
|
96
|
+
if (this.hasAttribute('src')) {
|
|
97
|
+
this._loadSrc(this.getAttribute('src'));
|
|
98
|
+
}
|
|
99
|
+
this.#render();
|
|
100
|
+
this.setAttribute('data-upgraded', '');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
disconnectedCallback() {
|
|
104
|
+
this.removeAttribute('data-upgraded');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
108
|
+
if (oldValue !== newValue && this.shadowRoot) {
|
|
109
|
+
if (name === 'src' && this.isConnected) {
|
|
110
|
+
this._loadSrc(newValue);
|
|
111
|
+
} else {
|
|
112
|
+
this.#render();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* ── Getters ───────────────────────────────────── */
|
|
118
|
+
|
|
119
|
+
get adrId() { return this.getAttribute('adr-id') || ''; }
|
|
120
|
+
get status() { return this.getAttribute('status') || 'proposed'; }
|
|
121
|
+
|
|
122
|
+
/** Read the title from the slotted heading's text content */
|
|
123
|
+
get adrTitle() {
|
|
124
|
+
const slotted = this.querySelector('[slot="title"]');
|
|
125
|
+
return slotted?.textContent?.trim() || this.#slotCache.get('title') || '';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Read the date from the slotted <time>'s datetime attribute or text */
|
|
129
|
+
get adrDate() {
|
|
130
|
+
const slotted = this.querySelector('[slot="date"]');
|
|
131
|
+
return slotted?.getAttribute('datetime') || slotted?.textContent?.trim() || this.#slotCache.get('date') || '';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get supersedes() {
|
|
135
|
+
const raw = this.getAttribute('supersedes') || '';
|
|
136
|
+
return raw ? raw.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
get supersededBy() {
|
|
140
|
+
const raw = this.getAttribute('superseded-by') || '';
|
|
141
|
+
return raw ? raw.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
get _detailLevel() {
|
|
145
|
+
if (this.getAttribute('detail')) return this.getAttribute('detail');
|
|
146
|
+
if (this.hasAttribute('compact')) return 'compact';
|
|
147
|
+
return 'full';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get _minimalLabel() {
|
|
151
|
+
return this.adrTitle || this.adrId || 'ADR';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* ── Data API ──────────────────────────────────── */
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Read the ADR as a plain data object. Mirrors what a consumer would
|
|
158
|
+
* assign to .data — useful for diffing or persistence.
|
|
159
|
+
*/
|
|
160
|
+
get data() {
|
|
161
|
+
return {
|
|
162
|
+
adrId: this.adrId || undefined,
|
|
163
|
+
status: this.status,
|
|
164
|
+
detail: this.getAttribute('detail') || undefined,
|
|
165
|
+
supersedes: this.supersedes.length ? this.supersedes : undefined,
|
|
166
|
+
supersededBy: this.supersededBy.length ? this.supersededBy : undefined,
|
|
167
|
+
title: this.adrTitle || undefined,
|
|
168
|
+
date: this.adrDate || undefined,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Set state attributes and slotted content from a plain object in one
|
|
174
|
+
* call. Idempotent for repeat calls.
|
|
175
|
+
*/
|
|
176
|
+
set data(value) {
|
|
177
|
+
if (!value || typeof value !== 'object') return;
|
|
178
|
+
this._applyData(value);
|
|
179
|
+
this.#cacheSlotValues();
|
|
180
|
+
if (this.shadowRoot) this.#render();
|
|
181
|
+
this.dispatchEvent(new CustomEvent('adr-wc:data-changed', {
|
|
182
|
+
detail: { data: this.data, source: 'property' },
|
|
183
|
+
bubbles: true, composed: true,
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Apply a data record to attributes + slotted children. Used by both
|
|
189
|
+
* the .data setter and async _loadSrc.
|
|
190
|
+
* @param {Record<string, unknown>} data
|
|
191
|
+
*/
|
|
192
|
+
_applyData(data) {
|
|
193
|
+
if (data.adrId != null) this.setAttribute('adr-id', String(data.adrId));
|
|
194
|
+
if (data.status != null) this.setAttribute('status', String(data.status));
|
|
195
|
+
if (data.detail != null) this.setAttribute('detail', String(data.detail));
|
|
196
|
+
|
|
197
|
+
if (data.supersedes) {
|
|
198
|
+
this.setAttribute('supersedes', Array.isArray(data.supersedes) ? data.supersedes.join(',') : String(data.supersedes));
|
|
199
|
+
}
|
|
200
|
+
if (data.supersededBy) {
|
|
201
|
+
this.setAttribute('superseded-by', Array.isArray(data.supersededBy) ? data.supersededBy.join(',') : String(data.supersededBy));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (data.title && !this.querySelector('[slot="title"]')) {
|
|
205
|
+
const h = document.createElement('h3');
|
|
206
|
+
h.slot = 'title';
|
|
207
|
+
h.textContent = String(data.title);
|
|
208
|
+
this.appendChild(h);
|
|
209
|
+
}
|
|
210
|
+
if (data.date && !this.querySelector('[slot="date"]')) {
|
|
211
|
+
const t = document.createElement('time');
|
|
212
|
+
t.slot = 'date';
|
|
213
|
+
t.setAttribute('datetime', String(data.date));
|
|
214
|
+
t.textContent = new Date(String(data.date)).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
|
|
215
|
+
this.appendChild(t);
|
|
216
|
+
}
|
|
217
|
+
for (const key of ['context', 'decision']) {
|
|
218
|
+
if (data[key] && !this.querySelector(`[slot="${key}"]`)) {
|
|
219
|
+
const p = document.createElement('p');
|
|
220
|
+
p.slot = key;
|
|
221
|
+
p.textContent = String(data[key]);
|
|
222
|
+
this.appendChild(p);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (data.consequences && !this.querySelector('[slot="consequences"]')) {
|
|
226
|
+
const ul = document.createElement('ul');
|
|
227
|
+
ul.slot = 'consequences';
|
|
228
|
+
const items = Array.isArray(data.consequences) ? data.consequences : [data.consequences];
|
|
229
|
+
for (const item of items) {
|
|
230
|
+
const li = document.createElement('li');
|
|
231
|
+
li.textContent = String(item);
|
|
232
|
+
ul.appendChild(li);
|
|
233
|
+
}
|
|
234
|
+
this.appendChild(ul);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* ── JSON loading ──────────────────────────────── */
|
|
239
|
+
|
|
240
|
+
async _loadSrc(url) {
|
|
241
|
+
if (!url) return;
|
|
242
|
+
const root = /** @type {ShadowRoot} */ (this.shadowRoot);
|
|
243
|
+
root.innerHTML = `<style>${styles}</style><div class="state-msg">Loading\u2026</div>`;
|
|
244
|
+
try {
|
|
245
|
+
const res = await fetch(url);
|
|
246
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
247
|
+
const data = await res.json();
|
|
248
|
+
|
|
249
|
+
this._applyData(data);
|
|
250
|
+
this.#cacheSlotValues();
|
|
251
|
+
this.#render();
|
|
252
|
+
} catch (err) {
|
|
253
|
+
root.innerHTML = `<style>${styles}</style>` +
|
|
254
|
+
`<div class="state-msg state-msg--error">Could not load: ${esc(/** @type {Error} */ (err).message)}</div>`;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* ── Render ────────────────────────────────────── */
|
|
259
|
+
|
|
260
|
+
#render() {
|
|
261
|
+
const root = /** @type {ShadowRoot} */ (this.shadowRoot);
|
|
262
|
+
const statusInfo = STATUSES[this.status] || STATUSES.proposed;
|
|
263
|
+
const level = this._detailLevel;
|
|
264
|
+
const ariaLabel = this.adrId ? `ADR: ${esc(this.adrId)}` : 'Architectural Decision Record';
|
|
265
|
+
const hasDate = !!this.querySelector('[slot="date"]') || this.#slotCache.has('date');
|
|
266
|
+
|
|
267
|
+
if (level === 'minimal') {
|
|
268
|
+
root.innerHTML = `<style>${styles}</style>
|
|
269
|
+
<article class="adr-card adr-card--minimal" role="article" aria-label="${ariaLabel}"
|
|
270
|
+
tabindex="0">
|
|
271
|
+
<div class="adr-body">
|
|
272
|
+
<div class="adr-meta">
|
|
273
|
+
${this.adrId ? `<span class="adr-id">${esc(this.adrId)}</span>` : ''}
|
|
274
|
+
<span class="adr-status" style="color:${statusInfo.color};background:${statusInfo.bg}">${esc(statusInfo.label)}</span>
|
|
275
|
+
</div>
|
|
276
|
+
<div class="adr-title-wrap">
|
|
277
|
+
<slot name="title"><span class="adr-title-fallback">${esc(this._minimalLabel)}</span></slot>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</article>`;
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
root.innerHTML = `<style>${styles}</style>
|
|
285
|
+
<article class="adr-card adr-card--${level}" role="article" aria-label="${ariaLabel}">
|
|
286
|
+
|
|
287
|
+
<header class="adr-header">
|
|
288
|
+
<div class="adr-meta">
|
|
289
|
+
${this.adrId ? `<span class="adr-id">${esc(this.adrId)}</span>` : ''}
|
|
290
|
+
${hasDate ? `<span class="adr-date-wrap">${lucideSvg(ICONS.calendar)} <slot name="date"></slot></span>` : ''}
|
|
291
|
+
</div>
|
|
292
|
+
<div class="adr-badges">
|
|
293
|
+
<span class="adr-status" style="color:${statusInfo.color};background:${statusInfo.bg}">${esc(statusInfo.label)}</span>
|
|
294
|
+
</div>
|
|
295
|
+
</header>
|
|
296
|
+
|
|
297
|
+
<div class="adr-body">
|
|
298
|
+
<div class="adr-title-wrap">
|
|
299
|
+
<slot name="title"><span class="adr-title-fallback">[Untitled ADR]</span></slot>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
${this.supersedes.length ? `
|
|
303
|
+
<div class="adr-links">
|
|
304
|
+
<span class="adr-links-label">Supersedes</span>
|
|
305
|
+
${this.supersedes.map(id =>
|
|
306
|
+
`<a class="adr-link" href="#${esc(id)}">${lucideSvg(ICONS.arrowRight)} ${esc(id)}</a>`
|
|
307
|
+
).join('')}
|
|
308
|
+
</div>` : ''}
|
|
309
|
+
|
|
310
|
+
${this.supersededBy.length ? `
|
|
311
|
+
<div class="adr-links">
|
|
312
|
+
<span class="adr-links-label">Superseded by</span>
|
|
313
|
+
${this.supersededBy.map(id =>
|
|
314
|
+
`<a class="adr-link" href="#${esc(id)}">${lucideSvg(ICONS.arrowRight)} ${esc(id)}</a>`
|
|
315
|
+
).join('')}
|
|
316
|
+
</div>` : ''}
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<div class="adr-sections">
|
|
320
|
+
<div class="adr-section">
|
|
321
|
+
<div class="adr-section-header">
|
|
322
|
+
<div class="adr-section-icon context">${lucideSvg(ICONS.context)}</div>
|
|
323
|
+
<span class="adr-section-title">Context</span>
|
|
324
|
+
</div>
|
|
325
|
+
<div class="adr-section-content">
|
|
326
|
+
<slot name="context"><em class="slot-fallback">No context provided.</em></slot>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<div class="adr-section">
|
|
331
|
+
<div class="adr-section-header">
|
|
332
|
+
<div class="adr-section-icon decision">${lucideSvg(ICONS.decision)}</div>
|
|
333
|
+
<span class="adr-section-title">Decision</span>
|
|
334
|
+
</div>
|
|
335
|
+
<div class="adr-section-content">
|
|
336
|
+
<slot name="decision"><em class="slot-fallback">No decision recorded.</em></slot>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<div class="adr-section">
|
|
341
|
+
<div class="adr-section-header">
|
|
342
|
+
<div class="adr-section-icon consequences">${lucideSvg(ICONS.consequences)}</div>
|
|
343
|
+
<span class="adr-section-title">Consequences</span>
|
|
344
|
+
</div>
|
|
345
|
+
<div class="adr-section-content">
|
|
346
|
+
<slot name="consequences"><em class="slot-fallback">No consequences documented.</em></slot>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
</article>`;
|
|
352
|
+
|
|
353
|
+
// Compact: mark empty slot sections
|
|
354
|
+
if (level === 'compact') {
|
|
355
|
+
for (const section of root.querySelectorAll('.adr-section')) {
|
|
356
|
+
const slot = section.querySelector('slot');
|
|
357
|
+
if (slot && slot.assignedNodes().length === 0) {
|
|
358
|
+
section.setAttribute('data-empty', '');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this.dispatchEvent(new CustomEvent('adr-wc:ready', {
|
|
364
|
+
detail: {
|
|
365
|
+
adrId: this.adrId,
|
|
366
|
+
title: this.adrTitle,
|
|
367
|
+
status: this.status,
|
|
368
|
+
},
|
|
369
|
+
bubbles: true, composed: true,
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
registerComponent('adr-wc', AdrWc);
|
|
375
|
+
|
|
376
|
+
export { AdrWc };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
adr-wc — static (zero-JS) form
|
|
3
|
+
Slotted content renders as children before upgrade.
|
|
4
|
+
Title and date are content, not attributes.
|
|
5
|
+
-->
|
|
6
|
+
<adr-wc adr-id="ADR-001" status="accepted">
|
|
7
|
+
<h2 slot="title">Use PostgreSQL for persistence</h2>
|
|
8
|
+
<time slot="date" datetime="2026-01-15">January 15, 2026</time>
|
|
9
|
+
<p slot="context">We need a relational database that supports complex queries, full-text search, and vector operations for our AI-powered features. The team has strong SQL experience.</p>
|
|
10
|
+
<p slot="decision">Use PostgreSQL 16 with the pgvector extension for vector similarity search and pg_trgm for full-text search.</p>
|
|
11
|
+
<ul slot="consequences">
|
|
12
|
+
<li>Good: Mature ecosystem with excellent tooling and community support</li>
|
|
13
|
+
<li>Good: pgvector avoids a separate vector database</li>
|
|
14
|
+
<li>Bad: Operational overhead vs managed NoSQL alternatives</li>
|
|
15
|
+
<li>Bad: Horizontal scaling requires careful sharding strategy</li>
|
|
16
|
+
</ul>
|
|
17
|
+
</adr-wc>
|