@formicoidea/labre-framework-edgy 0.41.0 → 0.43.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/consts.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * uniformly to the element bounds — so the three circles always stay circular
7
7
  * and the pictos/labels keep their relative proportions at any size.
8
8
  */
9
+ import { refScale as fitReference } from '@formicoidea/labre-core/blocks/surface';
9
10
  /** Reference design size (the validated mockup canvas). */
10
11
  export const REF_W = 680;
11
12
  export const REF_H = 400;
@@ -38,43 +39,116 @@ export const FONT_FAMILY = 'Inter, sans-serif';
38
39
  * Uniform fit of the reference design into an element of size `w × h`: the
39
40
  * scale factor plus the centering offsets (letterboxed). Shared by the renderer
40
41
  * (to draw) and the view (to map clicks back into reference coords).
42
+ *
43
+ * The fit itself is the library's ({@link fitReference}, R37): only the
44
+ * reference size is this framework's business.
41
45
  */
42
46
  export function refScale(w, h) {
43
- const s = Math.min(w / REF_W, h / REF_H);
44
- return { s, ox: (w - REF_W * s) / 2, oy: (h - REF_H * s) / 2 };
47
+ return fitReference(w, h, REF_W, REF_H);
45
48
  }
46
49
  /**
47
- * Bounding box of the three circles in reference coords (small padding). The
48
- * REF margins around it only exist for the facet name labels — when a diagram
49
- * hides them (`cropToCircles`), the renderer fits THIS box into the element
50
- * bounds instead, so the background hugs the Venn.
50
+ * The three circles, and nothing else: their exact bounding box in reference
51
+ * coords. Both crop boxes below are this box plus what each of them has to make
52
+ * room for, so the two can never disagree about where the Venn is.
51
53
  */
52
- export const CROP = (() => {
53
- const pad = 8;
54
+ const CIRCLES = (() => {
54
55
  const ax = VENN.cx - 0.866 * VENN.r0; // Identity / Architecture centres
55
56
  const abY = VENN.cy - 0.5 * VENN.r0;
56
57
  const cY = VENN.cy + VENN.r0; // Experience centre
57
- const minX = ax - VENN.R - pad;
58
- const maxX = VENN.cx + 0.866 * VENN.r0 + VENN.R + pad;
59
- const minY = abY - VENN.R - pad;
60
- const maxY = cY + VENN.R + pad;
61
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
58
+ return {
59
+ minX: ax - VENN.R,
60
+ maxX: VENN.cx + 0.866 * VENN.r0 + VENN.R,
61
+ minY: abY - VENN.R,
62
+ maxY: cY + VENN.R,
63
+ };
62
64
  })();
63
65
  /**
64
- * Crop box KEEPING room for the three facet name labels (drawn outside the
65
- * circles): the circles box plus a fixed text allowance left/right and below.
66
- * Used when a cropped diagram still shows its labels.
66
+ * Bounding box of the three circles in reference coords, with just enough
67
+ * padding to see their outline. When a diagram hides its facet names
68
+ * (`cropToCircles` without `showLabels`) the renderer fits THIS box into the
69
+ * element bounds, so the background hugs the Venn.
70
+ *
71
+ * ## Why the padding is 2.5 and not the 8 it was
72
+ *
73
+ * The padding is the ONLY empty band such a board has, and it is spent at the
74
+ * element's scale, not at the reference one. The EDGY dynamic template draws
75
+ * its board at `DYN_SCALE = 4.8`, so eight reference units of padding were
76
+ * **32.4 model units** of nothing on each of the four sides — the board a user
77
+ * has to catch by its border (R37) sat a good inch away from the circles, which
78
+ * is what the recette of 23/09/2026 reported. 2.5 leaves 1.25 past the circles'
79
+ * own 2.5-wide outline, i.e. 6 model units at the dynamic board's scale and
80
+ * under 2 at a facets board's.
81
+ *
82
+ * `templates/dynamic.ts` places every element of the dynamic board in
83
+ * coordinates relative to this box (`dynToModel`) and sizes the board from it,
84
+ * so the template follows in the same breath: both are derived here, and a
85
+ * freshly inserted template is aligned by construction. A board built from the
86
+ * OLD template keeps its `xywh` and sees the Venn grow 3.8 % inside it — see
87
+ * the changeset.
67
88
  */
68
- export const CROP_LABELED = (() => {
69
- const sideAllowance = 140; // label offset (10) + text width headroom
70
- const bottomAllowance = 35; // experience label below the bottom circle
89
+ export const CROP = (() => {
90
+ const pad = 2.5;
71
91
  return {
72
- x: CROP.x - sideAllowance,
73
- y: CROP.y,
74
- w: CROP.w + 2 * sideAllowance,
75
- h: CROP.h + bottomAllowance,
92
+ x: CIRCLES.minX - pad,
93
+ y: CIRCLES.minY - pad,
94
+ w: CIRCLES.maxX - CIRCLES.minX + 2 * pad,
95
+ h: CIRCLES.maxY - CIRCLES.minY + 2 * pad,
76
96
  };
77
97
  })();
98
+ /**
99
+ * How far past the circles each side of the labelled box has to reach, in REF
100
+ * units. MEASURED on the canvas, in the shipped wording, at
101
+ * {@link LABEL_FONT_SIZE}; each number is that reach plus 4 or 5 units of
102
+ * gutter.
103
+ *
104
+ * It used to be one symmetric number — `sideAllowance = 140` either side of the
105
+ * padded box, `bottomAllowance = 35` under it — and the three words are not
106
+ * symmetric at all. "Identity" is written right-aligned 10 left of the left
107
+ * circle and reaches 63.6 past the circles; "Architecture" is written
108
+ * left-aligned 10 right of the right circle and reaches 98.1; "Experience"
109
+ * hangs 29.2 below the bottom one (a `middle` baseline plus the descender of
110
+ * its "p"); and above them there is no word at all, only the 1.25 of circle
111
+ * stroke. So a facets diagram was born inside 126.6 model units of nothing on
112
+ * its left and 74.8 on its right — and a framework background is caught by its
113
+ * border and by nothing else
114
+ * (`model/src/elements/framework-background/hit-test.ts`), which put the
115
+ * catchable edge a seventh of the board away from anything drawn.
116
+ *
117
+ * ## Why constants rather than a measurement at paint time
118
+ *
119
+ * Because the three names are EDITABLE props of the element (`identityLabel` &
120
+ * co) and `cropLabeledScale` is not only what the renderer draws through —
121
+ * `element-view.ts` maps every click back through it too. Deriving the box from
122
+ * the current text would rescale and re-centre the whole Venn on each
123
+ * keystroke, and move the click mapping under the user's finger while they
124
+ * type; two boards of the same size would also draw their circles at two
125
+ * different sizes. The allowance is furniture: it is measured once, here.
126
+ *
127
+ * A name rewritten to something much longer than the vocabulary therefore
128
+ * reaches past the element's edge. It paints (the surface clips nothing); only
129
+ * the SVG export, which crops to the `xywh`, would cut it.
130
+ */
131
+ const LABEL_ALLOWANCE = {
132
+ /** No word up there: the circle's own stroke, and room to see it. */
133
+ top: 5,
134
+ /** "Identity", right-aligned, reaching left of the circles. */
135
+ left: 68,
136
+ /** "Architecture", left-aligned, reaching right of them. */
137
+ right: 103,
138
+ /** "Experience", under the bottom circle, descender included. */
139
+ bottom: 32,
140
+ };
141
+ /**
142
+ * Crop box KEEPING room for the three facet name labels (drawn outside the
143
+ * circles): the circles plus {@link LABEL_ALLOWANCE}, which is asymmetric
144
+ * because the words are. Used when a cropped diagram still shows its labels.
145
+ */
146
+ export const CROP_LABELED = {
147
+ x: CIRCLES.minX - LABEL_ALLOWANCE.left,
148
+ y: CIRCLES.minY - LABEL_ALLOWANCE.top,
149
+ w: CIRCLES.maxX - CIRCLES.minX + LABEL_ALLOWANCE.left + LABEL_ALLOWANCE.right,
150
+ h: CIRCLES.maxY - CIRCLES.minY + LABEL_ALLOWANCE.top + LABEL_ALLOWANCE.bottom,
151
+ };
78
152
  function fitScale(box) {
79
153
  return (w, h) => {
80
154
  const s = Math.min(w / box.w, h / box.h);
@@ -1,29 +1,27 @@
1
+ import type { EditableBackgroundLabel } from '@formicoidea/labre-core/blocks/surface';
2
+ import { FrameworkBackgroundView } from '@formicoidea/labre-core/blocks/surface';
1
3
  import type { EdgyFacetsElementModel } from '@formicoidea/labre-core/model';
2
- import type { EditorHost } from '@formicoidea/labre-core/std';
3
- import type { PointTestOptions } from '@formicoidea/labre-core/std/gfx';
4
- import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
5
- export declare class EdgyView extends GfxElementModelView<EdgyFacetsElementModel> {
4
+ /**
5
+ * The EDGY facets diagram's view: the shared background gesture, aimed by this
6
+ * framework's own label layout.
7
+ *
8
+ * The three facet names are NOT declared in a `FrameworkBackgroundDef` — the
9
+ * diagram is a Venn figure fitted into a reference space, and its labels hang
10
+ * off the circles rather than off a plot — so the hit test stays here, mapped
11
+ * with the SAME fit the renderer used. Everything above it (where a
12
+ * double-click is allowed to land, the in-place `<input>`, the commit that
13
+ * refuses to write an untouched value) comes from
14
+ * {@link FrameworkBackgroundView}, which is where this diagram's own copy of it
15
+ * went — see issue #355.
16
+ */
17
+ export declare class EdgyView extends FrameworkBackgroundView<EdgyFacetsElementModel> {
6
18
  static type: string;
7
- /** The in-place `<input>` used to edit a label, or null when idle. */
8
- private _labelEditor;
9
- onCreated(): void;
10
- onDestroyed(): void;
11
- /** The editable facet label under a MODEL-space point, or null. */
12
- private _labelAt;
13
19
  /**
14
- * The diagram is SELECTED by its border (`EdgyFacetsElementModel`), but its
15
- * three facet labels must still receive the double-click that renames them.
16
- *
17
- * Same seam Wardley uses, for the same reason: the pointer router asks the
18
- * VIEW, so a framework declares its own gesture zones next to the code that
19
- * draws them, and the model layer keeps a hit test that only says what a
20
- * click SELECTS.
20
+ * A diagram is SELECTED by its border (`EdgyFacetsElementModel`), and the
21
+ * three facet labels widen that for the pointer — the base class' doing; this
22
+ * method only says where they are.
21
23
  */
22
- includesPoint(x: number, y: number, options: PointTestOptions, host: EditorHost): boolean;
23
- /** Double-click on a label → edit its text in place. */
24
- private _onDblClick;
25
- private _openLabelEditor;
26
- private _closeLabelEditor;
24
+ protected labelAt(lx: number, ly: number, w: number, h: number): EditableBackgroundLabel | null;
27
25
  }
28
26
  /**
29
27
  * Resize gating: the resize handles are hidden unless `model.resizeEnabled` is
@@ -1,40 +1,31 @@
1
- import { EdgelessCRUDIdentifier } from '@formicoidea/labre-core/blocks/surface';
2
- import { rotatePoint } from '@formicoidea/labre-core/global/gfx';
3
- import { GfxElementModelView, GfxViewInteractionExtension, } from '@formicoidea/labre-core/std/gfx';
1
+ import { FrameworkBackgroundView } from '@formicoidea/labre-core/blocks/surface';
2
+ import { GfxViewInteractionExtension } from '@formicoidea/labre-core/std/gfx';
4
3
  import { cropLabeledScale, refScale } from './consts.js';
5
- import { getEdgyLabelHits, hitTestEdgyLabel, } from './label-layout.js';
6
- export class EdgyView extends GfxElementModelView {
7
- constructor() {
8
- super(...arguments);
9
- /** The in-place `<input>` used to edit a label, or null when idle. */
10
- this._labelEditor = null;
11
- }
4
+ import { getEdgyLabelHits, hitTestEdgyLabel } from './label-layout.js';
5
+ /**
6
+ * The EDGY facets diagram's view: the shared background gesture, aimed by this
7
+ * framework's own label layout.
8
+ *
9
+ * The three facet names are NOT declared in a `FrameworkBackgroundDef` — the
10
+ * diagram is a Venn figure fitted into a reference space, and its labels hang
11
+ * off the circles rather than off a plot — so the hit test stays here, mapped
12
+ * with the SAME fit the renderer used. Everything above it (where a
13
+ * double-click is allowed to land, the in-place `<input>`, the commit that
14
+ * refuses to write an untouched value) comes from
15
+ * {@link FrameworkBackgroundView}, which is where this diagram's own copy of it
16
+ * went — see issue #355.
17
+ */
18
+ export class EdgyView extends FrameworkBackgroundView {
12
19
  static { this.type = 'edgy'; }
13
- onCreated() {
14
- super.onCreated();
15
- this.on('dblclick', e => this._onDblClick(e));
16
- }
17
- onDestroyed() {
18
- this._closeLabelEditor();
19
- super.onDestroyed();
20
- }
21
- /** The editable facet label under a MODEL-space point, or null. */
22
- _labelAt(mx, my) {
20
+ /**
21
+ * A diagram is SELECTED by its border (`EdgyFacetsElementModel`), and the
22
+ * three facet labels widen that for the pointer — the base class' doing; this
23
+ * method only says where they are.
24
+ */
25
+ labelAt(lx, ly, w, h) {
23
26
  // No labels shown → nothing to edit (and the cropped mapping differs).
24
27
  if (!this.model.showLabels)
25
28
  return null;
26
- const [bx, by, w, h] = this.model.deserializedXYWH;
27
- // Convert the model-space point into element-local coordinates, undoing the
28
- // element rotation around its center.
29
- let lx = mx - bx;
30
- let ly = my - by;
31
- const rot = this.model.rotate ?? 0;
32
- if (rot) {
33
- const center = [bx + w / 2, by + h / 2];
34
- const [ux, uy] = rotatePoint([mx, my], center, -rot);
35
- lx = ux - bx;
36
- ly = uy - by;
37
- }
38
29
  // Map element-local coords into the fixed reference space the labels live
39
30
  // in — with the SAME fit the renderer used (cropped or letterboxed).
40
31
  const { s, ox, oy } = this.model.cropToCircles
@@ -42,91 +33,11 @@ export class EdgyView extends GfxElementModelView {
42
33
  : refScale(w, h);
43
34
  const rx = (lx - ox) / s;
44
35
  const ry = (ly - oy) / s;
45
- return hitTestEdgyLabel(getEdgyLabelHits(this.model), rx, ry);
46
- }
47
- /**
48
- * The diagram is SELECTED by its border (`EdgyFacetsElementModel`), but its
49
- * three facet labels must still receive the double-click that renames them.
50
- *
51
- * Same seam Wardley uses, for the same reason: the pointer router asks the
52
- * VIEW, so a framework declares its own gesture zones next to the code that
53
- * draws them, and the model layer keeps a hit test that only says what a
54
- * click SELECTS.
55
- */
56
- includesPoint(x, y, options, host) {
57
- if (super.includesPoint(x, y, options, host))
58
- return true;
59
- return this._labelAt(x, y) !== null;
60
- }
61
- /** Double-click on a label → edit its text in place. */
62
- _onDblClick(e) {
63
- if (this.model.isLocked())
64
- return;
65
- const [mx, my] = this.gfx.viewport.toModelCoord(e.x, e.y);
66
- const hit = this._labelAt(mx, my);
36
+ const hit = hitTestEdgyLabel(getEdgyLabelHits(this.model), rx, ry);
67
37
  if (!hit)
68
- return;
69
- this._openLabelEditor(hit.field, e);
70
- }
71
- _openLabelEditor(field, e) {
72
- this._closeLabelEditor();
73
- const input = document.createElement('input');
74
- input.value = String(this.model[field] ?? '');
75
- Object.assign(input.style, {
76
- position: 'fixed',
77
- left: `${e.raw.clientX}px`,
78
- top: `${e.raw.clientY}px`,
79
- transform: 'translate(-50%, -50%)',
80
- zIndex: '10000',
81
- minWidth: '140px',
82
- padding: '3px 8px',
83
- font: '14px Inter, sans-serif',
84
- color: 'var(--affine-text-primary-color, #1f2328)',
85
- background: 'var(--affine-background-overlay-panel-color, #ffffff)',
86
- border: '1px solid var(--affine-primary-color, #1e96eb)',
87
- borderRadius: '6px',
88
- boxShadow: 'var(--affine-shadow-2, 0 2px 8px rgba(0,0,0,0.18))',
89
- outline: 'none',
90
- });
91
- document.body.append(input);
92
- this._labelEditor = input;
93
- // Mark the element as "editing" so the global edgeless key handlers
94
- // (delete, escape, etc.) don't act on it while the user types.
95
- this.gfx.selection.set({ elements: [this.model.id], editing: true });
96
- input.focus();
97
- input.select();
98
- const commit = () => {
99
- if (this._labelEditor !== input)
100
- return;
101
- const value = input.value;
102
- this._closeLabelEditor();
103
- this.gfx.std.store.captureSync();
104
- this.gfx.std
105
- .get(EdgelessCRUDIdentifier)
106
- .updateElement(this.model.id, { [field]: value });
107
- };
108
- input.addEventListener('keydown', ev => {
109
- ev.stopPropagation();
110
- if (ev.key === 'Enter') {
111
- ev.preventDefault();
112
- commit();
113
- }
114
- else if (ev.key === 'Escape') {
115
- ev.preventDefault();
116
- this._closeLabelEditor();
117
- }
118
- });
119
- input.addEventListener('blur', commit);
120
- }
121
- _closeLabelEditor() {
122
- if (!this._labelEditor)
123
- return;
124
- const input = this._labelEditor;
125
- this._labelEditor = null;
126
- input.remove();
127
- if (this.isConnected) {
128
- this.gfx.selection.set({ elements: [this.model.id], editing: false });
129
- }
38
+ return null;
39
+ // The facet names carry a hard default, so the drawn words ARE the prop.
40
+ return { prop: hit.field, text: String(this.model[hit.field] ?? '') };
130
41
  }
131
42
  }
132
43
  /**
@@ -1,4 +1,4 @@
1
- import { ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
1
+ import { ElementRendererExtension, taggedPath2D, } 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
4
  import { PERSON_GLYPH_PATHS, PERSON_GLYPH_VIEWBOX } from './consts.js';
@@ -33,7 +33,7 @@ export const edgyNode = (model, ctx, matrix, renderer, rc, bound) => {
33
33
  ctx.translate(-PERSON_GLYPH_VIEWBOX / 2, -PERSON_GLYPH_VIEWBOX / 2);
34
34
  ctx.fillStyle = color;
35
35
  for (const d of PERSON_GLYPH_PATHS) {
36
- ctx.fill(new Path2D(d));
36
+ ctx.fill(taggedPath2D(d));
37
37
  }
38
38
  };
39
39
  export const EdgyNodeRendererExtension = ElementRendererExtension('edgyNode', edgyNode);
@@ -111,7 +111,11 @@ const NAME = 'EDGY dynamic';
111
111
  export const edgyDynamicTemplate = {
112
112
  name: NAME,
113
113
  type: 'template',
114
- preview: `<svg width="100%" height="100%" viewBox="0 0 135 80" xmlns="http://www.w3.org/2000/svg"><circle cx="55" cy="34" r="18" fill="#00ea4e" opacity="0.9"/><circle cx="80" cy="34" r="18" fill="#034cee" opacity="0.9"/><circle cx="67" cy="54" r="18" fill="#ff0056" opacity="0.9"/><path d="M55 30 H80 M56 31 L66 52 M79 31 L69 52" stroke="#fff" stroke-width="2"/><rect x="51" y="26" width="8" height="8" fill="#fff"/><circle cx="80" cy="30" r="4" fill="#fff"/><path d="M62 49 h6 l3 3 -3 3 h-6 z" fill="#fff"/></svg>`,
114
+ // The thumbnail follows the board: the circles are drawn where the cropped
115
+ // {@link CROP} box puts them once it is fitted into the 135 × 80 tile
116
+ // (height-bound, `s = 80 / CROP.h`), so the tile shows the same framing the
117
+ // template actually inserts rather than a Venn floating in a tile of its own.
118
+ preview: `<svg width="100%" height="100%" viewBox="0 0 135 80" xmlns="http://www.w3.org/2000/svg"><circle cx="53.6" cy="28" r="27.2" fill="#00ea4e" opacity="0.9"/><circle cx="81.4" cy="28" r="27.2" fill="#034cee" opacity="0.9"/><circle cx="67.5" cy="52" r="27.2" fill="#ff0056" opacity="0.9"/><path d="M53.6 28 H81.4 M55.1 30.6 L66 49.4 M79.9 30.6 L69 49.4" stroke="#fff" stroke-width="3"/><rect x="48.6" y="23" width="10" height="10" fill="#fff"/><circle cx="81.4" cy="28" r="5" fill="#fff"/><path d="M63 48 h7 l4 4 -4 4 h-7 z" fill="#fff"/></svg>`,
115
119
  // Stamped by hand, and it is the one `commandId` in this pack that is a
116
120
  // literal rather than a derivation: importing the registry here would close
117
121
  // the very loop this module exists to avoid. The coverage test pairs the two
@@ -518,7 +518,7 @@ const ATTRS = 'width="100%" height="100%" viewBox="0 0 135 80" xmlns="http://www
518
518
  * `build` takes the OPTIONAL inserting editor: called with none, at module
519
519
  * load, for the English `content`; called again with the real `std` as
520
520
  * {@link Template.localize}, so the two builds read the very same layout and
521
- * differ only in the words a seed resolves to (`docs/adr/0016`).
521
+ * differ only in the words a seed resolves to (`docs/adr/0023`).
522
522
  */
523
523
  function tpl(name, preview, build, nameKey) {
524
524
  return {
@@ -1,4 +1,4 @@
1
- import { type ChromeWording, type ToolbarContext } from '@formicoidea/labre-core/shared/services';
1
+ import { type ChromeWording, type ToolbarContext, type ToolbarModuleConfig } from '@formicoidea/labre-core/shared/services';
2
2
  import { type TemplateResult } from 'lit';
3
3
  /** Every wording above, for `translations.ts`'s manifest contribution. */
4
4
  export declare const EDGY_TOOLBAR_WORDINGS: readonly ChromeWording[];
@@ -17,11 +17,6 @@ export declare const edgyToolbarConfig: {
17
17
  icon: TemplateResult;
18
18
  active(ctx: ToolbarContext): boolean;
19
19
  run(ctx: ToolbarContext): void;
20
- }, {
21
- id: string;
22
- tooltipWording: ChromeWording;
23
- icon: TemplateResult<1>;
24
- run(ctx: ToolbarContext): void;
25
20
  }];
26
21
  readonly when: (ctx: ToolbarContext) => boolean;
27
22
  };
@@ -41,12 +36,11 @@ export declare const edgyBoardToolbarConfig: {
41
36
  icon: TemplateResult;
42
37
  active(ctx: ToolbarContext): boolean;
43
38
  run(ctx: ToolbarContext): void;
44
- }, {
45
- id: string;
46
- tooltipWording: ChromeWording;
47
- icon: TemplateResult<1>;
48
- run(ctx: ToolbarContext): void;
49
39
  }];
50
40
  readonly when: (ctx: ToolbarContext) => boolean;
51
41
  };
52
42
  export declare const edgyBoardToolbarExtension: import("@formicoidea/labre-core/store").ExtensionType;
43
+ export declare const edgyToolingToolbarConfig: ToolbarModuleConfig;
44
+ export declare const edgyBoardToolingToolbarConfig: ToolbarModuleConfig;
45
+ export declare const edgyToolingToolbarExtension: import("@formicoidea/labre-core/store").ExtensionType;
46
+ export declare const edgyBoardToolingToolbarExtension: import("@formicoidea/labre-core/store").ExtensionType;
@@ -1,10 +1,8 @@
1
- import { EdgelessCRUDIdentifier } from '@formicoidea/labre-core/blocks/surface';
2
- import { createAutoLegend, dddLegendIcon } from '@formicoidea/labre-ddd-shared';
1
+ import { EdgelessCRUDIdentifier, legendToolbarAction, validationToolbarConfig, } from '@formicoidea/labre-core/blocks/surface';
3
2
  import { EdgyBoardElementModel, EdgyFacetsElementModel, } from '@formicoidea/labre-core/model';
4
- import { BOARD_LEGEND_NOTATION, BOARD_RESIZE_TOGGLE, TelemetryProvider, ToolbarModuleExtension, } from '@formicoidea/labre-core/shared/services';
3
+ import { BOARD_RESIZE_TOGGLE, ToolbarModuleExtension, } from '@formicoidea/labre-core/shared/services';
5
4
  import { BlockFlavourIdentifier } from '@formicoidea/labre-core/std';
6
5
  import { html } from 'lit';
7
- import { EDGY_AUTO_LEGEND } from '../legend.js';
8
6
  /** The two toolbar tooltips this file used to hard-code as English literals. */
9
7
  const FACET_LABELS_TOGGLE = [
10
8
  'com.labre.edgy.toolbar.facet-labels',
@@ -96,46 +94,12 @@ function booleanToggle(Model, id, tooltip, icon, prop) {
96
94
  },
97
95
  };
98
96
  }
99
- /**
100
- * The legend action, shared by the two EDGY backgrounds: both frame the same
101
- * notation, so both answer the same question — "what is actually drawn here" —
102
- * with the same table ({@link EDGY_AUTO_LEGEND}) and the same box.
103
- *
104
- * Registered ALWAYS-ON (`EdgyRenderViewExtension`) with the toggles it sits
105
- * next to: a legend is real, editable elements written into the document, so
106
- * generating one is authoring, not tooling a flag may take away
107
- * (`docs/adr/0009`).
108
- */
109
- function legendAction(Model, id) {
110
- return {
111
- id,
112
- tooltipWording: BOARD_LEGEND_NOTATION,
113
- icon: dddLegendIcon,
114
- run(ctx) {
115
- const background = ctx.getSurfaceModelsByType(Model)[0];
116
- if (!background)
117
- return;
118
- createAutoLegend(ctx.std, background, EDGY_AUTO_LEGEND);
119
- ctx.std.getOptional(TelemetryProvider)?.track('FrameworkLegendCreated', {
120
- // The WIRE value from `frameworks.ts` (`telemetryKey`), which for EDGY
121
- // happens to be the module id itself. Same field set as Wardley's and
122
- // the three DDD legend buttons, so the four are comparable.
123
- framework: 'edgy',
124
- element: 'legend',
125
- page: 'whiteboard editor',
126
- segment: 'element toolbar',
127
- module: 'edgy toolbar',
128
- });
129
- },
130
- };
131
- }
132
97
  export const edgyToolbarConfig = {
133
98
  actions: [
134
99
  booleanToggle(EdgyFacetsElementModel, 'a.toggle-resize', BOARD_RESIZE_TOGGLE, ResizeIcon, 'resizeEnabled'),
135
100
  booleanToggle(EdgyFacetsElementModel, 'b.toggle-labels', FACET_LABELS_TOGGLE, LabelsIcon, 'showLabels'),
136
101
  // No spotlight toggle here: the hover spotlight is BOARD logic and lives on
137
102
  // the EDGY board toolbar below. The Venn only carries APPEARANCE (#195).
138
- legendAction(EdgyFacetsElementModel, 'c.legend'),
139
103
  ],
140
104
  when: ctx => ctx.getSurfaceModelsByType(EdgyFacetsElementModel).length > 0,
141
105
  };
@@ -147,7 +111,6 @@ export const edgyBoardToolbarConfig = {
147
111
  actions: [
148
112
  booleanToggle(EdgyBoardElementModel, 'a.toggle-resize', BOARD_RESIZE_TOGGLE, ResizeIcon, 'resizeEnabled'),
149
113
  booleanToggle(EdgyBoardElementModel, 'b.toggle-spotlight', HOVER_SPOTLIGHT_TOGGLE, SpotlightIcon, 'spotlightEnabled'),
150
- legendAction(EdgyBoardElementModel, 'c.legend'),
151
114
  ],
152
115
  when: ctx => ctx.getSurfaceModelsByType(EdgyBoardElementModel).length > 0,
153
116
  };
@@ -155,3 +118,55 @@ export const edgyBoardToolbarExtension = ToolbarModuleExtension({
155
118
  id: BlockFlavourIdentifier('affine:surface:edgyBoard'),
156
119
  config: edgyBoardToolbarConfig,
157
120
  });
121
+ /**
122
+ * A background's flag-gated row, WHOLE: the legend button and the Validation
123
+ * dropdown, in ONE module — registered by `EdgyViewExtension` on the `custom:`
124
+ * flavour slot of each of the two frames.
125
+ *
126
+ * ## Why one module and not two
127
+ *
128
+ * `renderToolbar` merges exactly four slots per element and
129
+ * `ToolbarModuleExtension` binds by DI variant, so a second module claiming
130
+ * `custom:affine:surface:edgy` throws `DuplicateServiceDefinitionError` before
131
+ * the editor finishes setting up. The always-on module above holds the
132
+ * APPEARANCE toggles a stored board must keep; these two entries are gated by
133
+ * the same flag and appear together or not at all.
134
+ *
135
+ * ## Why the legend moved out of the always-on half
136
+ *
137
+ * It used to sit with the toggles, argued as authoring. `docs/adr/0026` (which
138
+ * amends `docs/adr/0009`) reverses that reading: GENERATING a legend is
139
+ * tooling — it is the framework telling you what its notation means — while the
140
+ * legend it wrote is content, made of plain shapes, text and this pack's own
141
+ * always-on renderers, and keeps being painted with the flag off.
142
+ *
143
+ * `c.legend` and not the factory's default `b.`: the sort is lexicographic and
144
+ * both frames already spend `a.` and `b.` on their toggles.
145
+ */
146
+ function toolingConfig(Model) {
147
+ return {
148
+ actions: [
149
+ legendToolbarAction({
150
+ id: 'c.legend',
151
+ Model,
152
+ owner: 'edgy',
153
+ framework: 'edgy',
154
+ }),
155
+ // The generic dropdown, not an EDGY variant of it: the config names no
156
+ // framework — it reads the registered rules and profiles — so this is the
157
+ // very same object wardley, bpmn, the context map and C4 register.
158
+ ...validationToolbarConfig.actions,
159
+ ],
160
+ when: (ctx) => ctx.getSurfaceModelsByType(Model).length > 0,
161
+ };
162
+ }
163
+ export const edgyToolingToolbarConfig = toolingConfig(EdgyFacetsElementModel);
164
+ export const edgyBoardToolingToolbarConfig = toolingConfig(EdgyBoardElementModel);
165
+ export const edgyToolingToolbarExtension = ToolbarModuleExtension({
166
+ id: BlockFlavourIdentifier('custom:affine:surface:edgy'),
167
+ config: edgyToolingToolbarConfig,
168
+ });
169
+ export const edgyBoardToolingToolbarExtension = ToolbarModuleExtension({
170
+ id: BlockFlavourIdentifier('custom:affine:surface:edgyBoard'),
171
+ config: edgyBoardToolingToolbarConfig,
172
+ });
@@ -10,6 +10,8 @@ export declare class EdgelessEdgySeniorButton extends EdgelessEdgySeniorButton_b
10
10
  enableActiveBackground: boolean;
11
11
  type: typeof EmptyTool;
12
12
  private _toggleMenu;
13
+ /** Lit while one of THIS framework's artefacts is armed. See the Wardley one. */
14
+ private get _armed();
13
15
  render(): import("lit-html").TemplateResult<1>;
14
16
  }
15
17
  export {};
@@ -1,7 +1,7 @@
1
1
  import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { EmptyTool } from '@formicoidea/labre-core/gfx/pointer';
3
3
  import { translateKey } from '@formicoidea/labre-core/shared/services';
4
- import { EdgelessToolbarToolMixin } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
4
+ import { armedArtefact, EdgelessToolbarToolMixin, } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
5
5
  import { SignalWatcher } from '@formicoidea/labre-core/global/lit';
6
6
  import { css, html, LitElement } from 'lit';
7
7
  import { edgyToolbarIcon } from './icons.js';
@@ -72,6 +72,10 @@ export class EdgelessEdgySeniorButton extends EdgelessToolbarToolMixin(SignalWat
72
72
  const menu = this.createPopper('edgeless-edgy-menu', this);
73
73
  menu.element.edgeless = this.edgeless;
74
74
  }
75
+ /** Lit while one of THIS framework's artefacts is armed. See the Wardley one. */
76
+ get _armed() {
77
+ return armedArtefact(this.gfx)?.owner === 'edgy';
78
+ }
75
79
  render() {
76
80
  return html `<edgeless-toolbar-button
77
81
  class="edgy-button"
@@ -79,7 +83,7 @@ export class EdgelessEdgySeniorButton extends EdgelessToolbarToolMixin(SignalWat
79
83
  ? ''
80
84
  : translateKey(this.edgeless.std, 'com.labre.framework.edgy', 'EDGY')}
81
85
  .tooltipOffset=${4}
82
- .active=${!!this.popper}
86
+ .active=${!!this.popper || this._armed}
83
87
  @click=${this._toggleMenu}
84
88
  >
85
89
  <div class="edgy-root">
@@ -1,3 +1,4 @@
1
+ import type { FrameworkPalette, NamedPalette } from '@formicoidea/labre-core/components/color-picker';
1
2
  import { type ChromeWording } from '@formicoidea/labre-core/shared/services';
2
3
  /**
3
4
  * The twelve EDGY swatches' own names, resolved by `resolvePaletteLabel`
@@ -23,4 +24,17 @@ export declare const EDGY_PALETTE_WORDING_ORGANISATION_LIGHT: ChromeWording;
23
24
  export declare const EDGY_PALETTE_WORDING_BRAND_LIGHT: ChromeWording;
24
25
  export declare const EDGY_PALETTE_WORDING_PRODUCT_LIGHT: ChromeWording;
25
26
  export declare const EDGY_PALETTE_WORDINGS: readonly ChromeWording[];
27
+ /**
28
+ * From the default editor palette we keep ONLY the neutrals (greys, white,
29
+ * black, transparent) — the historical colours are dropped in favour of the
30
+ * EDGY swatches above.
31
+ */
32
+ export declare const EDGY_PALETTE_LIST: NamedPalette[];
33
+ /**
34
+ * EDGY's page of the colour pickers' carousel (`docs/adr/0027`) — the same
35
+ * list its node picker is seeded with, offered on every other picker too.
36
+ * Registered from the FLAG-GATED view extension: offering hues is tooling
37
+ * (`docs/adr/0009`).
38
+ */
39
+ export declare const EDGY_FRAMEWORK_PALETTE: FrameworkPalette;
26
40
  export declare const edgyNodeToolbarExtension: import("@formicoidea/labre-core/store").ExtensionType;
@@ -142,10 +142,21 @@ const EDGY_PALETTES = [
142
142
  * black, transparent) — the historical colours are dropped in favour of the
143
143
  * EDGY swatches above.
144
144
  */
145
- const EDGY_PALETTE_LIST = [
145
+ export const EDGY_PALETTE_LIST = [
146
146
  ...EDGY_PALETTES,
147
147
  ...neutralPalettes(),
148
148
  ];
149
+ /**
150
+ * EDGY's page of the colour pickers' carousel (`docs/adr/0027`) — the same
151
+ * list its node picker is seeded with, offered on every other picker too.
152
+ * Registered from the FLAG-GATED view extension: offering hues is tooling
153
+ * (`docs/adr/0009`).
154
+ */
155
+ export const EDGY_FRAMEWORK_PALETTE = {
156
+ framework: 'edgy',
157
+ labelWording: ['com.labre.framework.edgy', 'EDGY'],
158
+ palettes: EDGY_PALETTE_LIST,
159
+ };
149
160
  /**
150
161
  * EDGY fill / stroke colour picker — identical to the shape one but seeded with
151
162
  * the EDGY palette swatches (`.palettes`).