@formicoidea/labre-framework-wardley 0.32.0 → 0.33.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/actions.d.ts +49 -2
- package/dist/actions.js +113 -21
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +166 -4
- package/dist/export.d.ts +211 -0
- package/dist/export.js +655 -0
- package/dist/gradient.js +1 -1
- package/dist/import.d.ts +116 -0
- package/dist/import.js +905 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +22 -0
- package/dist/interchange.d.ts +80 -0
- package/dist/interchange.js +138 -0
- package/dist/node/node-renderer.js +1 -1
- package/dist/rules.js +16 -0
- package/dist/templates/index.js +15 -3
- package/dist/templates/maps.js +26 -6
- package/dist/toolbar/config.js +3 -1
- package/dist/toolbar/icons.d.ts +20 -0
- package/dist/toolbar/icons.js +34 -0
- package/dist/toolbar/senior-tool.js +1 -0
- package/dist/toolbar/wardley-senior-button.js +12 -6
- package/dist/view.js +13 -2
- package/package.json +2 -2
package/dist/actions.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { type WardleyBgVariant } from '@formicoidea/labre-core/model';
|
|
2
|
-
import type {
|
|
1
|
+
import { WardleyBackgroundElementModel, type WardleyBgVariant } from '@formicoidea/labre-core/model';
|
|
2
|
+
import type { BlockStdScope } from '@formicoidea/labre-core/std';
|
|
3
|
+
import { type GfxController } from '@formicoidea/labre-core/std/gfx';
|
|
4
|
+
import { type WardleyExportBoard } from './export.js';
|
|
3
5
|
/**
|
|
4
6
|
* The single-circle node flavours: one connectable ellipse + a label to its
|
|
5
7
|
* right, grouped. The glyph itself (anchor silhouette, ecosystem hatching,
|
|
@@ -61,10 +63,55 @@ export declare function createWardleyPipeline(gfx: GfxController): void;
|
|
|
61
63
|
* everything is grouped into one object.
|
|
62
64
|
*/
|
|
63
65
|
export declare function createWardleyMarket(gfx: GfxController): void;
|
|
66
|
+
/**
|
|
67
|
+
* Read an SVG the user picks as a SKETCH, and say what it cost.
|
|
68
|
+
*
|
|
69
|
+
* Wardley's first interchange command, and its whole implementation: the four
|
|
70
|
+
* steps live in {@link runInterchangeImportFile} and the reading lives in the
|
|
71
|
+
* declared capability, so this framework contributes a declaration and a
|
|
72
|
+
* label rather than a pipeline (`docs/adr/0012`, P1 and P3).
|
|
73
|
+
*
|
|
74
|
+
* It takes a `BlockStdScope` and not the `GfxController` the rest of this file
|
|
75
|
+
* runs on, because an import is not a drawing gesture: it opens a picker,
|
|
76
|
+
* writes a whole board in one undo step, moves the viewport and notifies —
|
|
77
|
+
* none of which a `GfxController` alone can do.
|
|
78
|
+
*/
|
|
79
|
+
export declare function importWardleySvgFile(std: BlockStdScope): Promise<void>;
|
|
64
80
|
/**
|
|
65
81
|
* Activate the native connector tool, pre-styled for a Wardley link (grey,
|
|
66
82
|
* solid, no arrow) or evolution arrow (red, dashed, FILLED triangle). The
|
|
67
83
|
* user then draws from one node to another (endpoints attach to centers).
|
|
68
84
|
*/
|
|
69
85
|
export declare function activateWardleyConnector(gfx: GfxController, kind: 'link' | 'arrow'): void;
|
|
86
|
+
/** The maps on the surface — what the export command is offered against. */
|
|
87
|
+
export declare function wardleyMapsOnBoard(std: BlockStdScope): WardleyBackgroundElementModel[];
|
|
88
|
+
/**
|
|
89
|
+
* Everything on the surface the exporter speaks about, in document order.
|
|
90
|
+
*
|
|
91
|
+
* The half that needs an editor, and only that half: reading the surface. The
|
|
92
|
+
* picking is {@link wardleyBoardFrom}, which the interchange capability calls
|
|
93
|
+
* with the same elements and no `std` at all (`docs/adr/0012`, P3).
|
|
94
|
+
*/
|
|
95
|
+
export declare function wardleyBoardOf(std: BlockStdScope): WardleyExportBoard;
|
|
96
|
+
/**
|
|
97
|
+
* Serialize the board as an OWM document and hand it to the browser.
|
|
98
|
+
*
|
|
99
|
+
* Three steps, and only the first and the last know what an editor is: read the
|
|
100
|
+
* surface, run the DECLARED capability, download what it produced. The middle
|
|
101
|
+
* step is not re-implemented here — the document, the filename and the content
|
|
102
|
+
* type all come out of `WARDLEY_OWM_EXPORT.run`, so the command and the
|
|
103
|
+
* registry cannot describe the same map differently. There is one door, and the
|
|
104
|
+
* registry is the label on it.
|
|
105
|
+
*/
|
|
106
|
+
export declare function exportOwmFile(std: BlockStdScope): void;
|
|
107
|
+
/**
|
|
108
|
+
* Read an `.owm` the user picks, draw it, and say what it cost.
|
|
109
|
+
*
|
|
110
|
+
* The whole of the import glue, and it is one line: the generic pipeline picks
|
|
111
|
+
* the file from the format's own declaration, runs the capability, mints the
|
|
112
|
+
* surface ids and repairs the link endpoints that named the file's names,
|
|
113
|
+
* brings the map into view and reports. Nothing in it is about Wardley except
|
|
114
|
+
* the capability handed to it.
|
|
115
|
+
*/
|
|
116
|
+
export declare function importOwmFile(std: BlockStdScope): Promise<void>;
|
|
70
117
|
export {};
|
package/dist/actions.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { backgroundSize, DefaultTool } from '@formicoidea/labre-core/blocks/surface';
|
|
1
|
+
import { backgroundSize, DefaultTool, runInterchangeImportFile, } from '@formicoidea/labre-core/blocks/surface';
|
|
2
2
|
import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
|
|
3
3
|
import { createGroupCommand } from '@formicoidea/labre-core/gfx/group';
|
|
4
|
-
import { ConnectorMode, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, } from '@formicoidea/labre-core/model';
|
|
5
|
-
import {
|
|
4
|
+
import { ConnectorMode, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, WardleyBackgroundElementModel, } from '@formicoidea/labre-core/model';
|
|
5
|
+
import { NotificationProvider, translateKey, } from '@formicoidea/labre-core/shared/services';
|
|
6
|
+
import { downloadBlob } from '@formicoidea/labre-core/shared/utils';
|
|
6
7
|
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
8
|
+
import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
|
|
7
9
|
import { WARDLEY_BACKGROUND } from './background.js';
|
|
10
|
+
import { WARDLEY_SVG_IMPORT } from './interchange.js';
|
|
11
|
+
import { wardleyBoardFrom, wardleySafeFilename, } from './export.js';
|
|
12
|
+
import { WARDLEY_OWM_EXPORT, WARDLEY_OWM_IMPORT } from './interchange.js';
|
|
8
13
|
import { ECOSYSTEM_LABEL, ECOSYSTEM_SIZE, HANDLE_SIZE, INERTIA_COLOR, INERTIA_SIZE, LABEL_DEFAULT, LABEL_FONT_SIZE, LABEL_GAP, LINK_GREY, LINK_STROKE_WIDTH, MARKET_DOT_RING, MARKET_DOT_SIZE, MARKET_DOT_STROKE_WIDTH, MARKET_LABEL, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, MARKET_SIZE, METHOD_FILL, METHOD_LABEL, METHOD_SIZE, NODE_FILL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, PIPELINE_HEIGHT, PIPELINE_LABEL, PIPELINE_WIDTH, WARDLEY_RED, } from './node/consts.js';
|
|
9
14
|
import { WARDLEY_ROLE } from './roles.js';
|
|
10
15
|
/**
|
|
@@ -292,30 +297,28 @@ export function createWardleyMarket(gfx) {
|
|
|
292
297
|
const labelId = addLabel(surface, MARKET_LABEL, cx + R + LABEL_GAP, cy - LABEL_H / 2);
|
|
293
298
|
finish(gfx, group(gfx, [circleId, ...dotIds, ...connIds, labelId]));
|
|
294
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Read an SVG the user picks as a SKETCH, and say what it cost.
|
|
302
|
+
*
|
|
303
|
+
* Wardley's first interchange command, and its whole implementation: the four
|
|
304
|
+
* steps live in {@link runInterchangeImportFile} and the reading lives in the
|
|
305
|
+
* declared capability, so this framework contributes a declaration and a
|
|
306
|
+
* label rather than a pipeline (`docs/adr/0012`, P1 and P3).
|
|
307
|
+
*
|
|
308
|
+
* It takes a `BlockStdScope` and not the `GfxController` the rest of this file
|
|
309
|
+
* runs on, because an import is not a drawing gesture: it opens a picker,
|
|
310
|
+
* writes a whole board in one undo step, moves the viewport and notifies —
|
|
311
|
+
* none of which a `GfxController` alone can do.
|
|
312
|
+
*/
|
|
313
|
+
export async function importWardleySvgFile(std) {
|
|
314
|
+
await runInterchangeImportFile(std, WARDLEY_SVG_IMPORT);
|
|
315
|
+
}
|
|
295
316
|
/**
|
|
296
317
|
* Activate the native connector tool, pre-styled for a Wardley link (grey,
|
|
297
318
|
* solid, no arrow) or evolution arrow (red, dashed, FILLED triangle). The
|
|
298
319
|
* user then draws from one node to another (endpoints attach to centers).
|
|
299
320
|
*/
|
|
300
321
|
export function activateWardleyConnector(gfx, kind) {
|
|
301
|
-
const props = kind === 'arrow'
|
|
302
|
-
? {
|
|
303
|
-
mode: ConnectorMode.Straight,
|
|
304
|
-
stroke: WARDLEY_RED,
|
|
305
|
-
strokeStyle: StrokeStyle.Dash,
|
|
306
|
-
strokeWidth: LINK_STROKE_WIDTH,
|
|
307
|
-
frontEndpointStyle: PointStyle.None,
|
|
308
|
-
rearEndpointStyle: PointStyle.Triangle,
|
|
309
|
-
}
|
|
310
|
-
: {
|
|
311
|
-
mode: ConnectorMode.Straight,
|
|
312
|
-
stroke: LINK_GREY,
|
|
313
|
-
strokeStyle: StrokeStyle.Solid,
|
|
314
|
-
strokeWidth: LINK_STROKE_WIDTH,
|
|
315
|
-
frontEndpointStyle: PointStyle.None,
|
|
316
|
-
rearEndpointStyle: PointStyle.None,
|
|
317
|
-
};
|
|
318
|
-
gfx.std.get(EditPropsStore).recordLastProps('connector', props);
|
|
319
322
|
gfx.tool.setTool(ConnectorTool, {
|
|
320
323
|
mode: ConnectorMode.Straight,
|
|
321
324
|
// The value-chain link IS the "depends on" edge of a Wardley map; the
|
|
@@ -324,7 +327,96 @@ export function activateWardleyConnector(gfx, kind) {
|
|
|
324
327
|
// specialising the other: W1 is about where an arrow points and must never
|
|
325
328
|
// fall on a dependency.
|
|
326
329
|
role: kind === 'link' ? WARDLEY_ROLE.dependency : WARDLEY_ROLE.changeArrow,
|
|
330
|
+
// The look rides on the activation, never through the last-props store:
|
|
331
|
+
// the plain connector tool must keep the user's own style (#144 M1).
|
|
332
|
+
style: kind === 'arrow'
|
|
333
|
+
? {
|
|
334
|
+
stroke: WARDLEY_RED,
|
|
335
|
+
strokeStyle: StrokeStyle.Dash,
|
|
336
|
+
strokeWidth: LINK_STROKE_WIDTH,
|
|
337
|
+
frontEndpointStyle: PointStyle.None,
|
|
338
|
+
rearEndpointStyle: PointStyle.Triangle,
|
|
339
|
+
}
|
|
340
|
+
: {
|
|
341
|
+
stroke: LINK_GREY,
|
|
342
|
+
strokeStyle: StrokeStyle.Solid,
|
|
343
|
+
strokeWidth: LINK_STROKE_WIDTH,
|
|
344
|
+
frontEndpointStyle: PointStyle.None,
|
|
345
|
+
rearEndpointStyle: PointStyle.None,
|
|
346
|
+
},
|
|
327
347
|
});
|
|
328
348
|
// The wardley palette stays open (native sub-menu behaviour): it only
|
|
329
349
|
// closes on re-click of the senior button, another senior tool, or Escape.
|
|
330
350
|
}
|
|
351
|
+
/* ── Interchange: the OWM DSL, out and in (`docs/adr/0012`) ───────────── */
|
|
352
|
+
/**
|
|
353
|
+
* The one wording this file owns: what a WRITER could not say.
|
|
354
|
+
*
|
|
355
|
+
* The import's wordings live in the pipeline that writes them
|
|
356
|
+
* (`affine-block-surface`, `extensions/interchange-import.ts`) — one set of
|
|
357
|
+
* keys for every format, with the format's own name composed into them, so
|
|
358
|
+
* "OWM file imported" needs no key of its own. An export's losses are the
|
|
359
|
+
* capability's own sentences and have nowhere else to go.
|
|
360
|
+
*/
|
|
361
|
+
const EXPORT_WARNINGS_KEY = 'com.labre.commands.wardley.exportOwm.warnings';
|
|
362
|
+
const EXPORT_WARNINGS_FALLBACK = 'What this export could not write down';
|
|
363
|
+
/** The maps on the surface — what the export command is offered against. */
|
|
364
|
+
export function wardleyMapsOnBoard(std) {
|
|
365
|
+
const surface = std.get(GfxControllerIdentifier).surface;
|
|
366
|
+
return (surface?.elementModels ?? []).filter((model) => model instanceof WardleyBackgroundElementModel);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Everything on the surface the exporter speaks about, in document order.
|
|
370
|
+
*
|
|
371
|
+
* The half that needs an editor, and only that half: reading the surface. The
|
|
372
|
+
* picking is {@link wardleyBoardFrom}, which the interchange capability calls
|
|
373
|
+
* with the same elements and no `std` at all (`docs/adr/0012`, P3).
|
|
374
|
+
*/
|
|
375
|
+
export function wardleyBoardOf(std) {
|
|
376
|
+
return wardleyBoardFrom(std.get(GfxControllerIdentifier).surface?.elementModels ?? []);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Serialize the board as an OWM document and hand it to the browser.
|
|
380
|
+
*
|
|
381
|
+
* Three steps, and only the first and the last know what an editor is: read the
|
|
382
|
+
* surface, run the DECLARED capability, download what it produced. The middle
|
|
383
|
+
* step is not re-implemented here — the document, the filename and the content
|
|
384
|
+
* type all come out of `WARDLEY_OWM_EXPORT.run`, so the command and the
|
|
385
|
+
* registry cannot describe the same map differently. There is one door, and the
|
|
386
|
+
* registry is the label on it.
|
|
387
|
+
*/
|
|
388
|
+
export function exportOwmFile(std) {
|
|
389
|
+
const elements = std.get(GfxControllerIdentifier).surface?.elementModels ?? [];
|
|
390
|
+
const title = std.store.workspace.meta.getDocMeta(std.store.id)?.title;
|
|
391
|
+
const { text, filename, mime, warnings } = WARDLEY_OWM_EXPORT.run(elements, {
|
|
392
|
+
name: wardleySafeFilename(title),
|
|
393
|
+
});
|
|
394
|
+
// The charset is the browser's business, not the format's: `mime` is what an
|
|
395
|
+
// `.owm` IS, and this is how a blob is told to carry it.
|
|
396
|
+
downloadBlob(new Blob([text], { type: `${mime};charset=utf-8` }), filename);
|
|
397
|
+
// A warning is never an error: the file downloaded, and it is valid; what it
|
|
398
|
+
// could not say is what this names. A board with two maps on it, a component
|
|
399
|
+
// with no name, an evolution arrow that also climbs the value chain — each is
|
|
400
|
+
// a sentence the format has no way to write down, and the person who clicked
|
|
401
|
+
// Export is the one entitled to hear about it.
|
|
402
|
+
if (!warnings || warnings.length === 0)
|
|
403
|
+
return;
|
|
404
|
+
std.getOptional(NotificationProvider)?.notify({
|
|
405
|
+
title: translateKey(std, EXPORT_WARNINGS_KEY, EXPORT_WARNINGS_FALLBACK),
|
|
406
|
+
message: warnings.join('\n'),
|
|
407
|
+
accent: 'warning',
|
|
408
|
+
duration: 8000,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Read an `.owm` the user picks, draw it, and say what it cost.
|
|
413
|
+
*
|
|
414
|
+
* The whole of the import glue, and it is one line: the generic pipeline picks
|
|
415
|
+
* the file from the format's own declaration, runs the capability, mints the
|
|
416
|
+
* surface ids and repairs the link endpoints that named the file's names,
|
|
417
|
+
* brings the map into view and reports. Nothing in it is about Wardley except
|
|
418
|
+
* the capability handed to it.
|
|
419
|
+
*/
|
|
420
|
+
export async function importOwmFile(std) {
|
|
421
|
+
await runInterchangeImportFile(std, WARDLEY_OWM_IMPORT);
|
|
422
|
+
}
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { CommandDescriptor } from '@formicoidea/labre-core/std';
|
|
2
2
|
import type { TemplateResult } from 'lit';
|
|
3
|
+
/**
|
|
4
|
+
* The Wardley registry: the thirteen toolbox entries, then the two directions
|
|
5
|
+
* of the OWM DSL and the SVG fallback (`docs/adr/0012`).
|
|
6
|
+
*/
|
|
3
7
|
export declare const wardleyCommands: CommandDescriptor[];
|
|
4
8
|
/** `iconKey` → template. Never travels through either manifest (ADR 0008). */
|
|
5
9
|
export declare const wardleyCommandIcons: Record<string, TemplateResult>;
|
package/dist/commands.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
|
|
2
|
-
import { activateWardleyConnector, createWardleyBackground, createWardleyInertia, createWardleyMarket, createWardleyNode, createWardleyPipeline, } from './actions.js';
|
|
3
|
-
import { WARDLEY_ROLE, WARDLEY_ROLES
|
|
4
|
-
import { wardleyAnchorIcon, wardleyArrowIcon, wardleyBackgroundIcon, wardleyBenefitIcon, wardleyComponentIcon, wardleyEcosystemIcon, wardleyEvolutionGradientIcon, wardleyInertiaIcon, wardleyLinkIcon, wardleyMarketIcon, wardleyMethodIcon, wardleyOpportunityIcon, wardleyPipelineIcon, } from './toolbar/icons.js';
|
|
2
|
+
import { activateWardleyConnector, createWardleyBackground, createWardleyInertia, createWardleyMarket, createWardleyNode, createWardleyPipeline, exportOwmFile, importOwmFile, importWardleySvgFile, wardleyMapsOnBoard, } from './actions.js';
|
|
3
|
+
import { WARDLEY_ROLE, WARDLEY_ROLES } from './roles.js';
|
|
4
|
+
import { wardleyAnchorIcon, wardleyArrowIcon, wardleyBackgroundIcon, wardleyBenefitIcon, wardleyComponentIcon, wardleyEcosystemIcon, wardleyEvolutionGradientIcon, wardleyExportOwmIcon, wardleyImportOwmIcon, wardleyImportSvgIcon, wardleyInertiaIcon, wardleyLinkIcon, wardleyMarketIcon, wardleyMethodIcon, wardleyOpportunityIcon, wardleyPipelineIcon, } from './toolbar/icons.js';
|
|
5
5
|
const SPECS = [
|
|
6
6
|
{
|
|
7
7
|
id: 'addBackground',
|
|
@@ -153,7 +153,7 @@ function gestureOf(spec) {
|
|
|
153
153
|
descriptionFallback: direction.gestureHintFallback,
|
|
154
154
|
};
|
|
155
155
|
}
|
|
156
|
-
|
|
156
|
+
const toolboxCommands = SPECS.map((spec, order) => ({
|
|
157
157
|
id: `wardley.${spec.id}`,
|
|
158
158
|
owner: 'wardley',
|
|
159
159
|
kind: spec.kind,
|
|
@@ -172,6 +172,165 @@ export const wardleyCommands = SPECS.map((spec, order) => ({
|
|
|
172
172
|
run: std => spec.run(std.get(GfxControllerIdentifier)),
|
|
173
173
|
telemetry: { framework: 'wardley', element: spec.element },
|
|
174
174
|
}));
|
|
175
|
+
/**
|
|
176
|
+
* The IMPORT — the first Wardley command whose subject is a whole map rather
|
|
177
|
+
* than something you draw, and the first that needs nothing on the board.
|
|
178
|
+
*
|
|
179
|
+
* ## `'senior-menu'`, per the PO decision of 2026-08-28
|
|
180
|
+
*
|
|
181
|
+
* An interpreted import lives in its framework's sub-menu. On an empty canvas
|
|
182
|
+
* the sub-menu is the first thing a user opens, and "start from the map
|
|
183
|
+
* somebody sent me" belongs in that row beside "start from a component" —
|
|
184
|
+
* asking them to find the catalogue sidepanel first is the friction the
|
|
185
|
+
* decision names. It is Wardley's fourteenth nomination, which is exactly the
|
|
186
|
+
* cap, so nothing about the sub-menu's arbitration changes and no button is
|
|
187
|
+
* pushed out of the row.
|
|
188
|
+
*
|
|
189
|
+
* No `'contextual-toolbar'`: a contextual toolbar is a statement about a
|
|
190
|
+
* SELECTION, and the moment this command is most wanted is on a board with
|
|
191
|
+
* nothing on it at all.
|
|
192
|
+
*
|
|
193
|
+
* ## `'editable'`
|
|
194
|
+
*
|
|
195
|
+
* An import needs no selection, but it WRITES, so a read-only document is one
|
|
196
|
+
* it cannot run on — and that is a precondition a catalogue has to be able to
|
|
197
|
+
* show. `'always'` would light the entry on a read-only board, do nothing when
|
|
198
|
+
* clicked, and put the same untruth into the manifest a host reads.
|
|
199
|
+
*/
|
|
200
|
+
const importCommand = {
|
|
201
|
+
id: 'wardley.importOwm',
|
|
202
|
+
owner: 'wardley',
|
|
203
|
+
kind: 'action',
|
|
204
|
+
labelKey: 'com.labre.commands.wardley.importOwm',
|
|
205
|
+
labelFallback: 'Import Wardley map (OWM)',
|
|
206
|
+
descriptionKey: 'com.labre.commands.wardley.importOwm.description',
|
|
207
|
+
descriptionFallback: 'Open an OnlineWardleyMaps .owm file as a map. What Labre cannot draw is kept in the document, and the import says what it was.',
|
|
208
|
+
category: 'interchange',
|
|
209
|
+
iconKey: 'wardley.import-owm',
|
|
210
|
+
surfaces: ['senior-menu', 'catalogue', 'palette', 'agent'],
|
|
211
|
+
order: SPECS.length,
|
|
212
|
+
scope: 'edgeless',
|
|
213
|
+
// Keyless by intent. The `w` chord already seats seven artefacts, and a
|
|
214
|
+
// framework binds past that by host override rather than by shipping a
|
|
215
|
+
// default — still bindable from Settings › Shortcuts.
|
|
216
|
+
defaultKeys: { mac: [], other: [] },
|
|
217
|
+
availability: 'editable',
|
|
218
|
+
run: std => void importOwmFile(std),
|
|
219
|
+
// `board:` and not `node:` — this one is launched with no map anywhere.
|
|
220
|
+
telemetry: { framework: 'wardley', element: 'board:import-owm' },
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* The EXPORT — the other direction of the same format.
|
|
224
|
+
*
|
|
225
|
+
* It declines `'senior-menu'`, and the asymmetry with the import above is the
|
|
226
|
+
* ruling BPMN already carries: the sub-menu is where a board COMES FROM, and an
|
|
227
|
+
* export is what you do to a board you already have. It keeps `'catalogue'`,
|
|
228
|
+
* which is the registry's own invariant rather than a category claim — a
|
|
229
|
+
* command missing from the catalogue is unreachable the moment its framework
|
|
230
|
+
* overflows the fourteen slots — plus the palette and the agent. No
|
|
231
|
+
* `'contextual-toolbar'` either, and that is a declaration rather than an
|
|
232
|
+
* oversight: a contextual-toolbar surface is rendered by an element's own
|
|
233
|
+
* `ToolbarModuleConfig`, and declaring one nothing invokes would put an entry
|
|
234
|
+
* in the manifest that no toolbar draws.
|
|
235
|
+
*
|
|
236
|
+
* `'always'` with a `when` on the BOARD, and the pair is deliberate. An export
|
|
237
|
+
* READS: it needs no selection, and it is offered on a locked map and on a
|
|
238
|
+
* read-only document — which is precisely the board somebody wants to take
|
|
239
|
+
* away. What it DOES need is a plot to measure coordinates against, and that is
|
|
240
|
+
* a fact about the surface rather than about the selection: a Wardley node has
|
|
241
|
+
* no `visibility` prop, so its position on the plot IS its coordinate, and with
|
|
242
|
+
* no map there is nothing to invert. `'selection'` would be BPMN's shape copied
|
|
243
|
+
* for the look of it — that command's precondition genuinely is a selected
|
|
244
|
+
* pool, and this one's is not.
|
|
245
|
+
*
|
|
246
|
+
* **v1 writes one map.** An OWM document is one map; a board holding several is
|
|
247
|
+
* written against the first in document order and the export says so out loud
|
|
248
|
+
* in its warnings.
|
|
249
|
+
*/
|
|
250
|
+
const exportCommand = {
|
|
251
|
+
id: 'wardley.exportOwm',
|
|
252
|
+
owner: 'wardley',
|
|
253
|
+
kind: 'action',
|
|
254
|
+
labelKey: 'com.labre.commands.wardley.exportOwm',
|
|
255
|
+
labelFallback: 'Export Wardley map (OWM)',
|
|
256
|
+
descriptionKey: 'com.labre.commands.wardley.exportOwm.description',
|
|
257
|
+
descriptionFallback: 'Download the map as an OnlineWardleyMaps .owm file, ready to open in any Wardley mapping tool.',
|
|
258
|
+
category: 'interchange',
|
|
259
|
+
iconKey: 'wardley.export-owm',
|
|
260
|
+
surfaces: ['catalogue', 'palette', 'agent'],
|
|
261
|
+
order: SPECS.length + 1,
|
|
262
|
+
scope: 'edgeless',
|
|
263
|
+
defaultKeys: { mac: [], other: [] },
|
|
264
|
+
availability: 'always',
|
|
265
|
+
run: exportOwmFile,
|
|
266
|
+
telemetry: { framework: 'wardley', element: 'board:export-owm' },
|
|
267
|
+
when: std => wardleyMapsOnBoard(std).length > 0,
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* The SVG FALLBACK import — the visual tier, named as such before the picker
|
|
271
|
+
* opens (`docs/adr/0012`, P2).
|
|
272
|
+
*
|
|
273
|
+
* ## Why the catalogue and not the senior row
|
|
274
|
+
*
|
|
275
|
+
* Because {@link importCommand} is already there. The sub-menu carries the
|
|
276
|
+
* framework's NATIVE format — the OWM DSL, which the ADR's roadmap calls the
|
|
277
|
+
* reference Wardley import — and that is the route a user should be pointed at:
|
|
278
|
+
* an `.owm` file carries `[visibility, evolution]` pairs, which ARE the map's
|
|
279
|
+
* meaning, so it round-trips. This one reads a picture. A fallback that
|
|
280
|
+
* outranked the real thing would be the platform offering the lossy door first,
|
|
281
|
+
* and Wardley's fourteen nominations are already exactly the cap — a fifteenth
|
|
282
|
+
* would push a button out of the row for the rarest thing anybody does to a map.
|
|
283
|
+
*
|
|
284
|
+
* So it lands one click away, in the artefact catalogue behind "More
|
|
285
|
+
* artefacts…", and keeps `'palette'` and `'agent'` so it stays findable by name
|
|
286
|
+
* and invocable by an agent.
|
|
287
|
+
*
|
|
288
|
+
* **Flagged for the PO** as a curation call rather than a technical one: it is
|
|
289
|
+
* a one-line change either way.
|
|
290
|
+
*
|
|
291
|
+
* ## The label names the tier before the file is read
|
|
292
|
+
*
|
|
293
|
+
* A map is coordinates, and this reader recovers none: it recognises circles
|
|
294
|
+
* and words. Saying so in the description is not modesty, it is the contract —
|
|
295
|
+
* "the import surface must name the tier before the file is read".
|
|
296
|
+
*/
|
|
297
|
+
const importSvgCommand = {
|
|
298
|
+
id: 'wardley.importSvg',
|
|
299
|
+
owner: 'wardley',
|
|
300
|
+
kind: 'action',
|
|
301
|
+
labelKey: 'com.labre.commands.wardley.importSvg',
|
|
302
|
+
labelFallback: 'Import SVG sketch',
|
|
303
|
+
descriptionKey: 'com.labre.commands.wardley.importSvg.description',
|
|
304
|
+
descriptionFallback: 'Best effort: recognises shapes and text, no round-trip. The axes and the evolution are not read — what arrives is a sketch you then promote.',
|
|
305
|
+
// The same section the two OWM directions are filed under, and the same one
|
|
306
|
+
// BPMN files its `.bpmn` pair under: a host that translated the header once
|
|
307
|
+
// has translated it for every framework.
|
|
308
|
+
category: 'interchange',
|
|
309
|
+
iconKey: 'wardley.import-svg',
|
|
310
|
+
surfaces: ['catalogue', 'palette', 'agent'],
|
|
311
|
+
// Last of the three interchange entries, which is also how the section reads
|
|
312
|
+
// for somebody scanning "what can I do with a file": the native format both
|
|
313
|
+
// ways, then the best-effort reader.
|
|
314
|
+
order: SPECS.length + 2,
|
|
315
|
+
scope: 'edgeless',
|
|
316
|
+
defaultKeys: { mac: [], other: [] },
|
|
317
|
+
// It WRITES, so a read-only document is one it cannot run on.
|
|
318
|
+
availability: 'editable',
|
|
319
|
+
run: importWardleySvgFile,
|
|
320
|
+
// `board:` and not `node:`: it is launched with nothing selected, and often
|
|
321
|
+
// with nothing on the canvas at all.
|
|
322
|
+
telemetry: { framework: 'wardley', element: 'board:import-svg' },
|
|
323
|
+
};
|
|
324
|
+
/**
|
|
325
|
+
* The Wardley registry: the thirteen toolbox entries, then the two directions
|
|
326
|
+
* of the OWM DSL and the SVG fallback (`docs/adr/0012`).
|
|
327
|
+
*/
|
|
328
|
+
export const wardleyCommands = [
|
|
329
|
+
...toolboxCommands,
|
|
330
|
+
importCommand,
|
|
331
|
+
exportCommand,
|
|
332
|
+
importSvgCommand,
|
|
333
|
+
];
|
|
175
334
|
/** `iconKey` → template. Never travels through either manifest (ADR 0008). */
|
|
176
335
|
export const wardleyCommandIcons = {
|
|
177
336
|
'wardley.background': wardleyBackgroundIcon,
|
|
@@ -187,4 +346,7 @@ export const wardleyCommandIcons = {
|
|
|
187
346
|
'wardley.link': wardleyLinkIcon,
|
|
188
347
|
'wardley.arrow': wardleyArrowIcon,
|
|
189
348
|
'wardley.inertia': wardleyInertiaIcon,
|
|
349
|
+
'wardley.import-owm': wardleyImportOwmIcon,
|
|
350
|
+
'wardley.export-owm': wardleyExportOwmIcon,
|
|
351
|
+
'wardley.import-svg': wardleyImportSvgIcon,
|
|
190
352
|
};
|
package/dist/export.d.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { ConnectorElementModel, TextElementModel, WardleyBackgroundElementModel, WardleyNodeElementModel } from '@formicoidea/labre-core/model';
|
|
2
|
+
import type { GfxPrimitiveElementModel } from '@formicoidea/labre-core/std/gfx';
|
|
3
|
+
/**
|
|
4
|
+
* A Wardley map as an OnlineWardleyMaps (OWM) DSL document — models in, text
|
|
5
|
+
* out (`docs/adr/0012`, P3).
|
|
6
|
+
*
|
|
7
|
+
* This is the function ADR 0012 records as owed: the Wardley serializer that
|
|
8
|
+
* exists today in **labre-mcp**, outside this repo, and is the ADR's one named
|
|
9
|
+
* violation of P3. It lands here so that both consumers — the editor command
|
|
10
|
+
* and the MCP tool — call one implementation, tested once, and so that the
|
|
11
|
+
* reader next door (`import.ts`) has a writer it agrees with about every
|
|
12
|
+
* coordinate, name and carried line.
|
|
13
|
+
*
|
|
14
|
+
* ## Pure, like its BPMN sibling
|
|
15
|
+
*
|
|
16
|
+
* Element models in, a string out. No `BlockStdScope`, no surface, no DOM, no
|
|
17
|
+
* clock, no randomness. `interchange.ts` is the thin adapter that names the
|
|
18
|
+
* file; `actions.ts` is the thinner one that downloads it.
|
|
19
|
+
*
|
|
20
|
+
* ## The plot IS the coordinate
|
|
21
|
+
*
|
|
22
|
+
* A Wardley node carries **no** `visibility` and **no** `evolution` prop — its
|
|
23
|
+
* position on the map's plot is the whole of what the map says about it. So the
|
|
24
|
+
* writer inverts the projection the reader applied: a node's centre, measured
|
|
25
|
+
* against the plot of the background it sits on, is the `[visibility,
|
|
26
|
+
* evolution]` pair OWM spells. Both numbers are written to exactly **two
|
|
27
|
+
* decimals**, and that stability is load-bearing rather than cosmetic: the
|
|
28
|
+
* fixed point `export(import(export(board)))` is byte-identical only because a
|
|
29
|
+
* value that survives one rounding survives every one after it. The reader
|
|
30
|
+
* tolerates any precision a foreign file happens to use.
|
|
31
|
+
*
|
|
32
|
+
* ## A name is a separate element, so it has to be found
|
|
33
|
+
*
|
|
34
|
+
* On this canvas the name of an artefact is a free text element beside it, not
|
|
35
|
+
* a prop on it (`roles.ts`, `WARDLEY_ROLE.label`). The writer therefore matches
|
|
36
|
+
* each label to the node it names by comparing where the label IS with where a
|
|
37
|
+
* label for that node WOULD be — see {@link matchLabels}, which is the one
|
|
38
|
+
* heuristic in this module and is documented as one.
|
|
39
|
+
*
|
|
40
|
+
* ## v1 reads one map
|
|
41
|
+
*
|
|
42
|
+
* An OWM document is one map. A surface holding several Wardley backgrounds is
|
|
43
|
+
* serialized against the FIRST in document order, and the export warns; the
|
|
44
|
+
* other maps' artefacts are written against that first plot, which is the
|
|
45
|
+
* honest behaviour (nothing is dropped) and is named in the warning so nobody
|
|
46
|
+
* discovers it from a file.
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* The format id, and therefore THE KEY foreign matter rides under on an element
|
|
50
|
+
* (ADR 0012, D2) — `interchange.owm`. Declared here and re-exported by
|
|
51
|
+
* `import.ts`, so a reader filing a fragment and a writer looking one up cannot
|
|
52
|
+
* disagree about where it went.
|
|
53
|
+
*/
|
|
54
|
+
export declare const WARDLEY_OWM_FORMAT_ID = "owm";
|
|
55
|
+
/**
|
|
56
|
+
* OWM's scope vocabulary — where a carried line came off (D2).
|
|
57
|
+
*
|
|
58
|
+
* The DSL is a flat list of statements with no nesting and no ids, so it needs
|
|
59
|
+
* exactly two `@`-prefixed role keys and never an element id:
|
|
60
|
+
*
|
|
61
|
+
* - `@document` — the whole file: the lines this reader has no artefact for,
|
|
62
|
+
* and the `title` it consumed. They ride on the map's background element,
|
|
63
|
+
* which is D6's stated asymmetry (delete the map and the residue goes with
|
|
64
|
+
* it) and is where `profileId` already lives for the same reason.
|
|
65
|
+
* - `@self` — the line an element WAS. Used for the verbatim tail of a mapped
|
|
66
|
+
* line, i.e. everything the writer would otherwise drop: `label [x, y]`,
|
|
67
|
+
* `(build)`, `inertia`, a trailing comment.
|
|
68
|
+
*/
|
|
69
|
+
export declare const OWM_SCOPE: {
|
|
70
|
+
readonly document: "@document";
|
|
71
|
+
readonly self: "@self";
|
|
72
|
+
};
|
|
73
|
+
/** Where a mapped line's un-modelled tail is filed, under `attrs['@self']`. */
|
|
74
|
+
export declare const OWM_TAIL_ATTR = "tail";
|
|
75
|
+
/** Where the file's own `title` is filed, under `attrs['@document']`. */
|
|
76
|
+
export declare const OWM_TITLE_ATTR = "title";
|
|
77
|
+
/**
|
|
78
|
+
* Keywords a line may open on, none of which can be a bare component name.
|
|
79
|
+
*
|
|
80
|
+
* Two different parsers care. `BaseStrategyRunner` claims a line for a keyword
|
|
81
|
+
* when the TRIMMED line opens on `"<keyword> "`, and `LinksExtractionStrategy`
|
|
82
|
+
* refuses to read a line as a link when it opens on any of these — so a
|
|
83
|
+
* component genuinely called `style` would silently stop being linkable. The
|
|
84
|
+
* writer quotes such a name rather than betting nobody ever picks one.
|
|
85
|
+
*/
|
|
86
|
+
export declare const OWM_KEYWORDS: Set<string>;
|
|
87
|
+
/**
|
|
88
|
+
* A name as the DSL spells it — bare when it can be, quoted and escaped
|
|
89
|
+
* otherwise.
|
|
90
|
+
*
|
|
91
|
+
* The escaping mirrors OWM's own `escapeComponentNameForMapText` /
|
|
92
|
+
* `unescapeComponentNameFromMapText` pair character for character, which is
|
|
93
|
+
* what makes `"Vente retail thés, accessoires, coffrets"` come back with its
|
|
94
|
+
* commas and its accents intact.
|
|
95
|
+
*/
|
|
96
|
+
export declare function owmName(raw: string): string;
|
|
97
|
+
/** A name in quotes, escaped as OWM's `escapeComponentNameForMapText` does. */
|
|
98
|
+
export declare function owmQuote(raw: string): string;
|
|
99
|
+
/**
|
|
100
|
+
* A coordinate, to exactly two decimals — the whole of the fixed point's
|
|
101
|
+
* arithmetic.
|
|
102
|
+
*
|
|
103
|
+
* `-0` is written as `0.00`, because `(-0).toFixed(2)` is `"-0.00"` and a
|
|
104
|
+
* node dropped one pixel above the plot's top edge would otherwise produce a
|
|
105
|
+
* file whose bytes depend on which side of zero a float landed on. A
|
|
106
|
+
* non-finite value (an element with no geometry) is written as `0.00` rather
|
|
107
|
+
* than as `NaN`, which no parser reads.
|
|
108
|
+
*/
|
|
109
|
+
export declare function owmNumber(value: number): string;
|
|
110
|
+
/** The plot of a map, in ABSOLUTE surface units. */
|
|
111
|
+
export interface OwmPlot {
|
|
112
|
+
/** Surface x of evolution `0`, and y of visibility `1`. */
|
|
113
|
+
x0: number;
|
|
114
|
+
y0: number;
|
|
115
|
+
width: number;
|
|
116
|
+
height: number;
|
|
117
|
+
}
|
|
118
|
+
/** The reference map an import lays out on, and an export falls back to. */
|
|
119
|
+
export declare const OWM_DEFAULT_MAP_WIDTH: number;
|
|
120
|
+
export declare const OWM_DEFAULT_MAP_HEIGHT: number;
|
|
121
|
+
/**
|
|
122
|
+
* The plot of a background element, in absolute units — the declaration's
|
|
123
|
+
* margins, never a hand-written inset.
|
|
124
|
+
*
|
|
125
|
+
* `templates/maps.ts` learned this the hard way: a plot copied as four numbers
|
|
126
|
+
* drifted from the drawn one, and a rule measuring against the declaration then
|
|
127
|
+
* judged nodes laid out against the copy. Both directions of this format read
|
|
128
|
+
* the same function for the same reason.
|
|
129
|
+
*/
|
|
130
|
+
export declare function owmPlotOf(bound: {
|
|
131
|
+
x: number;
|
|
132
|
+
y: number;
|
|
133
|
+
w: number;
|
|
134
|
+
h: number;
|
|
135
|
+
}): OwmPlot;
|
|
136
|
+
/** The default plot: a reference map at the origin. */
|
|
137
|
+
export declare function owmDefaultPlot(): OwmPlot;
|
|
138
|
+
/**
|
|
139
|
+
* `[visibility, evolution]` → a surface point.
|
|
140
|
+
*
|
|
141
|
+
* Mind the inversion, which is the one thing about these axes that is easy to
|
|
142
|
+
* get backwards and impossible to see in a test that only round-trips: OWM's
|
|
143
|
+
* visibility `1.0` is the TOP of the value chain, and a canvas' y grows
|
|
144
|
+
* downwards.
|
|
145
|
+
*/
|
|
146
|
+
export declare function owmPointOf(plot: OwmPlot, visibility: number, evolution: number): [number, number];
|
|
147
|
+
/** A surface point → `[visibility, evolution]`. The exact inverse. */
|
|
148
|
+
export declare function owmCoordsOf(plot: OwmPlot, x: number, y: number): {
|
|
149
|
+
visibility: number;
|
|
150
|
+
evolution: number;
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* The artefacts the writer speaks about, picked out of a surface's elements and
|
|
154
|
+
* kept in the order they were given.
|
|
155
|
+
*
|
|
156
|
+
* Document order matters for the same reason it does in BPMN: it decides which
|
|
157
|
+
* map is THE map when a board holds several, and it is the order every section
|
|
158
|
+
* of the produced file is written in — so a file exported twice from an
|
|
159
|
+
* untouched board is the same file, byte for byte.
|
|
160
|
+
*
|
|
161
|
+
* `notes` is every text element with NO role. That is a deliberate reading and
|
|
162
|
+
* not a leak: an OWM `note` IS a free text at a position, so a text somebody
|
|
163
|
+
* dropped on the map is written as one. Only a text carrying the `label` role
|
|
164
|
+
* is a name, and only a name is resolved onto a node.
|
|
165
|
+
*/
|
|
166
|
+
export interface WardleyExportBoard {
|
|
167
|
+
maps: WardleyBackgroundElementModel[];
|
|
168
|
+
nodes: WardleyNodeElementModel[];
|
|
169
|
+
labels: TextElementModel[];
|
|
170
|
+
notes: TextElementModel[];
|
|
171
|
+
connectors: ConnectorElementModel[];
|
|
172
|
+
}
|
|
173
|
+
export declare function wardleyBoardFrom(elements: readonly GfxPrimitiveElementModel[]): WardleyExportBoard;
|
|
174
|
+
/**
|
|
175
|
+
* A name a file system will accept, minus the extension. BPMN's sanitizer,
|
|
176
|
+
* verbatim in behaviour and different only in its fallback — `map`, because
|
|
177
|
+
* that is what an OWM document is.
|
|
178
|
+
*/
|
|
179
|
+
export declare function wardleySafeFilename(raw: string | undefined): string;
|
|
180
|
+
/** The label box an import writes, and the width a prediction assumes. */
|
|
181
|
+
export declare const OWM_LABEL_WIDTH = 200;
|
|
182
|
+
export declare const OWM_LABEL_HEIGHT: number;
|
|
183
|
+
/** A text element's string, whether it is a `Y.Text` or a test's plain one. */
|
|
184
|
+
export declare function textOf(element: {
|
|
185
|
+
text?: unknown;
|
|
186
|
+
}): string;
|
|
187
|
+
export interface WardleyOwmExportOptions {
|
|
188
|
+
/** The board's own title, already sanitized by the caller. */
|
|
189
|
+
name?: string;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The board as an OWM document, plus what the format could not say.
|
|
193
|
+
*
|
|
194
|
+
* Sections in a fixed order — title, nodes, pipelines, notes, evolutions,
|
|
195
|
+
* links, carried lines — and DOCUMENT order inside each. That pairing is what
|
|
196
|
+
* makes the fixed point hold: the reader creates elements in the order it meets
|
|
197
|
+
* them, so a file's sections come back as a document whose order re-sections
|
|
198
|
+
* identically.
|
|
199
|
+
*/
|
|
200
|
+
export declare function exportWardleyOwmWithWarnings(board: WardleyExportBoard, options?: WardleyOwmExportOptions): {
|
|
201
|
+
text: string;
|
|
202
|
+
warnings: string[];
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* The board as an OWM document — models in, text out, and nothing else.
|
|
206
|
+
*
|
|
207
|
+
* The signature P3 names: exported from this package's index so that labre-mcp
|
|
208
|
+
* calls THIS function rather than keeping the copy ADR 0012 records as the one
|
|
209
|
+
* violation of it.
|
|
210
|
+
*/
|
|
211
|
+
export declare function exportWardleyOwm(board: WardleyExportBoard, options?: WardleyOwmExportOptions): string;
|