@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
@@ -13,6 +13,13 @@ export declare function copyImageBlob(block: ImageBlockComponent | ImageEdgeless
13
13
  */
14
14
  export declare function turnImageIntoCardView(block: ImageBlockComponent | ImageEdgelessBlockComponent): Promise<void>;
15
15
  export declare function shouldResizeImage(node: Node, target: EventTarget | null): boolean;
16
+ /**
17
+ * One picture from the system file chooser, uploaded to the blob store and
18
+ * measured, for a gesture that writes it itself (ADR 0044). `null` when the
19
+ * file is not an image, is over the size limit or cannot be measured; the
20
+ * user has been told why by a toast.
21
+ */
22
+ export declare function imageSourceFromFile(std: BlockStdScope, file: File): Promise<ImageBlockSource | null>;
16
23
  export declare function addSiblingImageBlocks(std: BlockStdScope, files: File[], targetModel: BlockModel, placement?: 'after' | 'before'): Promise<string[]>;
17
24
  export declare function addImageBlocks(std: BlockStdScope, files: File[], parent?: BlockModel | string | null, parentIndex?: number): Promise<string[]>;
18
25
  export declare function addImages(std: BlockStdScope, files: File[], options: {
@@ -165,6 +165,19 @@ async function buildPropsWith(std, file) {
165
165
  }
166
166
  return { size, sourceId, ...imageSize };
167
167
  }
168
+ /**
169
+ * One picture from the system file chooser, uploaded to the blob store and
170
+ * measured, for a gesture that writes it itself (ADR 0044). `null` when the
171
+ * file is not an image, is over the size limit or cannot be measured; the
172
+ * user has been told why by a toast.
173
+ */
174
+ export async function imageSourceFromFile(std, file) {
175
+ if (!file.type.startsWith('image/'))
176
+ return null;
177
+ if (hasExceeded(std, [file]))
178
+ return null;
179
+ return buildPropsWith(std, file).catch(() => null);
180
+ }
168
181
  export async function addSiblingImageBlocks(std, files, targetModel, placement = 'after') {
169
182
  files = files.filter(file => file.type.startsWith('image/'));
170
183
  if (!files.length)
@@ -1,8 +1,11 @@
1
1
  import { ViewExtensionProvider, } from '../affine-ext-loader/index.js';
2
+ import { CommandExtension } from '../std/index.js';
2
3
  import { EdgelessClipboardImageConfig } from './edgeless-clipboard-config.js';
3
4
  import { effects } from './effects.js';
4
5
  import { ImageEdgelessBlockInteraction } from './image-edgeless-block.js';
5
6
  import { ImageBlockSpec } from './image-spec.js';
7
+ import { imageReplaceCommands } from './replace/commands.js';
8
+ import { shapeReplaceWithImageToolbarExtension } from './replace/toolbar.js';
6
9
  export class ImageViewExtension extends ViewExtensionProvider {
7
10
  constructor() {
8
11
  super(...arguments);
@@ -18,6 +21,10 @@ export class ImageViewExtension extends ViewExtensionProvider {
18
21
  if (this.isEdgeless(context.scope)) {
19
22
  context.register(EdgelessClipboardImageConfig);
20
23
  context.register(ImageEdgelessBlockInteraction);
24
+ // « Replace with image / with shape » (ADR 0044): the shape toolbar's
25
+ // entry is a second module on the shape flavour, owned by this package.
26
+ context.register(shapeReplaceWithImageToolbarExtension);
27
+ context.register(CommandExtension(imageReplaceCommands));
21
28
  }
22
29
  }
23
30
  }
@@ -2,11 +2,20 @@ import { NoteBlockModel } from '../../affine-model/index.js';
2
2
  import { type BlockStdScope, ShadowlessElement } from '../../std/index.js';
3
3
  declare const EdgelessPageBlockTitle_base: typeof ShadowlessElement & import("../../global/utils/types.js").Constructor<import("../../global/lit/with-disposable.js").DisposableClass>;
4
4
  /**
5
- * The head of the page's note on the canvas: the host's title, if its
6
- * `NoteConfigExtension` draws one, then the title band (ADR 0035 §11). The
7
- * band sits inside the note's DOM so it follows the zoom like the rest of the
8
- * note (REQ-PR-7); it draws nothing when the host registers no
9
- * `TitleBandProvider`.
5
+ * The head of the page's note on the canvas: the title, then the title band
6
+ * (ADR 0035 §11). The band sits inside the note's DOM so it follows the zoom
7
+ * like the rest of the note (REQ-PR-7); it draws nothing when the host
8
+ * registers no `TitleBandProvider`.
9
+ *
10
+ * The title is the host's `NoteConfigExtension.pageBlockTitle` when it
11
+ * registers one (`nothing` to draw none), else the document title itself.
12
+ * Until 0.59 a host without that config got no title on the canvas at all:
13
+ * the note was already wired for a `<doc-title>` (ArrowUp, the click routing
14
+ * of `EdgelessNoteInteraction`, the band not drawn twice) but nothing put one
15
+ * there. The fallback edits `root.props.title` like the page view's, behind
16
+ * the same note mask as the note's content, so it is editable only once the
17
+ * note is being edited, and never on a read-only store. Page mode never
18
+ * mounts this element, so the page view's own title is not doubled.
10
19
  */
11
20
  export declare class EdgelessPageBlockTitle extends EdgelessPageBlockTitle_base {
12
21
  render(): import("lit-html").TemplateResult<1> | undefined;
@@ -41,11 +41,20 @@ import { property } from 'lit/decorators.js';
41
41
  import { NoteConfigExtension } from '../config.js';
42
42
  import * as styles from './edgeless-page-block-title.css.js';
43
43
  /**
44
- * The head of the page's note on the canvas: the host's title, if its
45
- * `NoteConfigExtension` draws one, then the title band (ADR 0035 §11). The
46
- * band sits inside the note's DOM so it follows the zoom like the rest of the
47
- * note (REQ-PR-7); it draws nothing when the host registers no
48
- * `TitleBandProvider`.
44
+ * The head of the page's note on the canvas: the title, then the title band
45
+ * (ADR 0035 §11). The band sits inside the note's DOM so it follows the zoom
46
+ * like the rest of the note (REQ-PR-7); it draws nothing when the host
47
+ * registers no `TitleBandProvider`.
48
+ *
49
+ * The title is the host's `NoteConfigExtension.pageBlockTitle` when it
50
+ * registers one (`nothing` to draw none), else the document title itself.
51
+ * Until 0.59 a host without that config got no title on the canvas at all:
52
+ * the note was already wired for a `<doc-title>` (ArrowUp, the click routing
53
+ * of `EdgelessNoteInteraction`, the band not drawn twice) but nothing put one
54
+ * there. The fallback edits `root.props.title` like the page view's, behind
55
+ * the same note mask as the note's content, so it is editable only once the
56
+ * note is being edited, and never on a read-only store. Page mode never
57
+ * mounts this element, so the page view's own title is not doubled.
49
58
  */
50
59
  let EdgelessPageBlockTitle = (() => {
51
60
  let _classDecorators = [requiredProperties({
@@ -77,12 +86,10 @@ let EdgelessPageBlockTitle = (() => {
77
86
  render() {
78
87
  if (!this.note.isPageBlock())
79
88
  return;
80
- const title = this.std
81
- .getOptional(NoteConfigExtension.identifier)
82
- ?.pageBlockTitle({
83
- note: this.note,
84
- std: this.std,
85
- });
89
+ const pageBlockTitle = this.std.getOptional(NoteConfigExtension.identifier)?.pageBlockTitle;
90
+ const title = pageBlockTitle
91
+ ? pageBlockTitle({ note: this.note, std: this.std })
92
+ : html `<doc-title .doc=${this.note.store}></doc-title>`;
86
93
  return html `<div class=${styles.pageBlockTitle}>
87
94
  ${title}
88
95
  <labre-title-band
@@ -1,13 +1,20 @@
1
1
  import type { NoteBlockModel } from '../affine-model/index.js';
2
2
  import { type BlockStdScope } from '../std/index.js';
3
- import type { TemplateResult } from 'lit';
3
+ import type { nothing, TemplateResult } from 'lit';
4
4
  type NoteBlockContext = {
5
5
  note: NoteBlockModel;
6
6
  std: BlockStdScope;
7
7
  };
8
8
  export type NoteConfig = {
9
9
  edgelessNoteHeader: (context: NoteBlockContext) => TemplateResult;
10
- pageBlockTitle: (context: NoteBlockContext) => TemplateResult;
10
+ /**
11
+ * The title at the head of the page note on the canvas. Absent, the note
12
+ * draws the document title itself (`<doc-title>`, editable like the rest
13
+ * of the note); a host that draws its own title elsewhere returns
14
+ * `nothing`. Optional since 0.59: a config typed against 0.58 still
15
+ * compiles (`note-config-shape.unit.spec.ts`).
16
+ */
17
+ pageBlockTitle?: (context: NoteBlockContext) => TemplateResult | typeof nothing;
11
18
  /**
12
19
  * @returns if the viewport fit animation executed
13
20
  */
@@ -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 EdgelessClipboardNoteConfig extends EdgelessClipboardConfig {
4
4
  static readonly key = "affine:note";
5
- createBlock(note: BlockSnapshot): Promise<null | string>;
5
+ prepare(note: BlockSnapshot): Promise<PreparedClipboardBlock | null>;
6
+ createBlock(note: BlockSnapshot, context: ClipboardConfigCreationContext): Promise<null | string>;
6
7
  }
@@ -1,19 +1,22 @@
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 EdgelessClipboardNoteConfig extends EdgelessClipboardConfig {
4
4
  static { this.key = 'affine:note'; }
5
- async createBlock(note) {
5
+ async prepare(note) {
6
6
  const oldId = note.id;
7
7
  delete note.props.index;
8
8
  if (!note.props.xywh) {
9
9
  console.error(`Note block(id: ${oldId}) does not have xywh property`);
10
10
  return null;
11
11
  }
12
- const newId = await this.onBlockSnapshotPaste(note, this.std.store, this.std.store.root.id);
13
- if (!newId) {
12
+ const prepared = await this.prepareBlockSnapshotPaste(note, this.std.store, this.std.store.root.id);
13
+ if (!prepared) {
14
14
  console.error(`Failed to paste note block(id: ${oldId})`);
15
15
  return null;
16
16
  }
17
- return newId;
17
+ return prepared;
18
+ }
19
+ createBlock(note, context) {
20
+ return this.createPreparedBlock(note, context);
18
21
  }
19
22
  }
@@ -3,6 +3,10 @@ import { BlockMarkdownAdapterExtension, IN_PARAGRAPH_NODE_CONTEXT_KEY, isCallout
3
3
  import { nanoid } from '../../store/index.js';
4
4
  const PARAGRAPH_MDAST_TYPE = new Set(['paragraph', 'heading', 'blockquote']);
5
5
  const isParagraphMDASTType = (node) => PARAGRAPH_MDAST_TYPE.has(node.type);
6
+ const isImageOnlyParagraph = (node) => node.type === 'paragraph' &&
7
+ node.children.some(child => child.type === 'image') &&
8
+ node.children.every(child => child.type === 'image' ||
9
+ (child.type === 'text' && child.value.trim() === ''));
6
10
  export const paragraphBlockMarkdownAdapterMatcher = {
7
11
  flavour: ParagraphBlockSchema.model.flavour,
8
12
  toMatch: o => isParagraphMDASTType(o.node) && !isCalloutNode(o.node),
@@ -12,6 +16,13 @@ export const paragraphBlockMarkdownAdapterMatcher = {
12
16
  const { walkerContext, deltaConverter } = context;
13
17
  switch (o.node.type) {
14
18
  case 'paragraph': {
19
+ // mdast wraps a standalone image (or images split by a line
20
+ // break) in a paragraph; the image matcher emits the block itself,
21
+ // so opening a paragraph here left an empty one above it (#521).
22
+ // Markdown cannot express an empty paragraph: none is lost.
23
+ if (isImageOnlyParagraph(o.node)) {
24
+ return;
25
+ }
15
26
  walkerContext.setGlobalContext(IN_PARAGRAPH_NODE_CONTEXT_KEY, true);
16
27
  walkerContext
17
28
  .openNode({
@@ -0,0 +1,17 @@
1
+ import type { AnyCommandDescriptor } from '../../std/index.js';
2
+ import type { Store } from '../../store/index.js';
3
+ /**
4
+ * Strip the invisible characters of every text of `store` — each block's
5
+ * text, the title, table and database cells, and through the surface block
6
+ * every canvas element's text, labels and titles — in ONE transaction, so one
7
+ * undo puts them all back. Writes nothing on a read-only store, and nothing
8
+ * when there is nothing to remove (no empty undo entry).
9
+ *
10
+ * `blockCount` counts blocks: the canvas is one, its elements live in the
11
+ * surface block.
12
+ */
13
+ export declare function removeInvisibleCharacters(store: Store): {
14
+ count: number;
15
+ blockCount: number;
16
+ };
17
+ export declare const invisibleCharactersCommands: AnyCommandDescriptor[];
@@ -0,0 +1,123 @@
1
+ /**
2
+ * The special characters of a text (`text.invisibles.*`): the zero-width
3
+ * spaces, word joiners and tag characters an AI text generator or a
4
+ * "watermarking" tool leaves behind, invisible to the reader and carried
5
+ * along by every copy.
6
+ *
7
+ * `owner: 'core'`, `scope: 'global'`: a paragraph is the same text in both
8
+ * views, and a canvas label is text too. On the palette, the agent and the
9
+ * host's view menu (`'view-menu'`, ADR 0034 amendment of 2026-10-13): the
10
+ * library draws no menu of its own, a host lists them with `getViewActions`
11
+ * and runs them through `runCommand`.
12
+ *
13
+ * Telemetry is emitted from the bodies, for `doc.copyLink`'s reason: the
14
+ * counts are facts of the invocation and the commands belong to no framework
15
+ * (see the telemetry README, "Special characters").
16
+ */
17
+ import { InvisibleCharacters, NotificationProvider, TelemetryProvider, translateKey, } from '../../affine-shared/services/index.js';
18
+ import { collectYTexts, stripInvisiblesFromYText, } from '../../affine-shared/utils/index.js';
19
+ import { findInvisibleCharacters } from '../../std/inline/index.js';
20
+ import { ROOT_TOAST_INVISIBLES_REMOVED } from '../translations.js';
21
+ /**
22
+ * Code keeps its characters: a zero-width space in a string literal is the
23
+ * program's business, not a watermark.
24
+ */
25
+ const UNTOUCHED_FLAVOURS = new Set(['affine:code']);
26
+ /**
27
+ * Strip the invisible characters of every text of `store` — each block's
28
+ * text, the title, table and database cells, and through the surface block
29
+ * every canvas element's text, labels and titles — in ONE transaction, so one
30
+ * undo puts them all back. Writes nothing on a read-only store, and nothing
31
+ * when there is nothing to remove (no empty undo entry).
32
+ *
33
+ * `blockCount` counts blocks: the canvas is one, its elements live in the
34
+ * surface block.
35
+ */
36
+ export function removeInvisibleCharacters(store) {
37
+ if (store.readonly)
38
+ return { count: 0, blockCount: 0 };
39
+ const touched = store
40
+ .getAllModels()
41
+ .filter(model => !UNTOUCHED_FLAVOURS.has(model.flavour))
42
+ .map(model => ({ model, texts: collectYTexts(model.yBlock) }))
43
+ .filter(({ texts }) => texts.some(text => findInvisibleCharacters(text.toString()).length > 0));
44
+ if (touched.length === 0)
45
+ return { count: 0, blockCount: 0 };
46
+ let count = 0;
47
+ store.captureSync();
48
+ store.transact(() => {
49
+ for (const { texts } of touched) {
50
+ for (const text of texts)
51
+ count += stripInvisiblesFromYText(text);
52
+ }
53
+ });
54
+ store.captureSync();
55
+ return { count, blockCount: touched.length };
56
+ }
57
+ const removeInvisibles = {
58
+ id: 'text.invisibles.remove',
59
+ owner: 'core',
60
+ kind: 'action',
61
+ labelKey: 'com.labre.command.text.invisibles.remove',
62
+ labelFallback: 'Remove invisible characters',
63
+ descriptionKey: 'com.labre.command.text.invisibles.remove.description',
64
+ descriptionFallback: 'Remove the zero-width spaces, word joiners and other invisible characters AI tools leave in a text. Emoji, code and no-break spaces are kept.',
65
+ category: 'text',
66
+ keywords: ['invisible', 'watermark', 'zero-width', 'clean', 'AI'],
67
+ surfaces: ['palette', 'agent', 'view-menu'],
68
+ order: 3,
69
+ scope: 'global',
70
+ defaultKeys: { mac: [], other: [] },
71
+ availability: 'editable',
72
+ run: std => {
73
+ const { count, blockCount } = removeInvisibleCharacters(std.store);
74
+ std
75
+ .getOptional(NotificationProvider)
76
+ ?.toast(translateKey(std, ...ROOT_TOAST_INVISIBLES_REMOVED, { count }));
77
+ if (count === 0)
78
+ return;
79
+ std
80
+ .getOptional(TelemetryProvider)
81
+ ?.track('InvisibleCharactersRemoved', { count, blockCount });
82
+ },
83
+ };
84
+ /**
85
+ * Show or hide the special characters for THIS viewer: a dot per space, an
86
+ * arrow per tab, a degree sign per no-break space, a red mark per invisible
87
+ * character, ↵ and ¶ at line ends (`InvisibleCharacters`, `v-text`).
88
+ * Writes nothing to the document, so a reader may do it too.
89
+ */
90
+ const toggleInvisibles = {
91
+ id: 'text.invisibles.toggle',
92
+ owner: 'core',
93
+ kind: 'action',
94
+ labelKey: 'com.labre.command.text.invisibles.toggle',
95
+ labelFallback: 'Show special characters',
96
+ descriptionKey: 'com.labre.command.text.invisibles.toggle.description',
97
+ descriptionFallback: 'Show or hide spaces, tabs, no-break spaces, line ends and invisible characters on your screen only.',
98
+ category: 'display',
99
+ keywords: ['invisible', 'whitespace', 'pilcrow', 'formatting', 'marks'],
100
+ surfaces: ['palette', 'agent', 'view-menu'],
101
+ order: 2,
102
+ scope: 'global',
103
+ defaultKeys: { mac: [], other: [] },
104
+ when: std => !!std.getOptional(InvisibleCharacters),
105
+ // A signal read, so a menu that reads it inside an effect ticks and
106
+ // unticks with the display, whoever flips it.
107
+ checked: std => std.getOptional(InvisibleCharacters)?.visible$.value ?? false,
108
+ run: std => {
109
+ const display = std.getOptional(InvisibleCharacters);
110
+ if (!display)
111
+ return;
112
+ // Toggled first: an optional call skips its arguments when there is no
113
+ // telemetry provider.
114
+ const visible = display.toggle();
115
+ std
116
+ .getOptional(TelemetryProvider)
117
+ ?.track('InvisibleCharactersToggled', { visible });
118
+ },
119
+ };
120
+ export const invisibleCharactersCommands = [
121
+ toggleInvisibles,
122
+ removeInvisibles,
123
+ ];
@@ -42,41 +42,9 @@ export const createElementsFromClipboardDataCommand = (ctx, next) => {
42
42
  // born with its final index: written afterwards, each index moved an
43
43
  // element already on the board, through the layer manager, one by one.
44
44
  const newIndexes = pastedIndexes(std, elementsRawData, oldParents, context.originalIndexes);
45
- const blockIndexes = new Map();
46
- // What each block's snapshot asked for, read before its config creates
47
- // it; applied in the final transaction (`keepSourceLayerAndHide`).
48
- const blockRequests = new Map();
49
- // Canvas elements are written in as few transactions as the blocks allow,
50
- // so a peer receives one update for a run of them instead of one per
51
- // element (plus one per index fix-up). A block cannot join: its config's
52
- // `createBlock` may be async (notes, texts, images and attachments go
53
- // through the transformer, which awaits), and a Yjs transaction cannot
54
- // span an `await`. So the run of canvas elements before a block is written
55
- // first — the block may reference them (a frame's `childElementIds`) —
56
- // and the last run is written together with the final indexes.
57
- // ponytail: a block still costs its own transactions; a two-phase config
58
- // contract (async prepare, then a sync write) is the upgrade path.
59
- const pendingCanvasElements = [];
45
+ const items = [];
60
46
  const seen = new Set();
61
- const createPendingCanvasElements = () => {
62
- for (const data of pendingCanvasElements) {
63
- data.index = newIndexes.get(data.id) ?? data.index;
64
- const element = createCanvasElement(std, data, context, getNewXYWH(data.xywh));
65
- if (!element)
66
- continue;
67
- canvasElements.push(element);
68
- allElements.push(element);
69
- context.oldToNewIdMap.set(data.id, element.id);
70
- }
71
- pendingCanvasElements.length = 0;
72
- };
73
- // One gesture, one undo step: close whatever the previous gesture left
74
- // open in the undo manager's capture window.
75
- // ponytail: the paste's own transactions then merge only while each ends
76
- // within that window (500 ms) of the one before, so a block whose creation
77
- // takes longer splits the step; the two-phase contract above removes the
78
- // ceiling (the budget spec measures it on 406 elements).
79
- std.store.captureSync();
47
+ // Phase A: prepare, write nothing.
80
48
  for (const data of elementsRawData) {
81
49
  // Clipboard data written before `sortEdgelessElements` deduplicated can
82
50
  // list one element twice; the first copy is the one its container maps.
@@ -86,12 +54,9 @@ export const createElementsFromClipboardDataCommand = (ctx, next) => {
86
54
  const { data: blockSnapshot } = BlockSnapshotSchema.safeParse(data);
87
55
  if (!blockSnapshot) {
88
56
  assertType(data);
89
- pendingCanvasElements.push(data);
57
+ items.push({ element: data });
90
58
  continue;
91
59
  }
92
- if (pendingCanvasElements.length > 0) {
93
- std.store.transact(createPendingCanvasElements);
94
- }
95
60
  const oldId = blockSnapshot.id;
96
61
  const config = std.getOptional(EdgelessClipboardConfigIdentifier(blockSnapshot.flavour));
97
62
  if (!config)
@@ -111,24 +76,70 @@ export const createElementsFromClipboardDataCommand = (ctx, next) => {
111
76
  layer: blockSnapshot.props.layer,
112
77
  hiddenForEveryone: blockSnapshot.props.hiddenForEveryone,
113
78
  };
114
- const newId = await config.createBlock(blockSnapshot, context);
115
- if (!newId)
116
- continue;
117
- const block = std.store.getBlock(newId);
118
- if (!block)
79
+ const prepared = await config.prepare(blockSnapshot, context);
80
+ if (!prepared)
119
81
  continue;
120
- assertType(block.model);
121
- blockRequests.set(block.model, requested);
122
- blockModels.push(block.model);
123
- allElements.push(block.model);
124
- context.oldToNewIdMap.set(oldId, newId);
125
- const index = newIndexes.get(oldId);
126
- if (index)
127
- blockIndexes.set(block.model, index);
82
+ items.push({ oldId, prepared, requested });
128
83
  }
84
+ // The preparation awaited: the document may have turned readonly since.
85
+ if (std.store.readonly)
86
+ return { canvasElements, blockModels };
87
+ const written = [];
88
+ const legacy = [];
89
+ // One gesture, one undo step: close whatever the previous gesture left
90
+ // open in the undo manager's capture window. Taken after the preparation,
91
+ // so its duration no longer matters.
92
+ std.store.captureSync();
129
93
  std.store.transact(() => {
130
- createPendingCanvasElements();
131
- const parents = new Map();
94
+ for (const item of items) {
95
+ if ('element' in item) {
96
+ const data = item.element;
97
+ data.index = newIndexes.get(data.id) ?? data.index;
98
+ const element = createCanvasElement(std, data, context, getNewXYWH(data.xywh));
99
+ if (!element)
100
+ continue;
101
+ canvasElements.push(element);
102
+ allElements.push(element);
103
+ context.oldToNewIdMap.set(data.id, element.id);
104
+ continue;
105
+ }
106
+ let newId = null;
107
+ try {
108
+ newId = item.prepared.write();
109
+ }
110
+ catch (error) {
111
+ console.error(`Failed to paste block(id: ${item.oldId})`, error);
112
+ continue;
113
+ }
114
+ if (newId instanceof Promise) {
115
+ legacy.push({ block: item, id: newId });
116
+ continue;
117
+ }
118
+ if (!newId)
119
+ continue;
120
+ context.oldToNewIdMap.set(item.oldId, newId);
121
+ written.push({ ...item, newId });
122
+ }
123
+ });
124
+ const parents = new Map();
125
+ const finish = (blocks) => {
126
+ blocks.forEach(({ prepared }) => prepared.afterWrite?.());
127
+ const crud = std.get(EdgelessCRUDIdentifier);
128
+ for (const { oldId, newId, requested } of blocks) {
129
+ const block = std.store.getBlock(newId);
130
+ if (!block)
131
+ continue;
132
+ assertType(block.model);
133
+ blockModels.push(block.model);
134
+ allElements.push(block.model);
135
+ // A block config chooses its own index (a frame takes the top of the
136
+ // board, a note the transformer's): set the planned one afterwards.
137
+ const index = newIndexes.get(oldId);
138
+ if (index && block.model.index !== index) {
139
+ crud.updateElement(block.model.id, { index });
140
+ }
141
+ keepSourceLayerAndHide(std, block.model, requested);
142
+ }
132
143
  oldParents.forEach((oldParent, oldChild) => {
133
144
  const child = context.oldToNewIdMap.get(oldChild);
134
145
  const parent = context.oldToNewIdMap.get(oldParent);
@@ -136,15 +147,21 @@ export const createElementsFromClipboardDataCommand = (ctx, next) => {
136
147
  parents.set(child, parent);
137
148
  });
138
149
  releaseChildrenFromAdoptingFrames(std, allElements, parents);
139
- // A block config chooses its own index (a frame takes the top of the
140
- // board, a note the transformer's): set the planned one afterwards.
141
- const crud = std.get(EdgelessCRUDIdentifier);
142
- blockIndexes.forEach((index, block) => {
143
- if (block.index !== index)
144
- crud.updateElement(block.id, { index });
145
- });
146
- blockRequests.forEach((requested, block) => keepSourceLayerAndHide(std, block, requested));
147
- });
150
+ };
151
+ std.store.transact(() => finish(written));
152
+ if (legacy.length > 0) {
153
+ console.warn('EdgelessClipboardConfig: an async createBlock wrote after the paste; implement prepare() to keep the paste one undo step (ADR 0042).');
154
+ const late = [];
155
+ for (const { block, id } of legacy) {
156
+ const newId = await id;
157
+ if (!newId)
158
+ continue;
159
+ context.oldToNewIdMap.set(block.oldId, newId);
160
+ late.push({ ...block, newId });
161
+ }
162
+ if (!std.store.readonly)
163
+ std.store.transact(() => finish(late));
164
+ }
148
165
  return {
149
166
  canvasElements: canvasElements,
150
167
  blockModels: blockModels,
@@ -159,16 +176,16 @@ export const createElementsFromClipboardDataCommand = (ctx, next) => {
159
176
  * `crud.addElement`, which forwards every serialized prop (ADR 0031 §6 and
160
177
  * "What stays loadable"): its `layer` when it names a layer of this surface,
161
178
  * else the active one, and its `hiddenForEveryone`. Most
162
- * `EdgelessClipboardConfig.createBlock` rebuild the block from a handful of
179
+ * `EdgelessClipboardConfig.prepare` rebuild the block from a handful of
163
180
  * the snapshot's props (frame, image, attachment, bookmark, the embeds…), so
164
181
  * a duplicate fell in the active layer and came back visible. The rule is
165
182
  * applied once here, for every config, rather than in each of them; nothing
166
183
  * is written when the config already carried both.
167
184
  *
168
- * `requested` is read BEFORE `createBlock`: the snapshot-pasting configs
185
+ * `requested` is read BEFORE `prepare`: the snapshot-pasting configs
169
186
  * stamp `snapshot.props` in place, and an explicit default-layer request from
170
187
  * a clone gesture (`prepareCloneData`) is gone from it afterwards. Called
171
- * inside the paste's final transaction, beside the index fix-ups, so it adds
188
+ * inside the paste's second transaction, beside the index fix-ups, so it adds
172
189
  * no update of its own.
173
190
  */
174
191
  function keepSourceLayerAndHide(std, model, requested) {
@@ -261,6 +261,8 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
261
261
  },
262
262
  'Alt-2': ctx => {
263
263
  ctx.get('defaultState').event.preventDefault();
264
+ if (this.rootComponent.service.locked)
265
+ return;
264
266
  const selectedElements = this.gfx.selection.selectedElements;
265
267
  if (selectedElements.length === 0) {
266
268
  return;
@@ -1,6 +1,7 @@
1
1
  export * from './adapters/index.js';
2
2
  export * from './clipboard/index.js';
3
3
  export * from './commands/copy-link-commands.js';
4
+ export * from './commands/invisible-characters-commands.js';
4
5
  export * from './commands/page-export-commands.js';
5
6
  export * from './commands/pivot-commands.js';
6
7
  export * from './commands/reading-commands.js';
@@ -1,6 +1,7 @@
1
1
  export * from './adapters/index.js';
2
2
  export * from './clipboard/index.js';
3
3
  export * from './commands/copy-link-commands.js';
4
+ export * from './commands/invisible-characters-commands.js';
4
5
  export * from './commands/page-export-commands.js';
5
6
  export * from './commands/pivot-commands.js';
6
7
  export * from './commands/reading-commands.js';
@@ -43,6 +43,12 @@ export declare const ROOT_TOOLBAR_TURN_INTO_LINKED_DOC: ChromeWording;
43
43
  export declare const ROOT_TOOLBAR_EDIT_LINK: ChromeWording;
44
44
  export declare const ROOT_TOOLBAR_REMOVE_LINK: ChromeWording;
45
45
  export declare const ROOT_TOAST_ZOOM_TO_SELECTION: ChromeWording;
46
+ /**
47
+ * The toast `text.invisibles.remove` shows through the notification seam.
48
+ * `{{count}}` is filled by the host's catalogue, which pluralises on it; the
49
+ * English fallback stays neutral.
50
+ */
51
+ export declare const ROOT_TOAST_INVISIBLES_REMOVED: ChromeWording;
46
52
  /**
47
53
  * The Tab/Enter mindmap shortcuts' seed for a freshly added node — the same
48
54
  * word `MindmapElementModel.addNode`'s own default falls back to (that
@@ -117,6 +117,15 @@ export const ROOT_TOAST_ZOOM_TO_SELECTION = [
117
117
  'com.labre.root.toast.zoom-to-selection',
118
118
  'Zoom to selection',
119
119
  ];
120
+ /**
121
+ * The toast `text.invisibles.remove` shows through the notification seam.
122
+ * `{{count}}` is filled by the host's catalogue, which pluralises on it; the
123
+ * English fallback stays neutral.
124
+ */
125
+ export const ROOT_TOAST_INVISIBLES_REMOVED = [
126
+ 'com.labre.command.text.invisibles.removed',
127
+ '{{count}} invisible characters removed',
128
+ ];
120
129
  /**
121
130
  * The Tab/Enter mindmap shortcuts' seed for a freshly added node — the same
122
131
  * word `MindmapElementModel.addNode`'s own default falls back to (that
@@ -162,6 +171,7 @@ export const ROOT_CHROME_WORDINGS = [
162
171
  ROOT_TOOLBAR_TURN_INTO_LINKED_DOC,
163
172
  ROOT_TOOLBAR_EDIT_LINK,
164
173
  ROOT_TOAST_ZOOM_TO_SELECTION,
174
+ ROOT_TOAST_INVISIBLES_REMOVED,
165
175
  ];
166
176
  /** This package's own seed, joined into `PACKAGE_SEED_WORDINGS`. */
167
177
  export const ROOT_SEED_WORDINGS = [