@formicoidea/labre-core 0.58.0 → 0.59.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 (167) hide show
  1. package/architecture/code.c4 +94 -65
  2. package/architecture/domain.c4 +11 -5
  3. package/dist/_pkgs/affine-block-attachment/edgeless-clipboard-config.d.ts +3 -2
  4. package/dist/_pkgs/affine-block-attachment/edgeless-clipboard-config.js +18 -13
  5. package/dist/_pkgs/affine-block-callout/adapters/markdown.js +4 -1
  6. package/dist/_pkgs/affine-block-code/adapters/html.js +1 -1
  7. package/dist/_pkgs/affine-block-code/adapters/markdown/preprocessor.js +13 -1
  8. package/dist/_pkgs/affine-block-code/adapters/notion-html.js +1 -1
  9. package/dist/_pkgs/affine-block-code/code-block.d.ts +17 -2
  10. package/dist/_pkgs/affine-block-code/code-block.js +28 -23
  11. package/dist/_pkgs/affine-block-code/code-toolbar/components/lang-button.js +17 -4
  12. package/dist/_pkgs/affine-block-code/code-toolbar/components/preview-button.js +1 -1
  13. package/dist/_pkgs/affine-block-data-view/data-source.js +12 -5
  14. package/dist/_pkgs/affine-block-edgeless-text/edgeless-clipboard-config.d.ts +3 -2
  15. package/dist/_pkgs/affine-block-edgeless-text/edgeless-clipboard-config.js +8 -5
  16. package/dist/_pkgs/affine-block-image/asset-picker.d.ts +23 -2
  17. package/dist/_pkgs/affine-block-image/asset-picker.js +52 -12
  18. package/dist/_pkgs/affine-block-image/configs/replace-action.d.ts +8 -5
  19. package/dist/_pkgs/affine-block-image/configs/replace-action.js +8 -8
  20. package/dist/_pkgs/affine-block-image/configs/toolbar.js +2 -1
  21. package/dist/_pkgs/affine-block-image/crop/crop-controller.d.ts +15 -0
  22. package/dist/_pkgs/affine-block-image/crop/crop-controller.js +40 -2
  23. package/dist/_pkgs/affine-block-image/crop/crop-math.d.ts +22 -0
  24. package/dist/_pkgs/affine-block-image/crop/crop-math.js +55 -2
  25. package/dist/_pkgs/affine-block-image/crop/crop-overlay.d.ts +17 -3
  26. package/dist/_pkgs/affine-block-image/crop/crop-overlay.js +64 -6
  27. package/dist/_pkgs/affine-block-image/edgeless-clipboard-config.d.ts +3 -2
  28. package/dist/_pkgs/affine-block-image/edgeless-clipboard-config.js +20 -14
  29. package/dist/_pkgs/affine-block-image/image-block.js +1 -0
  30. package/dist/_pkgs/affine-block-image/image-edgeless-block.js +3 -0
  31. package/dist/_pkgs/affine-block-image/index.d.ts +2 -0
  32. package/dist/_pkgs/affine-block-image/index.js +2 -0
  33. package/dist/_pkgs/affine-block-image/replace/commands.d.ts +68 -0
  34. package/dist/_pkgs/affine-block-image/replace/commands.js +200 -0
  35. package/dist/_pkgs/affine-block-image/replace/replace-model.d.ts +44 -0
  36. package/dist/_pkgs/affine-block-image/replace/replace-model.js +168 -0
  37. package/dist/_pkgs/affine-block-image/replace/toolbar.d.ts +21 -0
  38. package/dist/_pkgs/affine-block-image/replace/toolbar.js +159 -0
  39. package/dist/_pkgs/affine-block-image/translations.d.ts +15 -2
  40. package/dist/_pkgs/affine-block-image/translations.js +35 -2
  41. package/dist/_pkgs/affine-block-image/utils.d.ts +7 -0
  42. package/dist/_pkgs/affine-block-image/utils.js +13 -0
  43. package/dist/_pkgs/affine-block-image/view.js +7 -0
  44. package/dist/_pkgs/affine-block-note/components/edgeless-page-block-title.d.ts +14 -5
  45. package/dist/_pkgs/affine-block-note/components/edgeless-page-block-title.js +18 -11
  46. package/dist/_pkgs/affine-block-note/config.d.ts +9 -2
  47. package/dist/_pkgs/affine-block-note/edgeless-clipboard-config.d.ts +3 -2
  48. package/dist/_pkgs/affine-block-note/edgeless-clipboard-config.js +8 -5
  49. package/dist/_pkgs/affine-block-paragraph/adapters/markdown.js +11 -0
  50. package/dist/_pkgs/affine-block-root/commands/invisible-characters-commands.d.ts +17 -0
  51. package/dist/_pkgs/affine-block-root/commands/invisible-characters-commands.js +123 -0
  52. package/dist/_pkgs/affine-block-root/edgeless/clipboard/command.js +82 -65
  53. package/dist/_pkgs/affine-block-root/edgeless/edgeless-keyboard.js +2 -0
  54. package/dist/_pkgs/affine-block-root/index.d.ts +1 -0
  55. package/dist/_pkgs/affine-block-root/index.js +1 -0
  56. package/dist/_pkgs/affine-block-root/translations.d.ts +6 -0
  57. package/dist/_pkgs/affine-block-root/translations.js +10 -0
  58. package/dist/_pkgs/affine-block-root/view.js +6 -1
  59. package/dist/_pkgs/affine-block-surface/commands/index.d.ts +1 -1
  60. package/dist/_pkgs/affine-block-surface/commands/index.js +1 -1
  61. package/dist/_pkgs/affine-block-surface/commands/reassociate-connectors.d.ts +9 -0
  62. package/dist/_pkgs/affine-block-surface/commands/reassociate-connectors.js +21 -12
  63. package/dist/_pkgs/affine-block-surface/extensions/clipboard-config.d.ts +33 -0
  64. package/dist/_pkgs/affine-block-surface/extensions/clipboard-config.js +53 -0
  65. package/dist/_pkgs/affine-block-surface/framework-background/background-view.js +2 -1
  66. package/dist/_pkgs/affine-block-surface/framework-background/band-widths.d.ts +104 -0
  67. package/dist/_pkgs/affine-block-surface/framework-background/band-widths.js +156 -0
  68. package/dist/_pkgs/affine-block-surface/framework-background/def.d.ts +87 -4
  69. package/dist/_pkgs/affine-block-surface/framework-background/def.js +26 -7
  70. package/dist/_pkgs/affine-block-surface/framework-background/ellipsis.d.ts +58 -0
  71. package/dist/_pkgs/affine-block-surface/framework-background/ellipsis.js +110 -0
  72. package/dist/_pkgs/affine-block-surface/framework-background/index.d.ts +3 -0
  73. package/dist/_pkgs/affine-block-surface/framework-background/index.js +3 -0
  74. package/dist/_pkgs/affine-block-surface/framework-background/labels.d.ts +12 -2
  75. package/dist/_pkgs/affine-block-surface/framework-background/labels.js +29 -5
  76. package/dist/_pkgs/affine-block-surface/framework-background/measure-text.d.ts +14 -0
  77. package/dist/_pkgs/affine-block-surface/framework-background/measure-text.js +31 -0
  78. package/dist/_pkgs/affine-block-surface/framework-background/renderer.d.ts +14 -1
  79. package/dist/_pkgs/affine-block-surface/framework-background/renderer.js +84 -31
  80. package/dist/_pkgs/affine-block-table/adapters/html.js +26 -14
  81. package/dist/_pkgs/affine-block-table/adapters/utils.d.ts +1 -0
  82. package/dist/_pkgs/affine-block-table/adapters/utils.js +23 -5
  83. package/dist/_pkgs/affine-block-table/header-menu.d.ts +13 -0
  84. package/dist/_pkgs/affine-block-table/header-menu.js +25 -0
  85. package/dist/_pkgs/affine-block-table/selection-controller.d.ts +15 -0
  86. package/dist/_pkgs/affine-block-table/selection-controller.js +62 -0
  87. package/dist/_pkgs/affine-block-table/table-cell-css.d.ts +7 -0
  88. package/dist/_pkgs/affine-block-table/table-cell-css.js +10 -0
  89. package/dist/_pkgs/affine-block-table/table-cell.d.ts +8 -0
  90. package/dist/_pkgs/affine-block-table/table-cell.js +35 -3
  91. package/dist/_pkgs/affine-block-table/table-data-manager.d.ts +14 -0
  92. package/dist/_pkgs/affine-block-table/table-data-manager.js +44 -3
  93. package/dist/_pkgs/affine-block-table/translations.d.ts +14 -0
  94. package/dist/_pkgs/affine-block-table/translations.js +22 -0
  95. package/dist/_pkgs/affine-gfx-mindmap/toolbar/mindmap-tool-button.d.ts +2 -2
  96. package/dist/_pkgs/affine-gfx-template/builtin/other.d.ts +4 -0
  97. package/dist/_pkgs/affine-gfx-template/builtin/other.js +1 -1
  98. package/dist/_pkgs/affine-gfx-template/builtin/slides/build.d.ts +81 -0
  99. package/dist/_pkgs/affine-gfx-template/builtin/slides/build.js +276 -0
  100. package/dist/_pkgs/affine-gfx-template/builtin/slides/consts.d.ts +126 -0
  101. package/dist/_pkgs/affine-gfx-template/builtin/slides/consts.js +101 -0
  102. package/dist/_pkgs/affine-gfx-template/builtin/slides/index.d.ts +8 -0
  103. package/dist/_pkgs/affine-gfx-template/builtin/slides/index.js +27 -0
  104. package/dist/_pkgs/affine-gfx-template/builtin/slides/masks.d.ts +83 -0
  105. package/dist/_pkgs/affine-gfx-template/builtin/slides/masks.js +674 -0
  106. package/dist/_pkgs/affine-gfx-template/contribute.js +12 -17
  107. package/dist/_pkgs/affine-gfx-template/make-snapshot.d.ts +14 -2
  108. package/dist/_pkgs/affine-gfx-template/make-snapshot.js +6 -2
  109. package/dist/_pkgs/affine-gfx-template/services/template-middlewares.d.ts +12 -0
  110. package/dist/_pkgs/affine-gfx-template/services/template-middlewares.js +48 -0
  111. package/dist/_pkgs/affine-gfx-template/services/template.js +2 -1
  112. package/dist/_pkgs/affine-gfx-template/toolbar/builtin-templates.d.ts +15 -1
  113. package/dist/_pkgs/affine-gfx-template/toolbar/builtin-templates.js +33 -37
  114. package/dist/_pkgs/affine-gfx-template/toolbar/template-type.d.ts +6 -0
  115. package/dist/_pkgs/affine-gfx-template/translations.d.ts +94 -0
  116. package/dist/_pkgs/affine-gfx-template/translations.js +441 -2
  117. package/dist/_pkgs/affine-model/blocks/table/table-model.d.ts +4 -0
  118. package/dist/_pkgs/affine-model/blocks/table/table-model.js +4 -0
  119. package/dist/_pkgs/affine-model/elements/bpmn/node.d.ts +46 -3
  120. package/dist/_pkgs/affine-model/elements/bpmn/node.js +47 -4
  121. package/dist/_pkgs/affine-model/elements/bpmn/pool.d.ts +20 -0
  122. package/dist/_pkgs/affine-model/elements/bpmn/pool.js +33 -4
  123. package/dist/_pkgs/affine-shared/services/edit-props-store.d.ts +3 -16
  124. package/dist/_pkgs/affine-shared/services/edit-props-store.js +16 -16
  125. package/dist/_pkgs/affine-shared/services/editor-setting-service.d.ts +3 -0
  126. package/dist/_pkgs/affine-shared/services/editor-setting-service.js +4 -0
  127. package/dist/_pkgs/affine-shared/services/index.d.ts +1 -0
  128. package/dist/_pkgs/affine-shared/services/index.js +1 -0
  129. package/dist/_pkgs/affine-shared/services/invisible-characters.d.ts +33 -0
  130. package/dist/_pkgs/affine-shared/services/invisible-characters.js +63 -0
  131. package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-cache.d.ts +0 -9
  132. package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-cache.js +3 -9
  133. package/dist/_pkgs/affine-shared/services/telemetry-service/lifecycle.d.ts +26 -0
  134. package/dist/_pkgs/affine-shared/services/telemetry-service/telemetry-service.d.ts +4 -3
  135. package/dist/_pkgs/affine-shared/services/telemetry-service/types.d.ts +11 -0
  136. package/dist/_pkgs/affine-shared/utils/index.d.ts +1 -0
  137. package/dist/_pkgs/affine-shared/utils/index.js +1 -0
  138. package/dist/_pkgs/affine-shared/utils/invisible-characters.d.ts +42 -0
  139. package/dist/_pkgs/affine-shared/utils/invisible-characters.js +81 -0
  140. package/dist/_pkgs/affine-widget-edgeless-zoom-toolbar/zoom-toolbar.js +13 -2
  141. package/dist/_pkgs/data-view/view-presets/table/pc/header/column-header.js +6 -0
  142. package/dist/_pkgs/data-view/view-presets/table/pc/table-view-ui-logic.js +5 -2
  143. package/dist/_pkgs/std/clipboard/clipboard.d.ts +15 -1
  144. package/dist/_pkgs/std/clipboard/clipboard.js +18 -0
  145. package/dist/_pkgs/std/inline/components/embed-gap.js +1 -1
  146. package/dist/_pkgs/std/inline/components/v-line.js +1 -1
  147. package/dist/_pkgs/std/inline/components/v-text.d.ts +15 -2
  148. package/dist/_pkgs/std/inline/components/v-text.js +147 -15
  149. package/dist/_pkgs/std/inline/consts.d.ts +5 -0
  150. package/dist/_pkgs/std/inline/consts.js +5 -0
  151. package/dist/_pkgs/std/inline/utils/index.d.ts +1 -0
  152. package/dist/_pkgs/std/inline/utils/index.js +1 -0
  153. package/dist/_pkgs/std/inline/utils/invisible-characters.d.ts +57 -0
  154. package/dist/_pkgs/std/inline/utils/invisible-characters.js +162 -0
  155. package/dist/_pkgs/std/inline/utils/point-conversion.js +3 -3
  156. package/dist/_pkgs/std/inline/utils/text.d.ts +8 -0
  157. package/dist/_pkgs/std/inline/utils/text.js +11 -2
  158. package/dist/_pkgs/store/model/store/crud.d.ts +28 -0
  159. package/dist/_pkgs/store/model/store/crud.js +97 -30
  160. package/dist/_pkgs/store/model/store/query.js +3 -2
  161. package/dist/_pkgs/store/model/store/store.d.ts +21 -0
  162. package/dist/_pkgs/store/model/store/store.js +88 -21
  163. package/dist/_pkgs/store/transformer/transformer.d.ts +46 -0
  164. package/dist/_pkgs/store/transformer/transformer.js +72 -0
  165. package/dist/commands.js +6 -1
  166. package/dist/translations.js +2 -0
  167. package/package.json +3 -4
@@ -0,0 +1,200 @@
1
+ import { ImageBlockModel, ShapeType } from '../../affine-model/index.js';
2
+ import { TelemetryProvider } from '../../affine-shared/services/index.js';
3
+ import { GfxControllerIdentifier } from '../../std/gfx/index.js';
4
+ import { z } from 'zod';
5
+ import { pickImageSource, replaceImageSource } from '../asset-picker.js';
6
+ import { IMAGE_REPLACE_PICTURE_DESCRIPTION, IMAGE_REPLACE_WITH_IMAGE, IMAGE_REPLACE_WITH_IMAGE_DESCRIPTION, IMAGE_REPLACE_WITH_SHAPE, IMAGE_REPLACE_WITH_SHAPE_DESCRIPTION, } from '../translations.js';
7
+ import { canReplaceImageWithShape, canReplaceShapeWithImage, replaceImageWithShape, replaceShapeWithImage, } from './replace-model.js';
8
+ /**
9
+ * The replace commands (ADR 0044), `owner: 'core'`: a shape and an image
10
+ * belong to no framework. Registered from the image view extension, so they
11
+ * go with the `image` flag; the toolbar entries invoke them through
12
+ * `runCommand`, so the refusal, the one undo step and the telemetry live
13
+ * here, once.
14
+ *
15
+ * They EMIT FROM THEIR BODY (listed in `registry.unit.spec.ts`'s
16
+ * self-emitting commands): `CommandDescriptor.telemetry` maps onto the
17
+ * framework creation events, and whether anything was replaced is a fact of
18
+ * the invocation — a cancelled file chooser replaces nothing.
19
+ */
20
+ export const replaceElementParams = z.object({
21
+ // The element to replace; absent, the single selected one.
22
+ elementId: z.string().min(1).optional(),
23
+ });
24
+ /**
25
+ * « Replace with shape » takes the shape picked in its sub-menu (ADR 0044,
26
+ * amendment of 2026-10-27). Both optional: absent, a plain rectangle, so the
27
+ * palette and the agent keep the v1 behaviour.
28
+ */
29
+ export const replaceWithShapeParams = replaceElementParams.extend({
30
+ shapeType: z.nativeEnum(ShapeType).optional(),
31
+ // The corner radius as a fraction of the box: 0.1 is the rounded rectangle.
32
+ radius: z.number().min(0).max(0.5).optional(),
33
+ });
34
+ function target(std, params) {
35
+ const parsed = replaceElementParams.safeParse(params ?? {});
36
+ if (!parsed.success)
37
+ return null;
38
+ const gfx = std.get(GfxControllerIdentifier);
39
+ const { elementId } = parsed.data;
40
+ if (elementId !== undefined)
41
+ return gfx.getElementById(elementId);
42
+ const selected = gfx.selection.selectedElements;
43
+ return selected.length === 1 ? selected[0] : null;
44
+ }
45
+ /** The shape the command would replace, or `null`. */
46
+ export function replaceableShape(std, params) {
47
+ const model = target(std, params);
48
+ return canReplaceShapeWithImage(std, model) ? model : null;
49
+ }
50
+ /** The canvas image the command would replace, or `null`. */
51
+ export function replaceableImage(std, params) {
52
+ const model = target(std, params);
53
+ return model instanceof ImageBlockModel &&
54
+ canReplaceImageWithShape(std, model)
55
+ ? model
56
+ : null;
57
+ }
58
+ /**
59
+ * The canvas image whose PICTURE the command would swap, or `null`. Looser
60
+ * than {@link replaceableImage}: the block stays (same id, caption and
61
+ * comments kept), so a captioned image may change its picture; a locked one
62
+ * may not.
63
+ */
64
+ export function replaceablePicture(std, params) {
65
+ const model = target(std, params);
66
+ if (std.store.readonly || !(model instanceof ImageBlockModel))
67
+ return null;
68
+ if (model.parent?.flavour !== 'affine:surface')
69
+ return null;
70
+ return model.isLocked() ? null : model;
71
+ }
72
+ function track(std, invocation, from, to) {
73
+ std.getOptional(TelemetryProvider)?.track('ElementReplaced', {
74
+ page: 'whiteboard editor',
75
+ from,
76
+ to,
77
+ control: invocation.source,
78
+ module: invocation.surface,
79
+ });
80
+ }
81
+ function select(std, id) {
82
+ std.get(GfxControllerIdentifier).selection.set({
83
+ elements: [id],
84
+ editing: false,
85
+ });
86
+ }
87
+ export const REPLACE_SHAPE_WITH_IMAGE = 'canvas.shape.replaceWithImage';
88
+ export const REPLACE_IMAGE_WITH_SHAPE = 'canvas.image.replaceWithShape';
89
+ export const REPLACE_IMAGE_WITH_IMAGE = 'canvas.image.replaceWithImage';
90
+ export const replaceShapeWithImageCommand = {
91
+ id: REPLACE_SHAPE_WITH_IMAGE,
92
+ owner: 'core',
93
+ kind: 'action',
94
+ labelKey: IMAGE_REPLACE_WITH_IMAGE[0],
95
+ labelFallback: IMAGE_REPLACE_WITH_IMAGE[1],
96
+ descriptionKey: IMAGE_REPLACE_WITH_IMAGE_DESCRIPTION[0],
97
+ descriptionFallback: IMAGE_REPLACE_WITH_IMAGE_DESCRIPTION[1],
98
+ keywords: ['replace', 'image', 'picture', 'shape'],
99
+ // Not `agent`: the picture comes from a dialog a person answers.
100
+ surfaces: ['contextual-toolbar', 'palette'],
101
+ scope: 'edgeless',
102
+ defaultKeys: { mac: [], other: [] },
103
+ availability: 'editable',
104
+ when: std => replaceableShape(std) !== null,
105
+ params: replaceElementParams,
106
+ run: async (std, invocation, params) => {
107
+ if (!replaceableShape(std, params))
108
+ return;
109
+ const image = await pickImageSource(std);
110
+ if (!image)
111
+ return;
112
+ // Re-read: the dialog took time, the shape may be gone, locked or the
113
+ // document read-only by now.
114
+ const shape = replaceableShape(std, params);
115
+ if (!shape)
116
+ return;
117
+ const id = replaceShapeWithImage(std, shape, image);
118
+ if (!id)
119
+ return;
120
+ select(std, id);
121
+ std.getOptional(TelemetryProvider)?.track('BlockCreated', {
122
+ page: 'whiteboard editor',
123
+ blockType: 'affine:image',
124
+ control: invocation.source,
125
+ module: invocation.surface,
126
+ });
127
+ track(std, invocation, 'shape', 'image');
128
+ },
129
+ };
130
+ export const replaceImageWithShapeCommand = {
131
+ id: REPLACE_IMAGE_WITH_SHAPE,
132
+ owner: 'core',
133
+ kind: 'action',
134
+ labelKey: IMAGE_REPLACE_WITH_SHAPE[0],
135
+ labelFallback: IMAGE_REPLACE_WITH_SHAPE[1],
136
+ descriptionKey: IMAGE_REPLACE_WITH_SHAPE_DESCRIPTION[0],
137
+ descriptionFallback: IMAGE_REPLACE_WITH_SHAPE_DESCRIPTION[1],
138
+ keywords: ['replace', 'shape', 'rectangle', 'placeholder', 'image'],
139
+ surfaces: ['contextual-toolbar', 'palette', 'agent'],
140
+ scope: 'edgeless',
141
+ defaultKeys: { mac: [], other: [] },
142
+ availability: 'editable',
143
+ when: std => replaceableImage(std) !== null,
144
+ params: replaceWithShapeParams,
145
+ run: (std, invocation, params) => {
146
+ const image = replaceableImage(std, params);
147
+ if (!image)
148
+ return;
149
+ const parsed = replaceWithShapeParams.safeParse(params ?? {});
150
+ if (!parsed.success)
151
+ return;
152
+ const { shapeType, radius } = parsed.data;
153
+ const id = replaceImageWithShape(std, image, { shapeType, radius });
154
+ if (!id)
155
+ return;
156
+ select(std, id);
157
+ track(std, invocation, 'image', 'shape');
158
+ },
159
+ };
160
+ /**
161
+ * « Replace with image » on a canvas IMAGE (ADR 0044, amendment of
162
+ * 2026-10-27): another picture in the same block, from the host's library
163
+ * when there is one, from the system file chooser otherwise — so the entry
164
+ * exists without a host. The block keeps its id, corner, width and caption
165
+ * (`replaceImageSource`); it reports `ElementReplaced { image → image }`.
166
+ */
167
+ export const replaceImageWithImageCommand = {
168
+ id: REPLACE_IMAGE_WITH_IMAGE,
169
+ owner: 'core',
170
+ kind: 'action',
171
+ labelKey: IMAGE_REPLACE_WITH_IMAGE[0],
172
+ labelFallback: IMAGE_REPLACE_WITH_IMAGE[1],
173
+ descriptionKey: IMAGE_REPLACE_PICTURE_DESCRIPTION[0],
174
+ descriptionFallback: IMAGE_REPLACE_PICTURE_DESCRIPTION[1],
175
+ keywords: ['replace', 'image', 'picture', 'library', 'swap'],
176
+ // Not `agent`: the picture comes from a dialog a person answers.
177
+ surfaces: ['contextual-toolbar', 'palette'],
178
+ scope: 'edgeless',
179
+ defaultKeys: { mac: [], other: [] },
180
+ availability: 'editable',
181
+ when: std => replaceablePicture(std) !== null,
182
+ params: replaceElementParams,
183
+ run: async (std, invocation, params) => {
184
+ if (!replaceablePicture(std, params))
185
+ return;
186
+ const source = await pickImageSource(std);
187
+ if (!source)
188
+ return;
189
+ // Re-read after the dialog, like the shape's.
190
+ const image = replaceablePicture(std, params);
191
+ if (!image || !replaceImageSource(image, source))
192
+ return;
193
+ track(std, invocation, 'image', 'image');
194
+ },
195
+ };
196
+ export const imageReplaceCommands = [
197
+ replaceShapeWithImageCommand,
198
+ replaceImageWithShapeCommand,
199
+ replaceImageWithImageCommand,
200
+ ];
@@ -0,0 +1,44 @@
1
+ import { type ImageBlockModel, type ImageBlockProps, ShapeElementModel, ShapeType } from '../../affine-model/index.js';
2
+ import type { BlockStdScope } from '../../std/index.js';
3
+ import type { ImageBlockSource } from '../utils.js';
4
+ /**
5
+ * Whether a shape may be replaced by an image. Refused: a read-only store, a
6
+ * locked shape, a NOTATION shape (it carries a role its framework reads), a
7
+ * shape with text (v1 never drops what the author typed) and a mind-map node
8
+ * (the mind map owns its tree).
9
+ */
10
+ export declare function canReplaceShapeWithImage(std: BlockStdScope, shape: unknown): shape is ShapeElementModel;
11
+ /**
12
+ * Whether a canvas image may be replaced by a shape. Refused: a read-only
13
+ * store, an image that is not on the canvas, a locked image, and one with a
14
+ * caption or comments (v1 never drops what the author wrote).
15
+ */
16
+ export declare function canReplaceImageWithShape(std: BlockStdScope, image: ImageBlockModel | null | undefined): image is ImageBlockModel;
17
+ /**
18
+ * The crop that cuts `image` to the proportions of `box`, as stored props:
19
+ * nothing when the picture already has them (the whole image is the absent
20
+ * key, ADR 0037).
21
+ */
22
+ export declare function cropToBox(box: {
23
+ w: number;
24
+ h: number;
25
+ }, image: Pick<ImageBlockSource, 'width' | 'height'>): Pick<ImageBlockProps, 'crop'>;
26
+ /**
27
+ * Put `image` in place of `shape`, cut to the shape's box. Answers the new
28
+ * image block's id, or `null` when the gesture is refused (nothing written).
29
+ */
30
+ export declare function replaceShapeWithImage(std: BlockStdScope, shape: ShapeElementModel, image: ImageBlockSource): string | null;
31
+ /** Which shape takes the image's place: a rectangle unless told otherwise. */
32
+ export type ReplacementShape = {
33
+ shapeType?: ShapeType;
34
+ /** The corner radius, a fraction of the box (the rounded rectangle's 0.1). */
35
+ radius?: number;
36
+ };
37
+ /**
38
+ * Put a shape in place of `image` — a rectangle by default, or the
39
+ * `shapeType` and `radius` picked in « Replace with shape » (ADR 0044,
40
+ * amendment of 2026-10-27) — at its box, rotation, depth and layer.
41
+ * The blob stays in the asset store (undo brings the image back, and other
42
+ * pages may show it). Answers the new shape's id, or `null` when refused.
43
+ */
44
+ export declare function replaceImageWithShape(std: BlockStdScope, image: ImageBlockModel, shape?: ReplacementShape): string | null;
@@ -0,0 +1,168 @@
1
+ import { applyCreationLayer, reassociateConnectors, sourceLayerOf, } from '../../affine-block-surface/index.js';
2
+ import { ImageBlockSchema, isWholeImage, MindmapElementModel, ShapeElementModel, ShapeType, } from '../../affine-model/index.js';
3
+ import { Bound } from '../../global/gfx/index.js';
4
+ import { GfxControllerIdentifier } from '../../std/gfx/index.js';
5
+ import { ratioCrop } from '../crop/crop-math.js';
6
+ /**
7
+ * « Replace with image » and « Replace with shape » (ADR 0044): a model of
8
+ * the other kind at the old one's place, then the old one deleted. A shape is
9
+ * a surface ELEMENT and an image a BLOCK, so this is not a morph: the id
10
+ * changes, on purpose.
11
+ *
12
+ * What crosses over, in ONE undo step (one `captureSync`): `xywh`,
13
+ * `rotate`, `index`, the user layer (`sourceLayerOf`, ADR 0031 §6), every
14
+ * connector end bound to the old id, and the old model's place in its group
15
+ * or frame. Nothing is written when the gesture is refused, so a refusal
16
+ * never leaves an empty undo step.
17
+ */
18
+ function surfaceOf(std) {
19
+ return (std.get(GfxControllerIdentifier).surface ??
20
+ null);
21
+ }
22
+ /**
23
+ * Whether a shape may be replaced by an image. Refused: a read-only store, a
24
+ * locked shape, a NOTATION shape (it carries a role its framework reads), a
25
+ * shape with text (v1 never drops what the author typed) and a mind-map node
26
+ * (the mind map owns its tree).
27
+ */
28
+ export function canReplaceShapeWithImage(std, shape) {
29
+ if (std.store.readonly)
30
+ return false;
31
+ if (!(shape instanceof ShapeElementModel))
32
+ return false;
33
+ if (shape.isLocked())
34
+ return false;
35
+ if (shape.role !== undefined)
36
+ return false;
37
+ if ((shape.text?.length ?? 0) > 0)
38
+ return false;
39
+ if (shape.group instanceof MindmapElementModel)
40
+ return false;
41
+ return true;
42
+ }
43
+ /**
44
+ * Whether a canvas image may be replaced by a shape. Refused: a read-only
45
+ * store, an image that is not on the canvas, a locked image, and one with a
46
+ * caption or comments (v1 never drops what the author wrote).
47
+ */
48
+ export function canReplaceImageWithShape(std, image) {
49
+ if (std.store.readonly || !image)
50
+ return false;
51
+ if (image.flavour !== ImageBlockSchema.model.flavour)
52
+ return false;
53
+ if (image.parent?.flavour !== 'affine:surface')
54
+ return false;
55
+ if (image.isLocked())
56
+ return false;
57
+ if (image.props.caption)
58
+ return false;
59
+ if (Object.keys(image.props.comments ?? {}).length > 0)
60
+ return false;
61
+ return true;
62
+ }
63
+ /**
64
+ * Move `oldModel`'s connectors and container membership onto `newModel`,
65
+ * then delete `oldModel`. Runs inside the caller's transaction. The new
66
+ * model joins the container BEFORE the old one leaves it, so a group is never
67
+ * seen empty.
68
+ */
69
+ function handOver(std, surface, oldModel, newModel) {
70
+ reassociateConnectors(surface, oldModel.id, newModel.id);
71
+ const container = oldModel.group;
72
+ if (container) {
73
+ container.addChild(newModel);
74
+ // oxlint-disable-next-line unicorn/prefer-dom-node-remove
75
+ container.removeChild(oldModel);
76
+ }
77
+ if (surface.hasElementById(oldModel.id)) {
78
+ surface.deleteElement(oldModel.id);
79
+ }
80
+ else {
81
+ std.store.deleteBlock(oldModel);
82
+ }
83
+ }
84
+ /**
85
+ * The crop that cuts `image` to the proportions of `box`, as stored props:
86
+ * nothing when the picture already has them (the whole image is the absent
87
+ * key, ADR 0037).
88
+ */
89
+ export function cropToBox(box, image) {
90
+ if (!(box.w > 0 && box.h > 0))
91
+ return {};
92
+ const rect = ratioCrop(box.w / box.h, image.width, image.height);
93
+ return isWholeImage(rect) ? {} : { crop: rect };
94
+ }
95
+ /**
96
+ * Put `image` in place of `shape`, cut to the shape's box. Answers the new
97
+ * image block's id, or `null` when the gesture is refused (nothing written).
98
+ */
99
+ export function replaceShapeWithImage(std, shape, image) {
100
+ const surface = surfaceOf(std);
101
+ if (!surface || !canReplaceShapeWithImage(std, shape))
102
+ return null;
103
+ if (!surface.hasElementById(shape.id))
104
+ return null;
105
+ if (!image.sourceId || !(image.width > 0 && image.height > 0))
106
+ return null;
107
+ const bound = Bound.deserialize(shape.xywh);
108
+ const props = {
109
+ sourceId: image.sourceId,
110
+ size: image.size,
111
+ // On the canvas `width`/`height` are the WHOLE picture's natural size
112
+ // and `xywh` the visible box (ADR 0037).
113
+ width: image.width,
114
+ height: image.height,
115
+ xywh: shape.xywh,
116
+ rotate: shape.rotate ?? 0,
117
+ index: shape.index,
118
+ layer: sourceLayerOf(shape),
119
+ ...cropToBox(bound, image),
120
+ };
121
+ applyCreationLayer(std, props);
122
+ const { store } = std;
123
+ store.captureSync();
124
+ // The block is created OUTSIDE the hand-over's transaction: a block added
125
+ // inside one has no model until it ends, and the container needs the
126
+ // model. Both writes land in the undo step `captureSync` just opened.
127
+ const id = store.addBlock(ImageBlockSchema.model.flavour, props, surface);
128
+ const created = store.getModelById(id);
129
+ if (created)
130
+ store.transact(() => handOver(std, surface, shape, created));
131
+ return id;
132
+ }
133
+ /**
134
+ * Put a shape in place of `image` — a rectangle by default, or the
135
+ * `shapeType` and `radius` picked in « Replace with shape » (ADR 0044,
136
+ * amendment of 2026-10-27) — at its box, rotation, depth and layer.
137
+ * The blob stays in the asset store (undo brings the image back, and other
138
+ * pages may show it). Answers the new shape's id, or `null` when refused.
139
+ */
140
+ export function replaceImageWithShape(std, image, shape = {}) {
141
+ const surface = surfaceOf(std);
142
+ if (!surface || !canReplaceImageWithShape(std, image))
143
+ return null;
144
+ if (!std.store.getBlock(image.id))
145
+ return null;
146
+ const props = {
147
+ type: 'shape',
148
+ shapeType: shape.shapeType ?? ShapeType.Rect,
149
+ radius: shape.radius ?? 0,
150
+ xywh: image.xywh,
151
+ rotate: image.props.rotate ?? 0,
152
+ index: image.index,
153
+ layer: sourceLayerOf(image),
154
+ };
155
+ // The surface's `beforeAdd` middleware stamps the layer too; resolving it
156
+ // here keeps the rule when no middleware is mounted.
157
+ applyCreationLayer(std, props);
158
+ const { store } = std;
159
+ let id = null;
160
+ store.captureSync();
161
+ store.transact(() => {
162
+ id = surface.addElement(props);
163
+ const created = surface.getElementById(id);
164
+ if (created)
165
+ handOver(std, surface, image, created);
166
+ });
167
+ return id;
168
+ }
@@ -0,0 +1,21 @@
1
+ import { type ToolbarAction } from '../../affine-shared/services/index.js';
2
+ /**
3
+ * « Replace » on a canvas image's toolbar (ADR 0044, amendment of
4
+ * 2026-10-27): ONE menu where there were two buttons, because the product
5
+ * owner wants the choice in one place — « Replace with image » (another
6
+ * picture, from the host's library or the system file chooser,
7
+ * `canvas.image.replaceWithImage`) and « Replace with shape », a sub-menu of
8
+ * the shape tool's shapes (`canvas.image.replaceWithShape` with the picked
9
+ * `shapeType` and `radius`). It keeps the id `a.replace`, so it sits where the
10
+ * old « Replace » did. Each entry shows only when its command would act.
11
+ */
12
+ export declare const imageReplaceMenuToolbarAction: ToolbarAction;
13
+ /**
14
+ * « Replace with image » on the SHAPE toolbar, registered from this package:
15
+ * `gfx/shape` never imports the image block. The bare
16
+ * `affine:surface:shape` key belongs to `shapeToolbarExtension`, so this
17
+ * second module carries an owner (`toolbarModuleKey`) and the registry hands
18
+ * it to the same row. Replacing a shape with ANOTHER shape is the shape
19
+ * toolbar's own « Switch shape type », not repeated here.
20
+ */
21
+ export declare const shapeReplaceWithImageToolbarExtension: import("../../store/index.js").ExtensionType;
@@ -0,0 +1,159 @@
1
+ import { ShapeComponentConfig } from '../../affine-gfx-shape/index.js';
2
+ import { EditorChevronDown, renderPickerMenuItems, } from '../../affine-components/toolbar/index.js';
3
+ import { getShapeRadius, getShapeType, ShapeElementModel, } from '../../affine-model/index.js';
4
+ import { ToolbarModuleExtension, toolbarModuleKey, translateKey, } from '../../affine-shared/services/index.js';
5
+ import { ArrowRightSmallIcon, ImageIcon, ReplaceIcon, ShapeIcon, } from '@blocksuite/icons/lit';
6
+ import { BlockFlavourIdentifier, runCommand } from '../../std/index.js';
7
+ import { html, nothing } from 'lit';
8
+ import { createRef, ref } from 'lit/directives/ref.js';
9
+ import { IMAGE_REPLACE_WITH_IMAGE, IMAGE_REPLACE_WITH_SHAPE, IMAGE_TOOLBAR_REPLACE, } from '../translations.js';
10
+ import { replaceableImage, replaceablePicture, replaceableShape, replaceImageWithImageCommand, replaceImageWithShapeCommand, replaceShapeWithImageCommand, } from './commands.js';
11
+ const FROM_TOOLBAR = {
12
+ surface: 'contextual-toolbar',
13
+ source: 'toolbar:general',
14
+ };
15
+ /**
16
+ * A click inside the shape sub-menu must not reach `document`: every
17
+ * `editor-menu-button` closes on a click outside its own trigger
18
+ * (`listenClickAway`), and the sub-menu's trigger and row are outside the
19
+ * Replace menu's trigger, so the outer menu would close — and take the
20
+ * sub-menu, which it contains, with it — on the very click that opens it.
21
+ */
22
+ const keepOpen = (event) => event.stopPropagation();
23
+ /**
24
+ * Beside the menu, as a sub-menu opens. One object, not a literal per render:
25
+ * `editor-menu-button` rebuilds its popper whenever `popperOptions` changes
26
+ * identity, which would drop an open sub-menu on any toolbar re-render.
27
+ */
28
+ const SUBMENU_POPPER = {
29
+ allowedPlacements: ['right-start', 'left-start'],
30
+ mainAxis: 8,
31
+ };
32
+ function renderShapeSubmenu(ctx, outer) {
33
+ const label = translateKey(ctx.std, ...IMAGE_REPLACE_WITH_SHAPE);
34
+ const onPick = (name) => {
35
+ if (!name)
36
+ return;
37
+ outer.value?.hide();
38
+ runCommand(ctx.std, replaceImageWithShapeCommand, FROM_TOOLBAR, {
39
+ shapeType: getShapeType(name),
40
+ radius: getShapeRadius(name),
41
+ });
42
+ };
43
+ // The shape tool's own list — names, icons and wordings — so the sub-menu
44
+ // offers exactly the shapes a user can draw, in the same order.
45
+ const items = ShapeComponentConfig.map(item => ({
46
+ key: translateKey(ctx.std, ...item.tooltipWording),
47
+ value: item.name,
48
+ icon: item.generalIcon,
49
+ disabled: item.disabled,
50
+ }));
51
+ return html `
52
+ <div @click=${keepOpen}>
53
+ <editor-menu-button
54
+ data-testid="image-replace-shape"
55
+ .contentPadding=${'4px 6px'}
56
+ .popperOptions=${SUBMENU_POPPER}
57
+ .button=${html `
58
+ <editor-icon-button
59
+ aria-label=${label}
60
+ data-testid="image-replace-shape-button"
61
+ .justify=${'space-between'}
62
+ .iconContainerPadding=${[4, 8]}
63
+ .iconContainerWidth=${'100%'}
64
+ >
65
+ ${ShapeIcon()}
66
+ <span class="label">${label}</span>
67
+ ${ArrowRightSmallIcon()}
68
+ </editor-icon-button>
69
+ `}
70
+ >
71
+ <div>
72
+ ${renderPickerMenuItems(items, null, onPick, 'image-replace-shape')}
73
+ </div>
74
+ </editor-menu-button>
75
+ </div>
76
+ `;
77
+ }
78
+ /**
79
+ * « Replace » on a canvas image's toolbar (ADR 0044, amendment of
80
+ * 2026-10-27): ONE menu where there were two buttons, because the product
81
+ * owner wants the choice in one place — « Replace with image » (another
82
+ * picture, from the host's library or the system file chooser,
83
+ * `canvas.image.replaceWithImage`) and « Replace with shape », a sub-menu of
84
+ * the shape tool's shapes (`canvas.image.replaceWithShape` with the picked
85
+ * `shapeType` and `radius`). It keeps the id `a.replace`, so it sits where the
86
+ * old « Replace » did. Each entry shows only when its command would act.
87
+ */
88
+ export const imageReplaceMenuToolbarAction = {
89
+ id: 'a.replace',
90
+ when: ctx => !ctx.readonly &&
91
+ (replaceablePicture(ctx.std) !== null ||
92
+ replaceableImage(ctx.std) !== null),
93
+ content(ctx) {
94
+ const picture = replaceablePicture(ctx.std) !== null;
95
+ const toShape = replaceableImage(ctx.std) !== null;
96
+ if (!picture && !toShape)
97
+ return null;
98
+ const label = translateKey(ctx.std, ...IMAGE_TOOLBAR_REPLACE);
99
+ const withImage = translateKey(ctx.std, ...IMAGE_REPLACE_WITH_IMAGE);
100
+ const outer = createRef();
101
+ return html `
102
+ <editor-menu-button
103
+ ${ref(outer)}
104
+ data-testid="image-replace-menu"
105
+ .contentPadding=${'8px'}
106
+ .button=${html `
107
+ <editor-icon-button
108
+ aria-label=${label}
109
+ data-testid="image-replace-button"
110
+ .tooltip=${label}
111
+ >
112
+ ${ReplaceIcon()} ${EditorChevronDown}
113
+ </editor-icon-button>
114
+ `}
115
+ >
116
+ <div data-size="large" data-orientation="vertical">
117
+ ${picture
118
+ ? html `
119
+ <editor-menu-action
120
+ aria-label=${withImage}
121
+ data-testid="image-replace-with-image"
122
+ @click=${() => runCommand(ctx.std, replaceImageWithImageCommand, FROM_TOOLBAR)}
123
+ >
124
+ ${ImageIcon()}
125
+ <span class="label">${withImage}</span>
126
+ </editor-menu-action>
127
+ `
128
+ : nothing}
129
+ ${toShape ? renderShapeSubmenu(ctx, outer) : nothing}
130
+ </div>
131
+ </editor-menu-button>
132
+ `;
133
+ },
134
+ };
135
+ const shapeReplaceToolbarConfig = {
136
+ actions: [
137
+ {
138
+ id: 'h.replace-with-image',
139
+ tooltip: IMAGE_REPLACE_WITH_IMAGE[1],
140
+ tooltipWording: IMAGE_REPLACE_WITH_IMAGE,
141
+ icon: ImageIcon(),
142
+ when: ctx => !ctx.readonly && replaceableShape(ctx.std) !== null,
143
+ run: ctx => runCommand(ctx.std, replaceShapeWithImageCommand, FROM_TOOLBAR),
144
+ },
145
+ ],
146
+ when: ctx => ctx.getSurfaceModelsByType(ShapeElementModel).length === 1,
147
+ };
148
+ /**
149
+ * « Replace with image » on the SHAPE toolbar, registered from this package:
150
+ * `gfx/shape` never imports the image block. The bare
151
+ * `affine:surface:shape` key belongs to `shapeToolbarExtension`, so this
152
+ * second module carries an owner (`toolbarModuleKey`) and the registry hands
153
+ * it to the same row. Replacing a shape with ANOTHER shape is the shape
154
+ * toolbar's own « Switch shape type », not repeated here.
155
+ */
156
+ export const shapeReplaceWithImageToolbarExtension = ToolbarModuleExtension({
157
+ id: BlockFlavourIdentifier(toolbarModuleKey('affine:surface:shape', 'image-replace')),
158
+ config: shapeReplaceToolbarConfig,
159
+ });
@@ -36,10 +36,23 @@ export declare const IMAGE_TOAST_DOWNLOADING: ChromeWording;
36
36
  export declare const IMAGE_TOAST_COPIED: ChromeWording;
37
37
  export declare const IMAGE_TOAST_READ_SIZE_FAILED: ChromeWording;
38
38
  /**
39
- * The image toolbar's "Replace" (`configs/replace-action.ts`), offered only
40
- * with a host asset dialog (`AssetPickerProvider`).
39
+ * The image toolbar's "Replace": the page button (`configs/replace-action.ts`)
40
+ * and the canvas menu that holds « Replace with image » and « Replace with
41
+ * shape » (`replace/toolbar.ts`, ADR 0044 amendment of 2026-10-27). Offered
42
+ * with or without a host: without one, the system file chooser.
41
43
  */
42
44
  export declare const IMAGE_TOOLBAR_REPLACE: ChromeWording;
45
+ /**
46
+ * « Replace with image » on a shape and « Replace with shape » on a canvas
47
+ * image (ADR 0044): the toolbar entries and the commands they run share these
48
+ * keys, so the palette and the toolbar say the same words.
49
+ */
50
+ export declare const IMAGE_REPLACE_WITH_IMAGE: ChromeWording;
51
+ export declare const IMAGE_REPLACE_WITH_IMAGE_DESCRIPTION: ChromeWording;
52
+ /** `canvas.image.replaceWithImage`: the same label, said of an image. */
53
+ export declare const IMAGE_REPLACE_PICTURE_DESCRIPTION: ChromeWording;
54
+ export declare const IMAGE_REPLACE_WITH_SHAPE: ChromeWording;
55
+ export declare const IMAGE_REPLACE_WITH_SHAPE_DESCRIPTION: ChromeWording;
43
56
  /**
44
57
  * This package's contribution to the translation-key manifest, listed in
45
58
  * `PACKAGE_WORDINGS` under source `chrome`
@@ -78,13 +78,41 @@ export const IMAGE_TOAST_READ_SIZE_FAILED = [
78
78
  'Failed to read image size, please try another image',
79
79
  ];
80
80
  /**
81
- * The image toolbar's "Replace" (`configs/replace-action.ts`), offered only
82
- * with a host asset dialog (`AssetPickerProvider`).
81
+ * The image toolbar's "Replace": the page button (`configs/replace-action.ts`)
82
+ * and the canvas menu that holds « Replace with image » and « Replace with
83
+ * shape » (`replace/toolbar.ts`, ADR 0044 amendment of 2026-10-27). Offered
84
+ * with or without a host: without one, the system file chooser.
83
85
  */
84
86
  export const IMAGE_TOOLBAR_REPLACE = [
85
87
  'com.labre.image.toolbar.replace',
86
88
  'Replace',
87
89
  ];
90
+ /**
91
+ * « Replace with image » on a shape and « Replace with shape » on a canvas
92
+ * image (ADR 0044): the toolbar entries and the commands they run share these
93
+ * keys, so the palette and the toolbar say the same words.
94
+ */
95
+ export const IMAGE_REPLACE_WITH_IMAGE = [
96
+ 'com.labre.image.replace.with-image',
97
+ 'Replace with image',
98
+ ];
99
+ export const IMAGE_REPLACE_WITH_IMAGE_DESCRIPTION = [
100
+ 'com.labre.image.replace.with-image.description',
101
+ 'Put a picture in place of this shape, cut to its box.',
102
+ ];
103
+ /** `canvas.image.replaceWithImage`: the same label, said of an image. */
104
+ export const IMAGE_REPLACE_PICTURE_DESCRIPTION = [
105
+ 'com.labre.image.replace.picture.description',
106
+ 'Put another picture in this image, from the library or a file.',
107
+ ];
108
+ export const IMAGE_REPLACE_WITH_SHAPE = [
109
+ 'com.labre.image.replace.with-shape',
110
+ 'Replace with shape',
111
+ ];
112
+ export const IMAGE_REPLACE_WITH_SHAPE_DESCRIPTION = [
113
+ 'com.labre.image.replace.with-shape.description',
114
+ 'Put a rectangle in place of this image.',
115
+ ];
88
116
  /**
89
117
  * This package's contribution to the translation-key manifest, listed in
90
118
  * `PACKAGE_WORDINGS` under source `chrome`
@@ -108,4 +136,9 @@ export const IMAGE_WORDINGS = [
108
136
  IMAGE_TOAST_COPIED,
109
137
  IMAGE_TOAST_READ_SIZE_FAILED,
110
138
  IMAGE_TOOLBAR_REPLACE,
139
+ IMAGE_REPLACE_WITH_IMAGE,
140
+ IMAGE_REPLACE_WITH_IMAGE_DESCRIPTION,
141
+ IMAGE_REPLACE_PICTURE_DESCRIPTION,
142
+ IMAGE_REPLACE_WITH_SHAPE,
143
+ IMAGE_REPLACE_WITH_SHAPE_DESCRIPTION,
111
144
  ];