@formicoidea/labre-core 0.57.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 (178) hide show
  1. package/architecture/code.c4 +94 -65
  2. package/architecture/domain.c4 +2510 -0
  3. package/architecture/likec4.config.json +3 -3
  4. package/dist/_pkgs/affine-block-attachment/edgeless-clipboard-config.d.ts +3 -2
  5. package/dist/_pkgs/affine-block-attachment/edgeless-clipboard-config.js +18 -13
  6. package/dist/_pkgs/affine-block-callout/adapters/markdown.js +4 -1
  7. package/dist/_pkgs/affine-block-code/adapters/html.js +1 -1
  8. package/dist/_pkgs/affine-block-code/adapters/markdown/preprocessor.js +13 -1
  9. package/dist/_pkgs/affine-block-code/adapters/notion-html.js +1 -1
  10. package/dist/_pkgs/affine-block-code/code-block.d.ts +17 -2
  11. package/dist/_pkgs/affine-block-code/code-block.js +28 -23
  12. package/dist/_pkgs/affine-block-code/code-toolbar/components/lang-button.js +17 -4
  13. package/dist/_pkgs/affine-block-code/code-toolbar/components/preview-button.js +1 -1
  14. package/dist/_pkgs/affine-block-data-view/data-source.js +12 -5
  15. package/dist/_pkgs/affine-block-edgeless-text/edgeless-clipboard-config.d.ts +3 -2
  16. package/dist/_pkgs/affine-block-edgeless-text/edgeless-clipboard-config.js +8 -5
  17. package/dist/_pkgs/affine-block-image/asset-picker.d.ts +23 -2
  18. package/dist/_pkgs/affine-block-image/asset-picker.js +52 -12
  19. package/dist/_pkgs/affine-block-image/configs/replace-action.d.ts +8 -5
  20. package/dist/_pkgs/affine-block-image/configs/replace-action.js +8 -8
  21. package/dist/_pkgs/affine-block-image/configs/toolbar.js +2 -1
  22. package/dist/_pkgs/affine-block-image/crop/crop-controller.d.ts +15 -0
  23. package/dist/_pkgs/affine-block-image/crop/crop-controller.js +40 -2
  24. package/dist/_pkgs/affine-block-image/crop/crop-math.d.ts +22 -0
  25. package/dist/_pkgs/affine-block-image/crop/crop-math.js +55 -2
  26. package/dist/_pkgs/affine-block-image/crop/crop-overlay.d.ts +17 -3
  27. package/dist/_pkgs/affine-block-image/crop/crop-overlay.js +64 -6
  28. package/dist/_pkgs/affine-block-image/edgeless-clipboard-config.d.ts +3 -2
  29. package/dist/_pkgs/affine-block-image/edgeless-clipboard-config.js +20 -14
  30. package/dist/_pkgs/affine-block-image/image-block.js +1 -0
  31. package/dist/_pkgs/affine-block-image/image-edgeless-block.js +3 -0
  32. package/dist/_pkgs/affine-block-image/index.d.ts +2 -0
  33. package/dist/_pkgs/affine-block-image/index.js +2 -0
  34. package/dist/_pkgs/affine-block-image/replace/commands.d.ts +68 -0
  35. package/dist/_pkgs/affine-block-image/replace/commands.js +200 -0
  36. package/dist/_pkgs/affine-block-image/replace/replace-model.d.ts +44 -0
  37. package/dist/_pkgs/affine-block-image/replace/replace-model.js +168 -0
  38. package/dist/_pkgs/affine-block-image/replace/toolbar.d.ts +21 -0
  39. package/dist/_pkgs/affine-block-image/replace/toolbar.js +159 -0
  40. package/dist/_pkgs/affine-block-image/translations.d.ts +15 -2
  41. package/dist/_pkgs/affine-block-image/translations.js +35 -2
  42. package/dist/_pkgs/affine-block-image/utils.d.ts +7 -0
  43. package/dist/_pkgs/affine-block-image/utils.js +13 -0
  44. package/dist/_pkgs/affine-block-image/view.js +7 -0
  45. package/dist/_pkgs/affine-block-note/components/edgeless-page-block-title.d.ts +14 -5
  46. package/dist/_pkgs/affine-block-note/components/edgeless-page-block-title.js +18 -11
  47. package/dist/_pkgs/affine-block-note/config.d.ts +9 -2
  48. package/dist/_pkgs/affine-block-note/edgeless-clipboard-config.d.ts +3 -2
  49. package/dist/_pkgs/affine-block-note/edgeless-clipboard-config.js +8 -5
  50. package/dist/_pkgs/affine-block-paragraph/adapters/markdown.js +11 -0
  51. package/dist/_pkgs/affine-block-root/commands/invisible-characters-commands.d.ts +17 -0
  52. package/dist/_pkgs/affine-block-root/commands/invisible-characters-commands.js +123 -0
  53. package/dist/_pkgs/affine-block-root/edgeless/clipboard/command.js +82 -65
  54. package/dist/_pkgs/affine-block-root/edgeless/edgeless-keyboard.js +2 -0
  55. package/dist/_pkgs/affine-block-root/index.d.ts +1 -0
  56. package/dist/_pkgs/affine-block-root/index.js +1 -0
  57. package/dist/_pkgs/affine-block-root/translations.d.ts +6 -0
  58. package/dist/_pkgs/affine-block-root/translations.js +10 -0
  59. package/dist/_pkgs/affine-block-root/view.js +6 -1
  60. package/dist/_pkgs/affine-block-surface/commands/index.d.ts +1 -1
  61. package/dist/_pkgs/affine-block-surface/commands/index.js +1 -1
  62. package/dist/_pkgs/affine-block-surface/commands/reassociate-connectors.d.ts +9 -0
  63. package/dist/_pkgs/affine-block-surface/commands/reassociate-connectors.js +21 -12
  64. package/dist/_pkgs/affine-block-surface/extensions/clipboard-config.d.ts +33 -0
  65. package/dist/_pkgs/affine-block-surface/extensions/clipboard-config.js +53 -0
  66. package/dist/_pkgs/affine-block-surface/extensions/reading-toolbar.js +12 -12
  67. package/dist/_pkgs/affine-block-surface/framework-background/background-view.js +2 -1
  68. package/dist/_pkgs/affine-block-surface/framework-background/band-widths.d.ts +104 -0
  69. package/dist/_pkgs/affine-block-surface/framework-background/band-widths.js +156 -0
  70. package/dist/_pkgs/affine-block-surface/framework-background/def.d.ts +87 -4
  71. package/dist/_pkgs/affine-block-surface/framework-background/def.js +26 -7
  72. package/dist/_pkgs/affine-block-surface/framework-background/ellipsis.d.ts +58 -0
  73. package/dist/_pkgs/affine-block-surface/framework-background/ellipsis.js +110 -0
  74. package/dist/_pkgs/affine-block-surface/framework-background/index.d.ts +3 -0
  75. package/dist/_pkgs/affine-block-surface/framework-background/index.js +3 -0
  76. package/dist/_pkgs/affine-block-surface/framework-background/labels.d.ts +12 -2
  77. package/dist/_pkgs/affine-block-surface/framework-background/labels.js +29 -5
  78. package/dist/_pkgs/affine-block-surface/framework-background/measure-text.d.ts +14 -0
  79. package/dist/_pkgs/affine-block-surface/framework-background/measure-text.js +31 -0
  80. package/dist/_pkgs/affine-block-surface/framework-background/renderer.d.ts +14 -1
  81. package/dist/_pkgs/affine-block-surface/framework-background/renderer.js +84 -31
  82. package/dist/_pkgs/affine-block-table/adapters/html.js +26 -14
  83. package/dist/_pkgs/affine-block-table/adapters/utils.d.ts +1 -0
  84. package/dist/_pkgs/affine-block-table/adapters/utils.js +23 -5
  85. package/dist/_pkgs/affine-block-table/header-menu.d.ts +13 -0
  86. package/dist/_pkgs/affine-block-table/header-menu.js +25 -0
  87. package/dist/_pkgs/affine-block-table/selection-controller.d.ts +15 -0
  88. package/dist/_pkgs/affine-block-table/selection-controller.js +62 -0
  89. package/dist/_pkgs/affine-block-table/table-cell-css.d.ts +7 -0
  90. package/dist/_pkgs/affine-block-table/table-cell-css.js +10 -0
  91. package/dist/_pkgs/affine-block-table/table-cell.d.ts +8 -0
  92. package/dist/_pkgs/affine-block-table/table-cell.js +35 -3
  93. package/dist/_pkgs/affine-block-table/table-data-manager.d.ts +14 -0
  94. package/dist/_pkgs/affine-block-table/table-data-manager.js +44 -3
  95. package/dist/_pkgs/affine-block-table/translations.d.ts +14 -0
  96. package/dist/_pkgs/affine-block-table/translations.js +22 -0
  97. package/dist/_pkgs/affine-components/highlight-dropdown-menu/highlight-duotone-icon.js +8 -8
  98. package/dist/_pkgs/affine-components/highlight-dropdown-menu/text-duotone-icon.js +11 -11
  99. package/dist/_pkgs/affine-fragment-doc-title/title-band/title-band.d.ts +3 -1
  100. package/dist/_pkgs/affine-fragment-doc-title/title-band/title-band.js +15 -2
  101. package/dist/_pkgs/affine-gfx-mindmap/toolbar/mindmap-tool-button.d.ts +2 -2
  102. package/dist/_pkgs/affine-gfx-template/builtin/other.d.ts +4 -0
  103. package/dist/_pkgs/affine-gfx-template/builtin/other.js +1 -1
  104. package/dist/_pkgs/affine-gfx-template/builtin/slides/build.d.ts +81 -0
  105. package/dist/_pkgs/affine-gfx-template/builtin/slides/build.js +276 -0
  106. package/dist/_pkgs/affine-gfx-template/builtin/slides/consts.d.ts +126 -0
  107. package/dist/_pkgs/affine-gfx-template/builtin/slides/consts.js +101 -0
  108. package/dist/_pkgs/affine-gfx-template/builtin/slides/index.d.ts +8 -0
  109. package/dist/_pkgs/affine-gfx-template/builtin/slides/index.js +27 -0
  110. package/dist/_pkgs/affine-gfx-template/builtin/slides/masks.d.ts +83 -0
  111. package/dist/_pkgs/affine-gfx-template/builtin/slides/masks.js +674 -0
  112. package/dist/_pkgs/affine-gfx-template/contribute.js +12 -17
  113. package/dist/_pkgs/affine-gfx-template/make-snapshot.d.ts +14 -2
  114. package/dist/_pkgs/affine-gfx-template/make-snapshot.js +6 -2
  115. package/dist/_pkgs/affine-gfx-template/services/template-middlewares.d.ts +12 -0
  116. package/dist/_pkgs/affine-gfx-template/services/template-middlewares.js +48 -0
  117. package/dist/_pkgs/affine-gfx-template/services/template.js +2 -1
  118. package/dist/_pkgs/affine-gfx-template/toolbar/builtin-templates.d.ts +15 -1
  119. package/dist/_pkgs/affine-gfx-template/toolbar/builtin-templates.js +33 -37
  120. package/dist/_pkgs/affine-gfx-template/toolbar/template-type.d.ts +6 -0
  121. package/dist/_pkgs/affine-gfx-template/translations.d.ts +94 -0
  122. package/dist/_pkgs/affine-gfx-template/translations.js +441 -2
  123. package/dist/_pkgs/affine-model/blocks/table/table-model.d.ts +4 -0
  124. package/dist/_pkgs/affine-model/blocks/table/table-model.js +4 -0
  125. package/dist/_pkgs/affine-model/elements/bpmn/node.d.ts +46 -3
  126. package/dist/_pkgs/affine-model/elements/bpmn/node.js +47 -4
  127. package/dist/_pkgs/affine-model/elements/bpmn/pool.d.ts +20 -0
  128. package/dist/_pkgs/affine-model/elements/bpmn/pool.js +33 -4
  129. package/dist/_pkgs/affine-shared/services/edit-props-store.d.ts +3 -16
  130. package/dist/_pkgs/affine-shared/services/edit-props-store.js +16 -16
  131. package/dist/_pkgs/affine-shared/services/editor-setting-service.d.ts +3 -0
  132. package/dist/_pkgs/affine-shared/services/editor-setting-service.js +4 -0
  133. package/dist/_pkgs/affine-shared/services/index.d.ts +1 -0
  134. package/dist/_pkgs/affine-shared/services/index.js +1 -0
  135. package/dist/_pkgs/affine-shared/services/invisible-characters.d.ts +33 -0
  136. package/dist/_pkgs/affine-shared/services/invisible-characters.js +63 -0
  137. package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-cache.d.ts +0 -9
  138. package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-cache.js +3 -9
  139. package/dist/_pkgs/affine-shared/services/telemetry-service/lifecycle.d.ts +26 -0
  140. package/dist/_pkgs/affine-shared/services/telemetry-service/telemetry-service.d.ts +4 -3
  141. package/dist/_pkgs/affine-shared/services/telemetry-service/types.d.ts +11 -0
  142. package/dist/_pkgs/affine-shared/utils/index.d.ts +1 -0
  143. package/dist/_pkgs/affine-shared/utils/index.js +1 -0
  144. package/dist/_pkgs/affine-shared/utils/invisible-characters.d.ts +42 -0
  145. package/dist/_pkgs/affine-shared/utils/invisible-characters.js +81 -0
  146. package/dist/_pkgs/affine-widget-edgeless-zoom-toolbar/index.js +26 -26
  147. package/dist/_pkgs/affine-widget-edgeless-zoom-toolbar/zoom-toolbar.js +13 -2
  148. package/dist/_pkgs/affine-widget-keyboard-toolbar/icons.js +98 -98
  149. package/dist/_pkgs/affine-widget-page-dragging-area/index.js +10 -10
  150. package/dist/_pkgs/affine-widget-viewport-overlay/index.js +13 -13
  151. package/dist/_pkgs/data-view/core/utils/menu-title.js +17 -17
  152. package/dist/_pkgs/data-view/view-presets/table/pc/header/column-header.js +6 -0
  153. package/dist/_pkgs/data-view/view-presets/table/pc/table-view-ui-logic.js +5 -2
  154. package/dist/_pkgs/std/clipboard/clipboard.d.ts +15 -1
  155. package/dist/_pkgs/std/clipboard/clipboard.js +18 -0
  156. package/dist/_pkgs/std/inline/components/embed-gap.js +1 -1
  157. package/dist/_pkgs/std/inline/components/v-line.js +1 -1
  158. package/dist/_pkgs/std/inline/components/v-text.d.ts +15 -2
  159. package/dist/_pkgs/std/inline/components/v-text.js +147 -15
  160. package/dist/_pkgs/std/inline/consts.d.ts +5 -0
  161. package/dist/_pkgs/std/inline/consts.js +5 -0
  162. package/dist/_pkgs/std/inline/utils/index.d.ts +1 -0
  163. package/dist/_pkgs/std/inline/utils/index.js +1 -0
  164. package/dist/_pkgs/std/inline/utils/invisible-characters.d.ts +57 -0
  165. package/dist/_pkgs/std/inline/utils/invisible-characters.js +162 -0
  166. package/dist/_pkgs/std/inline/utils/point-conversion.js +3 -3
  167. package/dist/_pkgs/std/inline/utils/text.d.ts +8 -0
  168. package/dist/_pkgs/std/inline/utils/text.js +11 -2
  169. package/dist/_pkgs/store/model/store/crud.d.ts +28 -0
  170. package/dist/_pkgs/store/model/store/crud.js +97 -30
  171. package/dist/_pkgs/store/model/store/query.js +3 -2
  172. package/dist/_pkgs/store/model/store/store.d.ts +21 -0
  173. package/dist/_pkgs/store/model/store/store.js +88 -21
  174. package/dist/_pkgs/store/transformer/transformer.d.ts +46 -0
  175. package/dist/_pkgs/store/transformer/transformer.js +72 -0
  176. package/dist/commands.js +6 -1
  177. package/dist/translations.js +2 -0
  178. package/package.json +3 -4
@@ -1,15 +1,26 @@
1
1
  import { type ImageBlockModel, type ImageCrop, type ImageCropMask } from '../../affine-model/index.js';
2
+ import type { BlockStdScope } from '../../std/index.js';
2
3
  import { nothing } from 'lit';
3
4
  import { type CropRect } from './crop-math.js';
4
5
  /** What the controller needs from the block it serves, page or canvas. */
5
6
  export interface CroppableImageBlock {
6
7
  readonly model: ImageBlockModel;
8
+ /** Read on the canvas only, to take the selection and the viewport. */
9
+ readonly std: Pick<BlockStdScope, 'get'>;
7
10
  querySelector<E extends Element = Element>(selectors: string): E | null;
8
11
  }
9
12
  /**
10
13
  * The « Rogner » state of one image block (ADR 0037), shared by the page and
11
14
  * the canvas components: which preset the crop frame is open on, if any, and
12
15
  * the gestures that write. The frame is local UI and never stored.
16
+ *
17
+ * On the canvas the open frame owns the pointer and the wheel: the image is
18
+ * selected in EDITING state, which hides the selected-rect handles that
19
+ * would otherwise sit on the frame's corners, and the viewport is locked,
20
+ * so the wheel zooms the picture inside the frame rather than the canvas.
21
+ * Both are taken by one open path and given back by one `stop`, whatever
22
+ * closes the frame (Escape, Enter, a press elsewhere, the toolbar, the block
23
+ * leaving the DOM, the selection moving elsewhere), so they cannot drift.
13
24
  */
14
25
  export declare class ImageCropController {
15
26
  private readonly _block;
@@ -17,6 +28,8 @@ export declare class ImageCropController {
17
28
  /** The mask the crop frame is open on, `null` when it is closed. */
18
29
  readonly editing$: import("@preact/signals-core").Signal<"free" | "16:9" | "4:3" | "1:1" | null>;
19
30
  readonly crop$: import("@preact/signals-core").ReadonlySignal<ImageCrop | undefined>;
31
+ /** Gives the canvas back; set while the frame is open on the canvas. */
32
+ private _release;
20
33
  constructor(_block: CroppableImageBlock, _onCanvas: boolean);
21
34
  /**
22
35
  * The source's proportions: the loaded `<img>`'s pixel size, else what the
@@ -35,9 +48,11 @@ export declare class ImageCropController {
35
48
  * nothing until a drag.
36
49
  */
37
50
  choose(mask: ImageCropMask): void;
51
+ private _open;
38
52
  commit(rect: CropRect, mask?: "free" | "16:9" | "4:3" | "1:1"): boolean;
39
53
  /** « Image d'origine »: removes the crop; the bytes never changed. */
40
54
  restoreOriginal(): boolean;
55
+ /** Closes the frame and gives the canvas back; harmless when closed. */
41
56
  stop(): void;
42
57
  /** The frame, or nothing when it is closed or the page is readonly. */
43
58
  renderOverlay(src: string | null | undefined, rotate?: number): import("lit-html").TemplateResult<1> | typeof nothing;
@@ -1,5 +1,6 @@
1
1
  import { readImageCrop, } from '../../affine-model/index.js';
2
2
  import { Bound } from '../../global/gfx/index.js';
3
+ import { GfxControllerIdentifier } from '../../std/gfx/index.js';
3
4
  import { computed, signal } from '@preact/signals-core';
4
5
  import { html, nothing } from 'lit';
5
6
  import { styleMap } from 'lit/directives/style-map.js';
@@ -9,6 +10,14 @@ import { writeImageCrop } from './write-crop.js';
9
10
  * The « Rogner » state of one image block (ADR 0037), shared by the page and
10
11
  * the canvas components: which preset the crop frame is open on, if any, and
11
12
  * the gestures that write. The frame is local UI and never stored.
13
+ *
14
+ * On the canvas the open frame owns the pointer and the wheel: the image is
15
+ * selected in EDITING state, which hides the selected-rect handles that
16
+ * would otherwise sit on the frame's corners, and the viewport is locked,
17
+ * so the wheel zooms the picture inside the frame rather than the canvas.
18
+ * Both are taken by one open path and given back by one `stop`, whatever
19
+ * closes the frame (Escape, Enter, a press elsewhere, the toolbar, the block
20
+ * leaving the DOM, the selection moving elsewhere), so they cannot drift.
12
21
  */
13
22
  export class ImageCropController {
14
23
  constructor(_block, _onCanvas) {
@@ -17,6 +26,8 @@ export class ImageCropController {
17
26
  /** The mask the crop frame is open on, `null` when it is closed. */
18
27
  this.editing$ = signal(null);
19
28
  this.crop$ = computed(() => readImageCrop(this._block.model.props.crop$.value));
29
+ /** Gives the canvas back; set while the frame is open on the canvas. */
30
+ this._release = null;
20
31
  }
21
32
  /**
22
33
  * The source's proportions: the loaded `<img>`'s pixel size, else what the
@@ -56,17 +67,44 @@ export class ImageCropController {
56
67
  return;
57
68
  this.commit(presetCrop(mask, natural.width, natural.height), mask);
58
69
  }
70
+ this._open(mask);
71
+ }
72
+ _open(mask) {
59
73
  this.editing$.value = mask;
74
+ if (!this._onCanvas || this._release)
75
+ return;
76
+ const { selection, viewport } = this._block.std.get(GfxControllerIdentifier);
77
+ const { id, store } = this._block.model;
78
+ const wasLocked = viewport.locked;
79
+ viewport.locked = true;
80
+ selection.set({ elements: [id], editing: true });
81
+ // Another selection (a shortcut, the selection pane, a peer's undo
82
+ // removing the block) closes the frame instead of leaving it orphaned.
83
+ const watch = selection.slots.updated.subscribe(() => {
84
+ if (!selection.editing || !selection.has(id))
85
+ this.stop();
86
+ });
87
+ this._release = () => {
88
+ watch.unsubscribe();
89
+ viewport.locked = wasLocked;
90
+ if (selection.editing && selection.has(id) && store.hasBlock(id)) {
91
+ selection.set({ elements: [id], editing: false });
92
+ }
93
+ };
60
94
  }
61
95
  commit(rect, mask = this.editing$.peek() ?? 'free') {
62
96
  return writeImageCrop(this._block.model, rect, mask, this._onCanvas);
63
97
  }
64
98
  /** « Image d'origine »: removes the crop; the bytes never changed. */
65
99
  restoreOriginal() {
66
- this.editing$.value = null;
100
+ this.stop();
67
101
  return writeImageCrop(this._block.model, undefined, 'free', this._onCanvas);
68
102
  }
103
+ /** Closes the frame and gives the canvas back; harmless when closed. */
69
104
  stop() {
105
+ const release = this._release;
106
+ this._release = null;
107
+ release?.();
70
108
  this.editing$.value = null;
71
109
  }
72
110
  /** The frame, or nothing when it is closed or the page is readonly. */
@@ -88,7 +126,7 @@ export class ImageCropController {
88
126
  .ratio=${ratio}
89
127
  .rotate=${rotate}
90
128
  .onCommit=${(rect) => {
91
- this.commit(rect);
129
+ this.commit(rect, mask);
92
130
  }}
93
131
  .onExit=${() => this.stop()}
94
132
  ></affine-image-crop-overlay>`;
@@ -14,6 +14,13 @@ export declare function maskRatio(mask: ImageCropMask, naturalWidth: number, nat
14
14
  * writes before the author adjusts it. Free is the whole image.
15
15
  */
16
16
  export declare function presetCrop(mask: ImageCropMask, naturalWidth: number, naturalHeight: number): CropRect;
17
+ /**
18
+ * The largest centred rectangle whose width over height, in SOURCE PIXELS, is
19
+ * `ratio`: a preset's window, and the cut a shape's box makes in the picture
20
+ * that replaces it (ADR 0044). The whole image when the size or the ratio is
21
+ * unknown.
22
+ */
23
+ export declare function ratioCrop(ratio: number, naturalWidth: number, naturalHeight: number): CropRect;
17
24
  /** The window dragged by a normalised delta, kept inside the image. */
18
25
  export declare function moveCrop(start: CropRect, dx: number, dy: number): CropRect;
19
26
  /**
@@ -22,6 +29,21 @@ export declare function moveCrop(start: CropRect, dx: number, dy: number): CropR
22
29
  * and then shrinks to fit the image, so it never leaves the source.
23
30
  */
24
31
  export declare function resizeCrop(start: CropRect, corner: CropCorner, dx: number, dy: number, k: number | null): CropRect;
32
+ /** Scale of the window per 100 units of wheel `deltaY`. */
33
+ export declare const CROP_WHEEL_STEP = 1.05;
34
+ /**
35
+ * The window's scale for one wheel event: below 1 when the wheel turns up
36
+ * (`deltaY < 0`), which shrinks the window and so zooms into the picture.
37
+ */
38
+ export declare function cropWheelFactor(deltaY: number): number;
39
+ /**
40
+ * The window scaled by `factor` about a point of the source (`px`, `py`,
41
+ * normalised): that point stays under the pointer. The aspect is `k`
42
+ * (normalised `w / h`) when a mask holds one, else the window's own; the
43
+ * window never outgrows the image nor shrinks below the minimum side, and is
44
+ * then pushed back inside the image.
45
+ */
46
+ export declare function zoomCrop(start: CropRect, factor: number, px: number, py: number, k: number | null): CropRect;
25
47
  export declare function sameCropRect(a: CropRect, b: CropRect): boolean;
26
48
  /**
27
49
  * The canvas box once the crop changes from `from` to `to`, for a block whose
@@ -23,9 +23,21 @@ export function maskRatio(mask, naturalWidth, naturalHeight) {
23
23
  * writes before the author adjusts it. Free is the whole image.
24
24
  */
25
25
  export function presetCrop(mask, naturalWidth, naturalHeight) {
26
- const k = maskRatio(mask, naturalWidth, naturalHeight);
27
- if (k === null)
26
+ if (mask === 'free')
28
27
  return WHOLE_IMAGE;
28
+ return ratioCrop(MASK_RATIOS[mask], naturalWidth, naturalHeight);
29
+ }
30
+ /**
31
+ * The largest centred rectangle whose width over height, in SOURCE PIXELS, is
32
+ * `ratio`: a preset's window, and the cut a shape's box makes in the picture
33
+ * that replaces it (ADR 0044). The whole image when the size or the ratio is
34
+ * unknown.
35
+ */
36
+ export function ratioCrop(ratio, naturalWidth, naturalHeight) {
37
+ if (!(ratio > 0) || naturalWidth <= 0 || naturalHeight <= 0) {
38
+ return WHOLE_IMAGE;
39
+ }
40
+ const k = (ratio * naturalHeight) / naturalWidth;
29
41
  let w = k;
30
42
  let h = 1;
31
43
  if (w > 1) {
@@ -88,6 +100,47 @@ export function resizeCrop(start, corner, dx, dy, k) {
88
100
  h,
89
101
  };
90
102
  }
103
+ /** Scale of the window per 100 units of wheel `deltaY`. */
104
+ export const CROP_WHEEL_STEP = 1.05;
105
+ /** At most this many steps per wheel event: a fast flick stays readable. */
106
+ const CROP_WHEEL_MAX_STEPS = 3;
107
+ /**
108
+ * The window's scale for one wheel event: below 1 when the wheel turns up
109
+ * (`deltaY < 0`), which shrinks the window and so zooms into the picture.
110
+ */
111
+ export function cropWheelFactor(deltaY) {
112
+ const steps = clamp(deltaY / 100, -CROP_WHEEL_MAX_STEPS, CROP_WHEEL_MAX_STEPS);
113
+ return CROP_WHEEL_STEP ** steps;
114
+ }
115
+ /**
116
+ * The window scaled by `factor` about a point of the source (`px`, `py`,
117
+ * normalised): that point stays under the pointer. The aspect is `k`
118
+ * (normalised `w / h`) when a mask holds one, else the window's own; the
119
+ * window never outgrows the image nor shrinks below the minimum side, and is
120
+ * then pushed back inside the image.
121
+ */
122
+ export function zoomCrop(start, factor, px, py, k) {
123
+ const ratio = k ?? start.w / start.h;
124
+ const min = IMAGE_CROP_MIN_SIDE;
125
+ let w = start.w * factor;
126
+ let h = w / ratio;
127
+ // Fit inside the image first, then grow to the minimum: a ratio too
128
+ // extreme for both keeps the minimum, as `resizeCrop` does.
129
+ const shrink = Math.min(1, 1 / w, 1 / h);
130
+ w *= shrink;
131
+ h *= shrink;
132
+ const grow = Math.max(1, min / w, min / h);
133
+ w *= grow;
134
+ h *= grow;
135
+ const sx = w / start.w;
136
+ const sy = h / start.h;
137
+ return {
138
+ x: clamp(px - (px - start.x) * sx, 0, Math.max(0, 1 - w)),
139
+ y: clamp(py - (py - start.y) * sy, 0, Math.max(0, 1 - h)),
140
+ w,
141
+ h,
142
+ };
143
+ }
91
144
  export function sameCropRect(a, b) {
92
145
  return (Math.abs(a.x - b.x) < EPSILON &&
93
146
  Math.abs(a.y - b.y) < EPSILON &&
@@ -11,9 +11,17 @@ declare const ImageCropOverlay_base: typeof LitElement & import("../../global/ut
11
11
  * release hands it to `onCommit` once — one gesture, one write, one undo
12
12
  * step, decided by the caller (`writeImageCrop`).
13
13
  *
14
- * Pointer events never reach the editor while the frame is open: a press on
15
- * the frame would otherwise start a canvas drag or a page selection. Escape,
16
- * Enter or a press anywhere else calls `onExit`.
14
+ * The wheel over the frame zooms the picture inside it: it scales the window
15
+ * about the pointer, the ratio held, inside the image. One scroll is one
16
+ * write: the draft is handed to `onCommit` once the wheel has been quiet for
17
+ * a moment, or at once when a drag starts or the frame is left by Escape,
18
+ * Enter or a press elsewhere. A frame closed by anything else (the toolbar,
19
+ * the block leaving) drops a scroll still pending rather than write after it.
20
+ *
21
+ * Pointer and wheel events never reach the editor while the frame is open: a
22
+ * press on the frame would otherwise start a canvas drag or a page
23
+ * selection, a wheel would zoom or scroll the canvas. Escape, Enter or a
24
+ * press anywhere else calls `onExit`.
17
25
  */
18
26
  export declare class ImageCropOverlay extends ImageCropOverlay_base {
19
27
  static styles: import("lit").CSSResult;
@@ -28,11 +36,17 @@ export declare class ImageCropOverlay extends ImageCropOverlay_base {
28
36
  accessor onExit: () => void;
29
37
  accessor draft: CropRect;
30
38
  private _dragging;
39
+ /** The pending end of a wheel gesture, `null` when none is under way. */
40
+ private _wheelTimer;
31
41
  /** Removes the listeners of the gesture in progress, if any. */
32
42
  private _releaseGesture;
33
43
  private readonly _stop;
34
44
  private readonly _onOutsidePointer;
35
45
  private readonly _onKeydown;
46
+ /** Ends the wheel gesture under way, if any, with its one write. */
47
+ private _commitWheel;
48
+ private _dropWheel;
49
+ private readonly _onWheel;
36
50
  /**
37
51
  * The document-wide listeners (a press elsewhere, Escape, Enter) live only
38
52
  * as long as this element: the frame is rendered while it is open and
@@ -38,8 +38,10 @@ import { css, html, LitElement } from 'lit';
38
38
  import { property, state } from 'lit/decorators.js';
39
39
  import { repeat } from 'lit/directives/repeat.js';
40
40
  import { styleMap } from 'lit/directives/style-map.js';
41
- import { moveCrop, resizeCrop, WHOLE_IMAGE, } from './crop-math.js';
41
+ import { cropWheelFactor, moveCrop, resizeCrop, WHOLE_IMAGE, zoomCrop, } from './crop-math.js';
42
42
  const CORNERS = ['nw', 'ne', 'sw', 'se'];
43
+ /** Quiet time after the last wheel event that ends one wheel gesture. */
44
+ const WHEEL_COMMIT_DELAY_MS = 150;
43
45
  const rectStyle = (rect) => styleMap({
44
46
  left: `${rect.x * 100}%`,
45
47
  top: `${rect.y * 100}%`,
@@ -56,9 +58,17 @@ const rectStyle = (rect) => styleMap({
56
58
  * release hands it to `onCommit` once — one gesture, one write, one undo
57
59
  * step, decided by the caller (`writeImageCrop`).
58
60
  *
59
- * Pointer events never reach the editor while the frame is open: a press on
60
- * the frame would otherwise start a canvas drag or a page selection. Escape,
61
- * Enter or a press anywhere else calls `onExit`.
61
+ * The wheel over the frame zooms the picture inside it: it scales the window
62
+ * about the pointer, the ratio held, inside the image. One scroll is one
63
+ * write: the draft is handed to `onCommit` once the wheel has been quiet for
64
+ * a moment, or at once when a drag starts or the frame is left by Escape,
65
+ * Enter or a press elsewhere. A frame closed by anything else (the toolbar,
66
+ * the block leaving) drops a scroll still pending rather than write after it.
67
+ *
68
+ * Pointer and wheel events never reach the editor while the frame is open: a
69
+ * press on the frame would otherwise start a canvas drag or a page
70
+ * selection, a wheel would zoom or scroll the canvas. Escape, Enter or a
71
+ * press anywhere else calls `onExit`.
62
72
  */
63
73
  let ImageCropOverlay = (() => {
64
74
  let _classSuper = WithDisposable(LitElement);
@@ -94,12 +104,15 @@ let ImageCropOverlay = (() => {
94
104
  this.#onExit_accessor_storage = (__runInitializers(this, _onCommit_extraInitializers), __runInitializers(this, _onExit_initializers, () => { }));
95
105
  this.#draft_accessor_storage = (__runInitializers(this, _onExit_extraInitializers), __runInitializers(this, _draft_initializers, WHOLE_IMAGE));
96
106
  this._dragging = (__runInitializers(this, _draft_extraInitializers), false);
107
+ /** The pending end of a wheel gesture, `null` when none is under way. */
108
+ this._wheelTimer = null;
97
109
  /** Removes the listeners of the gesture in progress, if any. */
98
110
  this._releaseGesture = null;
99
111
  this._stop = (event) => event.stopPropagation();
100
112
  this._onOutsidePointer = (event) => {
101
113
  if (event.composedPath().includes(this))
102
114
  return;
115
+ this._commitWheel();
103
116
  this.onExit();
104
117
  };
105
118
  this._onKeydown = (event) => {
@@ -107,8 +120,31 @@ let ImageCropOverlay = (() => {
107
120
  return;
108
121
  event.preventDefault();
109
122
  event.stopPropagation();
123
+ this._commitWheel();
110
124
  this.onExit();
111
125
  };
126
+ this._onWheel = (event) => {
127
+ event.preventDefault();
128
+ event.stopPropagation();
129
+ if (this._dragging)
130
+ return;
131
+ const { width, height, cos, sin, scale } = this._geometry();
132
+ if (width <= 0 || height <= 0)
133
+ return;
134
+ // The pointer in the source's normalised coordinates: the frame is the
135
+ // whole source, possibly rotated about its centre.
136
+ const box = this.getBoundingClientRect();
137
+ const cdx = event.clientX - (box.left + box.width / 2);
138
+ const cdy = event.clientY - (box.top + box.height / 2);
139
+ const px = ((cdx * cos + cdy * sin) / scale + width / 2) / width;
140
+ const py = ((-cdx * sin + cdy * cos) / scale + height / 2) / height;
141
+ // A line or page delta (Firefox) counts as one full step per unit.
142
+ const deltaY = event.deltaMode === 0 ? event.deltaY : event.deltaY * 100;
143
+ this.draft = zoomCrop(this.draft, cropWheelFactor(deltaY), Math.min(Math.max(px, 0), 1), Math.min(Math.max(py, 0), 1), this.ratio);
144
+ if (this._wheelTimer !== null)
145
+ clearTimeout(this._wheelTimer);
146
+ this._wheelTimer = setTimeout(() => this._commitWheel(), WHEEL_COMMIT_DELAY_MS);
147
+ };
112
148
  }
113
149
  static {
114
150
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
@@ -229,6 +265,20 @@ let ImageCropOverlay = (() => {
229
265
  #draft_accessor_storage;
230
266
  get draft() { return this.#draft_accessor_storage; }
231
267
  set draft(value) { this.#draft_accessor_storage = value; }
268
+ /** Ends the wheel gesture under way, if any, with its one write. */
269
+ _commitWheel() {
270
+ if (this._wheelTimer === null)
271
+ return;
272
+ clearTimeout(this._wheelTimer);
273
+ this._wheelTimer = null;
274
+ this.onCommit(this.draft);
275
+ }
276
+ _dropWheel() {
277
+ if (this._wheelTimer === null)
278
+ return;
279
+ clearTimeout(this._wheelTimer);
280
+ this._wheelTimer = null;
281
+ }
232
282
  /**
233
283
  * The document-wide listeners (a press elsewhere, Escape, Enter) live only
234
284
  * as long as this element: the frame is rendered while it is open and
@@ -237,15 +287,22 @@ let ImageCropOverlay = (() => {
237
287
  connectedCallback() {
238
288
  super.connectedCallback();
239
289
  this.disposables.add(() => this._releaseGesture?.());
290
+ this.disposables.add(() => this._dropWheel());
240
291
  this.disposables.addFromEvent(this, 'pointerdown', this._stop);
241
292
  this.disposables.addFromEvent(this, 'click', this._stop);
242
293
  this.disposables.addFromEvent(this, 'dblclick', this._stop);
294
+ this.disposables.addFromEvent(this, 'wheel', this._onWheel, {
295
+ passive: false,
296
+ });
243
297
  this.disposables.addFromEvent(document, 'pointerdown', this._onOutsidePointer, true);
244
298
  this.disposables.addFromEvent(document, 'keydown', this._onKeydown, true);
245
299
  }
246
300
  willUpdate(changed) {
247
- if (changed.has('crop') && !this._dragging)
248
- this.draft = this.crop;
301
+ if (!changed.has('crop') || this._dragging)
302
+ return;
303
+ // A crop written meanwhile (a preset, a peer) wins over a pending scroll.
304
+ this._dropWheel();
305
+ this.draft = this.crop;
249
306
  }
250
307
  /**
251
308
  * Client pixels per layout pixel, read from the rendered box: the canvas
@@ -267,6 +324,7 @@ let ImageCropOverlay = (() => {
267
324
  return;
268
325
  event.preventDefault();
269
326
  event.stopPropagation();
327
+ this._commitWheel();
270
328
  const start = this.draft;
271
329
  const originX = event.clientX;
272
330
  const originY = event.clientY;
@@ -1,6 +1,7 @@
1
- import { EdgelessClipboardConfig } from '../affine-block-surface/index.js';
1
+ import { type ClipboardConfigCreationContext, EdgelessClipboardConfig, type PreparedClipboardBlock } from '../affine-block-surface/index.js';
2
2
  import { type BlockSnapshot } from '../store/index.js';
3
3
  export declare class EdgelessClipboardImageConfig extends EdgelessClipboardConfig {
4
4
  static readonly key = "affine:image";
5
- createBlock(image: BlockSnapshot): Promise<string | null>;
5
+ prepare(image: BlockSnapshot): Promise<PreparedClipboardBlock | null>;
6
+ createBlock(image: BlockSnapshot, context: ClipboardConfigCreationContext): Promise<string | null>;
6
7
  }
@@ -1,25 +1,31 @@
1
- import { EdgelessClipboardConfig } from '../affine-block-surface/index.js';
1
+ import { EdgelessClipboardConfig, } from '../affine-block-surface/index.js';
2
2
  import {} from '../store/index.js';
3
3
  export class EdgelessClipboardImageConfig extends EdgelessClipboardConfig {
4
4
  static { this.key = 'affine:image'; }
5
- async createBlock(image) {
5
+ async prepare(image) {
6
6
  const { xywh, rotate, sourceId, size, width, height, caption, crop } = image.props;
7
7
  if (!this.surface)
8
8
  return null;
9
9
  if (!(await this.std.workspace.blobSync.get(sourceId))) {
10
10
  return null;
11
11
  }
12
- return this.crud.addBlock('affine:image', {
13
- caption,
14
- sourceId,
15
- xywh,
16
- rotate,
17
- size,
18
- width,
19
- height,
20
- // The box above is the cropped one: without its crop the copy would
21
- // stretch the whole picture into it (ADR 0037).
22
- crop,
23
- }, this.surface.model.id);
12
+ const surfaceId = this.surface.model.id;
13
+ return {
14
+ write: () => this.crud.addBlock('affine:image', {
15
+ caption,
16
+ sourceId,
17
+ xywh,
18
+ rotate,
19
+ size,
20
+ width,
21
+ height,
22
+ // The box above is the cropped one: without its crop the copy
23
+ // would stretch the whole picture into it (ADR 0037).
24
+ crop,
25
+ }, surfaceId),
26
+ };
27
+ }
28
+ createBlock(image, context) {
29
+ return this.createPreparedBlock(image, context);
24
30
  }
25
31
  }
@@ -148,6 +148,7 @@ let ImageBlockComponent = (() => {
148
148
  this.resourceController.setEngine(this.std.store.blobSync);
149
149
  this.disposables.add(this.resourceController.subscribe());
150
150
  this.disposables.add(this.resourceController);
151
+ this.disposables.add(() => this.cropController.stop());
151
152
  this.disposables.add(this.model.props.sourceId$.subscribe(() => {
152
153
  this.refreshData();
153
154
  }));
@@ -133,6 +133,9 @@ let ImageEdgelessBlockComponent = (() => {
133
133
  this.resourceController.setEngine(this.std.store.blobSync);
134
134
  this.disposables.add(this.resourceController.subscribe());
135
135
  this.disposables.add(this.resourceController);
136
+ // A crop frame left open would keep the canvas locked and the image in
137
+ // editing state after the block is gone (ADR 0037, 2026-10-27).
138
+ this.disposables.add(() => this.cropController.stop());
136
139
  this.disposables.add(this.model.props.sourceId$.subscribe(() => {
137
140
  this.refreshData();
138
141
  }));
@@ -6,6 +6,8 @@ export * from './image-block.js';
6
6
  export * from './image-edgeless-block.js';
7
7
  export * from './image-service.js';
8
8
  export * from './image-spec.js';
9
+ export { imageReplaceCommands, REPLACE_IMAGE_WITH_IMAGE, REPLACE_IMAGE_WITH_SHAPE, REPLACE_SHAPE_WITH_IMAGE, } from './replace/commands.js';
10
+ export { replaceImageWithShape, replaceShapeWithImage, } from './replace/replace-model.js';
9
11
  export * from './turbo/image-layout-handler.js';
10
12
  export * from './turbo/image-painter.worker.js';
11
13
  export { addImages, addSiblingImageBlocks, downloadImageBlob } from './utils.js';
@@ -6,6 +6,8 @@ export * from './image-block.js';
6
6
  export * from './image-edgeless-block.js';
7
7
  export * from './image-service.js';
8
8
  export * from './image-spec.js';
9
+ export { imageReplaceCommands, REPLACE_IMAGE_WITH_IMAGE, REPLACE_IMAGE_WITH_SHAPE, REPLACE_SHAPE_WITH_IMAGE, } from './replace/commands.js';
10
+ export { replaceImageWithShape, replaceShapeWithImage, } from './replace/replace-model.js';
9
11
  export * from './turbo/image-layout-handler.js';
10
12
  export * from './turbo/image-painter.worker.js';
11
13
  export { addImages, addSiblingImageBlocks, downloadImageBlob } from './utils.js';
@@ -0,0 +1,68 @@
1
+ import { ImageBlockModel, ShapeType } from '../../affine-model/index.js';
2
+ import type { AnyCommandDescriptor, BlockStdScope, CommandDescriptor } from '../../std/index.js';
3
+ import { z } from 'zod';
4
+ /**
5
+ * The replace commands (ADR 0044), `owner: 'core'`: a shape and an image
6
+ * belong to no framework. Registered from the image view extension, so they
7
+ * go with the `image` flag; the toolbar entries invoke them through
8
+ * `runCommand`, so the refusal, the one undo step and the telemetry live
9
+ * here, once.
10
+ *
11
+ * They EMIT FROM THEIR BODY (listed in `registry.unit.spec.ts`'s
12
+ * self-emitting commands): `CommandDescriptor.telemetry` maps onto the
13
+ * framework creation events, and whether anything was replaced is a fact of
14
+ * the invocation — a cancelled file chooser replaces nothing.
15
+ */
16
+ export declare const replaceElementParams: z.ZodObject<{
17
+ elementId: z.ZodOptional<z.ZodString>;
18
+ }, "strip", z.ZodTypeAny, {
19
+ elementId?: string | undefined;
20
+ }, {
21
+ elementId?: string | undefined;
22
+ }>;
23
+ export type ReplaceElementParams = z.infer<typeof replaceElementParams>;
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 declare const replaceWithShapeParams: z.ZodObject<{
30
+ elementId: z.ZodOptional<z.ZodString>;
31
+ } & {
32
+ shapeType: z.ZodOptional<z.ZodNativeEnum<typeof ShapeType>>;
33
+ radius: z.ZodOptional<z.ZodNumber>;
34
+ }, "strip", z.ZodTypeAny, {
35
+ radius?: number | undefined;
36
+ shapeType?: ShapeType | undefined;
37
+ elementId?: string | undefined;
38
+ }, {
39
+ radius?: number | undefined;
40
+ shapeType?: ShapeType | undefined;
41
+ elementId?: string | undefined;
42
+ }>;
43
+ export type ReplaceWithShapeParams = z.infer<typeof replaceWithShapeParams>;
44
+ /** The shape the command would replace, or `null`. */
45
+ export declare function replaceableShape(std: BlockStdScope, params?: unknown): import("../../affine-model/index.js").ShapeElementModel | null;
46
+ /** The canvas image the command would replace, or `null`. */
47
+ export declare function replaceableImage(std: BlockStdScope, params?: unknown): ImageBlockModel | null;
48
+ /**
49
+ * The canvas image whose PICTURE the command would swap, or `null`. Looser
50
+ * than {@link replaceableImage}: the block stays (same id, caption and
51
+ * comments kept), so a captioned image may change its picture; a locked one
52
+ * may not.
53
+ */
54
+ export declare function replaceablePicture(std: BlockStdScope, params?: unknown): ImageBlockModel | null;
55
+ export declare const REPLACE_SHAPE_WITH_IMAGE = "canvas.shape.replaceWithImage";
56
+ export declare const REPLACE_IMAGE_WITH_SHAPE = "canvas.image.replaceWithShape";
57
+ export declare const REPLACE_IMAGE_WITH_IMAGE = "canvas.image.replaceWithImage";
58
+ export declare const replaceShapeWithImageCommand: CommandDescriptor<ReplaceElementParams>;
59
+ export declare const replaceImageWithShapeCommand: CommandDescriptor<ReplaceWithShapeParams>;
60
+ /**
61
+ * « Replace with image » on a canvas IMAGE (ADR 0044, amendment of
62
+ * 2026-10-27): another picture in the same block, from the host's library
63
+ * when there is one, from the system file chooser otherwise — so the entry
64
+ * exists without a host. The block keeps its id, corner, width and caption
65
+ * (`replaceImageSource`); it reports `ElementReplaced { image → image }`.
66
+ */
67
+ export declare const replaceImageWithImageCommand: CommandDescriptor<ReplaceElementParams>;
68
+ export declare const imageReplaceCommands: AnyCommandDescriptor[];