eidosmd 0.1.0 → 0.3.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 +50 -29
- package/browser/dist/assets/index-Cc3cNWHY.css +1 -0
- package/browser/dist/assets/index-DQgCQRa5.js +46 -0
- package/browser/dist/favicon.svg +5 -0
- package/browser/dist/index.html +15 -0
- package/browser/dist/mark.svg +4 -0
- package/dist/src/cli.js +7 -0
- package/dist/src/commands/agents.js +1 -1
- package/dist/src/commands/canvas.js +77 -0
- package/dist/src/commands/check.js +10 -4
- package/dist/src/commands/configure.js +201 -0
- package/dist/src/commands/framework.js +37 -7
- package/dist/src/commands/index.js +4 -4
- package/dist/src/commands/init.js +5 -0
- package/dist/src/commands/instructions.js +1 -1
- package/dist/src/commands/list.js +8 -8
- package/dist/src/commands/migrate.js +32 -0
- package/dist/src/commands/new.js +3 -3
- package/dist/src/commands/property.js +125 -0
- package/dist/src/commands/seeds.js +5 -5
- package/dist/src/commands/setup.js +131 -0
- package/dist/src/commands/version.js +44 -0
- package/dist/src/commands/whoami.js +4 -4
- package/dist/src/context.js +5 -5
- package/dist/src/core/blueprint.js +16 -11
- package/dist/src/core/canvas-schema.js +148 -0
- package/dist/src/core/canvas.js +732 -0
- package/dist/src/core/check.js +221 -70
- package/dist/src/core/convert.js +7 -6
- package/dist/src/core/edits.js +1381 -0
- package/dist/src/core/framework-markdown.js +119 -34
- package/dist/src/core/framework-model.js +62 -10
- package/dist/src/core/framework-structured.js +222 -47
- package/dist/src/core/framework.js +13 -13
- package/dist/src/core/frontmatter.js +61 -1
- package/dist/src/core/git.js +84 -0
- package/dist/src/core/index-leaf.js +2 -2
- package/dist/src/core/links.js +87 -0
- package/dist/src/core/markdown.js +16 -9
- package/dist/src/core/me.js +16 -8
- package/dist/src/core/migrate.js +319 -0
- package/dist/src/core/naming.js +1 -1
- package/dist/src/core/regions.js +117 -0
- package/dist/src/core/root.js +2 -2
- package/dist/src/core/scaffold.js +33 -27
- package/dist/src/core/seed.js +187 -71
- package/dist/src/core/server.js +1410 -53
- package/dist/src/core/settings.js +232 -0
- package/dist/src/core/store.js +315 -0
- package/dist/src/core/template.js +32 -0
- package/dist/src/core/versions.js +84 -0
- package/dist/src/output.js +4 -1
- package/dist/src/program.js +421 -41
- package/instructions/authoring.md +15 -12
- package/instructions/configuring.md +81 -34
- package/instructions/init-required.md +4 -4
- package/instructions/overview.md +21 -9
- package/instructions/validating.md +9 -6
- package/package.json +21 -12
- package/standard/EIDOS.md +142 -259
- package/standard/seeds/README.md +12 -16
- package/standard/seeds/book/Framework.yaml +61 -0
- package/standard/seeds/book/README.md +10 -5
- package/standard/seeds/book/_gitignore +9 -3
- package/standard/seeds/book/me.md +1 -1
- package/standard/seeds/book/roles/README.md +3 -3
- package/standard/seeds/book/roles/framework-owner.md +2 -2
- package/standard/seeds/book/{shapes → templates}/chapter.full.md +0 -8
- package/standard/seeds/book/{shapes → templates}/chapter.sketch.md +0 -7
- package/standard/seeds/book/{shapes → templates}/frame.market.md +0 -6
- package/standard/seeds/book/templates/frame.premise.md +17 -0
- package/standard/seeds/book/{shapes → templates}/frame.reader.md +0 -6
- package/standard/seeds/book/{shapes → templates}/frame.voice.md +0 -7
- package/standard/seeds/research/Framework.yaml +61 -0
- package/standard/seeds/research/README.md +10 -5
- package/standard/seeds/research/_gitignore +9 -3
- package/standard/seeds/research/me.md +1 -1
- package/standard/seeds/research/roles/README.md +3 -3
- package/standard/seeds/research/roles/framework-owner.md +2 -2
- package/standard/seeds/research/{shapes → templates}/frame.ethics.md +0 -6
- package/standard/seeds/research/{shapes → templates}/frame.method.md +0 -7
- package/standard/seeds/research/{shapes → templates}/frame.prior-work.md +0 -6
- package/standard/seeds/research/{shapes → templates}/frame.question.md +0 -7
- package/standard/seeds/research/{shapes → templates}/investigation.full.md +0 -8
- package/standard/seeds/research/{shapes → templates}/investigation.note.md +0 -7
- package/standard/seeds/software/Framework.yaml +62 -0
- package/standard/seeds/software/README.md +7 -6
- package/standard/seeds/software/_gitignore +9 -3
- package/standard/seeds/software/me.md +1 -1
- package/standard/seeds/software/roles/README.md +3 -3
- package/standard/seeds/software/roles/framework-owner.md +2 -2
- package/standard/seeds/software/roles/project-manager.md +2 -2
- package/standard/seeds/software/roles/stakeholder.md +1 -1
- package/standard/seeds/software/{shapes → templates}/frame.architecture.md +0 -7
- package/standard/seeds/software/{shapes → templates}/frame.audience.md +1 -8
- package/standard/seeds/software/{shapes → templates}/frame.criteria.md +0 -8
- package/standard/seeds/software/{shapes → templates}/frame.market.md +0 -8
- package/standard/seeds/software/{shapes → templates}/spec.full.md +0 -8
- package/standard/seeds/software/{shapes → templates}/spec.micro.md +0 -9
- package/browser/index.html +0 -268
- package/dist/src/commands/convert.js +0 -30
- package/dist/src/core/shape.js +0 -26
- package/standard/seeds/book/Framework.md +0 -87
- package/standard/seeds/book/shapes/frame.premise.md +0 -24
- package/standard/seeds/research/Framework.md +0 -88
- package/standard/seeds/software/Framework.md +0 -88
- /package/standard/seeds/software/{shapes → templates}/.gitkeep +0 -0
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
// Canvas maps: the design tool. A canvas is one YAML file in this tool's own
|
|
2
|
+
// folder, `.eidos/plugins/eidosmd/maps/`, a Figma-style file of pages; a node
|
|
3
|
+
// on a page is either a sketch (a label and a kind, living only here) or a
|
|
4
|
+
// blueprint, referenced by id, whose title, summary, and styled properties
|
|
5
|
+
// are read from the file at render time. The canvas holds only what a
|
|
6
|
+
// blueprint cannot: the flows between nodes, frames, and layout overrides.
|
|
7
|
+
// A snapshot of the whole root is a version in the framework document.
|
|
8
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { parse as parseYaml, Document } from 'yaml';
|
|
11
|
+
import { blueprintId, blueprintTitle, propertyString } from './blueprint.js';
|
|
12
|
+
import { FRAMEWORK_DIR, PLUGINS_DIR, readValueStyle, TOOL_KEY } from './framework-model.js';
|
|
13
|
+
import { kebab } from './naming.js';
|
|
14
|
+
// Relative to `.eidos/`: the folder the standard reserves for this tool.
|
|
15
|
+
export const MAPS_DIR = `${PLUGINS_DIR}/${TOOL_KEY}/maps`;
|
|
16
|
+
// Where an edge meets a node: one of eight points on its border. Absent,
|
|
17
|
+
// the browser picks the side that faces the other node.
|
|
18
|
+
export const ANCHORS = ['top-left', 'top', 'top-right', 'right', 'bottom-right', 'bottom', 'bottom-left', 'left'];
|
|
19
|
+
const isAnchor = (value) => typeof value === 'string' && ANCHORS.includes(value);
|
|
20
|
+
// How an edge ends: nothing, an open arrowhead, a filled or open triangle,
|
|
21
|
+
// an open circle, an open diamond, or a bar across the line. The end defaults
|
|
22
|
+
// to an arrow, the start to nothing.
|
|
23
|
+
export const EDGE_CAPS = ['none', 'arrow', 'triangle', 'triangle-open', 'circle', 'diamond', 'bar'];
|
|
24
|
+
const isCap = (value) => typeof value === 'string' && EDGE_CAPS.includes(value);
|
|
25
|
+
export const FRAME_REF = 'frame:';
|
|
26
|
+
export const isFrameRef = (id) => id.startsWith(FRAME_REF);
|
|
27
|
+
export class CanvasError extends Error {
|
|
28
|
+
status;
|
|
29
|
+
constructor(message, status = 400) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'CanvasError';
|
|
32
|
+
this.status = status;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const isPlain = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
36
|
+
const text = (value) => (value === undefined || value === null ? '' : String(value));
|
|
37
|
+
const list = (value) => (Array.isArray(value) ? value : []);
|
|
38
|
+
export function mapsDir(framework) {
|
|
39
|
+
return path.join(framework.root, FRAMEWORK_DIR, MAPS_DIR);
|
|
40
|
+
}
|
|
41
|
+
export function canvasFile(framework, id) {
|
|
42
|
+
return path.join(mapsDir(framework), `${id}.yaml`);
|
|
43
|
+
}
|
|
44
|
+
function readNode(value, where, problems) {
|
|
45
|
+
if (!isPlain(value) || text(value['id']) === '') {
|
|
46
|
+
problems.push(`${where}: a node needs an id`);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const node = { id: text(value['id']) };
|
|
50
|
+
if (text(value['blueprint']) !== '') {
|
|
51
|
+
node.blueprint = text(value['blueprint']);
|
|
52
|
+
}
|
|
53
|
+
else if (isPlain(value['sketch'])) {
|
|
54
|
+
const sketch = value['sketch'];
|
|
55
|
+
node.sketch = { label: text(sketch['label']) };
|
|
56
|
+
if (text(sketch['kind']) !== '')
|
|
57
|
+
node.sketch.kind = text(sketch['kind']);
|
|
58
|
+
if (text(sketch['status']) !== '')
|
|
59
|
+
node.sketch.status = text(sketch['status']);
|
|
60
|
+
if (text(sketch['note']) !== '')
|
|
61
|
+
node.sketch.note = text(sketch['note']);
|
|
62
|
+
if (isPlain(sketch['properties'])) {
|
|
63
|
+
const properties = {};
|
|
64
|
+
for (const [key, held] of Object.entries(sketch['properties']))
|
|
65
|
+
if (text(held) !== '')
|
|
66
|
+
properties[key] = text(held);
|
|
67
|
+
if (Object.keys(properties).length > 0)
|
|
68
|
+
node.sketch.properties = properties;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (isPlain(value['sticky'])) {
|
|
72
|
+
const sticky = value['sticky'];
|
|
73
|
+
node.sticky = { text: text(sticky['text']) };
|
|
74
|
+
if (text(sticky['color']) !== '')
|
|
75
|
+
node.sticky.color = text(sticky['color']);
|
|
76
|
+
}
|
|
77
|
+
else if (isPlain(value['text'])) {
|
|
78
|
+
const given = value['text'];
|
|
79
|
+
node.text = { text: text(given['text']) };
|
|
80
|
+
if (given['size'] === 'small' || given['size'] === 'medium' || given['size'] === 'large')
|
|
81
|
+
node.text.size = given['size'];
|
|
82
|
+
if (typeof given['font'] === 'number' && given['font'] >= 8 && given['font'] <= 64)
|
|
83
|
+
node.text.font = Math.round(given['font']);
|
|
84
|
+
}
|
|
85
|
+
else if (isPlain(value['annotation'])) {
|
|
86
|
+
const given = value['annotation'];
|
|
87
|
+
node.annotation = { text: text(given['text']) };
|
|
88
|
+
if (text(given['color']) !== '')
|
|
89
|
+
node.annotation.color = text(given['color']);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
problems.push(`${where} (${node.id}): a node is a blueprint id, a sketch, a sticky, a text, or an annotation`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (text(value['frame']) !== '')
|
|
96
|
+
node.frame = text(value['frame']);
|
|
97
|
+
if (value['locked'] === true)
|
|
98
|
+
node.locked = true;
|
|
99
|
+
const style = readStyle(value['style']);
|
|
100
|
+
if (style)
|
|
101
|
+
node.style = style;
|
|
102
|
+
if (value['styled'] === true)
|
|
103
|
+
node.styled = true;
|
|
104
|
+
const box = readBox(value['box']);
|
|
105
|
+
if (box)
|
|
106
|
+
node.box = box;
|
|
107
|
+
if (isPlain(value['link'])) {
|
|
108
|
+
const link = value['link'];
|
|
109
|
+
node.link = {};
|
|
110
|
+
if (text(link['page']) !== '')
|
|
111
|
+
node.link.page = text(link['page']);
|
|
112
|
+
if (text(link['canvas']) !== '')
|
|
113
|
+
node.link.canvas = text(link['canvas']);
|
|
114
|
+
}
|
|
115
|
+
return node;
|
|
116
|
+
}
|
|
117
|
+
export function readBox(value) {
|
|
118
|
+
if (!isPlain(value) || typeof value['w'] !== 'number' || typeof value['h'] !== 'number')
|
|
119
|
+
return null;
|
|
120
|
+
return { w: Math.max(40, Math.round(value['w'])), h: Math.max(24, Math.round(value['h'])) };
|
|
121
|
+
}
|
|
122
|
+
export const readStyle = readValueStyle;
|
|
123
|
+
function readPage(value, index, problems) {
|
|
124
|
+
const where = `pages[${index}]`;
|
|
125
|
+
if (!isPlain(value) || text(value['id']) === '') {
|
|
126
|
+
problems.push(`${where}: a page needs an id`);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const page = { id: text(value['id']), title: text(value['title']) || text(value['id']), frames: [], nodes: [], edges: [], layout: {}, comments: [] };
|
|
130
|
+
for (const frame of list(value['frames'])) {
|
|
131
|
+
if (isPlain(frame) && text(frame['id']) !== '') {
|
|
132
|
+
const item = { id: text(frame['id']), title: text(frame['title']) || text(frame['id']) };
|
|
133
|
+
if (typeof frame['x'] === 'number' && typeof frame['y'] === 'number' && typeof frame['w'] === 'number' && typeof frame['h'] === 'number') {
|
|
134
|
+
item.x = frame['x'];
|
|
135
|
+
item.y = frame['y'];
|
|
136
|
+
item.w = frame['w'];
|
|
137
|
+
item.h = frame['h'];
|
|
138
|
+
}
|
|
139
|
+
if (text(frame['color']) !== '')
|
|
140
|
+
item.color = text(frame['color']);
|
|
141
|
+
if (frame['locked'] === true)
|
|
142
|
+
item.locked = true;
|
|
143
|
+
if (text(frame['frame']) !== '' && text(frame['frame']) !== item.id)
|
|
144
|
+
item.frame = text(frame['frame']);
|
|
145
|
+
page.frames.push(item);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (const comment of list(value['comments'])) {
|
|
149
|
+
if (!isPlain(comment) || text(comment['id']) === '' || typeof comment['x'] !== 'number' || typeof comment['y'] !== 'number')
|
|
150
|
+
continue;
|
|
151
|
+
const item = { id: text(comment['id']), x: comment['x'], y: comment['y'], messages: [] };
|
|
152
|
+
if (text(comment['node']) !== '')
|
|
153
|
+
item.node = text(comment['node']);
|
|
154
|
+
if (comment['resolved'] === true)
|
|
155
|
+
item.resolved = true;
|
|
156
|
+
for (const message of list(comment['messages'])) {
|
|
157
|
+
if (isPlain(message) && text(message['text']) !== '') {
|
|
158
|
+
const entry = { author: text(message['author']) || 'someone', text: text(message['text']), at: text(message['at']) };
|
|
159
|
+
if (text(message['role']) !== '')
|
|
160
|
+
entry.role = text(message['role']);
|
|
161
|
+
if (text(message['user']) !== '')
|
|
162
|
+
entry.user = text(message['user']);
|
|
163
|
+
item.messages.push(entry);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
page.comments.push(item);
|
|
167
|
+
}
|
|
168
|
+
list(value['nodes']).forEach((node, position) => {
|
|
169
|
+
const read = readNode(node, `${where} (${page.id}) nodes[${position}]`, problems);
|
|
170
|
+
if (read)
|
|
171
|
+
page.nodes.push(read);
|
|
172
|
+
});
|
|
173
|
+
for (const edge of list(value['edges'])) {
|
|
174
|
+
if (isPlain(edge) && text(edge['from']) !== '' && text(edge['to']) !== '') {
|
|
175
|
+
const item = { from: text(edge['from']), to: text(edge['to']) };
|
|
176
|
+
if (text(edge['label']) !== '')
|
|
177
|
+
item.label = text(edge['label']);
|
|
178
|
+
if (typeof edge['label_at'] === 'number' && edge['label_at'] >= 0 && edge['label_at'] <= 1)
|
|
179
|
+
item.label_at = edge['label_at'];
|
|
180
|
+
if (typeof edge['bend'] === 'number')
|
|
181
|
+
item.bend = edge['bend'];
|
|
182
|
+
if (isAnchor(edge['from_anchor']))
|
|
183
|
+
item.from_anchor = edge['from_anchor'];
|
|
184
|
+
if (isAnchor(edge['to_anchor']))
|
|
185
|
+
item.to_anchor = edge['to_anchor'];
|
|
186
|
+
if (text(edge['color']) !== '')
|
|
187
|
+
item.color = text(edge['color']);
|
|
188
|
+
if (edge['style'] === 'dashed')
|
|
189
|
+
item.style = 'dashed';
|
|
190
|
+
if (isCap(edge['start']))
|
|
191
|
+
item.start = edge['start'];
|
|
192
|
+
if (isCap(edge['end']))
|
|
193
|
+
item.end = edge['end'];
|
|
194
|
+
page.edges.push(item);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (isPlain(value['layout'])) {
|
|
198
|
+
for (const [id, position] of Object.entries(value['layout'])) {
|
|
199
|
+
if (isPlain(position) && typeof position['x'] === 'number' && typeof position['y'] === 'number') {
|
|
200
|
+
page.layout[id] = { x: position['x'], y: position['y'] };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return page;
|
|
205
|
+
}
|
|
206
|
+
export function parseCanvas(source, id, file) {
|
|
207
|
+
const problems = [];
|
|
208
|
+
let value;
|
|
209
|
+
try {
|
|
210
|
+
value = parseYaml(source);
|
|
211
|
+
}
|
|
212
|
+
catch (cause) {
|
|
213
|
+
problems.push(`not valid YAML: ${cause instanceof Error ? (cause.message.split('\n')[0] ?? '') : String(cause)}`);
|
|
214
|
+
}
|
|
215
|
+
const document = isPlain(value) ? value : {};
|
|
216
|
+
const pages = list(document['pages'])
|
|
217
|
+
.map((page, index) => readPage(page, index, problems))
|
|
218
|
+
.filter((page) => page !== null);
|
|
219
|
+
if (pages.length === 0 && problems.length === 0) {
|
|
220
|
+
problems.push('a canvas needs at least one page');
|
|
221
|
+
}
|
|
222
|
+
return { id, file, title: text(document['title']) || id, pages, problems };
|
|
223
|
+
}
|
|
224
|
+
export function listCanvases(framework) {
|
|
225
|
+
const dir = mapsDir(framework);
|
|
226
|
+
if (!existsSync(dir)) {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
return readdirSync(dir)
|
|
230
|
+
.filter((entry) => /\.ya?ml$/i.test(entry))
|
|
231
|
+
.sort()
|
|
232
|
+
.map((entry) => loadCanvas(framework, entry.replace(/\.ya?ml$/i, '')));
|
|
233
|
+
}
|
|
234
|
+
export function loadCanvas(framework, id) {
|
|
235
|
+
const file = canvasFile(framework, id);
|
|
236
|
+
if (!existsSync(file)) {
|
|
237
|
+
throw new CanvasError(`no canvas '${id}' under .eidos/${MAPS_DIR}/`, 404);
|
|
238
|
+
}
|
|
239
|
+
return parseCanvas(readFileSync(file, 'utf8'), id, file);
|
|
240
|
+
}
|
|
241
|
+
export function saveCanvas(canvas) {
|
|
242
|
+
const document = { title: canvas.title };
|
|
243
|
+
document['pages'] = canvas.pages.map((page) => {
|
|
244
|
+
const item = { id: page.id, title: page.title };
|
|
245
|
+
if (page.frames.length > 0) {
|
|
246
|
+
item['frames'] = page.frames.map((frame) => {
|
|
247
|
+
const out = { id: frame.id, title: frame.title };
|
|
248
|
+
if (frame.x !== undefined && frame.y !== undefined && frame.w !== undefined && frame.h !== undefined) {
|
|
249
|
+
out['x'] = frame.x;
|
|
250
|
+
out['y'] = frame.y;
|
|
251
|
+
out['w'] = frame.w;
|
|
252
|
+
out['h'] = frame.h;
|
|
253
|
+
}
|
|
254
|
+
if (frame.color)
|
|
255
|
+
out['color'] = frame.color;
|
|
256
|
+
if (frame.locked)
|
|
257
|
+
out['locked'] = true;
|
|
258
|
+
if (frame.frame)
|
|
259
|
+
out['frame'] = frame.frame;
|
|
260
|
+
return out;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
item['nodes'] = page.nodes.map((node) => {
|
|
264
|
+
const out = { id: node.id };
|
|
265
|
+
if (node.blueprint)
|
|
266
|
+
out['blueprint'] = node.blueprint;
|
|
267
|
+
if (node.sketch)
|
|
268
|
+
out['sketch'] = node.sketch;
|
|
269
|
+
if (node.sticky)
|
|
270
|
+
out['sticky'] = node.sticky;
|
|
271
|
+
if (node.text)
|
|
272
|
+
out['text'] = node.text;
|
|
273
|
+
if (node.annotation)
|
|
274
|
+
out['annotation'] = node.annotation;
|
|
275
|
+
if (node.frame)
|
|
276
|
+
out['frame'] = node.frame;
|
|
277
|
+
if (node.link)
|
|
278
|
+
out['link'] = node.link;
|
|
279
|
+
if (node.locked)
|
|
280
|
+
out['locked'] = true;
|
|
281
|
+
if (node.style)
|
|
282
|
+
out['style'] = node.style;
|
|
283
|
+
if (node.styled)
|
|
284
|
+
out['styled'] = true;
|
|
285
|
+
if (node.box)
|
|
286
|
+
out['box'] = node.box;
|
|
287
|
+
return out;
|
|
288
|
+
});
|
|
289
|
+
item['edges'] = page.edges;
|
|
290
|
+
if (Object.keys(page.layout).length > 0)
|
|
291
|
+
item['layout'] = page.layout;
|
|
292
|
+
if (page.comments.length > 0) {
|
|
293
|
+
item['comments'] = page.comments.map((comment) => {
|
|
294
|
+
const out = { id: comment.id, x: comment.x, y: comment.y };
|
|
295
|
+
if (comment.node)
|
|
296
|
+
out['node'] = comment.node;
|
|
297
|
+
if (comment.resolved)
|
|
298
|
+
out['resolved'] = true;
|
|
299
|
+
out['messages'] = comment.messages;
|
|
300
|
+
return out;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return item;
|
|
304
|
+
});
|
|
305
|
+
const yamlDocument = new Document(document);
|
|
306
|
+
yamlDocument.commentBefore = ' A canvas map, authored in `eidos browser`. Nodes are sketches, stickies, or blueprints; the canvas holds the flows, frames, layout, and comment threads. Documented by `eidos canvas --help`.';
|
|
307
|
+
mkdirSync(path.dirname(canvas.file), { recursive: true });
|
|
308
|
+
writeFileSync(canvas.file, yamlDocument.toString({ lineWidth: 0 }), 'utf8');
|
|
309
|
+
}
|
|
310
|
+
export function createCanvas(framework, title) {
|
|
311
|
+
const id = kebab(title);
|
|
312
|
+
if (id === '') {
|
|
313
|
+
throw new CanvasError('a canvas needs a title');
|
|
314
|
+
}
|
|
315
|
+
const file = canvasFile(framework, id);
|
|
316
|
+
if (existsSync(file)) {
|
|
317
|
+
throw new CanvasError(`canvas '${id}' already exists`, 409);
|
|
318
|
+
}
|
|
319
|
+
const canvas = { id, file, title, pages: [{ id: 'overview', title: 'Overview', frames: [], nodes: [], edges: [], layout: {}, comments: [] }], problems: [] };
|
|
320
|
+
saveCanvas(canvas);
|
|
321
|
+
return canvas;
|
|
322
|
+
}
|
|
323
|
+
export function findPage(canvas, pageId) {
|
|
324
|
+
const page = canvas.pages.find((candidate) => candidate.id === pageId);
|
|
325
|
+
if (!page) {
|
|
326
|
+
throw new CanvasError(`no page '${pageId}' in canvas '${canvas.id}'`, 404);
|
|
327
|
+
}
|
|
328
|
+
return page;
|
|
329
|
+
}
|
|
330
|
+
export function findNode(page, nodeId) {
|
|
331
|
+
const node = page.nodes.find((candidate) => candidate.id === nodeId);
|
|
332
|
+
if (!node) {
|
|
333
|
+
throw new CanvasError(`no node '${nodeId}' on page '${page.id}'`, 404);
|
|
334
|
+
}
|
|
335
|
+
return node;
|
|
336
|
+
}
|
|
337
|
+
// What an edge may end on: a node, or a frame's border as `frame:<id>`.
|
|
338
|
+
export function findEndpoint(page, id) {
|
|
339
|
+
if (isFrameRef(id)) {
|
|
340
|
+
if (!page.frames.some((frame) => frame.id === id.slice(FRAME_REF.length)))
|
|
341
|
+
throw new CanvasError(`no frame '${id.slice(FRAME_REF.length)}' on page '${page.id}'`, 404);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
findNode(page, id);
|
|
345
|
+
}
|
|
346
|
+
export function nextCommentId(page) {
|
|
347
|
+
let n = page.comments.length + 1;
|
|
348
|
+
while (page.comments.some((comment) => comment.id === `c${n}`))
|
|
349
|
+
n += 1;
|
|
350
|
+
return `c${n}`;
|
|
351
|
+
}
|
|
352
|
+
// A page's contents replaced whole, as the browser's undo does: what is
|
|
353
|
+
// given is read the way a file is, so a bad shape is a problem, never a
|
|
354
|
+
// crash, and ids are kept.
|
|
355
|
+
export function replacePageContents(page, given) {
|
|
356
|
+
const problems = [];
|
|
357
|
+
const read = readPage({ id: page.id, title: page.title, ...(isPlain(given) ? given : {}) }, 0, problems);
|
|
358
|
+
if (!read || problems.length > 0) {
|
|
359
|
+
throw new CanvasError(`the page could not be read: ${problems.join('; ') || 'not a page'}`);
|
|
360
|
+
}
|
|
361
|
+
page.frames = read.frames;
|
|
362
|
+
page.nodes = read.nodes;
|
|
363
|
+
page.edges = read.edges;
|
|
364
|
+
page.layout = read.layout;
|
|
365
|
+
page.comments = read.comments;
|
|
366
|
+
return problems;
|
|
367
|
+
}
|
|
368
|
+
export function nextNodeId(page) {
|
|
369
|
+
let n = page.nodes.length + 1;
|
|
370
|
+
while (page.nodes.some((node) => node.id === `n${n}`))
|
|
371
|
+
n += 1;
|
|
372
|
+
return `n${n}`;
|
|
373
|
+
}
|
|
374
|
+
// --- rendering ---------------------------------------------------------------
|
|
375
|
+
export const STICKY_SIZE = 176;
|
|
376
|
+
export const NODE_WIDTH = 220;
|
|
377
|
+
// The properties the framework marks for the canvas, in Schema order.
|
|
378
|
+
export function canvasProperties(framework) {
|
|
379
|
+
return framework.schema.custom.filter((property) => property.canvas !== undefined);
|
|
380
|
+
}
|
|
381
|
+
// The style a property gives one value, matched without regard to case.
|
|
382
|
+
export function valueStyle(hint, value) {
|
|
383
|
+
const key = Object.keys(hint.styles ?? {}).find((candidate) => candidate.toLowerCase() === value.toLowerCase());
|
|
384
|
+
return key ? (hint.styles?.[key] ?? null) : null;
|
|
385
|
+
}
|
|
386
|
+
// The default look a node's property values give it. Properties are read in
|
|
387
|
+
// schema order and, field by field, the top-most that sets it for the value
|
|
388
|
+
// wins, so the order of the custom block is the precedence.
|
|
389
|
+
export function styleFor(framework, values) {
|
|
390
|
+
const found = {};
|
|
391
|
+
const shown = [];
|
|
392
|
+
for (const property of canvasProperties(framework)) {
|
|
393
|
+
const value = values[property.name];
|
|
394
|
+
if (value === undefined || value === '')
|
|
395
|
+
continue;
|
|
396
|
+
const style = valueStyle(property.canvas, value);
|
|
397
|
+
if (style) {
|
|
398
|
+
if (found.shape === undefined && style.shape)
|
|
399
|
+
found.shape = style.shape;
|
|
400
|
+
if (found.color === undefined && style.color)
|
|
401
|
+
found.color = style.color;
|
|
402
|
+
if (found.fill === undefined && style.fill)
|
|
403
|
+
found.fill = style.fill;
|
|
404
|
+
if (found.fill_color === undefined && style.fill_color)
|
|
405
|
+
found.fill_color = style.fill_color;
|
|
406
|
+
if (found.size === undefined && style.size)
|
|
407
|
+
found.size = style.size;
|
|
408
|
+
}
|
|
409
|
+
if (property.canvas.show)
|
|
410
|
+
shown.push({ property: property.name, value });
|
|
411
|
+
}
|
|
412
|
+
return { ...withLegacyShape(found), shown };
|
|
413
|
+
}
|
|
414
|
+
// Shapes that were really colors in the first canvases (dark, accent, text)
|
|
415
|
+
// read as a box with the fill they meant.
|
|
416
|
+
function withLegacyShape(style) {
|
|
417
|
+
const legacy = { dark: { fill: 'solid', fill_color: '#191b1f' }, accent: { fill: 'tint' }, text: { fill: 'none' } };
|
|
418
|
+
const meant = style.shape ? legacy[style.shape] : undefined;
|
|
419
|
+
return {
|
|
420
|
+
shape: meant ? 'box' : (style.shape ?? 'box'),
|
|
421
|
+
color: style.color ?? null,
|
|
422
|
+
fill: style.fill ?? meant?.fill ?? 'solid',
|
|
423
|
+
fill_color: style.fill_color ?? meant?.fill_color ?? null,
|
|
424
|
+
size: style.size ?? 'medium',
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
// The override laid over the default: each field the node sets itself, the
|
|
428
|
+
// rest from the properties.
|
|
429
|
+
function drawn(node, defaults) {
|
|
430
|
+
const style = node.style ?? {};
|
|
431
|
+
const own = withLegacyShape({
|
|
432
|
+
shape: style.shape ?? defaults.shape,
|
|
433
|
+
...(style.color ?? defaults.color ? { color: style.color ?? defaults.color ?? '' } : {}),
|
|
434
|
+
fill: style.fill ?? defaults.fill,
|
|
435
|
+
...(style.fill_color ?? defaults.fill_color ? { fill_color: style.fill_color ?? defaults.fill_color ?? '' } : {}),
|
|
436
|
+
size: style.size ?? defaults.size,
|
|
437
|
+
});
|
|
438
|
+
return { shape: own.shape, color: own.color, fill: own.fill, fill_color: own.fill_color, size: own.size };
|
|
439
|
+
}
|
|
440
|
+
// A simple layered layout from the edges: a node's column is the longest path
|
|
441
|
+
// from a source to it, its row its order within the column. Frames are drawn
|
|
442
|
+
// around their members by the page. Overrides win.
|
|
443
|
+
export function computeLayout(page) {
|
|
444
|
+
const ids = page.nodes.map((node) => node.id);
|
|
445
|
+
const incoming = new Map(ids.map((id) => [id, []]));
|
|
446
|
+
const outgoing = new Map(ids.map((id) => [id, []]));
|
|
447
|
+
for (const edge of page.edges) {
|
|
448
|
+
if (incoming.has(edge.to) && outgoing.has(edge.from) && edge.from !== edge.to) {
|
|
449
|
+
incoming.get(edge.to)?.push(edge.from);
|
|
450
|
+
outgoing.get(edge.from)?.push(edge.to);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// Back edges (an edge into a node still on the DFS stack) close cycles;
|
|
454
|
+
// they are drawn but do not count for columns.
|
|
455
|
+
const back = new Set();
|
|
456
|
+
const state = new Map();
|
|
457
|
+
const visit = (id) => {
|
|
458
|
+
state.set(id, 1);
|
|
459
|
+
for (const next of outgoing.get(id) ?? []) {
|
|
460
|
+
const seen = state.get(next);
|
|
461
|
+
if (seen === 1)
|
|
462
|
+
back.add(`${id}|${next}`);
|
|
463
|
+
else if (seen === undefined)
|
|
464
|
+
visit(next);
|
|
465
|
+
}
|
|
466
|
+
state.set(id, 2);
|
|
467
|
+
};
|
|
468
|
+
for (const id of ids) {
|
|
469
|
+
if (!state.has(id))
|
|
470
|
+
visit(id);
|
|
471
|
+
}
|
|
472
|
+
const column = new Map(ids.map((id) => [id, 0]));
|
|
473
|
+
const remaining = new Map(ids.map((id) => [id, (incoming.get(id) ?? []).filter((parent) => !back.has(`${parent}|${id}`)).length]));
|
|
474
|
+
const queue = ids.filter((id) => remaining.get(id) === 0);
|
|
475
|
+
while (queue.length > 0) {
|
|
476
|
+
const id = queue.shift();
|
|
477
|
+
for (const next of outgoing.get(id) ?? []) {
|
|
478
|
+
if (back.has(`${id}|${next}`))
|
|
479
|
+
continue;
|
|
480
|
+
column.set(next, Math.max(column.get(next) ?? 0, (column.get(id) ?? 0) + 1));
|
|
481
|
+
remaining.set(next, (remaining.get(next) ?? 1) - 1);
|
|
482
|
+
if (remaining.get(next) === 0)
|
|
483
|
+
queue.push(next);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Within a column, a frame's members sit together so frames stay contiguous.
|
|
487
|
+
const frameOf = new Map(page.nodes.map((node) => [node.id, node.frame ?? '']));
|
|
488
|
+
const ordered = [...ids].sort((a, b) => (column.get(a) ?? 0) - (column.get(b) ?? 0) || (frameOf.get(a) ?? '').localeCompare(frameOf.get(b) ?? '') || ids.indexOf(a) - ids.indexOf(b));
|
|
489
|
+
const rows = new Map();
|
|
490
|
+
const out = {};
|
|
491
|
+
for (const id of ordered) {
|
|
492
|
+
const col = column.get(id) ?? 0;
|
|
493
|
+
const row = rows.get(col) ?? 0;
|
|
494
|
+
rows.set(col, row + 1);
|
|
495
|
+
out[id] = { x: 40 + col * 270, y: 60 + row * 140 };
|
|
496
|
+
}
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
499
|
+
export function renderPage(framework, blueprints, page) {
|
|
500
|
+
const computed = computeLayout(page);
|
|
501
|
+
return page.nodes.map((node) => {
|
|
502
|
+
const override = page.layout[node.id];
|
|
503
|
+
const position = override ? { ...override, computed: false } : { ...(computed[node.id] ?? { x: 40, y: 40 }), computed: true };
|
|
504
|
+
if (node.blueprint) {
|
|
505
|
+
const blueprint = blueprints.find((candidate) => blueprintId(candidate) === node.blueprint) ?? null;
|
|
506
|
+
const values = {};
|
|
507
|
+
if (blueprint) {
|
|
508
|
+
for (const property of canvasProperties(framework)) {
|
|
509
|
+
const value = propertyString(blueprint, property.name);
|
|
510
|
+
if (value !== null)
|
|
511
|
+
values[property.name] = value;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const style = styleFor(framework, values);
|
|
515
|
+
return {
|
|
516
|
+
id: node.id,
|
|
517
|
+
kind: 'blueprint',
|
|
518
|
+
locked: node.locked === true,
|
|
519
|
+
label: blueprint ? blueprintTitle(blueprint) : node.blueprint,
|
|
520
|
+
summary: blueprint ? propertyString(blueprint, 'summary') : null,
|
|
521
|
+
blueprint: node.blueprint,
|
|
522
|
+
path: blueprint?.rel ?? null,
|
|
523
|
+
frame: node.frame ?? null,
|
|
524
|
+
link: node.link ?? null,
|
|
525
|
+
values,
|
|
526
|
+
style: drawn(node, style),
|
|
527
|
+
font: null,
|
|
528
|
+
box: node.box ?? null,
|
|
529
|
+
defaults: { shape: style.shape, color: style.color, fill: style.fill, fill_color: style.fill_color, size: style.size },
|
|
530
|
+
override: node.style ?? null,
|
|
531
|
+
styled: node.styled === true,
|
|
532
|
+
shown: style.shown,
|
|
533
|
+
position,
|
|
534
|
+
missing: blueprint === null,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
if (node.text) {
|
|
538
|
+
return {
|
|
539
|
+
id: node.id,
|
|
540
|
+
kind: 'text',
|
|
541
|
+
locked: node.locked === true,
|
|
542
|
+
label: node.text.text,
|
|
543
|
+
summary: null,
|
|
544
|
+
blueprint: null,
|
|
545
|
+
path: null,
|
|
546
|
+
frame: node.frame ?? null,
|
|
547
|
+
link: node.link ?? null,
|
|
548
|
+
values: {},
|
|
549
|
+
style: { shape: 'text', color: node.style?.color ?? null, fill: 'none', fill_color: null, size: node.text.size ?? 'medium' },
|
|
550
|
+
font: node.text.font ?? null,
|
|
551
|
+
box: node.box ?? null,
|
|
552
|
+
defaults: { shape: 'text', color: null, fill: 'solid', fill_color: null, size: 'medium' },
|
|
553
|
+
override: node.style ?? null,
|
|
554
|
+
styled: node.styled === true,
|
|
555
|
+
shown: [],
|
|
556
|
+
position,
|
|
557
|
+
missing: false,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
if (node.annotation) {
|
|
561
|
+
return {
|
|
562
|
+
id: node.id,
|
|
563
|
+
kind: 'annotation',
|
|
564
|
+
locked: node.locked === true,
|
|
565
|
+
label: node.annotation.text,
|
|
566
|
+
summary: null,
|
|
567
|
+
blueprint: null,
|
|
568
|
+
path: null,
|
|
569
|
+
frame: node.frame ?? null,
|
|
570
|
+
link: node.link ?? null,
|
|
571
|
+
values: {},
|
|
572
|
+
style: { shape: 'annotation', color: node.annotation.color ?? null, fill: 'solid', fill_color: null, size: node.style?.size ?? 'medium' },
|
|
573
|
+
font: null,
|
|
574
|
+
box: node.box ?? null,
|
|
575
|
+
defaults: { shape: 'annotation', color: null, fill: 'solid', fill_color: null, size: 'medium' },
|
|
576
|
+
override: node.style ?? null,
|
|
577
|
+
styled: node.styled === true,
|
|
578
|
+
shown: [],
|
|
579
|
+
position,
|
|
580
|
+
missing: false,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
if (node.sticky) {
|
|
584
|
+
return {
|
|
585
|
+
id: node.id,
|
|
586
|
+
kind: 'sticky',
|
|
587
|
+
locked: node.locked === true,
|
|
588
|
+
label: node.sticky.text,
|
|
589
|
+
summary: null,
|
|
590
|
+
blueprint: null,
|
|
591
|
+
path: null,
|
|
592
|
+
frame: node.frame ?? null,
|
|
593
|
+
link: node.link ?? null,
|
|
594
|
+
values: {},
|
|
595
|
+
style: { shape: 'sticky', color: node.sticky.color ?? null, fill: 'solid', fill_color: null, size: node.style?.size ?? 'medium' },
|
|
596
|
+
font: null,
|
|
597
|
+
box: node.box ?? null,
|
|
598
|
+
defaults: { shape: 'sticky', color: null, fill: 'solid', fill_color: null, size: 'medium' },
|
|
599
|
+
override: node.style ?? null,
|
|
600
|
+
styled: node.styled === true,
|
|
601
|
+
shown: [],
|
|
602
|
+
position,
|
|
603
|
+
missing: false,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
const sketch = node.sketch ?? { label: node.id };
|
|
607
|
+
const values = sketchValues(framework, sketch);
|
|
608
|
+
const style = styleFor(framework, values);
|
|
609
|
+
return {
|
|
610
|
+
id: node.id,
|
|
611
|
+
kind: 'sketch',
|
|
612
|
+
locked: node.locked === true,
|
|
613
|
+
label: sketch.label,
|
|
614
|
+
summary: sketch.note ?? null,
|
|
615
|
+
blueprint: null,
|
|
616
|
+
path: null,
|
|
617
|
+
frame: node.frame ?? null,
|
|
618
|
+
link: node.link ?? null,
|
|
619
|
+
values,
|
|
620
|
+
style: drawn(node, style),
|
|
621
|
+
font: null,
|
|
622
|
+
box: node.box ?? null,
|
|
623
|
+
defaults: { shape: style.shape, color: style.color, fill: style.fill, fill_color: style.fill_color, size: style.size },
|
|
624
|
+
override: node.style ?? null,
|
|
625
|
+
styled: node.styled === true,
|
|
626
|
+
shown: style.shown,
|
|
627
|
+
position,
|
|
628
|
+
missing: false,
|
|
629
|
+
};
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
// A sketch's property values: the ones it holds by name, with the legacy
|
|
633
|
+
// `kind` and `status` read as the first shape- and color-styled properties.
|
|
634
|
+
export function sketchValues(framework, sketch) {
|
|
635
|
+
const values = {};
|
|
636
|
+
const styles = (property) => Object.values(property.canvas.styles ?? {});
|
|
637
|
+
const kindProperty = canvasProperties(framework).find((property) => styles(property).some((style) => style.shape));
|
|
638
|
+
const statusProperty = canvasProperties(framework).find((property) => styles(property).some((style) => style.color));
|
|
639
|
+
if (sketch.kind && kindProperty)
|
|
640
|
+
values[kindProperty.name] = sketch.kind;
|
|
641
|
+
if (sketch.status && statusProperty)
|
|
642
|
+
values[statusProperty.name] = sketch.status;
|
|
643
|
+
for (const [key, value] of Object.entries(sketch.properties ?? {}))
|
|
644
|
+
values[key] = value;
|
|
645
|
+
return values;
|
|
646
|
+
}
|
|
647
|
+
export function renderComments(page, nodes) {
|
|
648
|
+
return page.comments.map((comment) => {
|
|
649
|
+
const anchor = comment.node ? nodes.find((node) => node.id === comment.node) : undefined;
|
|
650
|
+
return { ...comment, at: anchor ? { x: anchor.position.x + comment.x, y: anchor.position.y + comment.y } : { x: comment.x, y: comment.y } };
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
// A frame's rectangle: the one it was drawn with, else a box around its
|
|
654
|
+
// members, the way frames were before they could be drawn.
|
|
655
|
+
export function frameRect(frame, nodes) {
|
|
656
|
+
if (frame.x !== undefined && frame.y !== undefined && frame.w !== undefined && frame.h !== undefined) {
|
|
657
|
+
return { x: frame.x, y: frame.y, w: frame.w, h: frame.h };
|
|
658
|
+
}
|
|
659
|
+
const members = nodes.filter((node) => node.frame === frame.id);
|
|
660
|
+
if (members.length === 0)
|
|
661
|
+
return null;
|
|
662
|
+
const size = (node) => (node.kind === 'sticky' ? { w: STICKY_SIZE, h: STICKY_SIZE } : node.kind === 'text' ? { w: 200, h: 40 } : { w: NODE_WIDTH, h: 112 });
|
|
663
|
+
const minX = Math.min(...members.map((node) => node.position.x)) - 20;
|
|
664
|
+
const minY = Math.min(...members.map((node) => node.position.y)) - 40;
|
|
665
|
+
const maxX = Math.max(...members.map((node) => node.position.x + size(node).w)) + 20;
|
|
666
|
+
const maxY = Math.max(...members.map((node) => node.position.y + size(node).h)) + 20;
|
|
667
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
668
|
+
}
|
|
669
|
+
// --- export -------------------------------------------------------------------
|
|
670
|
+
// JSON Canvas 1.0 (Obsidian): each frame a group, each node a text card, each
|
|
671
|
+
// edge a directed edge. Positions come from the same layout the page shows.
|
|
672
|
+
export function toJsonCanvas(framework, blueprints, page) {
|
|
673
|
+
const rendered = renderPage(framework, blueprints, page);
|
|
674
|
+
const W = 220;
|
|
675
|
+
const H = 80;
|
|
676
|
+
const nodes = [];
|
|
677
|
+
for (const node of rendered) {
|
|
678
|
+
const lines = [node.label, node.summary ?? ''].filter((line) => line !== '');
|
|
679
|
+
const sticky = node.kind === 'sticky';
|
|
680
|
+
const item = { id: node.id, type: node.path ? 'file' : 'text', x: node.position.x, y: node.position.y, width: sticky ? STICKY_SIZE : W, height: sticky ? STICKY_SIZE : node.kind === 'text' ? 40 : H };
|
|
681
|
+
if (node.path) {
|
|
682
|
+
item['file'] = node.path;
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
item['text'] = lines.join('\n');
|
|
686
|
+
}
|
|
687
|
+
if (node.style.color)
|
|
688
|
+
item['color'] = node.style.color;
|
|
689
|
+
nodes.push(item);
|
|
690
|
+
}
|
|
691
|
+
for (const frame of page.frames) {
|
|
692
|
+
const rect = frameRect(frame, rendered);
|
|
693
|
+
if (!rect)
|
|
694
|
+
continue;
|
|
695
|
+
const group = { id: `frame-${frame.id}`, type: 'group', label: frame.title, x: rect.x, y: rect.y, width: rect.w, height: rect.h };
|
|
696
|
+
if (frame.color)
|
|
697
|
+
group['color'] = frame.color;
|
|
698
|
+
nodes.push(group);
|
|
699
|
+
}
|
|
700
|
+
const side = (anchor, fallback) => (anchor ? (anchor.includes('top') ? 'top' : anchor.includes('bottom') ? 'bottom' : anchor) : fallback);
|
|
701
|
+
const endpoint = (id) => (isFrameRef(id) ? `frame-${id.slice(FRAME_REF.length)}` : id);
|
|
702
|
+
const edges = page.edges.map((edge, index) => {
|
|
703
|
+
const item = { id: `e${index + 1}`, fromNode: endpoint(edge.from), fromSide: side(edge.from_anchor, 'right'), toNode: endpoint(edge.to), toSide: side(edge.to_anchor, 'left'), toEnd: edge.end === 'none' ? 'none' : 'arrow' };
|
|
704
|
+
if (edge.label)
|
|
705
|
+
item['label'] = edge.label;
|
|
706
|
+
return item;
|
|
707
|
+
});
|
|
708
|
+
return { nodes, edges };
|
|
709
|
+
}
|
|
710
|
+
// --- the software seed's default palette, written by init --------------------
|
|
711
|
+
// The legend the owner already uses, as values of the seed's own properties.
|
|
712
|
+
const shaped = (shape) => ({ shape });
|
|
713
|
+
const colored = (color) => ({ color });
|
|
714
|
+
export const DEFAULT_CANVAS_HINTS = {
|
|
715
|
+
type: {
|
|
716
|
+
show: true,
|
|
717
|
+
styles: { page: shaped('pill'), feature: shaped('box'), capability: shaped('box'), integration: shaped('box'), background: shaped('box'), module: shaped('box'), decision: shaped('parallelogram'), note: shaped('box') },
|
|
718
|
+
},
|
|
719
|
+
status: {
|
|
720
|
+
show: true,
|
|
721
|
+
// the browser's standard colors (PALETTE in the canvas model), so a default and a hand-picked swatch never clash
|
|
722
|
+
styles: { Draft: colored('#8b9099'), Intake: colored('#d3a53c'), 'In Progress': colored('#5f80cf'), Done: colored('#7fae6c'), Archived: colored('#8b9099'), Deprecated: colored('#cf7b8f'), 'needs-clarification': colored('#5f80cf'), maybe: colored('#e08a63'), visionary: colored('#4aa39f') },
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
export function applyDefaultCanvasHints(properties) {
|
|
726
|
+
for (const property of properties) {
|
|
727
|
+
const hint = DEFAULT_CANVAS_HINTS[property.name];
|
|
728
|
+
if (hint && !property.canvas) {
|
|
729
|
+
property.canvas = hint;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|