@formicoidea/labre-core 0.46.2 → 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 (50) hide show
  1. package/dist/_pkgs/affine-block-data-view/columns/index.d.ts +1 -1
  2. package/dist/_pkgs/affine-block-database/data-source.d.ts +1 -1
  3. package/dist/_pkgs/affine-block-database/properties/converts.d.ts +4 -4
  4. package/dist/_pkgs/affine-block-database/properties/index.d.ts +1 -1
  5. package/dist/_pkgs/affine-block-frame/commands.js +41 -1
  6. package/dist/_pkgs/affine-block-frame/frame-manager.d.ts +1 -1
  7. package/dist/_pkgs/affine-block-frame/view.js +7 -2
  8. package/dist/_pkgs/affine-block-note/components/edgeless-page-block-title.d.ts +7 -0
  9. package/dist/_pkgs/affine-block-note/components/edgeless-page-block-title.js +15 -1
  10. package/dist/_pkgs/affine-block-surface/extensions/canvas-grid/commands.js +20 -6
  11. package/dist/_pkgs/affine-block-surface/extensions/export-manager/commands.d.ts +2 -0
  12. package/dist/_pkgs/affine-block-surface/extensions/export-manager/commands.js +79 -0
  13. package/dist/_pkgs/affine-block-surface/extensions/export-manager/export-manager.d.ts +18 -1
  14. package/dist/_pkgs/affine-block-surface/extensions/export-manager/export-manager.js +53 -30
  15. package/dist/_pkgs/affine-block-surface/extensions/export-manager/index.d.ts +1 -0
  16. package/dist/_pkgs/affine-block-surface/extensions/export-manager/index.js +1 -0
  17. package/dist/_pkgs/affine-block-surface/view.js +11 -2
  18. package/dist/_pkgs/affine-fragment-doc-title/doc-title.d.ts +8 -0
  19. package/dist/_pkgs/affine-fragment-doc-title/doc-title.js +65 -4
  20. package/dist/_pkgs/affine-fragment-doc-title/effects.d.ts +2 -0
  21. package/dist/_pkgs/affine-fragment-doc-title/effects.js +2 -0
  22. package/dist/_pkgs/affine-fragment-doc-title/index.d.ts +2 -0
  23. package/dist/_pkgs/affine-fragment-doc-title/index.js +2 -0
  24. package/dist/_pkgs/affine-fragment-doc-title/title-band/format.d.ts +69 -0
  25. package/dist/_pkgs/affine-fragment-doc-title/title-band/format.js +182 -0
  26. package/dist/_pkgs/affine-fragment-doc-title/title-band/title-band.d.ts +62 -0
  27. package/dist/_pkgs/affine-fragment-doc-title/title-band/title-band.js +649 -0
  28. package/dist/_pkgs/affine-fragment-doc-title/translations.d.ts +21 -0
  29. package/dist/_pkgs/affine-fragment-doc-title/translations.js +58 -0
  30. package/dist/_pkgs/affine-shared/services/index.d.ts +2 -0
  31. package/dist/_pkgs/affine-shared/services/index.js +2 -0
  32. package/dist/_pkgs/affine-shared/services/properties-panel-service.d.ts +50 -0
  33. package/dist/_pkgs/affine-shared/services/properties-panel-service.js +17 -0
  34. package/dist/_pkgs/affine-shared/services/title-band-service.d.ts +124 -0
  35. package/dist/_pkgs/affine-shared/services/title-band-service.js +17 -0
  36. package/dist/_pkgs/data-view/property-presets/converts.d.ts +2 -2
  37. package/dist/_pkgs/data-view/property-presets/index.d.ts +1 -1
  38. package/dist/_pkgs/data-view/property-presets/number/cell-renderer.d.ts +1 -1
  39. package/dist/_pkgs/data-view/property-presets/number/define.d.ts +1 -1
  40. package/dist/_pkgs/data-view/property-presets/number/types.d.ts +2 -2
  41. package/dist/_pkgs/data-view/property-presets/pure-index.d.ts +2 -1
  42. package/dist/_pkgs/data-view/property-presets/pure-index.js +4 -0
  43. package/dist/_pkgs/std/extension/command-registry.d.ts +50 -1
  44. package/dist/_pkgs/std/extension/command-registry.js +26 -7
  45. package/dist/commands.js +2 -1
  46. package/dist/host-panels.d.ts +83 -2
  47. package/dist/host-panels.js +82 -1
  48. package/package.json +1 -1
  49. package/dist/_pkgs/affine-block-surface/extensions/export-manager/file-exporter.d.ts +0 -26
  50. package/dist/_pkgs/affine-block-surface/extensions/export-manager/file-exporter.js +0 -73
@@ -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,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';
@@ -1,9 +1,10 @@
1
+ import { ExportToPdfIcon, ExportToPngIcon, GridIcon, } from '@blocksuite/icons/lit';
1
2
  import { ViewExtensionProvider, } from '../affine-ext-loader/index.js';
2
3
  import { CanvasGrid, CanvasLocalVisibility, } from '../affine-shared/services/index.js';
3
4
  import { BlockViewExtension, CommandExtension, FlavourExtension, } from '../std/index.js';
4
5
  import { literal } from 'lit/static-html.js';
5
6
  import { effects } from './effects.js';
6
- import { auditCommands, CanvasActiveLayer, canvasGridCommands, EdgelessCRUDExtension, EdgelessLegacySlotExtension, EditPropsMiddlewareBuilder, exportSvgCommands, exportSvgToolbarExtension, mapQualityCommands, mapQualityWidget, ReadingManager, readingProposalWidget, readingToolbarExtension, selectionPaneCommands, SelectionPaneModel, SpotlightManager, UserLayerMiddlewareBuilder, userLayerCommands, ValidationManager, validationExceptionToolbarExtension, ValidationOverlay, violationDetailWidget, } from './extensions/index.js';
7
+ import { auditCommands, CanvasActiveLayer, canvasExportCommands, canvasGridCommands, EdgelessCRUDExtension, EdgelessLegacySlotExtension, EditPropsMiddlewareBuilder, exportSvgCommands, exportSvgToolbarExtension, mapQualityCommands, mapQualityWidget, ReadingManager, readingProposalWidget, readingToolbarExtension, selectionPaneCommands, SelectionPaneModel, SpotlightManager, UserLayerMiddlewareBuilder, userLayerCommands, ValidationManager, validationExceptionToolbarExtension, ValidationOverlay, violationDetailWidget, } from './extensions/index.js';
7
8
  import { ExportManagerExtension } from './extensions/export-manager/export-manager.js';
8
9
  import { BackgroundStackingExtension } from './framework-background/stacking.js';
9
10
  import { ElementHighlightOverlay } from './renderer/element-highlight-overlay.js';
@@ -87,7 +88,15 @@ export class SurfaceViewExtension extends ViewExtensionProvider {
87
88
  // the edgeless root's background, the surface container and the PNG
88
89
  // export; previews follow the document setting alone.
89
90
  context.register(CanvasGrid);
90
- context.register(CommandExtension(canvasGridCommands));
91
+ context.register(CommandExtension(canvasGridCommands, { 'canvas.grid': GridIcon() }));
92
+ // The canvas's PNG and PDF export (ADR 0034, amendment of 2026-10-13):
93
+ // the "This view" entries a host lists with `getViewActions`. Edgeless
94
+ // only — the page-mode export was never offered — and never gated: a
95
+ // picture of what is on the canvas is not a framework's tooling.
96
+ context.register(CommandExtension(canvasExportCommands, {
97
+ 'canvas.export.png': ExportToPngIcon(),
98
+ 'canvas.export.pdf': ExportToPdfIcon(),
99
+ }));
91
100
  // User layers (ADR 0031 §2-§6): the viewer's active layer (session
92
101
  // only), the `beforeAdd` middleware that stamps it on what is created,
93
102
  // and the commands the pane's layer rows run. Content, never gated.
@@ -13,6 +13,14 @@ export declare class DocTitle extends DocTitle_base {
13
13
  get inlineEditor(): import("../affine-shared/types/index.js").AffineInlineEditor | null;
14
14
  get inlineEditorContainer(): HTMLDivElement;
15
15
  connectedCallback(): void;
16
+ /**
17
+ * The `std` the title band reads its seams from, when the band belongs
18
+ * here: in the page view, never inside a note on the canvas, where
19
+ * `edgeless-page-block-title` draws the band itself (ADR 0035 §11), so a
20
+ * host that renders this title in the note header does not get two.
21
+ */
22
+ private get _bandStd();
23
+ firstUpdated(): void;
16
24
  render(): import("lit-html").TemplateResult<1>;
17
25
  private accessor _isComposing;
18
26
  private accessor _isReadonly;