@formicoidea/labre-framework-wardley 0.32.0 → 0.34.1

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 CHANGED
@@ -1,5 +1,7 @@
1
- import { type WardleyBgVariant } from '@formicoidea/labre-core/model';
2
- import type { GfxController } from '@formicoidea/labre-core/std/gfx';
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 { EditPropsStore } from '@formicoidea/labre-core/shared/services';
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
+ }
@@ -0,0 +1,18 @@
1
+ import type { ShortcutManifestEntry } from '@formicoidea/labre-core/std';
2
+ /**
3
+ * The Wardley commands as SHORTCUT-MANIFEST rows — id, label, chord, scope,
4
+ * owner — and nothing else.
5
+ *
6
+ * DATA ONLY, and that is the whole point (`docs/adr/0008` § Packaging). A
7
+ * `CommandDescriptor` carries its `run`, so a host settings pane that imports
8
+ * the package entry to list names and chords drags the entire action graph —
9
+ * the import/export machinery, the surface and gfx deep paths — into its
10
+ * chunk. This module has type-only imports, so the published bundle exposes it
11
+ * as `./commands-manifest`: a few hundred bytes that reference nothing.
12
+ *
13
+ * GENERATED-SHAPED, hand-committed: `commands-manifest.unit.spec.ts` asserts
14
+ * row-for-row equality with `toShortcutManifestEntry` over {@link wardleyCommands},
15
+ * so this file cannot drift from the commands it projects. Add a command and
16
+ * the test says exactly what to add here.
17
+ */
18
+ export declare const wardleyCommandsManifest: ShortcutManifestEntry[];
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The Wardley commands as SHORTCUT-MANIFEST rows — id, label, chord, scope,
3
+ * owner — and nothing else.
4
+ *
5
+ * DATA ONLY, and that is the whole point (`docs/adr/0008` § Packaging). A
6
+ * `CommandDescriptor` carries its `run`, so a host settings pane that imports
7
+ * the package entry to list names and chords drags the entire action graph —
8
+ * the import/export machinery, the surface and gfx deep paths — into its
9
+ * chunk. This module has type-only imports, so the published bundle exposes it
10
+ * as `./commands-manifest`: a few hundred bytes that reference nothing.
11
+ *
12
+ * GENERATED-SHAPED, hand-committed: `commands-manifest.unit.spec.ts` asserts
13
+ * row-for-row equality with `toShortcutManifestEntry` over {@link wardleyCommands},
14
+ * so this file cannot drift from the commands it projects. Add a command and
15
+ * the test says exactly what to add here.
16
+ */
17
+ export const wardleyCommandsManifest = [
18
+ {
19
+ id: 'wardley.addBackground',
20
+ owner: 'wardley',
21
+ labelKey: 'com.labre.keyboardShortcuts.wardley.addBackground',
22
+ labelFallback: 'Wardley map background',
23
+ scope: 'edgeless',
24
+ defaultKeys: { mac: ['w', 'b'], other: ['w', 'b'] },
25
+ },
26
+ {
27
+ id: 'wardley.addOpportunityBackground',
28
+ owner: 'wardley',
29
+ labelKey: 'com.labre.commands.wardley.addOpportunityBackground',
30
+ labelFallback: 'Opportunity background (gradient)',
31
+ scope: 'edgeless',
32
+ defaultKeys: { mac: [], other: [] },
33
+ },
34
+ {
35
+ id: 'wardley.addBenefitBackground',
36
+ owner: 'wardley',
37
+ labelKey: 'com.labre.commands.wardley.addBenefitBackground',
38
+ labelFallback: 'Benefit / Investment background (gradient)',
39
+ scope: 'edgeless',
40
+ defaultKeys: { mac: [], other: [] },
41
+ },
42
+ {
43
+ id: 'wardley.addEvolutionBackground',
44
+ owner: 'wardley',
45
+ labelKey: 'com.labre.commands.wardley.addEvolutionBackground',
46
+ labelFallback: 'Evolution background (Wardley presentation)',
47
+ scope: 'edgeless',
48
+ defaultKeys: { mac: [], other: [] },
49
+ },
50
+ {
51
+ id: 'wardley.addComponent',
52
+ owner: 'wardley',
53
+ labelKey: 'com.labre.keyboardShortcuts.wardley.addComponent',
54
+ labelFallback: 'Component',
55
+ scope: 'edgeless',
56
+ defaultKeys: { mac: ['w', 'c'], other: ['w', 'c'] },
57
+ },
58
+ {
59
+ id: 'wardley.addMethod',
60
+ owner: 'wardley',
61
+ labelKey: 'com.labre.keyboardShortcuts.wardley.addMethod',
62
+ labelFallback: 'Component + method',
63
+ scope: 'edgeless',
64
+ defaultKeys: { mac: ['w', 'm'], other: ['w', 'm'] },
65
+ },
66
+ {
67
+ id: 'wardley.addMarket',
68
+ owner: 'wardley',
69
+ labelKey: 'com.labre.commands.wardley.addMarket',
70
+ labelFallback: 'Market',
71
+ scope: 'edgeless',
72
+ defaultKeys: { mac: [], other: [] },
73
+ },
74
+ {
75
+ id: 'wardley.addEcosystem',
76
+ owner: 'wardley',
77
+ labelKey: 'com.labre.commands.wardley.addEcosystem',
78
+ labelFallback: 'Ecosystem',
79
+ scope: 'edgeless',
80
+ defaultKeys: { mac: [], other: [] },
81
+ },
82
+ {
83
+ id: 'wardley.addAnchor',
84
+ owner: 'wardley',
85
+ labelKey: 'com.labre.commands.wardley.addAnchor',
86
+ labelFallback: 'Anchor',
87
+ scope: 'edgeless',
88
+ defaultKeys: { mac: [], other: [] },
89
+ },
90
+ {
91
+ id: 'wardley.addPipeline',
92
+ owner: 'wardley',
93
+ labelKey: 'com.labre.keyboardShortcuts.wardley.addPipeline',
94
+ labelFallback: 'Pipeline',
95
+ scope: 'edgeless',
96
+ defaultKeys: { mac: ['w', 'p'], other: ['w', 'p'] },
97
+ },
98
+ {
99
+ id: 'wardley.linkTool',
100
+ owner: 'wardley',
101
+ labelKey: 'com.labre.keyboardShortcuts.wardley.linkTool',
102
+ labelFallback: 'Link',
103
+ scope: 'edgeless',
104
+ defaultKeys: { mac: ['w', 'l'], other: ['w', 'l'] },
105
+ },
106
+ {
107
+ id: 'wardley.evolutionArrow',
108
+ owner: 'wardley',
109
+ labelKey: 'com.labre.keyboardShortcuts.wardley.evolutionArrow',
110
+ labelFallback: 'Arrow (evolution)',
111
+ scope: 'edgeless',
112
+ defaultKeys: { mac: ['w', 'a'], other: ['w', 'a'] },
113
+ },
114
+ {
115
+ id: 'wardley.addInertia',
116
+ owner: 'wardley',
117
+ labelKey: 'com.labre.keyboardShortcuts.wardley.addInertia',
118
+ labelFallback: 'Inertia',
119
+ scope: 'edgeless',
120
+ defaultKeys: { mac: ['w', 'i'], other: ['w', 'i'] },
121
+ },
122
+ {
123
+ id: 'wardley.importOwm',
124
+ owner: 'wardley',
125
+ labelKey: 'com.labre.commands.wardley.importOwm',
126
+ labelFallback: 'Import Wardley map (OWM)',
127
+ scope: 'edgeless',
128
+ defaultKeys: { mac: [], other: [] },
129
+ },
130
+ {
131
+ id: 'wardley.exportOwm',
132
+ owner: 'wardley',
133
+ labelKey: 'com.labre.commands.wardley.exportOwm',
134
+ labelFallback: 'Export Wardley map (OWM)',
135
+ scope: 'edgeless',
136
+ defaultKeys: { mac: [], other: [] },
137
+ },
138
+ {
139
+ id: 'wardley.importSvg',
140
+ owner: 'wardley',
141
+ labelKey: 'com.labre.commands.wardley.importSvg',
142
+ labelFallback: 'Import SVG sketch',
143
+ scope: 'edgeless',
144
+ defaultKeys: { mac: [], other: [] },
145
+ },
146
+ ];
@@ -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, } from './roles.js';
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
- export const wardleyCommands = SPECS.map((spec, order) => ({
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
  };
@@ -1,4 +1,6 @@
1
1
  import type { WardleyBackgroundElementModel } from '@formicoidea/labre-core/model';
2
+ import type { EditorHost } from '@formicoidea/labre-core/std';
3
+ import type { PointTestOptions } from '@formicoidea/labre-core/std/gfx';
2
4
  import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
3
5
  export declare class WardleyView extends GfxElementModelView<WardleyBackgroundElementModel> {
4
6
  static type: string;
@@ -6,6 +8,26 @@ export declare class WardleyView extends GfxElementModelView<WardleyBackgroundEl
6
8
  private _labelEditor;
7
9
  onCreated(): void;
8
10
  onDestroyed(): void;
11
+ /**
12
+ * The editable label under a MODEL-space point, or null.
13
+ *
14
+ * Which labels exist, where they sit, what they SAY and which are editable
15
+ * all come from the declaration the renderer paints — one source, resolved
16
+ * through the same catalogue, so a label can never be drawn in one place
17
+ * and clicked in another, nor read one thing and open on another.
18
+ */
19
+ private _labelAt;
20
+ /**
21
+ * The map is SELECTED by its border (`WardleyBackgroundElementModel`), but
22
+ * its axis labels must still receive the double-click that renames them.
23
+ *
24
+ * So the two areas differ, and this is where they are allowed to: the pointer
25
+ * router asks the VIEW (`GfxViewEventManager`), and the view adds the zones
26
+ * the declaration draws its labels in. Picking is unaffected —
27
+ * `getElementByPoint` still asks the model, so a click in the middle of the
28
+ * map still goes to whatever the user put there.
29
+ */
30
+ includesPoint(x: number, y: number, options: PointTestOptions, host: EditorHost): boolean;
9
31
  /** Double-click on a label → edit its text in place. */
10
32
  private _onDblClick;
11
33
  /**