@tinyviber/a2h 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/README.md +229 -0
- package/bin/a2h.js +7 -0
- package/dist/actions/engine.d.ts +40 -0
- package/dist/actions/engine.d.ts.map +1 -0
- package/dist/actions/engine.js +227 -0
- package/dist/actions/engine.js.map +1 -0
- package/dist/actions/policy.d.ts +8 -0
- package/dist/actions/policy.d.ts.map +1 -0
- package/dist/actions/policy.js +55 -0
- package/dist/actions/policy.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +258 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +8 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +69 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/dist/ir/build.d.ts +8 -0
- package/dist/ir/build.d.ts.map +1 -0
- package/dist/ir/build.js +397 -0
- package/dist/ir/build.js.map +1 -0
- package/dist/ir/enrich.d.ts +21 -0
- package/dist/ir/enrich.d.ts.map +1 -0
- package/dist/ir/enrich.js +177 -0
- package/dist/ir/enrich.js.map +1 -0
- package/dist/ir/identity.d.ts +3 -0
- package/dist/ir/identity.d.ts.map +1 -0
- package/dist/ir/identity.js +91 -0
- package/dist/ir/identity.js.map +1 -0
- package/dist/ir/scoring.d.ts +7 -0
- package/dist/ir/scoring.d.ts.map +1 -0
- package/dist/ir/scoring.js +88 -0
- package/dist/ir/scoring.js.map +1 -0
- package/dist/parsers/code.d.ts +3 -0
- package/dist/parsers/code.d.ts.map +1 -0
- package/dist/parsers/code.js +17 -0
- package/dist/parsers/code.js.map +1 -0
- package/dist/parsers/diff.d.ts +11 -0
- package/dist/parsers/diff.d.ts.map +1 -0
- package/dist/parsers/diff.js +125 -0
- package/dist/parsers/diff.js.map +1 -0
- package/dist/parsers/json.d.ts +3 -0
- package/dist/parsers/json.d.ts.map +1 -0
- package/dist/parsers/json.js +16 -0
- package/dist/parsers/json.js.map +1 -0
- package/dist/parsers/log.d.ts +3 -0
- package/dist/parsers/log.d.ts.map +1 -0
- package/dist/parsers/log.js +105 -0
- package/dist/parsers/log.js.map +1 -0
- package/dist/parsers/markdown.d.ts +15 -0
- package/dist/parsers/markdown.d.ts.map +1 -0
- package/dist/parsers/markdown.js +89 -0
- package/dist/parsers/markdown.js.map +1 -0
- package/dist/parsers/text.d.ts +24 -0
- package/dist/parsers/text.d.ts.map +1 -0
- package/dist/parsers/text.js +98 -0
- package/dist/parsers/text.js.map +1 -0
- package/dist/presentation/blocks.d.ts +14 -0
- package/dist/presentation/blocks.d.ts.map +1 -0
- package/dist/presentation/blocks.js +87 -0
- package/dist/presentation/blocks.js.map +1 -0
- package/dist/presentation/present.d.ts +12 -0
- package/dist/presentation/present.d.ts.map +1 -0
- package/dist/presentation/present.js +115 -0
- package/dist/presentation/present.js.map +1 -0
- package/dist/providers/mock.d.ts +3 -0
- package/dist/providers/mock.d.ts.map +1 -0
- package/dist/providers/mock.js +171 -0
- package/dist/providers/mock.js.map +1 -0
- package/dist/providers/registry.d.ts +12 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +20 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/types.d.ts +64 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +40 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/renderer/assets/index.html +41 -0
- package/dist/renderer/assets/js/actions.js +205 -0
- package/dist/renderer/assets/js/api.js +75 -0
- package/dist/renderer/assets/js/artifacts.js +422 -0
- package/dist/renderer/assets/js/blocks.js +301 -0
- package/dist/renderer/assets/js/bus.js +21 -0
- package/dist/renderer/assets/js/cards.js +137 -0
- package/dist/renderer/assets/js/dom.js +61 -0
- package/dist/renderer/assets/js/format.js +118 -0
- package/dist/renderer/assets/js/main.js +125 -0
- package/dist/renderer/assets/js/nav.js +80 -0
- package/dist/renderer/assets/js/router.js +64 -0
- package/dist/renderer/assets/js/store.js +69 -0
- package/dist/renderer/assets/js/views.js +356 -0
- package/dist/renderer/assets/styles.css +856 -0
- package/dist/renderer/content.d.ts +11 -0
- package/dist/renderer/content.d.ts.map +1 -0
- package/dist/renderer/content.js +114 -0
- package/dist/renderer/content.js.map +1 -0
- package/dist/scanner/classify.d.ts +17 -0
- package/dist/scanner/classify.d.ts.map +1 -0
- package/dist/scanner/classify.js +117 -0
- package/dist/scanner/classify.js.map +1 -0
- package/dist/scanner/git.d.ts +3 -0
- package/dist/scanner/git.d.ts.map +1 -0
- package/dist/scanner/git.js +43 -0
- package/dist/scanner/git.js.map +1 -0
- package/dist/scanner/ignore.d.ts +22 -0
- package/dist/scanner/ignore.d.ts.map +1 -0
- package/dist/scanner/ignore.js +57 -0
- package/dist/scanner/ignore.js.map +1 -0
- package/dist/scanner/scan.d.ts +8 -0
- package/dist/scanner/scan.d.ts.map +1 -0
- package/dist/scanner/scan.js +108 -0
- package/dist/scanner/scan.js.map +1 -0
- package/dist/security/boundary.d.ts +19 -0
- package/dist/security/boundary.d.ts.map +1 -0
- package/dist/security/boundary.js +59 -0
- package/dist/security/boundary.js.map +1 -0
- package/dist/security/urlPolicy.d.ts +4 -0
- package/dist/security/urlPolicy.d.ts.map +1 -0
- package/dist/security/urlPolicy.js +56 -0
- package/dist/security/urlPolicy.js.map +1 -0
- package/dist/security/workspaceRead.d.ts +25 -0
- package/dist/security/workspaceRead.d.ts.map +1 -0
- package/dist/security/workspaceRead.js +103 -0
- package/dist/security/workspaceRead.js.map +1 -0
- package/dist/semantics/load.d.ts +11 -0
- package/dist/semantics/load.d.ts.map +1 -0
- package/dist/semantics/load.js +537 -0
- package/dist/semantics/load.js.map +1 -0
- package/dist/semantics/resolve.d.ts +37 -0
- package/dist/semantics/resolve.d.ts.map +1 -0
- package/dist/semantics/resolve.js +211 -0
- package/dist/semantics/resolve.js.map +1 -0
- package/dist/semantics/types.d.ts +158 -0
- package/dist/semantics/types.d.ts.map +1 -0
- package/dist/semantics/types.js +3 -0
- package/dist/semantics/types.js.map +1 -0
- package/dist/server/server.d.ts +17 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +510 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/workspace.d.ts +53 -0
- package/dist/server/workspace.d.ts.map +1 -0
- package/dist/server/workspace.js +211 -0
- package/dist/server/workspace.js.map +1 -0
- package/dist/types.d.ts +570 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/dist/util/path.d.ts +11 -0
- package/dist/util/path.d.ts.map +1 -0
- package/dist/util/path.js +35 -0
- package/dist/util/path.js.map +1 -0
- package/dist/util/safeRead.d.ts +38 -0
- package/dist/util/safeRead.d.ts.map +1 -0
- package/dist/util/safeRead.js +117 -0
- package/dist/util/safeRead.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// Presentation block rendering.
|
|
2
|
+
//
|
|
3
|
+
// Every block kind a producer can author is drawn by a renderer registered
|
|
4
|
+
// here. Adding a new visual language to A2H means registering one more
|
|
5
|
+
// function — no changes to the router, the views, or the server.
|
|
6
|
+
//
|
|
7
|
+
// Unknown block types are not an error: they render as a quiet, inspectable
|
|
8
|
+
// fallback so a producer can adopt a newer block kind without breaking an
|
|
9
|
+
// older viewer.
|
|
10
|
+
|
|
11
|
+
import { el, append } from './dom.js';
|
|
12
|
+
import { statusLabel, statusTone, fmtStamp, prettify } from './format.js';
|
|
13
|
+
import { hrefFor } from './router.js';
|
|
14
|
+
|
|
15
|
+
const renderers = new Map();
|
|
16
|
+
|
|
17
|
+
export function registerBlock(type, renderer) {
|
|
18
|
+
renderers.set(type, renderer);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param blocks resolved blocks from the presentation payload
|
|
23
|
+
* @param ctx { data, renderActions(ids, options) }
|
|
24
|
+
* @param options { card: boolean } — whether each block gets its own surface
|
|
25
|
+
*/
|
|
26
|
+
export function renderBlocks(blocks, ctx, options = {}) {
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const block of blocks || []) {
|
|
29
|
+
const node = renderBlock(block, ctx, options);
|
|
30
|
+
if (node) out.push(node);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function renderBlock(block, ctx, options = {}) {
|
|
36
|
+
if (!block || typeof block.type !== 'string') return null;
|
|
37
|
+
const renderer = renderers.get(block.type);
|
|
38
|
+
const body = renderer ? renderer(block, ctx) : unknownBlock(block);
|
|
39
|
+
if (!body) return null;
|
|
40
|
+
return options.card ? el('section', { class: 'block-card' }, [body]) : body;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function headline(block) {
|
|
44
|
+
if (!block.title) return null;
|
|
45
|
+
return el('h3', { class: 'block-title', text: block.title });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function unknownBlock(block) {
|
|
49
|
+
return el('div', { class: 'block-unknown' }, [
|
|
50
|
+
el('p', {
|
|
51
|
+
class: 'notice-inline',
|
|
52
|
+
text: `This viewer does not know how to draw a "${block.type}" block.`,
|
|
53
|
+
}),
|
|
54
|
+
el('details', {}, [
|
|
55
|
+
el('summary', { text: 'Raw block' }),
|
|
56
|
+
el('pre', { class: 'raw', text: JSON.stringify(block, null, 2) }),
|
|
57
|
+
]),
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------- text-like
|
|
62
|
+
|
|
63
|
+
registerBlock('text', (block) => {
|
|
64
|
+
const tone = block.tone && block.tone !== 'neutral' ? ` tone-${block.tone}` : '';
|
|
65
|
+
return el('div', { class: 'block block-text' }, [
|
|
66
|
+
headline(block),
|
|
67
|
+
el('p', { class: `block-body${tone}`, text: block.text == null ? '' : String(block.text) }),
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
registerBlock('markdown', (block) => {
|
|
72
|
+
// `html` is rendered server-side by the same conservative renderer used for
|
|
73
|
+
// artifacts (raw HTML escaped, unsafe link protocols rejected).
|
|
74
|
+
const prose = el('div', { class: 'prose prose-block' });
|
|
75
|
+
prose.innerHTML = typeof block.html === 'string' ? block.html : '';
|
|
76
|
+
return el('div', { class: 'block block-markdown' }, [headline(block), prose]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
registerBlock('notice', (block) => {
|
|
80
|
+
const tone = block.tone || 'info';
|
|
81
|
+
return el('div', { class: `block-notice tone-${tone}` }, [
|
|
82
|
+
el('span', { class: 'notice-label', text: tone === 'error' ? 'Error' : tone === 'warn' ? 'Warning' : 'Note' }),
|
|
83
|
+
el('span', { class: 'notice-body', text: block.text == null ? '' : String(block.text) }),
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// -------------------------------------------------------------------- lists
|
|
88
|
+
|
|
89
|
+
registerBlock('list', (block) => {
|
|
90
|
+
const items = Array.isArray(block.items) ? block.items : [];
|
|
91
|
+
const list = el(block.ordered ? 'ol' : 'ul', { class: 'block-list' });
|
|
92
|
+
for (const item of items) {
|
|
93
|
+
if (!item || typeof item !== 'object') continue;
|
|
94
|
+
const row = el('li', { class: 'block-list-item' }, [
|
|
95
|
+
el('div', { class: 'block-list-head' }, [
|
|
96
|
+
item.href && isSafeHref(item.href) ? linkNode(item.href, item.title) : el('span', { class: 'block-list-title', text: item.title }),
|
|
97
|
+
item.status ? statusPill(item.status) : null,
|
|
98
|
+
]),
|
|
99
|
+
item.detail ? el('p', { class: 'block-list-detail', text: item.detail }) : null,
|
|
100
|
+
item.meta ? el('p', { class: 'block-list-meta', text: item.meta }) : null,
|
|
101
|
+
]);
|
|
102
|
+
list.appendChild(row);
|
|
103
|
+
}
|
|
104
|
+
return el('div', { class: 'block block-list' }, [headline(block), list]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
function linkNode(href, text) {
|
|
108
|
+
return el('a', { class: 'block-list-title link', href: resolveHref(href), text: text });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The same allowlist the server applies before this block is sent, applied
|
|
112
|
+
// again on the way to the DOM. `javascript:`, `data:`, `file:`, `vbscript:`
|
|
113
|
+
// and protocol-relative targets are not links here.
|
|
114
|
+
const SAFE_HREF_SCHEME = /^(?:https?|mailto):/i;
|
|
115
|
+
const HAS_SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
116
|
+
|
|
117
|
+
export function isSafeHref(href) {
|
|
118
|
+
if (typeof href !== 'string') return false;
|
|
119
|
+
const value = href.trim();
|
|
120
|
+
if (value === '') return false;
|
|
121
|
+
if (value.startsWith('#')) return true;
|
|
122
|
+
if (value.startsWith('//')) return false;
|
|
123
|
+
if (HAS_SCHEME.test(value)) return SAFE_HREF_SCHEME.test(value);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** A producer may write a workspace path; we route it as an artifact. */
|
|
128
|
+
export function resolveHref(href) {
|
|
129
|
+
if (typeof href !== 'string') return '#/';
|
|
130
|
+
const value = href.trim();
|
|
131
|
+
if (value === '') return '#/';
|
|
132
|
+
if (value.startsWith('#')) return value;
|
|
133
|
+
// An unsafe href loses its link rather than gaining a wrong one.
|
|
134
|
+
if (!isSafeHref(value)) return '#/';
|
|
135
|
+
if (HAS_SCHEME.test(value)) return value;
|
|
136
|
+
return hrefFor('artifact', value);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ------------------------------------------------------------------- tables
|
|
140
|
+
|
|
141
|
+
registerBlock('table', (block) => {
|
|
142
|
+
const columns = Array.isArray(block.columns) ? block.columns : [];
|
|
143
|
+
const rows = Array.isArray(block.rows) ? block.rows : [];
|
|
144
|
+
const table = el('table', { class: 'data-table' });
|
|
145
|
+
const thead = el('thead');
|
|
146
|
+
const headRow = el('tr');
|
|
147
|
+
for (const col of columns) {
|
|
148
|
+
headRow.appendChild(
|
|
149
|
+
el('th', { class: col.align === 'right' ? 'align-right' : '', text: col.label || col.key }),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
thead.appendChild(headRow);
|
|
153
|
+
table.appendChild(thead);
|
|
154
|
+
|
|
155
|
+
const tbody = el('tbody');
|
|
156
|
+
for (const row of rows) {
|
|
157
|
+
const tr = el('tr');
|
|
158
|
+
for (const col of columns) {
|
|
159
|
+
const value = row ? row[col.key] : '';
|
|
160
|
+
tr.appendChild(
|
|
161
|
+
el('td', {
|
|
162
|
+
class: col.align === 'right' ? 'align-right' : '',
|
|
163
|
+
text: value == null ? '' : String(value),
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
tbody.appendChild(tr);
|
|
168
|
+
}
|
|
169
|
+
table.appendChild(tbody);
|
|
170
|
+
|
|
171
|
+
return el('div', { class: 'block block-table' }, [
|
|
172
|
+
headline(block),
|
|
173
|
+
el('div', { class: 'table-wrap' }, [table]),
|
|
174
|
+
block.note ? el('p', { class: 'block-note', text: block.note }) : null,
|
|
175
|
+
]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// -------------------------------------------------------------- comparison
|
|
179
|
+
|
|
180
|
+
registerBlock('comparison', (block) => {
|
|
181
|
+
const options = Array.isArray(block.options) ? block.options : [];
|
|
182
|
+
const grid = el('div', { class: 'comparison' });
|
|
183
|
+
for (const option of options) {
|
|
184
|
+
const tone = option.tone && option.tone !== 'neutral' ? ` tone-${option.tone}` : '';
|
|
185
|
+
const col = el('div', { class: `comparison-option${tone}` }, [
|
|
186
|
+
el('h4', { class: 'comparison-title', text: option.title }),
|
|
187
|
+
option.summary ? el('p', { class: 'comparison-summary', text: option.summary }) : null,
|
|
188
|
+
]);
|
|
189
|
+
const facts = Array.isArray(option.facts) ? option.facts : [];
|
|
190
|
+
if (facts.length) {
|
|
191
|
+
const dl = el('dl', { class: 'kv' });
|
|
192
|
+
for (const fact of facts) {
|
|
193
|
+
dl.appendChild(el('dt', { text: fact.label }));
|
|
194
|
+
dl.appendChild(el('dd', { text: fact.value == null ? '' : String(fact.value) }));
|
|
195
|
+
}
|
|
196
|
+
col.appendChild(dl);
|
|
197
|
+
}
|
|
198
|
+
grid.appendChild(col);
|
|
199
|
+
}
|
|
200
|
+
return el('div', { class: 'block block-comparison' }, [
|
|
201
|
+
headline(block),
|
|
202
|
+
grid,
|
|
203
|
+
block.conclusion ? el('p', { class: 'block-conclusion', text: block.conclusion }) : null,
|
|
204
|
+
]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ------------------------------------------------------------------ metrics
|
|
208
|
+
|
|
209
|
+
registerBlock('metrics', (block) => {
|
|
210
|
+
const items = Array.isArray(block.items) ? block.items : [];
|
|
211
|
+
const grid = el('div', { class: 'metrics' });
|
|
212
|
+
for (const metric of items) {
|
|
213
|
+
const tone = metric.tone && metric.tone !== 'neutral' ? ` tone-${metric.tone}` : '';
|
|
214
|
+
grid.appendChild(
|
|
215
|
+
el('div', { class: `metric${tone}` }, [
|
|
216
|
+
el('span', { class: 'metric-label', text: metric.label }),
|
|
217
|
+
el('span', { class: 'metric-value' }, [
|
|
218
|
+
String(metric.value == null ? '' : metric.value),
|
|
219
|
+
metric.unit ? el('span', { class: 'metric-unit', text: metric.unit }) : null,
|
|
220
|
+
]),
|
|
221
|
+
metric.delta ? el('span', { class: 'metric-delta', text: metric.delta }) : null,
|
|
222
|
+
]),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return el('div', { class: 'block block-metrics' }, [headline(block), grid]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ----------------------------------------------------------------- timeline
|
|
229
|
+
|
|
230
|
+
registerBlock('timeline', (block) => {
|
|
231
|
+
const items = Array.isArray(block.items) ? block.items : [];
|
|
232
|
+
const list = el('ol', { class: 'timeline' });
|
|
233
|
+
for (const item of items) {
|
|
234
|
+
const tone = item.status ? statusTone(item.status) : 'neutral';
|
|
235
|
+
list.appendChild(
|
|
236
|
+
el('li', { class: 'timeline-item' }, [
|
|
237
|
+
el('span', { class: `timeline-dot tone-${tone}` }),
|
|
238
|
+
el('div', { class: 'timeline-body' }, [
|
|
239
|
+
el('div', { class: 'timeline-head' }, [
|
|
240
|
+
el('span', { class: 'timeline-title', text: item.title }),
|
|
241
|
+
item.at ? el('span', { class: 'timeline-at', text: fmtStamp(item.at) }) : null,
|
|
242
|
+
]),
|
|
243
|
+
item.detail ? el('p', { class: 'timeline-detail', text: item.detail }) : null,
|
|
244
|
+
]),
|
|
245
|
+
]),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return el('div', { class: 'block block-timeline' }, [headline(block), list]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// ------------------------------------------------------------------- status
|
|
252
|
+
|
|
253
|
+
registerBlock('status', (block) => {
|
|
254
|
+
return el('div', { class: 'block block-status' }, [
|
|
255
|
+
headline(block),
|
|
256
|
+
el('div', { class: 'status-band' }, [
|
|
257
|
+
statusPill(block.status),
|
|
258
|
+
block.detail ? el('span', { class: 'status-detail', text: block.detail }) : null,
|
|
259
|
+
]),
|
|
260
|
+
]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
registerBlock('keyvalue', (block) => {
|
|
264
|
+
const items = Array.isArray(block.items) ? block.items : [];
|
|
265
|
+
const dl = el('dl', { class: 'kv' });
|
|
266
|
+
for (const item of items) {
|
|
267
|
+
dl.appendChild(el('dt', { text: item.key }));
|
|
268
|
+
dl.appendChild(el('dd', { class: item.mono ? 'mono' : '', text: item.value == null ? '' : String(item.value) }));
|
|
269
|
+
}
|
|
270
|
+
return el('div', { class: 'block block-kv' }, [headline(block), dl]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ------------------------------------------------------------------ actions
|
|
274
|
+
|
|
275
|
+
registerBlock('actions', (block, ctx) => {
|
|
276
|
+
const ids = Array.isArray(block.ids) ? block.ids : [];
|
|
277
|
+
if (!ctx || !ctx.renderActions) return null;
|
|
278
|
+
const node = ctx.renderActions(ids, { compact: true });
|
|
279
|
+
if (!node) return null;
|
|
280
|
+
return el('div', { class: 'block block-actions' }, [headline(block), node]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// -------------------------------------------------------------------- atoms
|
|
284
|
+
|
|
285
|
+
export function statusPill(status) {
|
|
286
|
+
const tone = statusTone(status);
|
|
287
|
+
return el('span', { class: `status-pill tone-${tone}` }, [
|
|
288
|
+
el('span', { class: 'status-dot' }),
|
|
289
|
+
statusLabel(status),
|
|
290
|
+
]);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function tagRow(tags) {
|
|
294
|
+
const row = el('div', { class: 'card-tags' });
|
|
295
|
+
for (const tag of tags || []) {
|
|
296
|
+
row.appendChild(el('span', { class: `tag tag-${String(tag).toLowerCase()}`, text: tag }));
|
|
297
|
+
}
|
|
298
|
+
return row;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export { append, prettify };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Minimal event bus, so views never import each other.
|
|
2
|
+
|
|
3
|
+
const listeners = new Map();
|
|
4
|
+
|
|
5
|
+
export function subscribe(event, handler) {
|
|
6
|
+
if (!listeners.has(event)) listeners.set(event, new Set());
|
|
7
|
+
listeners.get(event).add(handler);
|
|
8
|
+
return () => listeners.get(event).delete(handler);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function emit(event) {
|
|
12
|
+
const set = listeners.get(event);
|
|
13
|
+
if (!set) return;
|
|
14
|
+
for (const handler of set) {
|
|
15
|
+
try {
|
|
16
|
+
handler();
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.error(`a2h: listener for "${event}" failed`, err);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Reusable surfaces: artifact cards, task cards, section headings.
|
|
2
|
+
// All of them are quiet containers — one card is one reading unit.
|
|
3
|
+
|
|
4
|
+
import { el } from './dom.js';
|
|
5
|
+
import { fmtBytes, fmtStamp, kindLabel, relTime, statusLabel, statusTone, fmtDuration } from './format.js';
|
|
6
|
+
import { statusPill, tagRow } from './blocks.js';
|
|
7
|
+
import { hrefFor } from './router.js';
|
|
8
|
+
import { renderActions } from './actions.js';
|
|
9
|
+
|
|
10
|
+
export function artifactCard(view) {
|
|
11
|
+
const card = el('a', { class: 'card', href: hrefFor('artifact', view.id) });
|
|
12
|
+
card.appendChild(el('div', { class: 'card-kind', text: kindLabel(view.kind) }));
|
|
13
|
+
if (view.tags && view.tags.length) card.appendChild(tagRow(view.tags));
|
|
14
|
+
card.appendChild(el('h3', { class: 'card-title', text: view.title }));
|
|
15
|
+
if (view.summary) card.appendChild(el('p', { class: 'card-summary', text: view.summary }));
|
|
16
|
+
const foot = el('div', { class: 'card-foot' });
|
|
17
|
+
if (view.meta && view.meta.size != null) foot.appendChild(el('span', { text: fmtBytes(view.meta.size) }));
|
|
18
|
+
if (view.meta && view.meta.mtimeMs) foot.appendChild(el('span', { text: relTime(view.meta.mtimeMs) }));
|
|
19
|
+
if (foot.childNodes.length) card.appendChild(foot);
|
|
20
|
+
return card;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function cardGrid(views) {
|
|
24
|
+
const grid = el('div', { class: 'card-grid' });
|
|
25
|
+
for (const view of views) grid.appendChild(artifactCard(view));
|
|
26
|
+
return grid;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function sectionHead(title, count, options = {}) {
|
|
30
|
+
const head = el('div', { class: 'section-head' }, [
|
|
31
|
+
el('h2', { class: 'section-title', text: title }),
|
|
32
|
+
]);
|
|
33
|
+
if (count != null) {
|
|
34
|
+
head.appendChild(el('span', { class: 'section-count', text: `${count} item${count === 1 ? '' : 's'}` }));
|
|
35
|
+
}
|
|
36
|
+
if (options.explicit) {
|
|
37
|
+
head.appendChild(el('span', { class: 'section-flag', text: 'declared' }));
|
|
38
|
+
}
|
|
39
|
+
return head;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A task is the biggest unit a human judges. The card leads with state and the
|
|
44
|
+
* single most recent run, because that is what "should I look at this?" needs.
|
|
45
|
+
*/
|
|
46
|
+
export function taskCard(task, options = {}) {
|
|
47
|
+
const card = el('a', { class: 'card card-task', href: hrefFor('task', task.id) });
|
|
48
|
+
const head = el('div', { class: 'task-head' }, [
|
|
49
|
+
el('h3', { class: 'card-title', text: task.title }),
|
|
50
|
+
statusPill(task.status),
|
|
51
|
+
]);
|
|
52
|
+
card.appendChild(head);
|
|
53
|
+
if (task.summary) card.appendChild(el('p', { class: 'card-summary', text: task.summary }));
|
|
54
|
+
|
|
55
|
+
if (task.progress && task.progress.total > 0) {
|
|
56
|
+
card.appendChild(progressBar(task.progress));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (task.metrics && task.metrics.length) {
|
|
60
|
+
const row = el('div', { class: 'card-metrics' });
|
|
61
|
+
for (const metric of task.metrics.slice(0, 3)) {
|
|
62
|
+
row.appendChild(
|
|
63
|
+
el('span', { class: 'card-metric' }, [
|
|
64
|
+
el('span', { class: 'card-metric-value', text: String(metric.value) + (metric.unit ? metric.unit : '') }),
|
|
65
|
+
el('span', { class: 'card-metric-label', text: metric.label }),
|
|
66
|
+
]),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
card.appendChild(row);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const latest = task.runs && task.runs[0];
|
|
73
|
+
if (latest) {
|
|
74
|
+
card.appendChild(
|
|
75
|
+
el('div', { class: 'card-run' }, [
|
|
76
|
+
el('span', { class: 'card-run-dot ' + `tone-${statusTone(latest.status)}` }),
|
|
77
|
+
el('span', { text: `Last run: ${latest.title}` }),
|
|
78
|
+
latest.startedAt ? el('span', { class: 'muted', text: fmtStamp(latest.startedAt) }) : null,
|
|
79
|
+
]),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const foot = el('div', { class: 'card-foot' });
|
|
84
|
+
if (task.artifacts.length) foot.appendChild(el('span', { text: `${task.artifacts.length} artifact${task.artifacts.length === 1 ? '' : 's'}` }));
|
|
85
|
+
if (task.runs.length) foot.appendChild(el('span', { text: `${task.runs.length} run${task.runs.length === 1 ? '' : 's'}` }));
|
|
86
|
+
if (task.owner) foot.appendChild(el('span', { text: task.owner }));
|
|
87
|
+
if (foot.childNodes.length) card.appendChild(foot);
|
|
88
|
+
return card;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function progressBar(progress) {
|
|
92
|
+
const pct = progress.total > 0 ? Math.round((progress.done / progress.total) * 100) : 0;
|
|
93
|
+
return el('div', { class: 'progress' }, [
|
|
94
|
+
el('div', { class: 'progress-track' }, [
|
|
95
|
+
el('div', { class: 'progress-fill', style: `width:${Math.max(0, Math.min(100, pct))}%` }),
|
|
96
|
+
]),
|
|
97
|
+
el('span', {
|
|
98
|
+
class: 'progress-label',
|
|
99
|
+
text: `${progress.label ? progress.label + ' ' : ''}${progress.done}/${progress.total}`,
|
|
100
|
+
}),
|
|
101
|
+
]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function runRow(run, options = {}) {
|
|
105
|
+
const wrap = el('div', { class: 'run' });
|
|
106
|
+
const head = el('div', { class: 'run-head' }, [
|
|
107
|
+
el('span', { class: 'run-title', text: run.title }),
|
|
108
|
+
statusPill(run.status),
|
|
109
|
+
]);
|
|
110
|
+
if (run.durationMs != null) head.appendChild(el('span', { class: 'muted', text: fmtDuration(run.durationMs) }));
|
|
111
|
+
if (run.startedAt) head.appendChild(el('span', { class: 'muted', text: fmtStamp(run.startedAt) }));
|
|
112
|
+
if (run.simulated) head.appendChild(el('span', { class: 'run-sim', text: 'simulated' }));
|
|
113
|
+
wrap.appendChild(head);
|
|
114
|
+
|
|
115
|
+
if (run.summary) wrap.appendChild(el('p', { class: 'run-summary', text: run.summary }));
|
|
116
|
+
|
|
117
|
+
if (run.steps && run.steps.length) {
|
|
118
|
+
const list = el('ol', { class: 'steps' });
|
|
119
|
+
for (const step of run.steps) {
|
|
120
|
+
list.appendChild(
|
|
121
|
+
el('li', { class: 'step' }, [
|
|
122
|
+
el('span', { class: `step-dot tone-${statusTone(step.status)}` }),
|
|
123
|
+
el('span', { class: 'step-title', text: step.title }),
|
|
124
|
+
el('span', { class: 'step-status', text: statusLabel(step.status) }),
|
|
125
|
+
step.detail ? el('span', { class: 'step-detail', text: step.detail }) : null,
|
|
126
|
+
]),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
wrap.appendChild(list);
|
|
130
|
+
}
|
|
131
|
+
return wrap;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function taskActions(task) {
|
|
135
|
+
if (!task.actions || task.actions.length === 0) return null;
|
|
136
|
+
return renderActions(task.actions.map((a) => a.id));
|
|
137
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Tiny DOM helpers. Deliberately not a framework: the viewer is a local,
|
|
2
|
+
// dependency-free app and every node here is created explicitly, so there is
|
|
3
|
+
// no template-injection surface.
|
|
4
|
+
|
|
5
|
+
export function el(tag, attrs, children) {
|
|
6
|
+
const node = document.createElement(tag);
|
|
7
|
+
if (attrs) {
|
|
8
|
+
for (const key of Object.keys(attrs)) {
|
|
9
|
+
const value = attrs[key];
|
|
10
|
+
if (value == null) continue;
|
|
11
|
+
if (key === 'class') node.className = value;
|
|
12
|
+
else if (key === 'text') node.textContent = value;
|
|
13
|
+
else if (key === 'html') node.innerHTML = value;
|
|
14
|
+
else if (key.startsWith('on') && typeof value === 'function') {
|
|
15
|
+
node.addEventListener(key.slice(2), value);
|
|
16
|
+
} else if (key === 'dataset') {
|
|
17
|
+
for (const d of Object.keys(value)) node.dataset[d] = value[d];
|
|
18
|
+
} else if (value !== false) {
|
|
19
|
+
node.setAttribute(key, value === true ? '' : value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
append(node, children);
|
|
24
|
+
return node;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function append(node, children) {
|
|
28
|
+
if (children == null) return node;
|
|
29
|
+
const list = Array.isArray(children) ? children : [children];
|
|
30
|
+
for (const child of list) {
|
|
31
|
+
if (child == null || child === false) continue;
|
|
32
|
+
node.appendChild(typeof child === 'string' ? document.createTextNode(child) : child);
|
|
33
|
+
}
|
|
34
|
+
return node;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function esc(value) {
|
|
38
|
+
return String(value == null ? '' : value)
|
|
39
|
+
.replace(/&/g, '&')
|
|
40
|
+
.replace(/</g, '<')
|
|
41
|
+
.replace(/>/g, '>')
|
|
42
|
+
.replace(/"/g, '"')
|
|
43
|
+
.replace(/'/g, ''');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function clear(node) {
|
|
47
|
+
node.textContent = '';
|
|
48
|
+
return node;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function fragment() {
|
|
52
|
+
return document.createDocumentFragment();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function byId(id) {
|
|
56
|
+
return document.getElementById(id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function spacer(px) {
|
|
60
|
+
return el('div', { class: 'spacer', style: `height:${px}px` });
|
|
61
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Formatting + label vocabularies. Everything a human reads about a piece of
|
|
2
|
+
// content is named here, so the visual language stays consistent.
|
|
3
|
+
|
|
4
|
+
export const KIND_LABEL = {
|
|
5
|
+
readme: 'Overview',
|
|
6
|
+
report: 'Report',
|
|
7
|
+
markdown: 'Note',
|
|
8
|
+
code: 'Code',
|
|
9
|
+
json: 'Data',
|
|
10
|
+
log: 'Log',
|
|
11
|
+
diff: 'Diff',
|
|
12
|
+
image: 'Image',
|
|
13
|
+
file: 'File',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const SOURCE_LABEL = {
|
|
17
|
+
explicit: 'declared',
|
|
18
|
+
frontmatter: 'frontmatter',
|
|
19
|
+
convention: 'by convention',
|
|
20
|
+
heuristic: 'inferred',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Noun form, for sentences like "declared in the manifest". SOURCE_LABEL reads
|
|
25
|
+
* as an adjective and produced "declared by declared".
|
|
26
|
+
*/
|
|
27
|
+
export const SOURCE_ORIGIN = {
|
|
28
|
+
explicit: 'the manifest',
|
|
29
|
+
frontmatter: 'frontmatter',
|
|
30
|
+
convention: 'convention',
|
|
31
|
+
heuristic: 'inference',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const STATUS_LABEL = {
|
|
35
|
+
pending: 'Pending',
|
|
36
|
+
running: 'Running',
|
|
37
|
+
succeeded: 'Succeeded',
|
|
38
|
+
failed: 'Failed',
|
|
39
|
+
partial: 'Partial',
|
|
40
|
+
blocked: 'Blocked',
|
|
41
|
+
cancelled: 'Cancelled',
|
|
42
|
+
info: 'Info',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const STATUS_TONE = {
|
|
46
|
+
pending: 'neutral',
|
|
47
|
+
running: 'info',
|
|
48
|
+
succeeded: 'good',
|
|
49
|
+
failed: 'bad',
|
|
50
|
+
partial: 'warn',
|
|
51
|
+
blocked: 'warn',
|
|
52
|
+
cancelled: 'neutral',
|
|
53
|
+
info: 'neutral',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function statusLabel(status) {
|
|
57
|
+
if (!status) return 'Unknown';
|
|
58
|
+
return STATUS_LABEL[status] || prettify(status);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function statusTone(status) {
|
|
62
|
+
return STATUS_TONE[status] || 'neutral';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Producer-defined roles/statuses get a readable label instead of raw slugs. */
|
|
66
|
+
export function prettify(value) {
|
|
67
|
+
return String(value == null ? '' : value)
|
|
68
|
+
.replace(/[-_]+/g, ' ')
|
|
69
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function kindLabel(kind) {
|
|
73
|
+
return KIND_LABEL[kind] || prettify(kind);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function fmtBytes(n) {
|
|
77
|
+
if (n == null) return '';
|
|
78
|
+
if (n < 1024) return `${n} B`;
|
|
79
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
80
|
+
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function relTime(ms) {
|
|
84
|
+
if (!ms) return '';
|
|
85
|
+
let diff = Date.now() - ms;
|
|
86
|
+
if (diff < 0) diff = 0;
|
|
87
|
+
if (diff < 60 * 1000) return 'just now';
|
|
88
|
+
if (diff < 3600 * 1000) return `${Math.floor(diff / 60000)}m ago`;
|
|
89
|
+
if (diff < 24 * 3600 * 1000) return `${Math.floor(diff / 3600000)}h ago`;
|
|
90
|
+
if (diff < 7 * 24 * 3600 * 1000) return `${Math.floor(diff / (24 * 3600000))}d ago`;
|
|
91
|
+
return new Date(ms).toLocaleDateString();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** ISO strings come from producers; render them compactly and locally. */
|
|
95
|
+
export function fmtStamp(value) {
|
|
96
|
+
if (!value) return '';
|
|
97
|
+
const ms = Date.parse(value);
|
|
98
|
+
if (!Number.isFinite(ms)) return String(value);
|
|
99
|
+
const diff = Date.now() - ms;
|
|
100
|
+
if (diff >= 0 && diff < 7 * 24 * 3600 * 1000) return relTime(ms);
|
|
101
|
+
return new Date(ms).toLocaleString();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function fmtDuration(ms) {
|
|
105
|
+
if (ms == null) return '';
|
|
106
|
+
if (ms < 1000) return `${ms} ms`;
|
|
107
|
+
const s = ms / 1000;
|
|
108
|
+
if (s < 60) return `${s.toFixed(s < 10 ? 1 : 0)} s`;
|
|
109
|
+
const m = Math.floor(s / 60);
|
|
110
|
+
if (m < 60) return `${m}m ${Math.round(s % 60)}s`;
|
|
111
|
+
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Paths are shown, not linked, unless there is somewhere to go. */
|
|
115
|
+
export function shortPath(path) {
|
|
116
|
+
if (!path) return '';
|
|
117
|
+
return path.length > 72 ? `…${path.slice(-69)}` : path;
|
|
118
|
+
}
|