@formicoidea/labre-core 0.46.1 → 0.47.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 (72) hide show
  1. package/dist/_pkgs/affine-block-bookmark/bookmark-block.js +5 -2
  2. package/dist/_pkgs/affine-block-data-view/columns/index.d.ts +1 -1
  3. package/dist/_pkgs/affine-block-database/data-source.d.ts +1 -1
  4. package/dist/_pkgs/affine-block-database/properties/converts.d.ts +4 -4
  5. package/dist/_pkgs/affine-block-database/properties/index.d.ts +1 -1
  6. package/dist/_pkgs/affine-block-database/properties/link/cell-renderer.js +2 -2
  7. package/dist/_pkgs/affine-block-embed/embed-figma-block/embed-figma-block.js +3 -1
  8. package/dist/_pkgs/affine-block-embed/embed-github-block/embed-github-block.js +3 -1
  9. package/dist/_pkgs/affine-block-embed/embed-loom-block/embed-loom-block.js +3 -1
  10. package/dist/_pkgs/affine-block-embed/embed-youtube-block/embed-youtube-block.js +3 -1
  11. package/dist/_pkgs/affine-block-frame/commands.js +41 -1
  12. package/dist/_pkgs/affine-block-frame/frame-manager.d.ts +1 -1
  13. package/dist/_pkgs/affine-block-frame/view.js +7 -2
  14. package/dist/_pkgs/affine-block-note/components/edgeless-page-block-title.d.ts +7 -0
  15. package/dist/_pkgs/affine-block-note/components/edgeless-page-block-title.js +15 -1
  16. package/dist/_pkgs/affine-block-surface/extensions/canvas-grid/commands.js +20 -6
  17. package/dist/_pkgs/affine-block-surface/extensions/export-manager/commands.d.ts +2 -0
  18. package/dist/_pkgs/affine-block-surface/extensions/export-manager/commands.js +79 -0
  19. package/dist/_pkgs/affine-block-surface/extensions/export-manager/export-manager.d.ts +18 -1
  20. package/dist/_pkgs/affine-block-surface/extensions/export-manager/export-manager.js +53 -30
  21. package/dist/_pkgs/affine-block-surface/extensions/export-manager/index.d.ts +1 -0
  22. package/dist/_pkgs/affine-block-surface/extensions/export-manager/index.js +1 -0
  23. package/dist/_pkgs/affine-block-surface/extensions/reading-toolbar.js +12 -12
  24. package/dist/_pkgs/affine-block-surface/view.js +11 -2
  25. package/dist/_pkgs/affine-components/highlight-dropdown-menu/highlight-duotone-icon.js +8 -8
  26. package/dist/_pkgs/affine-components/highlight-dropdown-menu/text-duotone-icon.js +11 -11
  27. package/dist/_pkgs/affine-components/link-preview/link.js +3 -3
  28. package/dist/_pkgs/affine-fragment-doc-title/doc-title.d.ts +8 -0
  29. package/dist/_pkgs/affine-fragment-doc-title/doc-title.js +65 -4
  30. package/dist/_pkgs/affine-fragment-doc-title/effects.d.ts +2 -0
  31. package/dist/_pkgs/affine-fragment-doc-title/effects.js +2 -0
  32. package/dist/_pkgs/affine-fragment-doc-title/index.d.ts +2 -0
  33. package/dist/_pkgs/affine-fragment-doc-title/index.js +2 -0
  34. package/dist/_pkgs/affine-fragment-doc-title/title-band/format.d.ts +69 -0
  35. package/dist/_pkgs/affine-fragment-doc-title/title-band/format.js +182 -0
  36. package/dist/_pkgs/affine-fragment-doc-title/title-band/title-band.d.ts +62 -0
  37. package/dist/_pkgs/affine-fragment-doc-title/title-band/title-band.js +649 -0
  38. package/dist/_pkgs/affine-fragment-doc-title/translations.d.ts +21 -0
  39. package/dist/_pkgs/affine-fragment-doc-title/translations.js +58 -0
  40. package/dist/_pkgs/affine-inline-footnote/footnote-node/footnote-node.js +3 -0
  41. package/dist/_pkgs/affine-inline-link/adapters/markdown/markdown-inline.js +5 -0
  42. package/dist/_pkgs/affine-inline-link/link-node/affine-link.js +6 -3
  43. package/dist/_pkgs/affine-shared/services/index.d.ts +2 -0
  44. package/dist/_pkgs/affine-shared/services/index.js +2 -0
  45. package/dist/_pkgs/affine-shared/services/properties-panel-service.d.ts +50 -0
  46. package/dist/_pkgs/affine-shared/services/properties-panel-service.js +17 -0
  47. package/dist/_pkgs/affine-shared/services/title-band-service.d.ts +124 -0
  48. package/dist/_pkgs/affine-shared/services/title-band-service.js +17 -0
  49. package/dist/_pkgs/affine-shared/utils/url.d.ts +15 -0
  50. package/dist/_pkgs/affine-shared/utils/url.js +30 -0
  51. package/dist/_pkgs/affine-widget-edgeless-selected-rect/edgeless-element-link.js +2 -2
  52. package/dist/_pkgs/affine-widget-edgeless-zoom-toolbar/index.js +26 -26
  53. package/dist/_pkgs/affine-widget-keyboard-toolbar/icons.js +98 -98
  54. package/dist/_pkgs/affine-widget-page-dragging-area/index.js +10 -10
  55. package/dist/_pkgs/affine-widget-viewport-overlay/index.js +13 -13
  56. package/dist/_pkgs/data-view/core/utils/menu-title.js +17 -17
  57. package/dist/_pkgs/data-view/property-presets/converts.d.ts +2 -2
  58. package/dist/_pkgs/data-view/property-presets/index.d.ts +1 -1
  59. package/dist/_pkgs/data-view/property-presets/number/cell-renderer.d.ts +1 -1
  60. package/dist/_pkgs/data-view/property-presets/number/define.d.ts +1 -1
  61. package/dist/_pkgs/data-view/property-presets/number/types.d.ts +2 -2
  62. package/dist/_pkgs/data-view/property-presets/pure-index.d.ts +2 -1
  63. package/dist/_pkgs/data-view/property-presets/pure-index.js +4 -0
  64. package/dist/_pkgs/data-view/view-presets/table/pc-virtual/group/top/group-title.js +18 -18
  65. package/dist/_pkgs/std/extension/command-registry.d.ts +50 -1
  66. package/dist/_pkgs/std/extension/command-registry.js +26 -7
  67. package/dist/commands.js +2 -1
  68. package/dist/host-panels.d.ts +83 -2
  69. package/dist/host-panels.js +82 -1
  70. package/package.json +1 -1
  71. package/dist/_pkgs/affine-block-surface/extensions/export-manager/file-exporter.d.ts +0 -26
  72. package/dist/_pkgs/affine-block-surface/extensions/export-manager/file-exporter.js +0 -73
@@ -35,7 +35,7 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
35
35
  import { CaptionedBlockComponent, SelectedStyle, } from '../affine-components/caption/index.js';
36
36
  import { ImageProxyService } from '../affine-shared/adapters/index.js';
37
37
  import { BlockCommentManager, CitationProvider, DocModeProvider, LinkPreviewServiceIdentifier, } from '../affine-shared/services/index.js';
38
- import { normalizeUrl, shouldRefreshOnPropsUpdate, } from '../affine-shared/utils/index.js';
38
+ import { isSafeLinkUrl, normalizeUrl, shouldRefreshOnPropsUpdate, } from '../affine-shared/utils/index.js';
39
39
  import { BlockSelection } from '../std/index.js';
40
40
  import { computed, signal } from '@preact/signals-core';
41
41
  import { html } from 'lit';
@@ -122,7 +122,10 @@ let BookmarkBlockComponent = (() => {
122
122
  selectionManager.setGroup('note', [blockSelection]);
123
123
  };
124
124
  this.open = () => {
125
- window.open(this.link, '_blank');
125
+ const { link } = this;
126
+ if (!isSafeLinkUrl(link))
127
+ return;
128
+ window.open(link, '_blank');
126
129
  };
127
130
  this.refreshData = () => {
128
131
  refreshBookmarkUrlData(this, this._fetchAbortController?.signal).catch(console.error);
@@ -6,7 +6,7 @@ export declare const queryBlockColumns: (PropertyMetaConfig<"checkbox", {}, bool
6
6
  color: string;
7
7
  }[];
8
8
  }, string[], string[]> | PropertyMetaConfig<"number", {
9
- format: "number" | "numberWithCommas" | "percent" | "currencyYen" | "currencyINR" | "currencyCNY" | "currencyUSD" | "currencyEUR" | "currencyGBP";
9
+ format: "number" | "percent" | "numberWithCommas" | "currencyINR" | "currencyYen" | "currencyCNY" | "currencyUSD" | "currencyEUR" | "currencyGBP";
10
10
  decimal?: number | undefined;
11
11
  }, number | null, number | null> | PropertyMetaConfig<"progress", {}, number, number> | PropertyMetaConfig<"select", {
12
12
  options: {
@@ -25,7 +25,7 @@ export declare class DatabaseBlockDataSource extends ExternalDataSourceBase {
25
25
  color: string;
26
26
  }[];
27
27
  }, string[], string[]> | PropertyMetaConfig<"number", {
28
- format: "number" | "numberWithCommas" | "percent" | "currencyYen" | "currencyINR" | "currencyCNY" | "currencyUSD" | "currencyEUR" | "currencyGBP";
28
+ format: "number" | "percent" | "numberWithCommas" | "currencyINR" | "currencyYen" | "currencyCNY" | "currencyUSD" | "currencyEUR" | "currencyGBP";
29
29
  decimal?: number | undefined;
30
30
  }, number | null, number | null> | PropertyMetaConfig<"progress", {}, number, number> | PropertyMetaConfig<"select", {
31
31
  options: {
@@ -18,14 +18,14 @@ export declare const databasePropertyConverts: ({
18
18
  from: any;
19
19
  to: any;
20
20
  convert: import("../index.js").ConvertFunction<import("../index.js").PropertyModel<"number", {
21
- format: "number" | "numberWithCommas" | "percent" | "currencyYen" | "currencyINR" | "currencyCNY" | "currencyUSD" | "currencyEUR" | "currencyGBP";
21
+ format: "number" | "percent" | "numberWithCommas" | "currencyINR" | "currencyYen" | "currencyCNY" | "currencyUSD" | "currencyEUR" | "currencyGBP";
22
22
  decimal?: number | undefined;
23
23
  }, number | null, number | null>, import("../index.js").PropertyModel<"progress", {}, number, number>>;
24
24
  } | {
25
25
  from: any;
26
26
  to: any;
27
27
  convert: import("../index.js").ConvertFunction<import("../index.js").PropertyModel<"progress", {}, number, number>, import("../index.js").PropertyModel<"number", {
28
- format: "number" | "numberWithCommas" | "percent" | "currencyYen" | "currencyINR" | "currencyCNY" | "currencyUSD" | "currencyEUR" | "currencyGBP";
28
+ format: "number" | "percent" | "numberWithCommas" | "currencyINR" | "currencyYen" | "currencyCNY" | "currencyUSD" | "currencyEUR" | "currencyGBP";
29
29
  decimal?: number | undefined;
30
30
  }, number | null, number | null>>;
31
31
  } | {
@@ -68,7 +68,7 @@ export declare const databasePropertyConverts: ({
68
68
  from: any;
69
69
  to: any;
70
70
  convert: import("../index.js").ConvertFunction<import("../index.js").PropertyModel<"rich-text", {}, import("./rich-text/define.js").RichTextCellType | undefined, string>, import("../index.js").PropertyModel<"number", {
71
- format: "number" | "numberWithCommas" | "percent" | "currencyYen" | "currencyINR" | "currencyCNY" | "currencyUSD" | "currencyEUR" | "currencyGBP";
71
+ format: "number" | "percent" | "numberWithCommas" | "currencyINR" | "currencyYen" | "currencyCNY" | "currencyUSD" | "currencyEUR" | "currencyGBP";
72
72
  decimal?: number | undefined;
73
73
  }, number | null, number | null>>;
74
74
  } | {
@@ -97,7 +97,7 @@ export declare const databasePropertyConverts: ({
97
97
  from: any;
98
98
  to: any;
99
99
  convert: import("../index.js").ConvertFunction<import("../index.js").PropertyModel<"number", {
100
- format: "number" | "numberWithCommas" | "percent" | "currencyYen" | "currencyINR" | "currencyCNY" | "currencyUSD" | "currencyEUR" | "currencyGBP";
100
+ format: "number" | "percent" | "numberWithCommas" | "currencyINR" | "currencyYen" | "currencyCNY" | "currencyUSD" | "currencyEUR" | "currencyGBP";
101
101
  decimal?: number | undefined;
102
102
  }, number | null, number | null>, import("../index.js").PropertyModel<"rich-text", {}, import("./rich-text/define.js").RichTextCellType | undefined, string>>;
103
103
  } | {
@@ -10,7 +10,7 @@ export declare const databaseBlockProperties: {
10
10
  }[];
11
11
  }, string[], string[]>;
12
12
  numberColumnConfig: import("../index.js").PropertyMetaConfig<"number", {
13
- format: "number" | "numberWithCommas" | "percent" | "currencyYen" | "currencyINR" | "currencyCNY" | "currencyUSD" | "currencyEUR" | "currencyGBP";
13
+ format: "number" | "percent" | "numberWithCommas" | "currencyINR" | "currencyYen" | "currencyCNY" | "currencyUSD" | "currencyEUR" | "currencyGBP";
14
14
  decimal?: number | undefined;
15
15
  }, number | null, number | null>;
16
16
  progressColumnConfig: import("../index.js").PropertyMetaConfig<"progress", {}, number, number>;
@@ -1,6 +1,6 @@
1
1
  import { RefNodeSlotsProvider } from '../../../affine-inline-reference/index.js';
2
2
  import { ParseDocUrlProvider } from '../../../affine-shared/services/index.js';
3
- import { isValidUrl, normalizeUrl, stopPropagation, } from '../../../affine-shared/utils/index.js';
3
+ import { isSafeLinkUrl, isValidUrl, normalizeUrl, stopPropagation, } from '../../../affine-shared/utils/index.js';
4
4
  import { BaseCellRenderer, createFromBaseCellRenderer, createIcon, } from '../../../data-view/index.js';
5
5
  import { EditIcon } from '@blocksuite/icons/lit';
6
6
  import { computed } from '@preact/signals-core';
@@ -101,7 +101,7 @@ export class LinkCell extends BaseCellRenderer {
101
101
  ? html `<span class="${linkedDocStyle}" @click="${this.openDoc}"
102
102
  >${docName}</span
103
103
  >`
104
- : isValidUrl(linkText)
104
+ : isValidUrl(linkText) && isSafeLinkUrl(linkText)
105
105
  ? html `<a
106
106
  data-testid="property-link-a"
107
107
  class="${inlineLinkNodeStyle}"
@@ -1,5 +1,5 @@
1
1
  import { OpenIcon } from '../../affine-components/icons/index.js';
2
- import { shouldRefreshOnPropsUpdate } from '../../affine-shared/utils/index.js';
2
+ import { isSafeLinkUrl, shouldRefreshOnPropsUpdate, } from '../../affine-shared/utils/index.js';
3
3
  import { BlockSelection } from '../../std/index.js';
4
4
  import { html, nothing } from 'lit';
5
5
  import { classMap } from 'lit/directives/class-map.js';
@@ -15,6 +15,8 @@ export class EmbedFigmaBlockComponent extends EmbedBlockComponent {
15
15
  if (!link.match(/^[a-zA-Z]+:\/\//)) {
16
16
  link = 'https://' + link;
17
17
  }
18
+ if (!isSafeLinkUrl(link))
19
+ return;
18
20
  window.open(link, '_blank');
19
21
  };
20
22
  this.refreshData = () => { };
@@ -35,7 +35,7 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
35
35
  import { LoadingIcon, OpenIcon } from '../../affine-components/icons/index.js';
36
36
  import { ImageProxyService } from '../../affine-shared/adapters/index.js';
37
37
  import { ThemeProvider } from '../../affine-shared/services/index.js';
38
- import { shouldRefreshOnPropsUpdate } from '../../affine-shared/utils/index.js';
38
+ import { isSafeLinkUrl, shouldRefreshOnPropsUpdate, } from '../../affine-shared/utils/index.js';
39
39
  import { BlockSelection, isGfxBlockComponent } from '../../std/index.js';
40
40
  import { html, nothing } from 'lit';
41
41
  import { property } from 'lit/decorators.js';
@@ -249,6 +249,8 @@ let EmbedGithubBlockComponent = (() => {
249
249
  if (!link.match(/^[a-zA-Z]+:\/\//)) {
250
250
  link = 'https://' + link;
251
251
  }
252
+ if (!isSafeLinkUrl(link))
253
+ return;
252
254
  window.open(link, '_blank');
253
255
  };
254
256
  this.refreshData = () => {
@@ -35,7 +35,7 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
35
35
  import { LoadingIcon, OpenIcon } from '../../affine-components/icons/index.js';
36
36
  import { ImageProxyService } from '../../affine-shared/adapters/index.js';
37
37
  import { ThemeProvider } from '../../affine-shared/services/index.js';
38
- import { shouldRefreshOnPropsUpdate } from '../../affine-shared/utils/index.js';
38
+ import { isSafeLinkUrl, shouldRefreshOnPropsUpdate, } from '../../affine-shared/utils/index.js';
39
39
  import { BlockSelection } from '../../std/index.js';
40
40
  import { html } from 'lit';
41
41
  import { property } from 'lit/decorators.js';
@@ -186,6 +186,8 @@ let EmbedLoomBlockComponent = (() => {
186
186
  if (!link.match(/^[a-zA-Z]+:\/\//)) {
187
187
  link = 'https://' + link;
188
188
  }
189
+ if (!isSafeLinkUrl(link))
190
+ return;
189
191
  window.open(link, '_blank');
190
192
  };
191
193
  this.refreshData = () => {
@@ -35,7 +35,7 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
35
35
  import { LoadingIcon, OpenIcon } from '../../affine-components/icons/index.js';
36
36
  import { ImageProxyService } from '../../affine-shared/adapters/index.js';
37
37
  import { ThemeProvider } from '../../affine-shared/services/index.js';
38
- import { shouldRefreshOnPropsUpdate } from '../../affine-shared/utils/index.js';
38
+ import { isSafeLinkUrl, shouldRefreshOnPropsUpdate, } from '../../affine-shared/utils/index.js';
39
39
  import { BlockSelection } from '../../std/index.js';
40
40
  import { html, nothing } from 'lit';
41
41
  import { property, state } from 'lit/decorators.js';
@@ -232,6 +232,8 @@ let EmbedYoutubeBlockComponent = (() => {
232
232
  if (!link.match(/^[a-zA-Z]+:\/\//)) {
233
233
  link = 'https://' + link;
234
234
  }
235
+ if (!isSafeLinkUrl(link))
236
+ return;
235
237
  window.open(link, '_blank');
236
238
  };
237
239
  this.refreshData = () => {
@@ -1,5 +1,8 @@
1
+ import { EditPropsStore } from '../affine-shared/services/index.js';
2
+ import { GfxControllerIdentifier } from '../std/gfx/index.js';
1
3
  import { z } from 'zod';
2
4
  import { reorderFramePresentation } from './frame-order.js';
5
+ import { PresentTool } from './present-tool.js';
3
6
  /**
4
7
  * The frames' commands, `owner: 'core'`: a frame belongs to no framework, and
5
8
  * `'core'` owners are exempt from the id-prefix rule (ADR 0008).
@@ -46,4 +49,41 @@ const reorderFrames = {
46
49
  reorderFramePresentation(std, parsed.data.ids, parsed.data.before);
47
50
  },
48
51
  };
49
- export const frameCommands = [reorderFrames];
52
+ /**
53
+ * Start the slideshow (ADR 0034, amendment of 2026-10-13): the frames in
54
+ * presentation order, full screen, Esc to leave — the presentation tool owns
55
+ * all of it from here. The entry a host's view menu (or its header's
56
+ * "Present" button) runs; a document without frames gets the tool's own
57
+ * "no frame" notice.
58
+ *
59
+ * Edgeless only, like the tool it arms: a host showing the page view switches
60
+ * to the canvas with its OWN mode toggle first (it owns the toggle's state and
61
+ * the persisted mode), then runs this on the new editor scope once the canvas
62
+ * has mounted. It reads, so a reader may present. The fit or fill choice is the
63
+ * viewer's, stored by the frame panel's setting (`presentFillScreen`).
64
+ */
65
+ const present = {
66
+ id: 'canvas.present',
67
+ owner: 'core',
68
+ kind: 'action',
69
+ labelKey: 'com.labre.command.canvas.present',
70
+ labelFallback: 'Present',
71
+ descriptionKey: 'com.labre.command.canvas.present.description',
72
+ descriptionFallback: 'Show the frames one after the other, full screen, in presentation order.',
73
+ category: 'present',
74
+ iconKey: 'canvas.present',
75
+ keywords: ['present', 'slides', 'slideshow', 'frames', 'presentation'],
76
+ surfaces: ['view-menu', 'palette'],
77
+ order: 20,
78
+ scope: 'edgeless',
79
+ defaultKeys: { mac: [], other: [] },
80
+ run: std => {
81
+ const fill = std
82
+ .getOptional(EditPropsStore)
83
+ ?.getStorage('presentFillScreen');
84
+ std.get(GfxControllerIdentifier).tool.setTool(PresentTool, {
85
+ mode: fill ? 'fill' : 'fit',
86
+ });
87
+ },
88
+ };
89
+ export const frameCommands = [reorderFrames, present];
@@ -35,7 +35,7 @@ export declare class EdgelessFrameManager extends GfxExtension {
35
35
  index: string;
36
36
  presentationIndex?: string;
37
37
  };
38
- }>(a: T, b: T): 1 | 0 | -1;
38
+ }>(a: T, b: T): 0 | 1 | -1;
39
39
  private _addChildrenToLegacyFrame;
40
40
  private _addFrameBlock;
41
41
  /**
@@ -1,3 +1,4 @@
1
+ import { PresentationIcon } from '@blocksuite/icons/lit';
1
2
  import { ViewExtensionProvider, } from '../affine-ext-loader/index.js';
2
3
  import { CommandExtension } from '../std/index.js';
3
4
  import { frameCommands } from './commands.js';
@@ -34,8 +35,12 @@ export class FrameViewExtension extends ViewExtensionProvider {
34
35
  context.register(FrameBlockInteraction);
35
36
  // The presentation order's one write (ADR 0034). The frame panel also
36
37
  // mounts in page mode: it runs the imported descriptor, not this
37
- // registration, so its reorder works in both modes.
38
- context.register(CommandExtension(frameCommands));
38
+ // registration, so its reorder works in both modes. `canvas.present`
39
+ // rides along: a host's view menu lists it in edgeless mode, and a
40
+ // `frame: false` assembly loses it with the rest of the frame tooling.
41
+ context.register(CommandExtension(frameCommands, {
42
+ 'canvas.present': PresentationIcon(),
43
+ }));
39
44
  }
40
45
  }
41
46
  }
@@ -1,6 +1,13 @@
1
1
  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
+ /**
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`.
10
+ */
4
11
  export declare class EdgelessPageBlockTitle extends EdgelessPageBlockTitle_base {
5
12
  render(): import("lit-html").TemplateResult<1> | undefined;
6
13
  accessor std: BlockStdScope;
@@ -40,6 +40,13 @@ import { html } from 'lit';
40
40
  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
+ /**
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`.
49
+ */
43
50
  let EdgelessPageBlockTitle = (() => {
44
51
  let _classDecorators = [requiredProperties({
45
52
  note: PropTypes.instanceOf(NoteBlockModel),
@@ -76,7 +83,14 @@ let EdgelessPageBlockTitle = (() => {
76
83
  note: this.note,
77
84
  std: this.std,
78
85
  });
79
- return html `<div class=${styles.pageBlockTitle}>${title}</div>`;
86
+ return html `<div class=${styles.pageBlockTitle}>
87
+ ${title}
88
+ <labre-title-band
89
+ .std=${this.std}
90
+ .docId=${this.std.store.id}
91
+ .surface=${'canvas'}
92
+ ></labre-title-band>
93
+ </div>`;
80
94
  }
81
95
  #std_accessor_storage = __runInitializers(this, _std_initializers, void 0);
82
96
  get std() { return this.#std_accessor_storage; }
@@ -1,9 +1,10 @@
1
1
  import { CanvasGrid, TelemetryProvider } from '../../../affine-shared/services/index.js';
2
2
  /**
3
3
  * The canvas grid's two commands (ADR 0031 §11, §12), `owner: 'core'`, on the
4
- * palette and the agent. The library has no page "more" menu — that belongs
5
- * to the host — so these are the grid's whole UI here; a host puts them in
6
- * its own menu through `runCommand`.
4
+ * palette, the agent and the host's view menu (`'view-menu'`, ADR 0034
5
+ * amendment of 2026-10-13). The library has no page "more" menu — that
6
+ * belongs to the host — so these are the grid's whole UI here; a host lists
7
+ * them with `getViewActions` and runs them through `runCommand`.
7
8
  *
8
9
  * Both self-emit `CanvasGridToggled` (see the telemetry README): `scope` is
9
10
  * which gesture ran, and a save that changes nothing reports nothing.
@@ -27,11 +28,17 @@ const toggleGrid = {
27
28
  labelFallback: 'Show or hide the grid',
28
29
  descriptionKey: 'com.labre.command.canvas.grid.toggle.description',
29
30
  descriptionFallback: 'Show or hide the canvas grid on your screen only. Nothing changes for anyone else.',
31
+ category: 'display',
32
+ iconKey: 'canvas.grid',
30
33
  keywords: ['grid', 'dots', 'background'],
31
- surfaces: ['palette', 'agent'],
34
+ surfaces: ['palette', 'agent', 'view-menu'],
35
+ order: 0,
32
36
  scope: 'edgeless',
33
37
  defaultKeys: { mac: [], other: [] },
34
38
  when: std => !!std.getOptional(CanvasGrid),
39
+ // What this viewer sees now. A signal read, so a menu that reads it inside
40
+ // an effect ticks and unticks with the grid, whoever flips it.
41
+ checked: std => std.getOptional(CanvasGrid)?.visible$.value ?? false,
35
42
  run: std => {
36
43
  const grid = std.getOptional(CanvasGrid);
37
44
  if (!grid)
@@ -52,12 +59,19 @@ const saveGridForEveryone = {
52
59
  labelFallback: 'Save the grid for everyone',
53
60
  descriptionKey: 'com.labre.command.canvas.grid.save-for-everyone.description',
54
61
  descriptionFallback: 'Make the grid as you see it now the setting of this document, for everyone who opens it.',
62
+ category: 'display',
55
63
  keywords: ['grid', 'dots', 'background', 'document'],
56
- surfaces: ['palette', 'agent'],
64
+ surfaces: ['palette', 'agent', 'view-menu'],
65
+ order: 1,
57
66
  scope: 'edgeless',
58
67
  defaultKeys: { mac: [], other: [] },
59
68
  availability: 'editable',
60
- when: std => !!std.getOptional(CanvasGrid),
69
+ // Offered once this viewer has a grid of their own (`source$` is
70
+ // `'local'`, i.e. after a toggle): that is what the gesture saves, and the
71
+ // follow-up the hosts' menus already showed after the toggle. A signal
72
+ // read, so a view menu read inside an effect shows the entry right after a
73
+ // toggle and drops it after the save.
74
+ when: std => std.getOptional(CanvasGrid)?.source$.value === 'local',
61
75
  run: std => {
62
76
  const saved = std.getOptional(CanvasGrid)?.saveForEveryone() ?? null;
63
77
  if (saved === null)
@@ -0,0 +1,2 @@
1
+ import type { AnyCommandDescriptor } from '../../../std/index.js';
2
+ export declare const canvasExportCommands: AnyCommandDescriptor[];
@@ -0,0 +1,79 @@
1
+ import { downloadBlob } from '../../../affine-shared/utils/index.js';
2
+ import { ExportManager } from './export-manager.js';
3
+ /**
4
+ * "Export as PNG" and "Export as PDF" of the canvas, in the command registry
5
+ * (ADR 0034, amendment of 2026-10-13). `owner: 'core'`: the picture is the
6
+ * canvas's, whatever drew on it.
7
+ *
8
+ * On the `'view-menu'` surface because the export is proper to the canvas
9
+ * VIEW (the host's "This view" section reads them through `getViewActions`),
10
+ * and on the palette. Not `'agent'`: an agent turn would drop a file on the
11
+ * user's disk they did not ask for.
12
+ *
13
+ * The library renders and the host decides where the bytes go: given
14
+ * `params.deliver` (`CommandFileParams`), the command hands the file over and
15
+ * downloads nothing — `runViewAction` passes one and returns the file. Without
16
+ * it (the palette, a bound chord) the command downloads the file itself, which
17
+ * is what a standalone editor needs. Nothing to draw, no file, no error.
18
+ *
19
+ * It READS, so there is no read-only guard: a reader exports what they see.
20
+ * No `telemetry` field, for `export.svg`'s reason — it names no framework —
21
+ * and usage is still measured by `runCommand`.
22
+ */
23
+ function deliverOf(params) {
24
+ const deliver = params?.deliver;
25
+ return typeof deliver === 'function' ? deliver : undefined;
26
+ }
27
+ async function exportCanvas(std, render, params) {
28
+ const exporter = std.getOptional(ExportManager);
29
+ if (!exporter)
30
+ return;
31
+ const file = await render(exporter);
32
+ if (!file)
33
+ return;
34
+ const deliver = deliverOf(params);
35
+ if (deliver)
36
+ deliver(file);
37
+ else
38
+ downloadBlob(file.blob, file.filename);
39
+ }
40
+ const exportPng = {
41
+ id: 'canvas.export.png',
42
+ owner: 'core',
43
+ kind: 'action',
44
+ labelKey: 'com.labre.command.canvas.export.png',
45
+ labelFallback: 'Export as PNG',
46
+ descriptionKey: 'com.labre.command.canvas.export.png.description',
47
+ descriptionFallback: 'Download the whole canvas as an image, as you see it on your screen.',
48
+ category: 'export',
49
+ iconKey: 'canvas.export.png',
50
+ keywords: ['png', 'image', 'export', 'download', 'picture'],
51
+ surfaces: ['view-menu', 'palette'],
52
+ order: 10,
53
+ scope: 'edgeless',
54
+ defaultKeys: { mac: [], other: [] },
55
+ when: std => !!std.getOptional(ExportManager),
56
+ run: (std, _invocation, params) => exportCanvas(std, exporter => exporter.pngFile(), params),
57
+ };
58
+ const exportPdf = {
59
+ id: 'canvas.export.pdf',
60
+ owner: 'core',
61
+ kind: 'action',
62
+ labelKey: 'com.labre.command.canvas.export.pdf',
63
+ labelFallback: 'Export as PDF',
64
+ descriptionKey: 'com.labre.command.canvas.export.pdf.description',
65
+ descriptionFallback: 'Download the whole canvas as a one-page PDF holding its image.',
66
+ category: 'export',
67
+ iconKey: 'canvas.export.pdf',
68
+ keywords: ['pdf', 'export', 'download', 'print'],
69
+ surfaces: ['view-menu', 'palette'],
70
+ order: 11,
71
+ scope: 'edgeless',
72
+ defaultKeys: { mac: [], other: [] },
73
+ when: std => !!std.getOptional(ExportManager),
74
+ run: (std, _invocation, params) => exportCanvas(std, exporter => exporter.pdfFile(), params),
75
+ };
76
+ export const canvasExportCommands = [
77
+ exportPng,
78
+ exportPdf,
79
+ ];
@@ -1,5 +1,5 @@
1
1
  import type { IBound } from '../../../global/gfx/index.js';
2
- import { type BlockStdScope, type EditorHost } from '../../../std/index.js';
2
+ import { type BlockStdScope, type CommandFile, type EditorHost } from '../../../std/index.js';
3
3
  import { GfxBlockElementModel, type GfxController, GfxPrimitiveElementModel } from '../../../std/gfx/index.js';
4
4
  import type { ExtensionType, Store } from '../../../store/index.js';
5
5
  import { CanvasRenderer } from '../../renderer/canvas-renderer.js';
@@ -39,7 +39,24 @@ export declare class ExportManager {
39
39
  edgelessToCanvas(surfaceRenderer: CanvasRenderer, bound: IBound, gfx: GfxController, blocks?: GfxBlockElementModel[], elements?: GfxPrimitiveElementModel[], edgelessBackground?: {
40
40
  zoom: number;
41
41
  }): Promise<HTMLCanvasElement | undefined>;
42
+ /**
43
+ * The canvas (or, in page mode, the document) as a PNG file, or `null` when
44
+ * there is nothing to draw. Renders, never downloads: the caller decides
45
+ * where the bytes go (`canvas.export.png` hands them to a host, or to the
46
+ * browser).
47
+ */
48
+ pngFile(): Promise<CommandFile | null>;
49
+ /**
50
+ * The same picture in a one-page PDF (`pdf-lib`, loaded on demand), or
51
+ * `null` when there is nothing to draw. The PDF is an image: its text is
52
+ * not selectable.
53
+ */
54
+ pdfFile(): Promise<CommandFile | null>;
55
+ /** Render, then hand the file to the browser. Nothing to draw, no file. */
42
56
  exportPdf(): Promise<void>;
57
+ /** Render, then hand the file to the browser. Nothing to draw, no file. */
43
58
  exportPng(): Promise<void>;
59
+ /** The document title, sanitised; the translated "Untitled" without one. */
60
+ private _fileStem;
44
61
  }
45
62
  export declare const ExportManagerExtension: ExtensionType;
@@ -2,14 +2,13 @@ import { ImageBlockModel } from '../../../affine-model/index.js';
2
2
  import { FetchUtils, ImageProxyService } from '../../../affine-shared/adapters/index.js';
3
3
  import { CANVAS_EXPORT_IGNORE_TAGS } from '../../../affine-shared/consts/index.js';
4
4
  import { CanvasGrid, CHROME_UNTITLED, translateKey, } from '../../../affine-shared/services/index.js';
5
- import { isInsidePageEditor, matchModels, normalizeCanvasExportColors, toLegacyColors, } from '../../../affine-shared/utils/index.js';
5
+ import { downloadBlob, isInsidePageEditor, matchModels, normalizeCanvasExportColors, safeFilename, toLegacyColors, } from '../../../affine-shared/utils/index.js';
6
6
  import { BlockSuiteError, ErrorCode } from '../../../global/exceptions/index.js';
7
7
  import { deserializeXYWH } from '../../../global/gfx/index.js';
8
8
  import { StdIdentifier, } from '../../../std/index.js';
9
9
  import { GfxBlockElementModel, GfxControllerIdentifier, GfxPrimitiveElementModel, isGfxGroupCompatibleModel, } from '../../../std/gfx/index.js';
10
10
  import { CanvasRenderer } from '../../renderer/canvas-renderer.js';
11
11
  import { getBgGridGap } from '../../utils/get-bg-grip-gap.js';
12
- import { FileExporter } from './file-exporter.js';
13
12
  /**
14
13
  * The SVG the PNG export paints the canvas background from: the background
15
14
  * colour, plus the dotted grid unless `gridColor` is `null` — what this
@@ -379,38 +378,62 @@ export class ExportManager {
379
378
  ctx.drawImage(surfaceCanvas, 50, 50, bound.w, bound.h);
380
379
  return canvas;
381
380
  }
382
- async exportPdf() {
383
- const rootModel = this.doc.root;
384
- if (!rootModel)
385
- return;
386
- const canvasImage = await this._toCanvas();
387
- if (!canvasImage) {
388
- return;
389
- }
381
+ /**
382
+ * The canvas (or, in page mode, the document) as a PNG file, or `null` when
383
+ * there is nothing to draw. Renders, never downloads: the caller decides
384
+ * where the bytes go (`canvas.export.png` hands them to a host, or to the
385
+ * browser).
386
+ */
387
+ async pngFile() {
388
+ if (!this.doc.root)
389
+ return null;
390
+ const canvas = await this._toCanvas();
391
+ if (!canvas)
392
+ return null;
393
+ const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
394
+ if (!blob)
395
+ return null;
396
+ return { blob, filename: `${this._fileStem()}.png` };
397
+ }
398
+ /**
399
+ * The same picture in a one-page PDF (`pdf-lib`, loaded on demand), or
400
+ * `null` when there is nothing to draw. The PDF is an image: its text is
401
+ * not selectable.
402
+ */
403
+ async pdfFile() {
404
+ if (!this.doc.root)
405
+ return null;
406
+ const canvas = await this._toCanvas();
407
+ if (!canvas)
408
+ return null;
390
409
  const PDFLib = await import('pdf-lib');
391
410
  const pdfDoc = await PDFLib.PDFDocument.create();
392
- const page = pdfDoc.addPage([canvasImage.width, canvasImage.height]);
393
- const imageEmbed = await pdfDoc.embedPng(canvasImage.toDataURL('PNG'));
394
- const { width, height } = imageEmbed.scale(1);
395
- page.drawImage(imageEmbed, {
396
- x: 0,
397
- y: 0,
398
- width,
399
- height,
400
- });
401
- const pdfBase64 = await pdfDoc.saveAsBase64({ dataUri: true });
402
- FileExporter.exportFile(rootModel.props.title.toString() + '.pdf', pdfBase64);
411
+ const page = pdfDoc.addPage([canvas.width, canvas.height]);
412
+ const image = await pdfDoc.embedPng(canvas.toDataURL('image/png'));
413
+ const { width, height } = image.scale(1);
414
+ page.drawImage(image, { x: 0, y: 0, width, height });
415
+ const bytes = await pdfDoc.save();
416
+ return {
417
+ blob: new Blob([bytes], { type: 'application/pdf' }),
418
+ filename: `${this._fileStem()}.pdf`,
419
+ };
403
420
  }
421
+ /** Render, then hand the file to the browser. Nothing to draw, no file. */
422
+ async exportPdf() {
423
+ const file = await this.pdfFile();
424
+ if (file)
425
+ downloadBlob(file.blob, file.filename);
426
+ }
427
+ /** Render, then hand the file to the browser. Nothing to draw, no file. */
404
428
  async exportPng() {
405
- const rootModel = this.doc.root;
406
- if (!rootModel)
407
- return;
408
- const canvasImage = await this._toCanvas();
409
- if (!canvasImage) {
410
- return;
411
- }
412
- const title = this.doc.root.props.title.toString();
413
- FileExporter.exportPng(title.trim() || translateKey(this.std, ...CHROME_UNTITLED), canvasImage.toDataURL('image/png'));
429
+ const file = await this.pngFile();
430
+ if (file)
431
+ downloadBlob(file.blob, file.filename);
432
+ }
433
+ /** The document title, sanitised; the translated "Untitled" without one. */
434
+ _fileStem() {
435
+ const root = this.doc.root;
436
+ return safeFilename(root?.props.title.toString(), translateKey(this.std, ...CHROME_UNTITLED));
414
437
  }
415
438
  }
416
439
  export const ExportManagerExtension = {
@@ -1 +1,2 @@
1
+ export { canvasExportCommands } from './commands.js';
1
2
  export { ExportManager, ExportManagerExtension } from './export-manager.js';
@@ -1 +1,2 @@
1
+ export { canvasExportCommands } from './commands.js';
1
2
  export { ExportManager, ExportManagerExtension } from './export-manager.js';
@@ -58,18 +58,18 @@ const READ_LABEL_FALLBACK = 'Read this component';
58
58
  * Drawn inline like its neighbours: this package does not depend on
59
59
  * `@blocksuite/icons` and one glyph is not worth a new dependency.
60
60
  */
61
- const ReadIcon = html `<svg
62
- width="20"
63
- height="20"
64
- viewBox="0 0 24 24"
65
- fill="none"
66
- stroke="currentColor"
67
- stroke-width="2"
68
- stroke-linecap="round"
69
- stroke-linejoin="round"
70
- >
71
- <path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z" />
72
- <circle cx="12" cy="12" r="3" />
61
+ const ReadIcon = html `<svg
62
+ width="20"
63
+ height="20"
64
+ viewBox="0 0 24 24"
65
+ fill="none"
66
+ stroke="currentColor"
67
+ stroke-width="2"
68
+ stroke-linecap="round"
69
+ stroke-linejoin="round"
70
+ >
71
+ <path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z" />
72
+ <circle cx="12" cy="12" r="3" />
73
73
  </svg>`;
74
74
  /** The command this module drives. Spelled once. */
75
75
  const ELEMENT_READ_ID = 'element.read';