@formicoidea/labre-core 0.34.2 → 0.36.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 (46) hide show
  1. package/dist/_pkgs/affine-block-surface/extensions/editor-anchored-panel.d.ts +18 -2
  2. package/dist/_pkgs/affine-block-surface/extensions/editor-anchored-panel.js +141 -8
  3. package/dist/_pkgs/affine-block-surface/extensions/map-quality-widget.js +16 -22
  4. package/dist/_pkgs/affine-block-surface/extensions/reading-widget.d.ts +18 -0
  5. package/dist/_pkgs/affine-block-surface/extensions/reading-widget.js +35 -12
  6. package/dist/_pkgs/affine-block-surface/extensions/reading.d.ts +8 -0
  7. package/dist/_pkgs/affine-block-surface/extensions/reading.js +17 -6
  8. package/dist/_pkgs/affine-block-surface/extensions/tags-toolbar.js +17 -2
  9. package/dist/_pkgs/affine-block-surface/extensions/validation-toolbar.js +9 -2
  10. package/dist/_pkgs/affine-block-surface/framework-background/def.d.ts +14 -11
  11. package/dist/_pkgs/affine-block-surface/framework-background/renderer.js +14 -7
  12. package/dist/_pkgs/affine-components/highlight-dropdown-menu/highlight-duotone-icon.js +8 -8
  13. package/dist/_pkgs/affine-components/highlight-dropdown-menu/text-duotone-icon.js +11 -11
  14. package/dist/_pkgs/affine-components/icons/text.js +426 -426
  15. package/dist/_pkgs/affine-components/toolbar/menu-button.js +40 -0
  16. package/dist/_pkgs/affine-gfx-connector/direction/direction-reveal.d.ts +2 -2
  17. package/dist/_pkgs/affine-gfx-connector/direction/direction-reveal.js +1 -1
  18. package/dist/_pkgs/affine-gfx-connector/direction/direction-widget.d.ts +1 -1
  19. package/dist/_pkgs/affine-gfx-connector/direction/direction-widget.js +10 -1
  20. package/dist/_pkgs/affine-gfx-shape/element-view.d.ts +42 -1
  21. package/dist/_pkgs/affine-gfx-shape/element-view.js +54 -0
  22. package/dist/_pkgs/affine-gfx-shape/toolbar/index.d.ts +1 -0
  23. package/dist/_pkgs/affine-gfx-shape/toolbar/index.js +1 -0
  24. package/dist/_pkgs/affine-gfx-shape/toolbar/palette-color-action.d.ts +25 -0
  25. package/dist/_pkgs/affine-gfx-shape/toolbar/palette-color-action.js +123 -0
  26. package/dist/_pkgs/affine-model/elements/c4/board.d.ts +49 -0
  27. package/dist/_pkgs/affine-model/elements/c4/board.js +68 -0
  28. package/dist/_pkgs/affine-model/elements/wardley/node.d.ts +21 -6
  29. package/dist/_pkgs/affine-model/elements/wardley/node.js +20 -5
  30. package/dist/_pkgs/affine-shared/services/telemetry-service/block-lifecycle-watcher.d.ts +16 -1
  31. package/dist/_pkgs/affine-shared/services/telemetry-service/block-lifecycle-watcher.js +27 -2
  32. package/dist/_pkgs/affine-widget-edgeless-toolbar/menu/slide-menu.js +1 -1
  33. package/dist/_pkgs/affine-widget-keyboard-toolbar/icons.js +98 -98
  34. package/dist/_pkgs/data-view/core/group-by/renderer/boolean-group.js +11 -11
  35. package/dist/_pkgs/data-view/core/utils/menu-title.js +17 -17
  36. package/dist/_pkgs/data-view/view-presets/table/pc-virtual/group/top/group-title.js +18 -18
  37. package/dist/_pkgs/std/extension/command-registry.d.ts +15 -8
  38. package/dist/_pkgs/std/gfx/index.d.ts +1 -1
  39. package/dist/_pkgs/std/gfx/index.js +1 -1
  40. package/dist/_pkgs/std/gfx/interactivity/gfx-view-event-handler.d.ts +57 -0
  41. package/dist/_pkgs/std/gfx/interactivity/gfx-view-event-handler.js +80 -25
  42. package/dist/_pkgs/std/gfx/model/surface/pivot.d.ts +55 -1
  43. package/dist/_pkgs/std/gfx/model/surface/pivot.js +87 -0
  44. package/dist/_pkgs/std/gfx/model/surface/role.d.ts +12 -1
  45. package/dist/extensions/command-telemetry.js +4 -0
  46. package/package.json +1 -1
@@ -247,6 +247,46 @@ export class EditorMenuAction extends LitElement {
247
247
  pointer-events: none;
248
248
  }
249
249
 
250
+ /*
251
+ * ...unless the row is a TOGGLE rather than one of N. The rule above reads
252
+ * a selected row as inert, which is right for a single choice — re-picking
253
+ * the option already in force is a no-op, so swallowing the click costs
254
+ * nothing. A toggle is the opposite: re-clicking the value in force is the
255
+ * ONLY way to un-pick it (the qualification dropdown's tag values), and an
256
+ * inert row leaves the user with a tag that cannot be removed. Such a row
257
+ * opts the pointer back in through data-toggle.
258
+ */
259
+ :host([data-selected][data-toggle]) {
260
+ pointer-events: auto;
261
+ }
262
+
263
+ /*
264
+ * An OPTION row: one of N choices, the one in force carrying
265
+ * data-selected. Opt-in through data-option, because a plain command row is
266
+ * not a choice and keeps its left-packed icon and its grey highlight.
267
+ *
268
+ * The shape invents nothing. It is what edgeless-tool-icon-button draws
269
+ * with justify='space-between' and its default active-mode-color -- the
270
+ * Regular/Semibold menu -- and what size-dropdown-menu already spells out
271
+ * for its own check list: label left, tick right and ONLY when selected,
272
+ * the selected row in the primary colour. The colour IS the affordance, so
273
+ * the background highlight goes, and no gutter is held open on the left for
274
+ * a tick that is not there.
275
+ */
276
+ :host([data-option]) {
277
+ justify-content: space-between;
278
+ }
279
+
280
+ :host([data-option][data-selected]) {
281
+ color: var(--affine-primary-color);
282
+ background-color: unset;
283
+ }
284
+
285
+ /* The tick takes the row's colour rather than the neutral icon one. */
286
+ :host([data-option][data-selected]) ::slotted(svg) {
287
+ color: inherit;
288
+ }
289
+
250
290
  :host(:hover.delete),
251
291
  :host(:hover.delete) ::slotted(svg) {
252
292
  background-color: var(--affine-background-error-color);
@@ -6,7 +6,7 @@ import { type TypedEdge } from './typed-edge.js';
6
6
  * **M2 of `docs/adr/0010` — "show it".**
7
7
  *
8
8
  * A typed edge reveals its orientation on hover and on selection: ONE mark, the
9
- * role's verb laid ALONG the link — `depends on` — in a box whose far end is a
9
+ * role's verb laid ALONG the link — `needs` — in a box whose far end is a
10
10
  * point aimed at the target. At rest the board keeps the canonical arrowless
11
11
  * look, because on a Wardley map a permanent head already means something else
12
12
  * (evolution movement), and two meanings on one glyph make both unreadable.
@@ -66,7 +66,7 @@ export interface EdgeLabelAnchor {
66
66
  * this flag: the point of the box must face the TARGET, so on a link running
67
67
  * right-to-left — where the turn put the box's right end at the SOURCE — the
68
68
  * point moves to the box's left end. The words themselves never reverse; a
69
- * mirrored `depends on` would be a smudge, not a rotation.
69
+ * mirrored `needs` would be a smudge, not a rotation.
70
70
  */
71
71
  flipped: boolean;
72
72
  }
@@ -5,7 +5,7 @@ import { asTypedEdge, edgeIsBound, roleVocabularies, } from './typed-edge.js';
5
5
  * **M2 of `docs/adr/0010` — "show it".**
6
6
  *
7
7
  * A typed edge reveals its orientation on hover and on selection: ONE mark, the
8
- * role's verb laid ALONG the link — `depends on` — in a box whose far end is a
8
+ * role's verb laid ALONG the link — `needs` — in a box whose far end is a
9
9
  * point aimed at the target. At rest the board keeps the canonical arrowless
10
10
  * look, because on a Wardley map a permanent head already means something else
11
11
  * (evolution movement), and two meanings on one glyph make both unreadable.
@@ -12,7 +12,7 @@ export declare const EDGE_DIRECTION_WIDGET = "affine-edge-direction-widget";
12
12
  * is an unreadable one):
13
13
  *
14
14
  * - **M2** — while a typed edge is hovered or selected, the role's VERB laid
15
- * along the link: `depends on`, in a box that ends in a point aimed at the
15
+ * along the link: `needs`, in a box that ends in a point aimed at the
16
16
  * provider. Since the PO acceptance of 02/08/2026 this is the only mark the
17
17
  * reveal draws; the canvas chevron it used to sit on top of is folded into
18
18
  * that point (see `direction-reveal.ts`). Since the second pass of the same
@@ -86,7 +86,7 @@ const EDGE_DIRECTION_Z_INDEX = 0;
86
86
  * is an unreadable one):
87
87
  *
88
88
  * - **M2** — while a typed edge is hovered or selected, the role's VERB laid
89
- * along the link: `depends on`, in a box that ends in a point aimed at the
89
+ * along the link: `needs`, in a box that ends in a point aimed at the
90
90
  * provider. Since the PO acceptance of 02/08/2026 this is the only mark the
91
91
  * reveal draws; the canvas chevron it used to sit on top of is folded into
92
92
  * that point (see `direction-reveal.ts`). Since the second pass of the same
@@ -139,6 +139,11 @@ let EdgeDirectionWidget = (() => {
139
139
  * point has no border to speak of, and a hairline diagonal at 40 % zoom is
140
140
  * a smudge. Solid house blue with white text reads in both themes and
141
141
  * cannot be mistaken for the map's own ink.
142
+ *
143
+ * The background here is the DEFAULT, which every role that declares no
144
+ * chipColor keeps: render() overrides it inline only for a role that asks
145
+ * for one, so a colour decision taken about one relation never repaints the
146
+ * others.
142
147
  */
143
148
  .edge-direction-label {
144
149
  --edge-direction-point: 9px;
@@ -325,6 +330,9 @@ let EdgeDirectionWidget = (() => {
325
330
  const verb = this._verbFor(edge);
326
331
  if (!verb)
327
332
  return nothing;
333
+ // Declared by the ROLE, defaulted by the stylesheet: only a relation that
334
+ // asked for its own colour gets an inline background at all.
335
+ const chipColor = edge.direction?.chipColor;
328
336
  const { viewport } = this.gfx;
329
337
  const [x, y] = viewport.toViewCoord(anchor.at[0], anchor.at[1]);
330
338
  // Two decimals: below what a reader can see on a 12 px box, and it keeps
@@ -341,6 +349,7 @@ let EdgeDirectionWidget = (() => {
341
349
  style=${styleMap({
342
350
  left: `${x}px`,
343
351
  top: `${y}px`,
352
+ ...(chipColor ? { background: chipColor } : {}),
344
353
  // Read right to left, as CSS composes it: scale by the zoom and turn
345
354
  // onto the line, both about the box's centre, and only THEN slide that
346
355
  // centre onto the anchor. The lengths in the class are model units, so
@@ -1,7 +1,24 @@
1
1
  import { ShapeElementModel } from '../affine-model/index.js';
2
2
  import { GfxElementModelView } from '../std/gfx/index.js';
3
3
  import { PolygonVertexEditingOverlay } from './overlay/polygon-vertex-editing-overlay.js';
4
- export declare class ShapeElementView extends GfxElementModelView<ShapeElementModel> {
4
+ /**
5
+ * The view every shape gets, and the base a framework NODE view extends when
6
+ * its model is a {@link ShapeElementModel} subclass.
7
+ *
8
+ * Generic over the model, with the plain shape as the default, so
9
+ * `WardleyNodeView extends ShapeElementView<WardleyNodeElementModel>` reads its
10
+ * own `kind` off `this.model` without a cast. Every existing call site names no
11
+ * type argument and is unchanged.
12
+ *
13
+ * A framework node that extends this inherits the polygon VERTEX EDITOR, which
14
+ * is the whole reason to: {@link enterVertexEditingMode} is what the toolbar's
15
+ * "Edit vertices" action calls, and the action checks `view instanceof
16
+ * ShapeElementView` — a node view outside this hierarchy makes that button
17
+ * silently do nothing. What such a subclass usually does NOT want is the other
18
+ * thing wired here, the double-click-to-edit; {@link editsTextOnDblClick} is
19
+ * the one line it has to say so.
20
+ */
21
+ export declare class ShapeElementView<T extends ShapeElementModel = ShapeElementModel> extends GfxElementModelView<T> {
5
22
  static type: string;
6
23
  /** Overlay for polygon vertex editing (only active for polygon shapes). */
7
24
  private _vertexEditingOverlay;
@@ -16,6 +33,17 @@ export declare class ShapeElementView extends GfxElementModelView<ShapeElementMo
16
33
  * visually correct on every drag frame.
17
34
  */
18
35
  private _syncConnectorPaths;
36
+ /**
37
+ * Whether a double-click on THIS element opens its own inner-text editor.
38
+ *
39
+ * True for every plain shape, which is what this editor has always done: a
40
+ * shape is a box you write in. It is a HOOK rather than a constant because a
41
+ * framework node is often not one — its name may be a text element grouped
42
+ * beside it, or the element may be a glyph's own wiring that must carry no
43
+ * words at all — and a subclass that overrode `onCreated` to dodge this one
44
+ * wiring would lose the vertex editor set up beside it.
45
+ */
46
+ protected editsTextOnDblClick(): boolean;
19
47
  private _initDblClickToEdit;
20
48
  /**
21
49
  * Whether a vertex is currently being dragged.
@@ -61,6 +89,19 @@ export declare class ShapeElementView extends GfxElementModelView<ShapeElementMo
61
89
  * The overlay is shown when a polygon is selected and removed when
62
90
  * deselected or when a non-polygon shape is selected.
63
91
  */
92
+ /**
93
+ * Whether THIS element's corners may be moved by hand.
94
+ *
95
+ * True for every plain polygon. The companion of {@link editsTextOnDblClick},
96
+ * and a hook for the same reason: a framework polygon's outline is often the
97
+ * NOTATION rather than a drawing — a Wardley accelerator points right and a
98
+ * decelerator points left, and that direction is the whole of what separates
99
+ * them — so hovering one must not offer a handle that can only turn a
100
+ * statement into a blob. Declining here skips the selection overlay, the
101
+ * pointer wiring and {@link enterVertexEditingMode} together, which is the
102
+ * point: a half-wired overlay is worse than none.
103
+ */
104
+ protected editsVerticesOnSelection(): boolean;
64
105
  private _initPolygonVertexEditing;
65
106
  private _ensureVertexEditingOverlay;
66
107
  private _removeVertexEditingOverlay;
@@ -6,6 +6,23 @@ import { GfxElementModelView, GfxViewInteractionExtension, } from '../std/gfx/in
6
6
  import { normalizeShapeBound } from './element-renderer/index.js';
7
7
  import { PolygonVertexEditingOverlay } from './overlay/polygon-vertex-editing-overlay.js';
8
8
  import { mountShapeTextEditor } from './text/edgeless-shape-text-editor.js';
9
+ /**
10
+ * The view every shape gets, and the base a framework NODE view extends when
11
+ * its model is a {@link ShapeElementModel} subclass.
12
+ *
13
+ * Generic over the model, with the plain shape as the default, so
14
+ * `WardleyNodeView extends ShapeElementView<WardleyNodeElementModel>` reads its
15
+ * own `kind` off `this.model` without a cast. Every existing call site names no
16
+ * type argument and is unchanged.
17
+ *
18
+ * A framework node that extends this inherits the polygon VERTEX EDITOR, which
19
+ * is the whole reason to: {@link enterVertexEditingMode} is what the toolbar's
20
+ * "Edit vertices" action calls, and the action checks `view instanceof
21
+ * ShapeElementView` — a node view outside this hierarchy makes that button
22
+ * silently do nothing. What such a subclass usually does NOT want is the other
23
+ * thing wired here, the double-click-to-edit; {@link editsTextOnDblClick} is
24
+ * the one line it has to say so.
25
+ */
9
26
  export class ShapeElementView extends GfxElementModelView {
10
27
  constructor() {
11
28
  super(...arguments);
@@ -78,8 +95,23 @@ export class ShapeElementView extends GfxElementModelView {
78
95
  ConnectorPathGenerator.updatePath(connector, null, elementGetter);
79
96
  }
80
97
  }
98
+ /**
99
+ * Whether a double-click on THIS element opens its own inner-text editor.
100
+ *
101
+ * True for every plain shape, which is what this editor has always done: a
102
+ * shape is a box you write in. It is a HOOK rather than a constant because a
103
+ * framework node is often not one — its name may be a text element grouped
104
+ * beside it, or the element may be a glyph's own wiring that must carry no
105
+ * words at all — and a subclass that overrode `onCreated` to dodge this one
106
+ * wiring would lose the vertex editor set up beside it.
107
+ */
108
+ editsTextOnDblClick() {
109
+ return true;
110
+ }
81
111
  _initDblClickToEdit() {
82
112
  this.on('dblclick', () => {
113
+ if (!this.editsTextOnDblClick())
114
+ return;
83
115
  const edgeless = this.std.view.getBlock(this.std.store.root.id);
84
116
  if (edgeless &&
85
117
  !this.model.isLocked() &&
@@ -333,9 +365,26 @@ export class ShapeElementView extends GfxElementModelView {
333
365
  * The overlay is shown when a polygon is selected and removed when
334
366
  * deselected or when a non-polygon shape is selected.
335
367
  */
368
+ /**
369
+ * Whether THIS element's corners may be moved by hand.
370
+ *
371
+ * True for every plain polygon. The companion of {@link editsTextOnDblClick},
372
+ * and a hook for the same reason: a framework polygon's outline is often the
373
+ * NOTATION rather than a drawing — a Wardley accelerator points right and a
374
+ * decelerator points left, and that direction is the whole of what separates
375
+ * them — so hovering one must not offer a handle that can only turn a
376
+ * statement into a blob. Declining here skips the selection overlay, the
377
+ * pointer wiring and {@link enterVertexEditingMode} together, which is the
378
+ * point: a half-wired overlay is worse than none.
379
+ */
380
+ editsVerticesOnSelection() {
381
+ return true;
382
+ }
336
383
  _initPolygonVertexEditing() {
337
384
  if (this.model.shapeType !== ShapeType.Polygon)
338
385
  return;
386
+ if (!this.editsVerticesOnSelection())
387
+ return;
339
388
  // Subscribe to selection changes to manage overlay lifecycle
340
389
  this.disposable.add(this.gfx.selection.slots.updated.subscribe(() => {
341
390
  const selectedIds = this.gfx.selection.selectedIds;
@@ -481,6 +530,11 @@ export class ShapeElementView extends GfxElementModelView {
481
530
  enterVertexEditingMode() {
482
531
  if (this.model.shapeType !== ShapeType.Polygon)
483
532
  return;
533
+ // The same hook the selection wiring reads, asked again here: this is the
534
+ // other door into the mode, and an element that declined the wiring would
535
+ // otherwise get an overlay with no pointer handlers behind it.
536
+ if (!this.editsVerticesOnSelection())
537
+ return;
484
538
  this._ensureVertexEditingOverlay();
485
539
  this._enterVertexEditingMode();
486
540
  }
@@ -1,4 +1,5 @@
1
1
  export * from './config.js';
2
2
  export * from './icons.js';
3
+ export * from './palette-color-action.js';
3
4
  export * from './senior-tool.js';
4
5
  export * from './shape-menu-config.js';
@@ -1,4 +1,5 @@
1
1
  export * from './config.js';
2
2
  export * from './icons.js';
3
+ export * from './palette-color-action.js';
3
4
  export * from './senior-tool.js';
4
5
  export * from './shape-menu-config.js';
@@ -0,0 +1,25 @@
1
+ import { type Color, type Palette, ShapeElementModel } from '../../affine-model/index.js';
2
+ import type { ToolbarAction } from '../../affine-shared/services/index.js';
3
+ export declare function neutralPalettes(): Palette[];
4
+ /**
5
+ * A fill / stroke colour picker identical to the shape one, seeded with the
6
+ * caller's swatches (`.palettes`) instead of the default editor palette.
7
+ *
8
+ * Frameworks whose nodes are {@link ShapeElementModel} subclasses (EDGY,
9
+ * Wardley, …) all want the same picker with their own colours in front of it,
10
+ * so the ~100 lines that wire `edgeless-shape-color-picker` to the models live
11
+ * here once rather than being copied per framework.
12
+ */
13
+ /**
14
+ * The last word on what a picked swatch actually WRITES as a model's fill.
15
+ *
16
+ * The identity by default, which is what a colour picker means everywhere. It
17
+ * exists because a swatch is a hue and some elements are washes: a Wardley
18
+ * ZONE is drawn over the map it groups, so its fill carries an alpha, and a
19
+ * picked swatch that replaced `#c6dbfc99` with an opaque `#5b9cf6` would hide
20
+ * the very map the zone is there to annotate. A framework that has such
21
+ * elements says so here — per model, because the same picker serves its
22
+ * washes and its opaque artefacts.
23
+ */
24
+ export type FillColorFor = (model: ShapeElementModel, value: Color) => Color;
25
+ export declare function paletteColorAction(id: string, palettes: Palette[], fillColorFor?: FillColorFor): ToolbarAction;
@@ -0,0 +1,123 @@
1
+ import { EdgelessCRUDIdentifier } from '../../affine-block-surface/index.js';
2
+ import { packColor, } from '../../affine-components/color-picker/index.js';
3
+ import { DefaultTheme, isTransparent, LineWidth, resolveColor, ShapeElementModel, StrokeStyle, } from '../../affine-model/index.js';
4
+ import { getMostCommonValue } from '../../affine-shared/utils/index.js';
5
+ import { html } from 'lit';
6
+ /**
7
+ * The neutrals of the default editor palette: greys, white, black, transparent.
8
+ *
9
+ * A framework that seeds the picker with its OWN swatches drops the historical
10
+ * editor colours — they say nothing in that notation — but keeps the neutrals,
11
+ * which every drawing needs and no notation owns.
12
+ */
13
+ const NEUTRAL_KEY = /grey|gray|white|black|transparent/i;
14
+ export function neutralPalettes() {
15
+ return DefaultTheme.Palettes.filter(p => NEUTRAL_KEY.test(p.key));
16
+ }
17
+ // Mirror of the shape color action's text-color rule.
18
+ function getTextColor(fillColor) {
19
+ if (fillColor === DefaultTheme.black)
20
+ return DefaultTheme.white;
21
+ if (fillColor === DefaultTheme.white)
22
+ return DefaultTheme.black;
23
+ return DefaultTheme.shapeTextColor;
24
+ }
25
+ export function paletteColorAction(id, palettes, fillColorFor = (_model, value) => value) {
26
+ return {
27
+ id,
28
+ when(ctx) {
29
+ return ctx.getSurfaceModelsByType(ShapeElementModel).length > 0;
30
+ },
31
+ content(ctx) {
32
+ const models = ctx.getSurfaceModelsByType(ShapeElementModel);
33
+ if (!models.length)
34
+ return null;
35
+ const enableCustomColor = ctx.features.getFlag('enable_color_picker');
36
+ const theme = ctx.theme.edgeless$.value;
37
+ const firstModel = models[0];
38
+ const originalFillColor = firstModel.fillColor;
39
+ const originalStrokeColor = firstModel.strokeColor;
40
+ const mapped = models.map(({ filled, fillColor, strokeColor, strokeWidth, strokeStyle }) => ({
41
+ fillColor: filled
42
+ ? resolveColor(fillColor, theme)
43
+ : DefaultTheme.transparent,
44
+ strokeColor: resolveColor(strokeColor, theme),
45
+ strokeWidth,
46
+ strokeStyle,
47
+ }));
48
+ const fillColor = getMostCommonValue(mapped, 'fillColor') ??
49
+ resolveColor(DefaultTheme.shapeFillColor, theme);
50
+ const strokeColor = getMostCommonValue(mapped, 'strokeColor') ??
51
+ resolveColor(DefaultTheme.shapeStrokeColor, theme);
52
+ const strokeWidth = getMostCommonValue(mapped, 'strokeWidth') ?? LineWidth.Four;
53
+ const strokeStyle = getMostCommonValue(mapped, 'strokeStyle') ?? StrokeStyle.Solid;
54
+ const pickColorWrapper = (field, pickCallback) => (e) => {
55
+ e.stopPropagation();
56
+ switch (e.detail.type) {
57
+ case 'pick':
58
+ pickCallback(e.detail.detail);
59
+ break;
60
+ case 'start':
61
+ ctx.store.captureSync();
62
+ models.forEach(model => model.stash(field));
63
+ break;
64
+ case 'end':
65
+ ctx.store.transact(() => {
66
+ models.forEach(model => model.pop(field));
67
+ });
68
+ }
69
+ };
70
+ const onPickFillColor = pickColorWrapper('fillColor', palette => {
71
+ const value = palette.value;
72
+ const filled = isTransparent(value);
73
+ const crud = ctx.std.get(EdgelessCRUDIdentifier);
74
+ models.forEach(model => {
75
+ // Per model, so a selection mixing a wash and an opaque artefact
76
+ // gets the right fill on each — and `packColor` runs on what the
77
+ // hook returned, since that is the value the document stores.
78
+ const props = packColor('fillColor', fillColorFor(model, value));
79
+ if (filled && !model.filled) {
80
+ const color = getTextColor(value);
81
+ Object.assign(props, { filled, color });
82
+ }
83
+ crud.updateElement(model.id, props);
84
+ });
85
+ });
86
+ const onPickStrokeColor = pickColorWrapper('strokeColor', palette => {
87
+ const props = packColor('strokeColor', palette.value);
88
+ const crud = ctx.std.get(EdgelessCRUDIdentifier);
89
+ models.forEach(model => crud.updateElement(model.id, props));
90
+ });
91
+ const onPickStrokeStyle = (e) => {
92
+ e.stopPropagation();
93
+ const { type, value } = e.detail;
94
+ const crud = ctx.std.get(EdgelessCRUDIdentifier);
95
+ const props = type === 'size'
96
+ ? { strokeWidth: value }
97
+ : { strokeStyle: value };
98
+ for (const model of models) {
99
+ crud.updateElement(model.id, props);
100
+ }
101
+ };
102
+ return html `
103
+ <edgeless-shape-color-picker
104
+ @pickFillColor=${onPickFillColor}
105
+ @pickStrokeColor=${onPickStrokeColor}
106
+ @pickStrokeStyle=${onPickStrokeStyle}
107
+ .palettes=${palettes}
108
+ .payload=${{
109
+ fillColor,
110
+ strokeColor,
111
+ strokeWidth,
112
+ strokeStyle,
113
+ originalFillColor,
114
+ originalStrokeColor,
115
+ theme,
116
+ enableCustomColor,
117
+ }}
118
+ >
119
+ </edgeless-shape-color-picker>
120
+ `;
121
+ },
122
+ };
123
+ }
@@ -1,6 +1,29 @@
1
1
  import type { SerializedXYWH } from '../../../global/gfx/index.js';
2
+ import type { PointTestOptions } from '../../../std/gfx/index.js';
2
3
  import type { FrameworkBackgroundProps } from '../framework-background/index.js';
3
4
  import { FrameworkBackgroundElementModel } from '../framework-background/index.js';
5
+ /**
6
+ * Height of the board's title band — the header strip across the top of the
7
+ * sheet the diagram title is written in, in model units.
8
+ *
9
+ * Declared HERE, in the model, and re-exported by `@labre/affine-gfx-c4` rather
10
+ * than the other way round, for the reason `POOL_BAND_WIDTH` is: the
11
+ * board's own hit test needs it (a board is clickable by its title band as well
12
+ * as its border), and `affine-model` cannot reach into `affine-block-surface`
13
+ * where the declaration is resolved. One number, owned by the layer that both
14
+ * the declaration and the hit test can read.
15
+ *
16
+ * It is the board's TOP MARGIN, and it always was — the deep inset the plot
17
+ * gives up so the title has somewhere to be written. Naming it says out loud
18
+ * what was already true, so the strip that is painted, the strip that is picked
19
+ * and the strip that opens the rename are one number rather than three.
20
+ *
21
+ * 56 is what the title has been written in since the pack shipped: a 20-unit
22
+ * title, sitting on a baseline a third of the band up from its foot, with the
23
+ * rest as breathing room above and below. Changing it moves the title, so it is
24
+ * a visual decision and not a hit-test one.
25
+ */
26
+ export declare const C4_BOARD_TITLE_BAND_HEIGHT = 56;
4
27
  /**
5
28
  * Which of C4's levels this board draws — the DECLARED fact, as opposed to the
6
29
  * title, which is free text and says whatever its author wants it to.
@@ -85,4 +108,30 @@ export declare class C4BoardElementModel extends FrameworkBackgroundElementModel
85
108
  * on a board that declares none — no fact, no rule (PRD principle 8).
86
109
  */
87
110
  accessor level: C4BoardLevel | undefined;
111
+ /**
112
+ * A board is picked by its BORDER — and by its TITLE BAND, the one carve-out
113
+ * this frame keeps (issues #194 / #197).
114
+ *
115
+ * The same carve-out a BPMN pool makes, for the same reason and by the same
116
+ * arithmetic: the band is the only part of the sheet that is the SHEET rather
117
+ * than the diagram drawn on it. The title is written there, nothing is ever
118
+ * dropped there by convention, and the plot below is where the work goes — so
119
+ * a click there belongs to whatever node is under the pointer.
120
+ *
121
+ * Without this the title became unreachable the day backgrounds stopped being
122
+ * picked by their area: the only thing left answering was the view's
123
+ * double-click zone, which is the tight box of the DRAWN glyphs, and a single
124
+ * click on the title selected nothing at all.
125
+ *
126
+ * ## Where the geometry comes from
127
+ *
128
+ * The band is the top margin, full width — one number
129
+ * ({@link C4_BOARD_TITLE_BAND_HEIGHT}), read from here by the framework
130
+ * declaration, so what is painted is what is selectable. There is no
131
+ * zoom-grown target here as there is on a pool: the pool grows its 28-unit
132
+ * strip to stay reachable when zoomed out, which would put the rename zone
133
+ * outside the selectable one. A 56-unit header does not need it, and the
134
+ * promise "what is painted is what is picked" is worth more than the pixels.
135
+ */
136
+ includesPoint(x: number, y: number, options?: PointTestOptions): boolean;
88
137
  }
@@ -32,8 +32,31 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
32
32
  }
33
33
  return useValue ? value : void 0;
34
34
  };
35
+ import { rotatePoint } from '../../../global/gfx/index.js';
35
36
  import { field } from '../../../std/gfx/index.js';
36
37
  import { FrameworkBackgroundElementModel } from '../framework-background/index.js';
38
+ /**
39
+ * Height of the board's title band — the header strip across the top of the
40
+ * sheet the diagram title is written in, in model units.
41
+ *
42
+ * Declared HERE, in the model, and re-exported by `@labre/affine-gfx-c4` rather
43
+ * than the other way round, for the reason `POOL_BAND_WIDTH` is: the
44
+ * board's own hit test needs it (a board is clickable by its title band as well
45
+ * as its border), and `affine-model` cannot reach into `affine-block-surface`
46
+ * where the declaration is resolved. One number, owned by the layer that both
47
+ * the declaration and the hit test can read.
48
+ *
49
+ * It is the board's TOP MARGIN, and it always was — the deep inset the plot
50
+ * gives up so the title has somewhere to be written. Naming it says out loud
51
+ * what was already true, so the strip that is painted, the strip that is picked
52
+ * and the strip that opens the rename are one number rather than three.
53
+ *
54
+ * 56 is what the title has been written in since the pack shipped: a 20-unit
55
+ * title, sitting on a baseline a third of the band up from its foot, with the
56
+ * rest as breathing room above and below. Changing it moves the title, so it is
57
+ * a visual decision and not a hit-test one.
58
+ */
59
+ export const C4_BOARD_TITLE_BAND_HEIGHT = 56;
37
60
  /**
38
61
  * The C4 board: the sheet one diagram of the model is drawn on — a context
39
62
  * diagram, a container diagram, a component diagram. A framework background like
@@ -119,6 +142,51 @@ let C4BoardElementModel = (() => {
119
142
  */
120
143
  get level() { return this.#level_accessor_storage; }
121
144
  set level(value) { this.#level_accessor_storage = value; }
145
+ /**
146
+ * A board is picked by its BORDER — and by its TITLE BAND, the one carve-out
147
+ * this frame keeps (issues #194 / #197).
148
+ *
149
+ * The same carve-out a BPMN pool makes, for the same reason and by the same
150
+ * arithmetic: the band is the only part of the sheet that is the SHEET rather
151
+ * than the diagram drawn on it. The title is written there, nothing is ever
152
+ * dropped there by convention, and the plot below is where the work goes — so
153
+ * a click there belongs to whatever node is under the pointer.
154
+ *
155
+ * Without this the title became unreachable the day backgrounds stopped being
156
+ * picked by their area: the only thing left answering was the view's
157
+ * double-click zone, which is the tight box of the DRAWN glyphs, and a single
158
+ * click on the title selected nothing at all.
159
+ *
160
+ * ## Where the geometry comes from
161
+ *
162
+ * The band is the top margin, full width — one number
163
+ * ({@link C4_BOARD_TITLE_BAND_HEIGHT}), read from here by the framework
164
+ * declaration, so what is painted is what is selectable. There is no
165
+ * zoom-grown target here as there is on a pool: the pool grows its 28-unit
166
+ * strip to stay reachable when zoomed out, which would put the rename zone
167
+ * outside the selectable one. A 56-unit header does not need it, and the
168
+ * promise "what is painted is what is picked" is worth more than the pixels.
169
+ */
170
+ includesPoint(x, y, options) {
171
+ if (super.includesPoint(x, y, options))
172
+ return true;
173
+ const [ex, ey, w, h] = this.deserializedXYWH;
174
+ // Element-local coordinates, undoing the element rotation about its centre
175
+ // — the band is axis-aligned inside the board, not on the canvas.
176
+ let lx = x - ex;
177
+ let ly = y - ey;
178
+ const rotate = this.rotate ?? 0;
179
+ if (rotate) {
180
+ const [ux, uy] = rotatePoint([x, y], [ex + w / 2, ey + h / 2], -rotate);
181
+ lx = ux - ex;
182
+ ly = uy - ey;
183
+ }
184
+ if (lx < 0 || lx > w || ly < 0)
185
+ return false;
186
+ // Clamped to a board shorter than its own header — the degenerate case the
187
+ // renderer clamps too.
188
+ return ly <= Math.min(C4_BOARD_TITLE_BAND_HEIGHT, h);
189
+ }
122
190
  constructor() {
123
191
  super(...arguments);
124
192
  __runInitializers(this, _level_extraInitializers);
@@ -1,16 +1,31 @@
1
1
  import { ShapeElementModel } from '../shape/index.js';
2
- export type WardleyNodeKind = 'component' | 'anchor' | 'pipeline' | 'handle' | 'market' | 'ecosystem' | 'method';
2
+ export type WardleyNodeKind = 'component' | 'anchor' | 'pipeline' | 'handle' | 'market' | 'ecosystem' | 'method' | 'porter' | 'accelerator' | 'decelerator' | 'area';
3
3
  /**
4
4
  * A Wardley map node. Extends {@link ShapeElementModel} (a native ellipse) so it
5
5
  * inherits ALL shape behaviour — editable stroke width / colors, native resize,
6
6
  * center connector anchor, the shape context toolbar — for free. `kind`
7
7
  * discriminates the plain `component`, the `anchor` (which the renderer
8
8
  * decorates with an inscribed person glyph), the two pieces of a pipeline (the
9
- * `pipeline` body + its square `handle`), the `market` outer circle, and the
10
- * `ecosystem` grey backing disk. Composite nodes (pipeline / market / ecosystem)
11
- * are built by grouping several of these + native connectors + a text label. The
12
- * text label is a SEPARATE native text element grouped with the node, not stored
13
- * here.
9
+ * `pipeline` body + its square `handle`), the `market` outer circle, the
10
+ * `ecosystem` grey backing disk, the `porter` circle — a Porter's-forces
11
+ * glyph, which is the one kind that is NOT a link in the value chain: it marks
12
+ * an external competition force acting on the map — and the `accelerator` /
13
+ * `decelerator` arrows, which are not links in the chain either: they annotate
14
+ * the CLIMATE, saying that something is speeding evolution up or slowing it
15
+ * down. Those two are the first kinds drawn as a native POLYGON rather than an
16
+ * ellipse or a rect — and the `area` is the third: a translucent ZONE of the
17
+ * map, drawn as a rect or as a polygon over the components it groups, and the
18
+ * only kind whose two shapes are the same kind (the `shapeType` says which).
19
+ * Composite nodes (pipeline / market / ecosystem / porter / accelerator /
20
+ * decelerator) are built by grouping several of these + native connectors + a
21
+ * text label. The text label is a SEPARATE native text element grouped with the
22
+ * node, not stored here — the `porter` and the `area` being the exceptions that
23
+ * prove it, since the porter's one letter and the area's name are the shape's
24
+ * own inner text.
25
+ *
26
+ * `kind` is ADDITIVE by construction: a value is only ever appended, so a
27
+ * document written before a value was appended carries none of it and opens
28
+ * unchanged. `area` is the most recent, and needs no migration.
14
29
  */
15
30
  export declare class WardleyNodeElementModel extends ShapeElementModel {
16
31
  get type(): string;