usedesign 0.4.0 → 0.6.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/dist/checks.d.ts +2 -0
- package/dist/checks.js +0 -0
- package/dist/checks.js.map +1 -1
- package/dist/cli.js +79 -29
- package/dist/cli.js.map +1 -1
- package/dist/conformance.js +6 -4
- package/dist/conformance.js.map +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/core.js +6 -3
- package/dist/core.js.map +1 -1
- package/dist/preview.d.ts +16 -0
- package/dist/preview.js +380 -0
- package/dist/preview.js.map +1 -0
- package/dist/schema/form-contract.schema.json +115 -0
- package/dist/schema/form-inventory.schema.json +37 -0
- package/dist/schema/operation-card.schema.json +23 -0
- package/dist/schema/storage-inventory.schema.json +50 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +153 -0
- package/dist/validate.js.map +1 -1
- package/package.json +1 -1
package/dist/preview.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `usedesign preview` — a rough, honest visualiser of form contracts.
|
|
3
|
+
*
|
|
4
|
+
* Generates ONE self-contained HTML file: no server, no dependencies, openable from disk. Three
|
|
5
|
+
* rails — the contract tree, the contract's content, and a grey-box wireframe of the selected
|
|
6
|
+
* form in a selected entity state. When a form inventory is present, every wireframe element
|
|
7
|
+
* carries a verdict badge: rendered, missing (the contract is ahead of the code), or rendered
|
|
8
|
+
* beyond the contract.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately crude: grey boxes and real labels. A pretty preview would start competing with
|
|
11
|
+
* the product's own design system and lose; the wireframe's job is to show WHAT the owner
|
|
12
|
+
* decided, state by state, and where reality disagrees.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { frontMatter, expandGlob } from "./core.js";
|
|
17
|
+
function bodyOf(path) {
|
|
18
|
+
const text = readFileSync(path, "utf8").replace(/^/, "").replace(/\r\n/g, "\n");
|
|
19
|
+
const end = text.indexOf("\n---", 3);
|
|
20
|
+
return end === -1 ? "" : text.slice(end + 4).trim();
|
|
21
|
+
}
|
|
22
|
+
export function collectPreviewData(config, base) {
|
|
23
|
+
const contracts = [];
|
|
24
|
+
for (const pattern of config.forms ?? []) {
|
|
25
|
+
for (const file of expandGlob(base, pattern)) {
|
|
26
|
+
const fm = frontMatter(file);
|
|
27
|
+
if (fm && fm["usedesign_form"] === 1)
|
|
28
|
+
contracts.push({ ...fm, __body: bodyOf(file), __file: file.split(/[\\/]/).pop() });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
let inventory = null;
|
|
32
|
+
let producedBy = "";
|
|
33
|
+
const location = config.form_inventory;
|
|
34
|
+
if (location && existsSync(join(base, location))) {
|
|
35
|
+
const data = JSON.parse(readFileSync(join(base, location), "utf8"));
|
|
36
|
+
producedBy = data.produced_by ?? "";
|
|
37
|
+
inventory = {};
|
|
38
|
+
for (const form of data.forms ?? []) {
|
|
39
|
+
const states = {};
|
|
40
|
+
for (const s of form.states ?? [])
|
|
41
|
+
states[s.state] = { fields: s.fields ?? [], controls: s.controls ?? [] };
|
|
42
|
+
inventory[form.screen] = states;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { contracts, inventory, producedBy };
|
|
46
|
+
}
|
|
47
|
+
export function renderPreviewHtml(data) {
|
|
48
|
+
const payload = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
49
|
+
return `<!doctype html>
|
|
50
|
+
<html lang="ru">
|
|
51
|
+
<head>
|
|
52
|
+
<meta charset="utf-8">
|
|
53
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
54
|
+
<title>usedesign — контракты форм</title>
|
|
55
|
+
<style>
|
|
56
|
+
:root{--bg:#1114;--line:#d8d8d2;--ink:#22251f;--dim:#7a7d74;--ok:#2c7a3f;--todo:#b3372b;--extra:#8a6d1a;--card:#fbfbf8;}
|
|
57
|
+
*{box-sizing:border-box}
|
|
58
|
+
body{margin:0;font:14px/1.45 ui-sans-serif,system-ui,'Segoe UI',sans-serif;color:var(--ink);background:#efefe9;height:100vh;display:flex;flex-direction:column}
|
|
59
|
+
header{padding:10px 16px;border-bottom:1px solid var(--line);background:var(--card);display:flex;gap:12px;align-items:baseline}
|
|
60
|
+
header h1{font-size:15px;margin:0}
|
|
61
|
+
header .sub{color:var(--dim);font-size:12px}
|
|
62
|
+
/* лента рейлов: горизонтальная, без переноса, каждый рейл — своя ширина и свой скролл */
|
|
63
|
+
.strip{flex:1;display:flex;overflow-x:auto;overflow-y:hidden;align-items:stretch;gap:0;justify-content:flex-start}
|
|
64
|
+
.rail{flex:0 0 auto;display:flex;flex-direction:column;min-width:0;background:var(--card);border-right:1px solid var(--line);position:relative}
|
|
65
|
+
.rail>.body{flex:1;overflow-y:auto;padding:12px 14px}
|
|
66
|
+
.rail h2{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--dim);margin:0;padding:10px 14px 6px}
|
|
67
|
+
.grip{position:absolute;top:0;right:-3px;width:6px;height:100%;cursor:col-resize;z-index:5}
|
|
68
|
+
.grip:hover{background:#0002}
|
|
69
|
+
/* рейл 1: дерево */
|
|
70
|
+
.tree a{display:block;padding:5px 8px;border-radius:6px;color:inherit;text-decoration:none}
|
|
71
|
+
.tree a:hover{background:#0000000d}
|
|
72
|
+
.tree a.sel{background:#22251f;color:#fff}
|
|
73
|
+
.tree .page{color:var(--dim);font-size:11px;margin:10px 0 2px;text-transform:uppercase;letter-spacing:.06em}
|
|
74
|
+
.tree .opens{margin-left:16px;border-left:1px dotted var(--line);padding-left:8px}
|
|
75
|
+
/* рейл 2: содержание */
|
|
76
|
+
.kv{margin:0 0 12px}
|
|
77
|
+
.kv dt{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.06em;margin-top:10px}
|
|
78
|
+
.kv dd{margin:2px 0 0}
|
|
79
|
+
.item{padding:7px 9px;border:1px solid var(--line);border-radius:8px;margin:6px 0;background:#fff}
|
|
80
|
+
.item .name{font-family:ui-monospace,monospace;font-size:12px}
|
|
81
|
+
.item .shows{color:var(--ink)}
|
|
82
|
+
.item .meta{color:var(--dim);font-size:12px}
|
|
83
|
+
.prose{white-space:pre-wrap;color:var(--dim);font-size:12.5px;border-top:1px dashed var(--line);padding-top:10px;margin-top:14px}
|
|
84
|
+
/* рейл 3: каркас */
|
|
85
|
+
.states{display:flex;gap:6px;flex-wrap:wrap;padding:0 14px 8px}
|
|
86
|
+
.states button{border:1px solid var(--line);background:#fff;border-radius:999px;padding:3px 10px;cursor:pointer;font:inherit;font-size:12px}
|
|
87
|
+
.states button.sel{background:#22251f;color:#fff;border-color:#22251f}
|
|
88
|
+
.wire{border:1px solid var(--line);border-radius:12px;background:#fff;padding:10px;display:flex;flex-direction:column;gap:8px}
|
|
89
|
+
.el{border:1px dashed #b9b9b0;border-radius:8px;padding:8px 10px;background:#f6f6f1}
|
|
90
|
+
.el .name{font-family:ui-monospace,monospace;font-size:11px;color:var(--dim)}
|
|
91
|
+
.el .shows{font-size:13px}
|
|
92
|
+
.btnrow{display:flex;gap:8px;flex-wrap:wrap;margin-top:4px}
|
|
93
|
+
/* группы по назначению: рамка + подпись роли; вид грубый намеренно */
|
|
94
|
+
.grp{border:1px solid var(--line);border-radius:10px;padding:8px;display:flex;flex-direction:column;gap:6px;background:#fff}
|
|
95
|
+
.grp .grphead{font-size:10.5px;color:var(--dim);text-transform:uppercase;letter-spacing:.06em;display:flex;justify-content:space-between;gap:8px}
|
|
96
|
+
.grp-header{background:#f2f2ea}
|
|
97
|
+
.grp-footer{background:#f2f2ea;border-top:3px solid var(--line)}
|
|
98
|
+
.grp-table .el, .grp-list .el{border-style:solid;border-left:3px solid #c9c9bf}
|
|
99
|
+
.grp-toolbar{flex-direction:row;flex-wrap:wrap;align-items:center}
|
|
100
|
+
.grp-menu{border-style:dashed;align-self:flex-end;max-width:70%}
|
|
101
|
+
.ungrouped{border-top:1px dashed var(--line);margin-top:8px;padding-top:8px}
|
|
102
|
+
.ctl{border:1px solid #22251f;border-radius:8px;padding:6px 12px;background:#fff;font-size:13px}
|
|
103
|
+
.ctl.rule{border-style:dashed;color:var(--dim)}
|
|
104
|
+
.ctl.removedghost{border-style:dotted;color:var(--todo);text-decoration:line-through}
|
|
105
|
+
.badge{display:inline-block;font-size:10.5px;border-radius:999px;padding:1px 7px;margin-left:6px;vertical-align:1px}
|
|
106
|
+
.b-ok{background:#e3f2e6;color:var(--ok)}
|
|
107
|
+
.b-todo{background:#fbe4e0;color:var(--todo)}
|
|
108
|
+
.b-extra{background:#f7eecb;color:var(--extra)}
|
|
109
|
+
.legend{color:var(--dim);font-size:11.5px;padding:8px 14px;border-top:1px solid var(--line)}
|
|
110
|
+
.extras{margin-top:12px}
|
|
111
|
+
.extras .el{border-style:dotted;background:#fdf9ea}
|
|
112
|
+
@media (max-width: 900px){ .strip{flex-direction:column;overflow:visible} .rail{width:auto !important;flex:0 0 auto;border-right:0;border-bottom:1px solid var(--line)} .grip{display:none} }
|
|
113
|
+
</style>
|
|
114
|
+
</head>
|
|
115
|
+
<body data-page="usedesign-preview">
|
|
116
|
+
<header data-panel="preview-header">
|
|
117
|
+
<h1>usedesign · контракты форм</h1>
|
|
118
|
+
<span class="sub" id="src"></span>
|
|
119
|
+
</header>
|
|
120
|
+
<div class="strip" id="strip" data-panel="rail-strip">
|
|
121
|
+
<section class="rail" id="rail-tree" data-panel="contract-tree" style="width:260px">
|
|
122
|
+
<h2>Контракты</h2>
|
|
123
|
+
<div class="body tree" id="tree" data-list="contracts"></div>
|
|
124
|
+
<div class="grip" data-control="resize-tree"></div>
|
|
125
|
+
</section>
|
|
126
|
+
<section class="rail" id="rail-doc" data-panel="contract-doc" style="width:420px">
|
|
127
|
+
<h2>Содержание</h2>
|
|
128
|
+
<div class="body" id="doc" data-group="contract-content"></div>
|
|
129
|
+
<div class="grip" data-control="resize-doc"></div>
|
|
130
|
+
</section>
|
|
131
|
+
<section class="rail" id="rail-wire" data-panel="contract-wireframe" style="width:460px">
|
|
132
|
+
<h2>Каркас-превью</h2>
|
|
133
|
+
<div class="states" id="states" data-group="state-toggle"></div>
|
|
134
|
+
<div class="body" id="wire" data-group="wireframe"></div>
|
|
135
|
+
<div class="legend" data-group="legend">
|
|
136
|
+
<span class="badge b-ok">есть</span> отрендерено ·
|
|
137
|
+
<span class="badge b-todo">нет в коде</span> контракт впереди кода ·
|
|
138
|
+
<span class="badge b-extra">вне контракта</span> отрендерено, но не решено
|
|
139
|
+
</div>
|
|
140
|
+
<div class="grip" data-control="resize-wire"></div>
|
|
141
|
+
</section>
|
|
142
|
+
</div>
|
|
143
|
+
<script id="data" type="application/json">${payload}</script>
|
|
144
|
+
<script>
|
|
145
|
+
const DATA = JSON.parse(document.getElementById('data').textContent);
|
|
146
|
+
const $ = (id) => document.getElementById(id);
|
|
147
|
+
const params = () => new URLSearchParams(location.search);
|
|
148
|
+
|
|
149
|
+
// ── рейлы: ширины с ограничениями, перетаскивание, сохранение ────────────────────────────────
|
|
150
|
+
const RAILS = [
|
|
151
|
+
{ el: 'rail-tree', grip: 0, def: 260, min: 180, max: 420 },
|
|
152
|
+
{ el: 'rail-doc', grip: 1, def: 420, min: 280, max: 680 },
|
|
153
|
+
{ el: 'rail-wire', grip: 2, def: 460, min: 320, max: 760 },
|
|
154
|
+
];
|
|
155
|
+
for (const r of RAILS) {
|
|
156
|
+
const saved = Number(localStorage.getItem('ud.rail.' + r.el));
|
|
157
|
+
const w = Number.isFinite(saved) && saved >= r.min && saved <= r.max ? saved : r.def;
|
|
158
|
+
$(r.el).style.width = w + 'px';
|
|
159
|
+
const grip = $(r.el).querySelector('.grip');
|
|
160
|
+
grip.addEventListener('pointerdown', (e) => {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
const startX = e.clientX, startW = $(r.el).getBoundingClientRect().width;
|
|
163
|
+
const move = (ev) => {
|
|
164
|
+
const w2 = Math.min(r.max, Math.max(r.min, startW + ev.clientX - startX));
|
|
165
|
+
$(r.el).style.width = w2 + 'px';
|
|
166
|
+
};
|
|
167
|
+
const up = () => {
|
|
168
|
+
localStorage.setItem('ud.rail.' + r.el, String($(r.el).getBoundingClientRect().width | 0));
|
|
169
|
+
window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up);
|
|
170
|
+
};
|
|
171
|
+
window.addEventListener('pointermove', move); window.addEventListener('pointerup', up);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// колесо над «тихим» местом листает ленту; клик по подрезанному рейлу довозит его
|
|
175
|
+
$('strip').addEventListener('wheel', (e) => {
|
|
176
|
+
let node = e.target;
|
|
177
|
+
while (node && node !== document.body) {
|
|
178
|
+
if (node.scrollHeight > node.clientHeight + 1 && getComputedStyle(node).overflowY !== 'hidden') return;
|
|
179
|
+
node = node.parentElement;
|
|
180
|
+
}
|
|
181
|
+
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) { $('strip').scrollLeft += e.deltaY; e.preventDefault(); }
|
|
182
|
+
}, { passive: false });
|
|
183
|
+
for (const r of RAILS) $(r.el).addEventListener('click', (e) => {
|
|
184
|
+
if (e.target.closest('.grip')) return;
|
|
185
|
+
const el = $(r.el), s = $('strip');
|
|
186
|
+
const left = el.offsetLeft - s.scrollLeft, right = left + el.offsetWidth;
|
|
187
|
+
if (left < 0 || right > s.clientWidth) el.scrollIntoView({ behavior: 'smooth', inline: 'nearest', block: 'nearest' });
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ── выбор: источник истины — URL ─────────────────────────────────────────────────────────────
|
|
191
|
+
function select(id, state, push = true) {
|
|
192
|
+
const q = params();
|
|
193
|
+
q.set('form', id);
|
|
194
|
+
if (state) q.set('state', state); else q.delete('state');
|
|
195
|
+
if (push) history.pushState(null, '', '?' + q.toString());
|
|
196
|
+
renderAll();
|
|
197
|
+
}
|
|
198
|
+
window.addEventListener('popstate', renderAll);
|
|
199
|
+
|
|
200
|
+
function current() {
|
|
201
|
+
const q = params();
|
|
202
|
+
const id = q.get('form') || (DATA.contracts[0] && DATA.contracts[0].id);
|
|
203
|
+
const c = DATA.contracts.find((x) => x.id === id) || DATA.contracts[0];
|
|
204
|
+
return { c, state: q.get('state') };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function statesOf(c) {
|
|
208
|
+
const set = new Set();
|
|
209
|
+
const inv = DATA.inventory && DATA.inventory[c.screen];
|
|
210
|
+
if (inv) for (const s of Object.keys(inv)) set.add(s);
|
|
211
|
+
for (const p of c.presents || []) for (const s of p.when || []) set.add(s);
|
|
212
|
+
for (const k of c.controls || []) for (const s of k.shown_when || []) set.add(s);
|
|
213
|
+
return set.size ? [...set] : ['—'];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function renderTree() {
|
|
217
|
+
const bag = {};
|
|
218
|
+
for (const c of DATA.contracts) (bag[c.page || '(без страницы)'] ||= []).push(c);
|
|
219
|
+
const opened = new Set();
|
|
220
|
+
for (const c of DATA.contracts) for (const k of c.controls || []) if (k.opens) opened.add(k.opens);
|
|
221
|
+
const { c: sel } = current();
|
|
222
|
+
let html = '';
|
|
223
|
+
for (const [page, list] of Object.entries(bag)) {
|
|
224
|
+
html += '<div class="page" data-group="page">' + page + '</div>';
|
|
225
|
+
for (const c of list.filter((x) => !opened.has(x.id))) html += leaf(c, sel, list, 0);
|
|
226
|
+
}
|
|
227
|
+
$('tree').innerHTML = html;
|
|
228
|
+
function leaf(c, sel, list, depth) {
|
|
229
|
+
let h = '<a href="?form=' + encodeURIComponent(c.id) + '" data-item="contract" data-id="' + c.id +
|
|
230
|
+
'" class="' + (sel && sel.id === c.id ? 'sel' : '') + '">' + c.screen + '<br><span style="font-size:11px;color:' +
|
|
231
|
+
(sel && sel.id === c.id ? '#cfcfc6' : 'var(--dim)') + '">' + c.id + '</span></a>';
|
|
232
|
+
const children = [];
|
|
233
|
+
for (const k of c.controls || []) if (k.opens) { const t = DATA.contracts.find((x) => x.id === k.opens); if (t) children.push(t); }
|
|
234
|
+
if (children.length) h += '<div class="opens">' + children.map((t) => leaf(t, sel, list, depth + 1)).join('') + '</div>';
|
|
235
|
+
return h;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
$('tree').addEventListener('click', (e) => {
|
|
239
|
+
const a = e.target.closest('a[data-id]');
|
|
240
|
+
if (a) { e.preventDefault(); select(a.getAttribute('data-id'), null); }
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
function renderDoc() {
|
|
244
|
+
const { c } = current();
|
|
245
|
+
if (!c) { $('doc').innerHTML = '<p>Контрактов не найдено.</p>'; return; }
|
|
246
|
+
let h = '<dl class="kv" data-group="identity">';
|
|
247
|
+
for (const [k, v] of [['id', c.id], ['screen', c.screen], ['page', c.page], ['entity', c.entity]])
|
|
248
|
+
if (v) h += '<dt>' + k + '</dt><dd>' + v + '</dd>';
|
|
249
|
+
h += '</dl>';
|
|
250
|
+
h += '<dl class="kv"><dt>presents — что человек обязан видеть</dt></dl>';
|
|
251
|
+
for (const p of c.presents || [])
|
|
252
|
+
h += '<div class="item" data-item="presents"><div class="name">' + p.field + (p.when ? ' <span class="meta">· ' + p.when.join(', ') + '</span>' : '') +
|
|
253
|
+
'</div><div class="shows">' + p.shows + '</div>' + (p.note ? '<div class="meta">' + p.note + '</div>' : '') + '</div>';
|
|
254
|
+
if ((c.controls || []).length) {
|
|
255
|
+
h += '<dl class="kv"><dt>controls</dt></dl>';
|
|
256
|
+
for (const k of c.controls || [])
|
|
257
|
+
h += '<div class="item" data-item="control"><div class="name">' + k.control +
|
|
258
|
+
(k.shown_when ? ' <span class="meta">· ' + k.shown_when.join(', ') + '</span>' : '') + '</div>' +
|
|
259
|
+
(k.calls ? '<div class="meta">calls: ' + k.calls + '</div>' : '') +
|
|
260
|
+
(k.opens ? '<div class="meta">opens: ' + k.opens + '</div>' : '') +
|
|
261
|
+
(k.shown_when_rule ? '<div class="meta">когда: ' + k.shown_when_rule + '</div>' : '') +
|
|
262
|
+
(k.behaviour ? '<div class="meta">' + k.behaviour + '</div>' : '') + '</div>';
|
|
263
|
+
}
|
|
264
|
+
if ((c.groups || []).length) {
|
|
265
|
+
h += '<dl class="kv"><dt>groups — как владелец группирует экран</dt></dl>';
|
|
266
|
+
for (const g of c.groups || [])
|
|
267
|
+
h += '<div class="item" data-item="group"><div class="name">' + g.group + ' <span class="meta">· ' + g.role + '</span></div>' +
|
|
268
|
+
'<div class="meta">' + (g.contains || []).join(' · ') + '</div>' +
|
|
269
|
+
(g.note ? '<div class="meta">' + g.note + '</div>' : '') + '</div>';
|
|
270
|
+
}
|
|
271
|
+
if ((c.removed || []).length) {
|
|
272
|
+
h += '<dl class="kv"><dt>removed — решено, что этого быть не должно</dt></dl>';
|
|
273
|
+
for (const r of c.removed || []) h += '<div class="item" data-item="removed"><div class="name">' + r.control + '</div><div class="meta">' + (r.was || '') + '</div></div>';
|
|
274
|
+
}
|
|
275
|
+
if (c.__body) h += '<div class="prose" data-group="prose">' + c.__body.replace(/</g, '<') + '</div>';
|
|
276
|
+
$('doc').innerHTML = h;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function renderWire() {
|
|
280
|
+
const { c, state: fromUrl } = current();
|
|
281
|
+
if (!c) { $('wire').innerHTML = ''; $('states').innerHTML = ''; return; }
|
|
282
|
+
const states = statesOf(c);
|
|
283
|
+
const state = states.includes(fromUrl) ? fromUrl : states[0];
|
|
284
|
+
$('states').innerHTML = states.map((s) =>
|
|
285
|
+
'<button data-control="state" data-state="' + s + '" class="' + (s === state ? 'sel' : '') + '">' + s + '</button>').join('');
|
|
286
|
+
const inv = DATA.inventory && DATA.inventory[c.screen];
|
|
287
|
+
const now = inv && inv[state];
|
|
288
|
+
const badge = (kind, label) => '<span class="badge b-' + kind + '">' + label + '</span>';
|
|
289
|
+
// Каркас рисуется ПО ГРУППАМ контракта: порядок групп = порядок на панели, роль группы —
|
|
290
|
+
// подпись и грубый вид (шапка/подвал/таблица/кнопки/меню). Всё, что владелец не сгруппировал,
|
|
291
|
+
// честно падает в «вне групп» — не спрятано и не разложено за него.
|
|
292
|
+
const fieldByName = new Map((c.presents || []).map((p) => [p.field, p]));
|
|
293
|
+
const controlByName = new Map((c.controls || []).map((k) => [k.control, k]));
|
|
294
|
+
const grouped = new Set();
|
|
295
|
+
for (const g of c.groups || []) {
|
|
296
|
+
for (const m of g.contains || []) grouped.add(m);
|
|
297
|
+
// Контейнер, который САМ показывает содержимое: якорь группы совпал с элементом presents —
|
|
298
|
+
// элемент рисуется первым внутри своей группы, а не вторым экземпляром «вне групп».
|
|
299
|
+
if (fieldByName.has(g.group)) grouped.add(g.group);
|
|
300
|
+
}
|
|
301
|
+
const claimedF = new Set((c.presents || []).map((p) => p.field));
|
|
302
|
+
const claimedC = new Set((c.controls || []).map((k) => k.control));
|
|
303
|
+
|
|
304
|
+
const fieldBox = (p) => {
|
|
305
|
+
if (p.when && !p.when.includes(state)) return '';
|
|
306
|
+
const v = !now ? '' : now.fields.includes(p.field) ? badge('ok', 'есть') : badge('todo', 'нет в коде');
|
|
307
|
+
return '<div class="el" data-item="wire-field" data-anchor="' + p.field + '"><div class="name">' + p.field + v + '</div><div class="shows">' + p.shows + '</div></div>';
|
|
308
|
+
};
|
|
309
|
+
const controlChip = (k) => {
|
|
310
|
+
if (k.shown_when && !k.shown_when.includes(state)) return '';
|
|
311
|
+
const v = !now ? '' : now.controls.includes(k.control) ? badge('ok', 'есть') : badge('todo', 'нет в коде');
|
|
312
|
+
return '<span class="ctl' + (k.shown_when ? '' : ' rule') + '" data-item="wire-control" data-anchor="' + k.control + '">' + k.control + v + '</span>';
|
|
313
|
+
};
|
|
314
|
+
const member = (name) =>
|
|
315
|
+
fieldByName.has(name) ? fieldBox(fieldByName.get(name)) :
|
|
316
|
+
controlByName.has(name) ? controlChip(controlByName.get(name)) : '';
|
|
317
|
+
|
|
318
|
+
const ROLE_LABEL = { header: 'шапка', footer: 'подвал', section: 'секция', table: 'таблица', list: 'список', toolbar: 'кнопки', menu: 'меню ⋯' };
|
|
319
|
+
let h = '<div class="wire" data-group="wire-canvas">';
|
|
320
|
+
for (const g of c.groups || []) {
|
|
321
|
+
const names = fieldByName.has(g.group) ? [g.group, ...(g.contains || [])] : (g.contains || []);
|
|
322
|
+
const inner = names.map(member).join('');
|
|
323
|
+
if (!inner) continue; // в этом состоянии группе нечего показать
|
|
324
|
+
h += '<div class="grp grp-' + g.role + '" data-item="wire-group" data-anchor="' + g.group + '" data-role="' + g.role + '">' +
|
|
325
|
+
'<div class="grphead"><span>' + g.group + '</span><span>' + (ROLE_LABEL[g.role] || g.role) + '</span></div>' + inner + '</div>';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const looseFields = (c.presents || []).filter((p) => !grouped.has(p.field)).map(fieldBox).join('');
|
|
329
|
+
let looseControls = (c.controls || []).filter((k) => !grouped.has(k.control)).map(controlChip).join('');
|
|
330
|
+
for (const r of c.removed || []) {
|
|
331
|
+
claimedC.add(r.control);
|
|
332
|
+
if (now && now.controls.includes(r.control))
|
|
333
|
+
looseControls += '<span class="ctl removedghost" data-item="wire-removed" data-anchor="' + r.control + '">' + r.control + badge('todo', 'должно исчезнуть') + '</span>';
|
|
334
|
+
}
|
|
335
|
+
if (looseFields || looseControls) {
|
|
336
|
+
h += '<div class="' + ((c.groups || []).length ? 'ungrouped' : '') + '" data-group="wire-ungrouped">' +
|
|
337
|
+
((c.groups || []).length ? '<div class="grphead"><span>вне групп</span></div>' : '') +
|
|
338
|
+
looseFields + (looseControls ? '<div class="btnrow" data-group="wire-controls">' + looseControls + '</div>' : '') + '</div>';
|
|
339
|
+
}
|
|
340
|
+
h += '</div>';
|
|
341
|
+
if (now) {
|
|
342
|
+
const extraF = now.fields.filter((f) => !claimedF.has(f));
|
|
343
|
+
const extraC = now.controls.filter((f) => !claimedC.has(f));
|
|
344
|
+
if (extraF.length || extraC.length) {
|
|
345
|
+
h += '<div class="extras" data-group="wire-extras"><dl class="kv"><dt>отрендерено, но владелец не решал</dt></dl>';
|
|
346
|
+
for (const f of extraF) h += '<div class="el"><div class="name">' + f + badge('extra', 'вне контракта') + '</div></div>';
|
|
347
|
+
if (extraC.length) h += '<div class="btnrow">' + extraC.map((x) => '<span class="ctl rule">' + x + badge('extra', 'вне контракта') + '</span>').join('') + '</div>';
|
|
348
|
+
h += '</div>';
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
$('wire').innerHTML = h;
|
|
352
|
+
}
|
|
353
|
+
$('states').addEventListener('click', (e) => {
|
|
354
|
+
const b = e.target.closest('button[data-state]');
|
|
355
|
+
if (b) select(current().c.id, b.getAttribute('data-state'));
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
function renderAll() {
|
|
359
|
+
document.getElementById('src').textContent =
|
|
360
|
+
DATA.contracts.length + ' контракт(ов)' + (DATA.producedBy ? ' · перечень: ' + DATA.producedBy : ' · перечня формы нет — каркас без вердиктов');
|
|
361
|
+
renderTree(); renderDoc(); renderWire();
|
|
362
|
+
}
|
|
363
|
+
renderAll();
|
|
364
|
+
</script>
|
|
365
|
+
</body>
|
|
366
|
+
</html>
|
|
367
|
+
`;
|
|
368
|
+
}
|
|
369
|
+
export function commandPreview(configPath, outPath, loadConfig) {
|
|
370
|
+
const { config, base } = loadConfig(configPath);
|
|
371
|
+
const data = collectPreviewData(config, base);
|
|
372
|
+
if (data.contracts.length === 0) {
|
|
373
|
+
console.error("usedesign preview: the config's `forms` patterns matched no contract");
|
|
374
|
+
return 1;
|
|
375
|
+
}
|
|
376
|
+
writeFileSync(outPath, renderPreviewHtml(data), "utf8");
|
|
377
|
+
console.log(`preview: ${data.contracts.length} contract(s) → ${outPath}${data.inventory ? " (с вердиктами по перечню формы)" : " (перечня формы нет — без вердиктов)"}`);
|
|
378
|
+
return 0;
|
|
379
|
+
}
|
|
380
|
+
//# sourceMappingURL=preview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview.js","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/nistoc/usedesign/schema/form-contract.schema.json",
|
|
4
|
+
"title": "Form contract",
|
|
5
|
+
"description": "What a screen MUST show, written by the owner — not what the code happens to render. The one artifact in this format whose truth is authored rather than measured: check 5 then keeps the built thing equal to the chosen thing. Lines that do not match the code yet are not errors in the contract; they are the product's TODO list, printed by every build.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["usedesign_form", "id", "screen", "presents"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"usedesign_form": { "const": 1 },
|
|
11
|
+
"id": { "type": "string", "pattern": "^[a-z0-9]+(\\.[a-z0-9-]+){2,}$" },
|
|
12
|
+
"screen": { "type": "string", "minLength": 1 },
|
|
13
|
+
"page": { "type": "string" },
|
|
14
|
+
"entity": {
|
|
15
|
+
"description": "Whose lifecycle the screen lives against. The state vocabulary is the entity's, computed from the cards that touch it — never declared here, or it would be a duplicate that drifts.",
|
|
16
|
+
"type": "string"
|
|
17
|
+
},
|
|
18
|
+
"presents": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"minItems": 1,
|
|
21
|
+
"items": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"required": ["field", "shows"],
|
|
24
|
+
"additionalProperties": false,
|
|
25
|
+
"properties": {
|
|
26
|
+
"field": {
|
|
27
|
+
"description": "The machine anchor: a data-field/data-section marker the rendered screen carries. The contract names the anchor; the form inventory proves it.",
|
|
28
|
+
"type": "string",
|
|
29
|
+
"minLength": 1
|
|
30
|
+
},
|
|
31
|
+
"shows": { "type": "string", "minLength": 1 },
|
|
32
|
+
"when": {
|
|
33
|
+
"description": "Entity states in which the element must be present. Absent = in every state the inventory has for this screen.",
|
|
34
|
+
"type": "array",
|
|
35
|
+
"minItems": 1,
|
|
36
|
+
"items": { "type": "string" }
|
|
37
|
+
},
|
|
38
|
+
"note": { "type": "string" }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"controls": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"items": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"required": ["control"],
|
|
47
|
+
"additionalProperties": false,
|
|
48
|
+
"properties": {
|
|
49
|
+
"control": { "type": "string", "minLength": 1 },
|
|
50
|
+
"calls": {
|
|
51
|
+
"description": "The operation card this control invokes, or null for local behaviour. Naming a card lets check 5 compare shown_when with the operation's data_transition.from — the measured rule: a control is available exactly in the states its operation departs from.",
|
|
52
|
+
"oneOf": [{ "type": "string" }, { "type": "null" }]
|
|
53
|
+
},
|
|
54
|
+
"shown_when": {
|
|
55
|
+
"type": "array",
|
|
56
|
+
"minItems": 1,
|
|
57
|
+
"items": { "type": "string" }
|
|
58
|
+
},
|
|
59
|
+
"shown_when_rule": {
|
|
60
|
+
"description": "Availability that depends on more than the entity state, in words. Check 5 can only verify presence somewhere, not the rule itself — written so the limit is declared, not discovered.",
|
|
61
|
+
"type": "string"
|
|
62
|
+
},
|
|
63
|
+
"opens": {
|
|
64
|
+
"description": "The form contract this control opens (a modal, an overlay) — a flat link, not a nested tree: nesting was refuted twice by measurement. The opened form is a contract of its own.",
|
|
65
|
+
"type": "string"
|
|
66
|
+
},
|
|
67
|
+
"behaviour": { "type": "string" },
|
|
68
|
+
"placement": { "type": "string" },
|
|
69
|
+
"note": { "type": "string" }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"groups": {
|
|
74
|
+
"description": "How the owner groups the screen by PURPOSE: headers, footers, tables, toolbars — and which elements and controls belong to each. Array order IS the group order; a separate order key would be a duplicate that drifts. Membership is authored, not yet verified: the form inventory records anchors flat, so check 5 proves a group's anchor exists but not who sits inside it — a declared limit, like shown_when_rule.",
|
|
75
|
+
"type": "array",
|
|
76
|
+
"items": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"required": ["group", "role", "contains"],
|
|
79
|
+
"additionalProperties": false,
|
|
80
|
+
"properties": {
|
|
81
|
+
"group": {
|
|
82
|
+
"description": "The container's own data-* anchor (data-section / data-panel-head).",
|
|
83
|
+
"type": "string",
|
|
84
|
+
"minLength": 1
|
|
85
|
+
},
|
|
86
|
+
"role": {
|
|
87
|
+
"description": "The group's purpose, not its looks.",
|
|
88
|
+
"enum": ["header", "footer", "section", "table", "list", "toolbar", "menu"]
|
|
89
|
+
},
|
|
90
|
+
"contains": {
|
|
91
|
+
"description": "Fields and controls of this contract, in display order.",
|
|
92
|
+
"type": "array",
|
|
93
|
+
"minItems": 1,
|
|
94
|
+
"items": { "type": "string" }
|
|
95
|
+
},
|
|
96
|
+
"note": { "type": "string" }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"removed": {
|
|
101
|
+
"description": "Controls the owner has decided must NOT exist. Presence anywhere is a finding — a decision is only real while something enforces it.",
|
|
102
|
+
"type": "array",
|
|
103
|
+
"items": {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"required": ["control"],
|
|
106
|
+
"additionalProperties": false,
|
|
107
|
+
"properties": {
|
|
108
|
+
"control": { "type": "string" },
|
|
109
|
+
"was": { "type": "string" },
|
|
110
|
+
"verdict": { "type": "string" }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/nistoc/usedesign/schema/form-inventory.schema.json",
|
|
4
|
+
"title": "Form inventory",
|
|
5
|
+
"description": "What the rendered UI says about ITSELF: for each screen, in each entity state, which machine anchors (data-field/data-section) and controls (data-action) it actually carries. Produced by rendering the real components in a test — never by parsing their source. The sibling of the route and storage inventories.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["usedesign_form_inventory", "produced_by", "forms"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"usedesign_form_inventory": { "const": 1 },
|
|
10
|
+
"produced_by": { "type": "string", "minLength": 3 },
|
|
11
|
+
"forms": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"items": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"required": ["screen", "states"],
|
|
16
|
+
"additionalProperties": false,
|
|
17
|
+
"properties": {
|
|
18
|
+
"screen": { "type": "string", "minLength": 1 },
|
|
19
|
+
"states": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"minItems": 1,
|
|
22
|
+
"items": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"required": ["state", "fields", "controls"],
|
|
25
|
+
"additionalProperties": false,
|
|
26
|
+
"properties": {
|
|
27
|
+
"state": { "type": "string", "minLength": 1 },
|
|
28
|
+
"fields": { "type": "array", "items": { "type": "string" } },
|
|
29
|
+
"controls": { "type": "array", "items": { "type": "string" } }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -607,6 +607,29 @@
|
|
|
607
607
|
"items": {
|
|
608
608
|
"type": "string"
|
|
609
609
|
}
|
|
610
|
+
},
|
|
611
|
+
"storage": {
|
|
612
|
+
"description": "Where the data physically lives, in terms the storage itself declares. `store` is a PATTERN, never a literal name: store names carry the environment (…-dev, …-prod) and a card that names one is true in one environment and false in every other. Checked against the storage inventory by check 4.",
|
|
613
|
+
"type": "array",
|
|
614
|
+
"minItems": 1,
|
|
615
|
+
"items": {
|
|
616
|
+
"type": "object",
|
|
617
|
+
"required": ["store"],
|
|
618
|
+
"additionalProperties": false,
|
|
619
|
+
"properties": {
|
|
620
|
+
"store": { "type": "string", "minLength": 1 },
|
|
621
|
+
"keyed_by": {
|
|
622
|
+
"type": "array",
|
|
623
|
+
"minItems": 1,
|
|
624
|
+
"items": { "type": "string" }
|
|
625
|
+
},
|
|
626
|
+
"via_index": {
|
|
627
|
+
"description": "The secondary index this operation depends on. Naming one is a promise that removing it breaks this operation.",
|
|
628
|
+
"type": "string"
|
|
629
|
+
},
|
|
630
|
+
"note": { "type": "string" }
|
|
631
|
+
}
|
|
632
|
+
}
|
|
610
633
|
}
|
|
611
634
|
}
|
|
612
635
|
},
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/nistoc/usedesign/schema/storage-inventory.schema.json",
|
|
4
|
+
"title": "Storage inventory",
|
|
5
|
+
"description": "What the storage says about ITSELF: which stores exist, what they are keyed by, and which secondary indexes they carry. Produced by dumping the deployed storage — never by reading application code. The sibling of the route inventory, and it exists for the same reason: a card's claim about where data lives is otherwise a claim nobody can be wrong about.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["usedesign_storage_inventory", "produced_by", "stores"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"usedesign_storage_inventory": { "const": 1 },
|
|
10
|
+
"produced_by": {
|
|
11
|
+
"description": "How this file was made, in words a human can check. An inventory of unknown origin is worse than none: it looks like evidence.",
|
|
12
|
+
"type": "string",
|
|
13
|
+
"minLength": 3
|
|
14
|
+
},
|
|
15
|
+
"environment": {
|
|
16
|
+
"description": "Which environment was dumped. Store names usually carry it as a suffix, and cards must never name one.",
|
|
17
|
+
"type": "string"
|
|
18
|
+
},
|
|
19
|
+
"stores": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"items": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"required": ["name", "keyed_by"],
|
|
24
|
+
"additionalProperties": false,
|
|
25
|
+
"properties": {
|
|
26
|
+
"name": { "type": "string", "minLength": 1 },
|
|
27
|
+
"keyed_by": {
|
|
28
|
+
"description": "The primary key attributes, in order: partition key first, then sort key if there is one.",
|
|
29
|
+
"type": "array",
|
|
30
|
+
"minItems": 1,
|
|
31
|
+
"items": { "type": "string" }
|
|
32
|
+
},
|
|
33
|
+
"indexes": {
|
|
34
|
+
"type": "array",
|
|
35
|
+
"items": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"required": ["name"],
|
|
38
|
+
"additionalProperties": false,
|
|
39
|
+
"properties": {
|
|
40
|
+
"name": { "type": "string" },
|
|
41
|
+
"keyed_by": { "type": "array", "items": { "type": "string" } }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"source": { "type": "string" }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/validate.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ export declare const MATURITY: string[];
|
|
|
10
10
|
export declare function schemaPathFor(name?: string): string;
|
|
11
11
|
/** Validate a card's *form* against the published JSON Schema. */
|
|
12
12
|
export declare function validateSchema(fm: Card): Finding[];
|
|
13
|
+
/** Validate a form contract against its published JSON Schema. */
|
|
14
|
+
export declare function validateFormSchema(fm: Card): Finding[];
|
|
15
|
+
/**
|
|
16
|
+
* Validate one form contract's *meaning*. `knownForms` enables the cross-contract link warning
|
|
17
|
+
* (`opens` pointing at a contract that is not in the validated set); pass null for a lone file.
|
|
18
|
+
*/
|
|
19
|
+
export declare function validateForm(fm: Card, filename?: string, knownForms?: Set<string> | null): Finding[];
|
|
13
20
|
/**
|
|
14
21
|
* Validate one card's *meaning*. `knownIds` enables the cross-card reference warnings; pass null
|
|
15
22
|
* when validating a card on its own, so that pointing at a card outside the set is not reported
|