@formicoidea/labre-framework-wardley 0.30.1 → 0.32.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.
Files changed (51) hide show
  1. package/dist/actions.d.ts +14 -18
  2. package/dist/actions.js +80 -39
  3. package/dist/audit-criteria.d.ts +31 -0
  4. package/dist/audit-criteria.js +90 -0
  5. package/dist/background.d.ts +14 -0
  6. package/dist/background.js +338 -0
  7. package/dist/commands.d.ts +5 -0
  8. package/dist/commands.js +190 -0
  9. package/dist/consts.d.ts +10 -4
  10. package/dist/consts.js +10 -9
  11. package/dist/descriptor.d.ts +8 -3
  12. package/dist/descriptor.js +6 -3
  13. package/dist/effects.d.ts +2 -2
  14. package/dist/effects.js +2 -2
  15. package/dist/element-renderer.d.ts +8 -5
  16. package/dist/element-renderer.js +12 -147
  17. package/dist/element-view.d.ts +8 -4
  18. package/dist/element-view.js +30 -22
  19. package/dist/gradient.d.ts +6 -11
  20. package/dist/gradient.js +58 -47
  21. package/dist/index.d.ts +9 -1
  22. package/dist/index.js +9 -1
  23. package/dist/legend.js +10 -2
  24. package/dist/natures.d.ts +50 -0
  25. package/dist/natures.js +93 -0
  26. package/dist/node/node-renderer.js +1 -1
  27. package/dist/nudges.d.ts +41 -0
  28. package/dist/nudges.js +69 -0
  29. package/dist/profiles.d.ts +2 -0
  30. package/dist/profiles.js +87 -0
  31. package/dist/reading.d.ts +3 -0
  32. package/dist/reading.js +129 -0
  33. package/dist/roles.d.ts +50 -0
  34. package/dist/roles.js +132 -0
  35. package/dist/rules.d.ts +2 -0
  36. package/dist/rules.js +270 -0
  37. package/dist/templates/index.js +54 -10
  38. package/dist/templates/maps.js +122 -18
  39. package/dist/toolbar/config.js +2 -2
  40. package/dist/toolbar/wardley-menu.d.ts +8 -15
  41. package/dist/toolbar/wardley-menu.js +8 -136
  42. package/dist/toolbar/wardley-senior-button.js +1 -1
  43. package/dist/translations.d.ts +16 -0
  44. package/dist/translations.js +24 -0
  45. package/dist/view.d.ts +17 -0
  46. package/dist/view.js +125 -22
  47. package/package.json +2 -2
  48. package/dist/label-layout.d.ts +0 -20
  49. package/dist/label-layout.js +0 -72
  50. package/dist/shortcuts.d.ts +0 -2
  51. package/dist/shortcuts.js +0 -37
@@ -1,150 +1,15 @@
1
- import { ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
2
- import { ARROW, CARD_RADIUS, COLORS, EVOLUTION_BOUNDARIES, FONT_FAMILY, FONTS, LINE, MARGIN, OFFSETS, } from './consts';
3
- import { paintGradientBackground } from './gradient';
4
- function roundRectPath(ctx, x, y, w, h, r) {
5
- const rr = Math.min(r, w / 2, h / 2);
6
- ctx.beginPath();
7
- ctx.moveTo(x + rr, y);
8
- ctx.arcTo(x + w, y, x + w, y + h, rr);
9
- ctx.arcTo(x + w, y + h, x, y + h, rr);
10
- ctx.arcTo(x, y + h, x, y, rr);
11
- ctx.arcTo(x, y, x + w, y, rr);
12
- ctx.closePath();
13
- }
1
+ import { createFrameworkBackgroundRenderer, ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
2
+ import { WARDLEY_BACKGROUND } from './background.js';
14
3
  /**
15
- * Canvas renderer for the Wardley map background — reproduces mockup C:
16
- * an L-shaped axes frame (no top/right border), dashed evolution dividers and
17
- * the symmetric axis labels.
4
+ * Canvas renderer for the Wardley map background.
18
5
  *
19
- * All sizes (fonts, margins, offsets, strokes) are FIXED model units they do
20
- * not scale with the element size. Only the plot interior scales.
6
+ * There is no Wardley drawing code any more (PF2.12): the map is an
7
+ * INSTANTIATION of the framework-background primitive, configured by the
8
+ * `WARDLEY_BACKGROUND` declaration. What used to be two hundred lines of
9
+ * `ctx.fillText` is now a declaration any other framework can write for itself.
10
+ *
11
+ * Exported as a function as well as an extension because the non-regression
12
+ * suite drives it directly with a canvas stub.
21
13
  */
22
- export const wardley = (model, ctx, matrix) => {
23
- const [, , w, h] = model.deserializedXYWH;
24
- const cx = w / 2;
25
- const cy = h / 2;
26
- ctx.setTransform(matrix.translateSelf(cx, cy).rotateSelf(model.rotate).translateSelf(-cx, -cy));
27
- const px0 = MARGIN.left;
28
- const px1 = w - MARGIN.right;
29
- const py0 = MARGIN.top;
30
- const py1 = h - MARGIN.bottom;
31
- const pw = px1 - px0;
32
- const ph = py1 - py0;
33
- const ex = (r) => px0 + r * pw;
34
- const line = (x1, y1, x2, y2) => {
35
- ctx.beginPath();
36
- ctx.moveTo(x1, y1);
37
- ctx.lineTo(x2, y2);
38
- ctx.stroke();
39
- };
40
- const vtext = (text, x, y, fontSize, color) => {
41
- ctx.save();
42
- ctx.translate(x, y);
43
- ctx.rotate(-Math.PI / 2);
44
- ctx.font = `${fontSize}px ${FONT_FAMILY}`;
45
- ctx.fillStyle = color;
46
- ctx.textAlign = 'center';
47
- ctx.textBaseline = 'alphabetic';
48
- ctx.fillText(text, 0, 0);
49
- ctx.restore();
50
- };
51
- // ── Card (element bounds) ───────────────────────────────────────────
52
- const inset = LINE.card / 2;
53
- roundRectPath(ctx, inset, inset, w - inset * 2, h - inset * 2, CARD_RADIUS);
54
- ctx.fillStyle = COLORS.card;
55
- ctx.fill();
56
- ctx.strokeStyle = COLORS.cardBorder;
57
- ctx.lineWidth = LINE.card;
58
- ctx.stroke();
59
- // ── Curve-driven gradient variants (inscribed in the frame) ─────────
60
- // Hidden when `showGradient` is false → plain white background.
61
- if (model.variant !== 'classic' && model.showGradient) {
62
- paintGradientBackground(ctx, model.variant, px0, px1, py0, py1);
63
- }
64
- // ── Optional evolution band tints ───────────────────────────────────
65
- if (model.banded) {
66
- const starts = [0, 0.175, 0.4, 0.7];
67
- const ends = [0.175, 0.4, 0.7, 1];
68
- for (let i = 0; i < 4; i++) {
69
- ctx.fillStyle = COLORS.band[i];
70
- ctx.fillRect(ex(starts[i]), py0, ex(ends[i]) - ex(starts[i]), ph);
71
- }
72
- }
73
- // ── Evolution phase dividers (dashed) ───────────────────────────────
74
- if (model.showColumnDividers) {
75
- ctx.strokeStyle = COLORS.divider;
76
- ctx.lineWidth = LINE.divider;
77
- ctx.setLineDash([5, 5]);
78
- for (const r of EVOLUTION_BOUNDARIES) {
79
- line(ex(r), py0, ex(r), py1);
80
- }
81
- ctx.setLineDash([]);
82
- }
83
- // ── Axes (L shape) + arrowheads ─────────────────────────────────────
84
- // X and Y axes are independently toggleable. Each line stops at the base of
85
- // its arrowhead (1px overlap, hidden under the triangle) so the line never
86
- // pokes past the tip on zoom.
87
- ctx.strokeStyle = COLORS.axis;
88
- ctx.lineWidth = LINE.axis;
89
- ctx.fillStyle = COLORS.axis;
90
- if (model.showXAxis) {
91
- line(px0, py1, px1 - ARROW + 1, py1); // X axis (arrow tip at px1)
92
- ctx.beginPath(); // X arrow (points right)
93
- ctx.moveTo(px1, py1);
94
- ctx.lineTo(px1 - ARROW, py1 - ARROW / 2);
95
- ctx.lineTo(px1 - ARROW, py1 + ARROW / 2);
96
- ctx.closePath();
97
- ctx.fill();
98
- }
99
- if (model.showYAxis) {
100
- line(px0, py1, px0, py0 + ARROW - 1); // Y axis (arrow tip at py0)
101
- ctx.beginPath(); // Y arrow (points up)
102
- ctx.moveTo(px0, py0);
103
- ctx.lineTo(px0 - ARROW / 2, py0 + ARROW);
104
- ctx.lineTo(px0 + ARROW / 2, py0 + ARROW);
105
- ctx.closePath();
106
- ctx.fill();
107
- }
108
- // ── Horizontal labels ───────────────────────────────────────────────
109
- ctx.textBaseline = 'alphabetic';
110
- // Phase (column) labels (left-aligned at each zone start)
111
- if (model.showColumnLabels) {
112
- ctx.font = `${FONTS.phase}px ${FONT_FAMILY}`;
113
- ctx.fillStyle = COLORS.label;
114
- ctx.textAlign = 'left';
115
- const phases = [
116
- [model.phase0, 0],
117
- [model.phase1, 0.175],
118
- [model.phase2, 0.4],
119
- [model.phase3, 0.7],
120
- ];
121
- for (const [label, start] of phases) {
122
- ctx.fillText(label, ex(start) + OFFSETS.phasePad, py1 + OFFSETS.phaseBaseline);
123
- }
124
- }
125
- // "Evolution" title near the X arrow (tied to the X axis)
126
- if (model.showXAxis) {
127
- ctx.font = `${FONTS.axis}px ${FONT_FAMILY}`;
128
- ctx.fillStyle = COLORS.axis;
129
- ctx.textAlign = 'right';
130
- ctx.fillText(model.xAxisTitle, px1 - OFFSETS.evolutionPadRight, py1 + OFFSETS.phaseBaseline);
131
- }
132
- // Direction indicators (Uncharted / Industrialized, top corners)
133
- if (model.showCornerLabels) {
134
- ctx.font = `${FONTS.direction}px ${FONT_FAMILY}`;
135
- ctx.fillStyle = COLORS.label;
136
- ctx.textAlign = 'left';
137
- ctx.fillText(model.evolutionStart, px0 + OFFSETS.directionPadLeft, py0 + OFFSETS.directionTop);
138
- ctx.textAlign = 'right';
139
- ctx.fillText(model.evolutionEnd, px1 - OFFSETS.directionPadRight, py0 + OFFSETS.directionTop);
140
- }
141
- // ── Rotated Y labels (hugging the axis, symmetric with the X labels) ─
142
- if (model.showYAxis) {
143
- vtext(model.yAxisTitle, px0 - OFFSETS.yHug, (py0 + py1) / 2, FONTS.axis, COLORS.axis);
144
- }
145
- if (model.showVisibilityLabels) {
146
- vtext(model.visibilityHigh, px0 - OFFSETS.yHug, py0 + OFFSETS.visibleTop, FONTS.visibility, COLORS.label);
147
- vtext(model.visibilityLow, px0 - OFFSETS.yHug, py1 - OFFSETS.invisibleBottom, FONTS.visibility, COLORS.label);
148
- }
149
- };
150
- export const WardleyElementRendererExtension = ElementRendererExtension('wardley', wardley);
14
+ export const wardley = createFrameworkBackgroundRenderer(WARDLEY_BACKGROUND);
15
+ export const WardleyElementRendererExtension = ElementRendererExtension(WARDLEY_BACKGROUND.type, wardley);
@@ -8,13 +8,17 @@ export declare class WardleyView extends GfxElementModelView<WardleyBackgroundEl
8
8
  onDestroyed(): void;
9
9
  /** Double-click on a label → edit its text in place. */
10
10
  private _onDblClick;
11
+ /**
12
+ * @param current the words currently DRAWN — which is the vocabulary, not
13
+ * `model[field]`, for a label the user has never renamed. Opening on the raw
14
+ * prop would show an empty box for a label that plainly reads "Evolution".
15
+ */
11
16
  private _openLabelEditor;
12
17
  private _closeLabelEditor;
13
18
  }
14
19
  /**
15
- * Resize gating: the resize handles are hidden unless `model.resizeEnabled` is
16
- * true. `beforeResize` is re-evaluated every time the allowed handles are
17
- * computed (manager.ts), so toggling the field from the toolbar updates the
18
- * handles reactively. Moving/selecting stays available throughout.
20
+ * Resize gating, from the primitive: the handles stay hidden until
21
+ * `resizeEnabled` is true the runtime half of the declaration's
22
+ * `geometry.resizable`.
19
23
  */
20
24
  export declare const WardleyInteraction: import("@formicoidea/labre-core/store").ExtensionType;
@@ -1,7 +1,8 @@
1
- import { EdgelessCRUDIdentifier } from '@formicoidea/labre-core/blocks/surface';
1
+ import { backgroundLabelHits, EdgelessCRUDIdentifier, FrameworkBackgroundInteractionExtension, hitTestBackgroundLabel, } from '@formicoidea/labre-core/blocks/surface';
2
+ import { TranslationProvider } from '@formicoidea/labre-core/shared/services';
2
3
  import { rotatePoint } from '@formicoidea/labre-core/global/gfx';
3
- import { GfxElementModelView, GfxViewInteractionExtension, } from '@formicoidea/labre-core/std/gfx';
4
- import { getWardleyLabelHits, hitTestWardleyLabel, } from './label-layout';
4
+ import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
5
+ import { isWardleyLabelProp, WARDLEY_BACKGROUND, } from './background.js';
5
6
  export class WardleyView extends GfxElementModelView {
6
7
  constructor() {
7
8
  super(...arguments);
@@ -34,15 +35,27 @@ export class WardleyView extends GfxElementModelView {
34
35
  lx = ux - bx;
35
36
  ly = uy - by;
36
37
  }
37
- const hit = hitTestWardleyLabel(getWardleyLabelHits(this.model, w, h), lx, ly);
38
+ // Which labels exist, where they sit, what they SAY and which are editable
39
+ // all come from the declaration the renderer paints — one source, resolved
40
+ // through the same catalogue, so a label can never be drawn in one place
41
+ // and clicked in another, nor read one thing and open on another.
42
+ const hit = hitTestBackgroundLabel(backgroundLabelHits(WARDLEY_BACKGROUND, this.model, w, h, this.gfx.std.getOptional(TranslationProvider)), lx, ly);
38
43
  if (!hit)
39
44
  return;
40
- this._openLabelEditor(hit.field, e);
45
+ // The declaration names the prop; this decides whether it may be written.
46
+ if (!isWardleyLabelProp(hit.prop))
47
+ return;
48
+ this._openLabelEditor(hit.prop, hit.text, e);
41
49
  }
42
- _openLabelEditor(field, e) {
50
+ /**
51
+ * @param current the words currently DRAWN — which is the vocabulary, not
52
+ * `model[field]`, for a label the user has never renamed. Opening on the raw
53
+ * prop would show an empty box for a label that plainly reads "Evolution".
54
+ */
55
+ _openLabelEditor(field, current, e) {
43
56
  this._closeLabelEditor();
44
57
  const input = document.createElement('input');
45
- input.value = String(this.model[field] ?? '');
58
+ input.value = current;
46
59
  Object.assign(input.style, {
47
60
  position: 'fixed',
48
61
  left: `${e.raw.clientX}px`,
@@ -73,6 +86,12 @@ export class WardleyView extends GfxElementModelView {
73
86
  return;
74
87
  const value = input.value;
75
88
  this._closeLabelEditor();
89
+ // Opening an editor is not renaming. Writing back an untouched value
90
+ // would persist the resolved VOCABULARY as the user's own text, freezing
91
+ // the label in whatever language it was read in and putting it beyond
92
+ // any catalogue for good — and it would push an empty entry onto undo.
93
+ if (value === current)
94
+ return;
76
95
  this.gfx.std.store.captureSync();
77
96
  this.gfx.std
78
97
  .get(EdgelessCRUDIdentifier)
@@ -103,19 +122,8 @@ export class WardleyView extends GfxElementModelView {
103
122
  }
104
123
  }
105
124
  /**
106
- * Resize gating: the resize handles are hidden unless `model.resizeEnabled` is
107
- * true. `beforeResize` is re-evaluated every time the allowed handles are
108
- * computed (manager.ts), so toggling the field from the toolbar updates the
109
- * handles reactively. Moving/selecting stays available throughout.
125
+ * Resize gating, from the primitive: the handles stay hidden until
126
+ * `resizeEnabled` is true the runtime half of the declaration's
127
+ * `geometry.resizable`.
110
128
  */
111
- export const WardleyInteraction = GfxViewInteractionExtension(WardleyView.type, {
112
- handleResize({ model }) {
113
- return {
114
- beforeResize({ set }) {
115
- if (!model.resizeEnabled) {
116
- set({ allowedHandlers: [] });
117
- }
118
- },
119
- };
120
- },
121
- });
129
+ export const WardleyInteraction = FrameworkBackgroundInteractionExtension(WARDLEY_BACKGROUND);
@@ -1,15 +1,10 @@
1
- /**
2
- * Curve-driven gradient backgrounds (Slice C). Each analytic background is a
3
- * smooth mathematical curve (piecewise asymmetric Gaussian bells); the gradient
4
- * opacity at each evolution position X follows that curve, normalised between
5
- * its own min and max — i.e. the gradient is strongest where the curve peaks and
6
- * fades to nothing at its minimum. Validated against the reference images at
7
- * `../wardley-mockups/gradient-backgrounds.html`.
8
- */
1
+ import type { BackgroundWashDef } from '@formicoidea/labre-core/blocks/surface';
9
2
  export declare const GRADIENT_GREEN = "#1f9e4d";
10
3
  export declare const GRADIENT_RED = "#d6455d";
11
4
  /**
12
- * Paint the curve-driven gradient over the plot rectangle [px0,px1]×[py0,py1]
13
- * in element-local coordinates. `classic` paints nothing.
5
+ * The washes the Wardley declaration ships, in painting order. Only those whose
6
+ * `variants` name the background's current `variant` are painted, and only
7
+ * while `showGradient` is on — so `classic` paints none of them and the frame
8
+ * stays plain white, exactly as before.
14
9
  */
15
- export declare function paintGradientBackground(ctx: CanvasRenderingContext2D, variant: 'opportunity' | 'benefit' | 'evolution-gradient', px0: number, px1: number, py0: number, py1: number): void;
10
+ export declare const WARDLEY_WASHES: readonly BackgroundWashDef[];
package/dist/gradient.js CHANGED
@@ -5,6 +5,11 @@
5
5
  * its own min and max — i.e. the gradient is strongest where the curve peaks and
6
6
  * fades to nothing at its minimum. Validated against the reference images at
7
7
  * `../wardley-mockups/gradient-backgrounds.html`.
8
+ *
9
+ * The curves are TABULATED ONCE, here, at module load: what the declaration
10
+ * ships — and what the primitive paints — is a table of `[offset, alpha]`
11
+ * stops, not a function. Nothing is evaluated at paint time, and the wash is
12
+ * data like the rest of the declaration (PF2.1).
8
13
  */
9
14
  const bell = (x, mu, s) => Math.exp(-0.5 * ((x - mu) / s) ** 2);
10
15
  const asym = (x, mu, sL, sR) => Math.exp(-0.5 * ((x - mu) / (x < mu ? sL : sR)) ** 2);
@@ -48,62 +53,68 @@ const GRADIENT_GREY = '#7c8389';
48
53
  const GRADIENT_MAX_OPACITY = 0.45;
49
54
  /** Peak opacity for the grey evolution-gradient variant. */
50
55
  const GREY_MAX_OPACITY = 0.38;
51
- function rgba(hex, alpha) {
52
- const r = parseInt(hex.slice(1, 3), 16);
53
- const g = parseInt(hex.slice(3, 5), 16);
54
- const b = parseInt(hex.slice(5, 7), 16);
55
- return `rgba(${r},${g},${b},${alpha})`;
56
- }
57
56
  /**
58
- * Add stops to a horizontal gradient (offset 0..1 spanning the plot width) for
59
- * a function-driven opacity profile within [x0, x1] (zero outside).
57
+ * Tabulate one opacity profile as gradient stops spanning the plot width:
58
+ * 49 samples inside [x0, x1], bracketed by a zero stop wherever the profile
59
+ * does not reach the edge of the plot.
60
60
  */
61
- function addStops(grad, hex, opacityFn, x0, x1, maxOp = GRADIENT_MAX_OPACITY) {
61
+ function stopTable(opacityFn, x0, x1, maxOp = GRADIENT_MAX_OPACITY) {
62
62
  const eps = 0.001;
63
+ const stops = [];
63
64
  if (x0 > eps)
64
- grad.addColorStop(Math.max(0, x0 - eps), rgba(hex, 0));
65
+ stops.push([Math.max(0, x0 - eps), 0]);
65
66
  const N = 48;
66
67
  for (let i = 0; i <= N; i++) {
67
68
  const x = x0 + ((x1 - x0) * i) / N;
68
- grad.addColorStop(clamp01(x), rgba(hex, clamp01(opacityFn(x)) * maxOp));
69
+ stops.push([clamp01(x), clamp01(opacityFn(x)) * maxOp]);
69
70
  }
70
71
  if (x1 < 1 - eps)
71
- grad.addColorStop(Math.min(1, x1 + eps), rgba(hex, 0));
72
+ stops.push([Math.min(1, x1 + eps), 0]);
73
+ return stops;
72
74
  }
75
+ // benefit: green where the curve is positive, red where negative.
76
+ const BEN_MAX_POS = RB.hi;
77
+ const BEN_MAX_NEG = -RB.lo;
73
78
  /**
74
- * Paint the curve-driven gradient over the plot rectangle [px0,px1]×[py0,py1]
75
- * in element-local coordinates. `classic` paints nothing.
79
+ * The washes the Wardley declaration ships, in painting order. Only those whose
80
+ * `variants` name the background's current `variant` are painted, and only
81
+ * while `showGradient` is on — so `classic` paints none of them and the frame
82
+ * stays plain white, exactly as before.
76
83
  */
77
- export function paintGradientBackground(ctx, variant, px0, px1, py0, py1) {
78
- const w = px1 - px0;
79
- const h = py1 - py0;
80
- if (variant === 'evolution-gradient') {
81
- const grey = ctx.createLinearGradient(px0, 0, px1, 0);
82
- addStops(grey, GRADIENT_GREY, fGrey, 0, 1, GREY_MAX_OPACITY);
83
- ctx.fillStyle = grey;
84
- ctx.fillRect(px0, py0, w, h);
85
- return;
86
- }
87
- if (variant === 'opportunity') {
88
- const green = ctx.createLinearGradient(px0, 0, px1, 0);
89
- addStops(green, GRADIENT_GREEN, x => norm(fDiff(x), RG.lo, RG.hi), DIFF_DOM[0], DIFF_DOM[1]);
90
- ctx.fillStyle = green;
91
- ctx.fillRect(px0, py0, w, h);
92
- const red = ctx.createLinearGradient(px0, 0, px1, 0);
93
- addStops(red, GRADIENT_RED, x => norm(fOper(x), RR.lo, RR.hi), OPER_DOM[0], OPER_DOM[1]);
94
- ctx.fillStyle = red;
95
- ctx.fillRect(px0, py0, w, h);
96
- return;
97
- }
98
- // benefit: green where the curve is positive, red where negative.
99
- const maxPos = RB.hi;
100
- const maxNeg = -RB.lo;
101
- const green = ctx.createLinearGradient(px0, 0, px1, 0);
102
- addStops(green, GRADIENT_GREEN, x => Math.max(0, fBen(x)) / maxPos, 0, 1);
103
- ctx.fillStyle = green;
104
- ctx.fillRect(px0, py0, w, h);
105
- const red = ctx.createLinearGradient(px0, 0, px1, 0);
106
- addStops(red, GRADIENT_RED, x => Math.max(0, -fBen(x)) / maxNeg, 0, 1);
107
- ctx.fillStyle = red;
108
- ctx.fillRect(px0, py0, w, h);
109
- }
84
+ export const WARDLEY_WASHES = [
85
+ {
86
+ id: 'evolution-grey',
87
+ variants: ['evolution-gradient'],
88
+ visibleProp: 'showGradient',
89
+ color: GRADIENT_GREY,
90
+ stops: stopTable(fGrey, 0, 1, GREY_MAX_OPACITY),
91
+ },
92
+ {
93
+ id: 'opportunity-differential',
94
+ variants: ['opportunity'],
95
+ visibleProp: 'showGradient',
96
+ color: GRADIENT_GREEN,
97
+ stops: stopTable(x => norm(fDiff(x), RG.lo, RG.hi), DIFF_DOM[0], DIFF_DOM[1]),
98
+ },
99
+ {
100
+ id: 'opportunity-operational',
101
+ variants: ['opportunity'],
102
+ visibleProp: 'showGradient',
103
+ color: GRADIENT_RED,
104
+ stops: stopTable(x => norm(fOper(x), RR.lo, RR.hi), OPER_DOM[0], OPER_DOM[1]),
105
+ },
106
+ {
107
+ id: 'benefit-positive',
108
+ variants: ['benefit'],
109
+ visibleProp: 'showGradient',
110
+ color: GRADIENT_GREEN,
111
+ stops: stopTable(x => Math.max(0, fBen(x)) / BEN_MAX_POS, 0, 1),
112
+ },
113
+ {
114
+ id: 'benefit-investment',
115
+ variants: ['benefit'],
116
+ visibleProp: 'showGradient',
117
+ color: GRADIENT_RED,
118
+ stops: stopTable(x => Math.max(0, -fBen(x)) / BEN_MAX_NEG, 0, 1),
119
+ },
120
+ ];
package/dist/index.d.ts CHANGED
@@ -1 +1,9 @@
1
- export { wardleyShortcuts } from './shortcuts';
1
+ export { WARDLEY_ROLE, WARDLEY_ROLES, type WardleyRole, type WardleyRoleId, } from './roles.js';
2
+ export { WARDLEY_AUDIT_CRITERIA } from './audit-criteria.js';
3
+ export { wardleyCommandIcons, wardleyCommands } from './commands.js';
4
+ export { wardleyTranslationEntries } from './translations.js';
5
+ export { WARDLEY_NATURE, WARDLEY_NATURE_TAG_ID, WARDLEY_TAG_DEFS, } from './natures.js';
6
+ export { WARDLEY_PROFILES } from './profiles.js';
7
+ export { WARDLEY_NUDGES } from './nudges.js';
8
+ export { WARDLEY_NAMING_CONVENTIONS, WARDLEY_READING } from './reading.js';
9
+ export { WARDLEY_RULES } from './rules.js';
package/dist/index.js CHANGED
@@ -1 +1,9 @@
1
- export { wardleyShortcuts } from './shortcuts';
1
+ export { WARDLEY_ROLE, WARDLEY_ROLES, } from './roles.js';
2
+ export { WARDLEY_AUDIT_CRITERIA } from './audit-criteria.js';
3
+ export { wardleyCommandIcons, wardleyCommands } from './commands.js';
4
+ export { wardleyTranslationEntries } from './translations.js';
5
+ export { WARDLEY_NATURE, WARDLEY_NATURE_TAG_ID, WARDLEY_TAG_DEFS, } from './natures.js';
6
+ export { WARDLEY_PROFILES } from './profiles.js';
7
+ export { WARDLEY_NUDGES } from './nudges.js';
8
+ export { WARDLEY_NAMING_CONVENTIONS, WARDLEY_READING } from './reading.js';
9
+ export { WARDLEY_RULES } from './rules.js';
package/dist/legend.js CHANGED
@@ -2,8 +2,8 @@ import { createGroupCommand } from '@formicoidea/labre-core/gfx/group';
2
2
  import { ConnectorElementModel, ConnectorMode, FontFamily, PointStyle, ShapeElementModel, ShapeStyle, StrokeStyle, WardleyNodeElementModel, } from '@formicoidea/labre-core/model';
3
3
  import { Bound } from '@formicoidea/labre-core/global/gfx';
4
4
  import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
5
- import { GRADIENT_GREEN, GRADIENT_RED } from './gradient';
6
- import { INERTIA_COLOR, LINK_GREY, LINK_STROKE_WIDTH, MARKET_DOT_STROKE_WIDTH, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, METHOD_FILL, NODE_FILL, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, WARDLEY_RED, } from './node/consts';
5
+ import { GRADIENT_GREEN, GRADIENT_RED } from './gradient.js';
6
+ import { INERTIA_COLOR, LINK_GREY, LINK_STROKE_WIDTH, MARKET_DOT_STROKE_WIDTH, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, METHOD_FILL, NODE_FILL, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, WARDLEY_RED, } from './node/consts.js';
7
7
  const LEGEND_ORDER = [
8
8
  'component',
9
9
  'anchor',
@@ -110,6 +110,14 @@ export function createWardleyLegend(std, bg) {
110
110
  xywh: new Bound(x, y, w, h).serialize(),
111
111
  });
112
112
  // ── glyph builders (real, editable elements), centred on (cx, cy) ─────
113
+ //
114
+ // DELIBERATELY ROLE-LESS. These are real `wardleyNode` elements, but a
115
+ // legend documents the map — it is not part of it. Giving its glyphs
116
+ // `wardley:component` & co. would make every legend entry count as an
117
+ // artefact and skew any rule written against roles (a legend would add a
118
+ // phantom component, anchor, market…). Neutral is the semantics we want;
119
+ // `kind` still drives their rendering. Frozen by a test in
120
+ // `__tests__/roles.unit.spec.ts`.
113
121
  const ellipse = (kind, d, fill, sw, cx, cy) => surface.addElement({
114
122
  type: 'wardleyNode',
115
123
  kind,
@@ -0,0 +1,50 @@
1
+ import type { UniverseTagDefs } from '@formicoidea/labre-core/shared/services';
2
+ /**
3
+ * The Wardley **nature** tag — the framework's type-3 contextual qualification
4
+ * (PRD level 3, ADR 0007).
5
+ *
6
+ * A component's nature answers "what KIND of thing is this?", which is a
7
+ * different question from its role ("what is it, on a map?") and from its
8
+ * position ("how evolved is it?"). Simon Wardley's four are activity, data,
9
+ * practice and knowledge, and mapping practitioners routinely draw all four on
10
+ * one map — the same circle, the same axes, four different things.
11
+ *
12
+ * ## Why this ships as DATA, on the host's own mechanism
13
+ *
14
+ * The library fixes the FORMAT of tag definitions; the application seeds them.
15
+ * Nothing here is privileged: this pack is registered through the same
16
+ * `UniverseTagDefsExtension` a host uses for its own taxonomy, and a client's
17
+ * private extension of Wardley — say a `criticality` tag — is a second pack
18
+ * with a different `packId` that merges with this one, with no library release.
19
+ * Shipping one real pack is what keeps that mechanism honest.
20
+ *
21
+ * ## Why it applies to `wardley:component` and not to `'*'`
22
+ *
23
+ * A nature qualifies a component. `market` and `ecosystem` specialise
24
+ * `wardley:component`, so they get it for free through `roleIsA` — that is the
25
+ * entire reason role hierarchy is data. The `anchor` (a user / need) is
26
+ * deliberately NOT a child of `component` and is deliberately NOT qualified
27
+ * here: a need has no nature, it has a demand. The map itself, the change
28
+ * arrow, the inertia bar and the labels are chrome or annotations and are not
29
+ * candidates either.
30
+ *
31
+ * ## Why `cardinality: 'single'`
32
+ *
33
+ * A component is one of the four, not several. Where practitioners disagree —
34
+ * "is a data pipeline data or an activity?" — the disagreement is the finding,
35
+ * and forcing one answer is what makes the finding visible. A multi-valued
36
+ * nature would let the ambiguity hide inside the element.
37
+ *
38
+ * Labels are English fallbacks: the host localizes them. The library never
39
+ * pretends a def's `label` is already translated for someone else's locale, and
40
+ * a pack shipped as a `.json` asset carries whatever the host put in it.
41
+ */
42
+ export declare const WARDLEY_NATURE_TAG_ID = "wardley:nature";
43
+ /** The four natures, as value ids. Ids are forever; a def is only deprecated. */
44
+ export declare const WARDLEY_NATURE: {
45
+ readonly activity: "wardley:nature/activity";
46
+ readonly data: "wardley:nature/data";
47
+ readonly practice: "wardley:nature/practice";
48
+ readonly knowledge: "wardley:nature/knowledge";
49
+ };
50
+ export declare const WARDLEY_TAG_DEFS: UniverseTagDefs;
@@ -0,0 +1,93 @@
1
+ import { WARDLEY_ROLE } from './roles.js';
2
+ /**
3
+ * The Wardley **nature** tag — the framework's type-3 contextual qualification
4
+ * (PRD level 3, ADR 0007).
5
+ *
6
+ * A component's nature answers "what KIND of thing is this?", which is a
7
+ * different question from its role ("what is it, on a map?") and from its
8
+ * position ("how evolved is it?"). Simon Wardley's four are activity, data,
9
+ * practice and knowledge, and mapping practitioners routinely draw all four on
10
+ * one map — the same circle, the same axes, four different things.
11
+ *
12
+ * ## Why this ships as DATA, on the host's own mechanism
13
+ *
14
+ * The library fixes the FORMAT of tag definitions; the application seeds them.
15
+ * Nothing here is privileged: this pack is registered through the same
16
+ * `UniverseTagDefsExtension` a host uses for its own taxonomy, and a client's
17
+ * private extension of Wardley — say a `criticality` tag — is a second pack
18
+ * with a different `packId` that merges with this one, with no library release.
19
+ * Shipping one real pack is what keeps that mechanism honest.
20
+ *
21
+ * ## Why it applies to `wardley:component` and not to `'*'`
22
+ *
23
+ * A nature qualifies a component. `market` and `ecosystem` specialise
24
+ * `wardley:component`, so they get it for free through `roleIsA` — that is the
25
+ * entire reason role hierarchy is data. The `anchor` (a user / need) is
26
+ * deliberately NOT a child of `component` and is deliberately NOT qualified
27
+ * here: a need has no nature, it has a demand. The map itself, the change
28
+ * arrow, the inertia bar and the labels are chrome or annotations and are not
29
+ * candidates either.
30
+ *
31
+ * ## Why `cardinality: 'single'`
32
+ *
33
+ * A component is one of the four, not several. Where practitioners disagree —
34
+ * "is a data pipeline data or an activity?" — the disagreement is the finding,
35
+ * and forcing one answer is what makes the finding visible. A multi-valued
36
+ * nature would let the ambiguity hide inside the element.
37
+ *
38
+ * Labels are English fallbacks: the host localizes them. The library never
39
+ * pretends a def's `label` is already translated for someone else's locale, and
40
+ * a pack shipped as a `.json` asset carries whatever the host put in it.
41
+ */
42
+ export const WARDLEY_NATURE_TAG_ID = 'wardley:nature';
43
+ /** The four natures, as value ids. Ids are forever; a def is only deprecated. */
44
+ export const WARDLEY_NATURE = {
45
+ activity: `${WARDLEY_NATURE_TAG_ID}/activity`,
46
+ data: `${WARDLEY_NATURE_TAG_ID}/data`,
47
+ practice: `${WARDLEY_NATURE_TAG_ID}/practice`,
48
+ knowledge: `${WARDLEY_NATURE_TAG_ID}/knowledge`,
49
+ };
50
+ export const WARDLEY_TAG_DEFS = {
51
+ formatVersion: 1,
52
+ // The id of this PACK, not of the framework: several packs may extend
53
+ // Wardley, and re-registering this one replaces it rather than duplicating
54
+ // it.
55
+ packId: 'wardley-core',
56
+ framework: 'wardley',
57
+ label: 'Wardley',
58
+ tags: [
59
+ {
60
+ id: WARDLEY_NATURE_TAG_ID,
61
+ label: 'Nature',
62
+ description: 'What kind of thing this component is: an activity, data, a practice or knowledge.',
63
+ cardinality: 'single',
64
+ appliesTo: [WARDLEY_ROLE.component],
65
+ // No `order`: it is the only tag this pack declares, and an absent order
66
+ // sorts by SEED order, which is what puts the library's pack ahead of a
67
+ // client's extension without either having to know about the other. A
68
+ // number here would be a claim about packs that do not exist yet.
69
+ values: [
70
+ {
71
+ id: WARDLEY_NATURE.activity,
72
+ label: 'Activity',
73
+ description: 'Something that is DONE — a step, a service, a process.',
74
+ },
75
+ {
76
+ id: WARDLEY_NATURE.data,
77
+ label: 'Data',
78
+ description: 'Something that is RECORDED — a dataset, a register.',
79
+ },
80
+ {
81
+ id: WARDLEY_NATURE.practice,
82
+ label: 'Practice',
83
+ description: 'A way of doing — a method, a convention, an operating model.',
84
+ },
85
+ {
86
+ id: WARDLEY_NATURE.knowledge,
87
+ label: 'Knowledge',
88
+ description: 'Something that is KNOWN — a model, a theory, a rule.',
89
+ },
90
+ ],
91
+ },
92
+ ],
93
+ };
@@ -1,7 +1,7 @@
1
1
  import { ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { shape as shapeRenderer } from '@formicoidea/labre-core/gfx/shape';
3
3
  import { DefaultTheme } from '@formicoidea/labre-core/model';
4
- import { ANCHOR, ECOSYSTEM, METHOD, NODE_FILL } from './consts';
4
+ import { ANCHOR, ECOSYSTEM, METHOD, NODE_FILL } from './consts.js';
5
5
  /**
6
6
  * Renderer for a Wardley node. The circle is drawn by REUSING the native shape
7
7
  * renderer (so stroke width, colors and theme behave exactly like a native