@formicoidea/labre-core 0.46.2 → 0.47.1

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 (91) hide show
  1. package/dist/_pkgs/affine-block-attachment/configs/slash-menu.js +2 -2
  2. package/dist/_pkgs/affine-block-attachment/translations.js +2 -2
  3. package/dist/_pkgs/affine-block-data-view/columns/index.d.ts +1 -1
  4. package/dist/_pkgs/affine-block-database/data-source.d.ts +1 -1
  5. package/dist/_pkgs/affine-block-database/properties/converts.d.ts +4 -4
  6. package/dist/_pkgs/affine-block-database/properties/index.d.ts +1 -1
  7. package/dist/_pkgs/affine-block-embed/embed-html-block/configs/toolbar.js +1 -1
  8. package/dist/_pkgs/affine-block-embed/embed-iframe-block/configs/toolbar.d.ts +1 -1
  9. package/dist/_pkgs/affine-block-embed/embed-iframe-block/configs/toolbar.js +1 -1
  10. package/dist/_pkgs/affine-block-embed/translations.d.ts +1 -1
  11. package/dist/_pkgs/affine-block-embed/translations.js +2 -2
  12. package/dist/_pkgs/affine-block-embed-doc/embed-linked-doc-block/configs/slash-menu.js +6 -6
  13. package/dist/_pkgs/affine-block-embed-doc/embed-linked-doc-block/configs/toolbar.js +1 -1
  14. package/dist/_pkgs/affine-block-embed-doc/embed-synced-doc-block/configs/toolbar.js +3 -3
  15. package/dist/_pkgs/affine-block-embed-doc/translations.d.ts +1 -1
  16. package/dist/_pkgs/affine-block-embed-doc/translations.js +8 -8
  17. package/dist/_pkgs/affine-block-frame/commands.js +41 -1
  18. package/dist/_pkgs/affine-block-frame/frame-manager.d.ts +1 -1
  19. package/dist/_pkgs/affine-block-frame/frame-toolbar.js +2 -2
  20. package/dist/_pkgs/affine-block-frame/translations.js +1 -1
  21. package/dist/_pkgs/affine-block-frame/view.js +7 -2
  22. package/dist/_pkgs/affine-block-note/components/edgeless-page-block-title.d.ts +7 -0
  23. package/dist/_pkgs/affine-block-note/components/edgeless-page-block-title.js +15 -1
  24. package/dist/_pkgs/affine-block-note/translations.js +8 -8
  25. package/dist/_pkgs/affine-block-root/commands/copy-link-commands.js +2 -2
  26. package/dist/_pkgs/affine-block-root/configs/toolbar.d.ts +1 -1
  27. package/dist/_pkgs/affine-block-root/configs/toolbar.js +1 -1
  28. package/dist/_pkgs/affine-block-root/edgeless/configs/toolbar/misc.d.ts +1 -1
  29. package/dist/_pkgs/affine-block-root/edgeless/configs/toolbar/more.d.ts +1 -1
  30. package/dist/_pkgs/affine-block-root/edgeless/configs/toolbar/more.js +1 -1
  31. package/dist/_pkgs/affine-block-root/translations.d.ts +1 -1
  32. package/dist/_pkgs/affine-block-root/translations.js +2 -2
  33. package/dist/_pkgs/affine-block-surface/extensions/canvas-grid/commands.js +21 -7
  34. package/dist/_pkgs/affine-block-surface/extensions/export-manager/commands.d.ts +2 -0
  35. package/dist/_pkgs/affine-block-surface/extensions/export-manager/commands.js +79 -0
  36. package/dist/_pkgs/affine-block-surface/extensions/export-manager/export-manager.d.ts +18 -1
  37. package/dist/_pkgs/affine-block-surface/extensions/export-manager/export-manager.js +53 -30
  38. package/dist/_pkgs/affine-block-surface/extensions/export-manager/index.d.ts +1 -0
  39. package/dist/_pkgs/affine-block-surface/extensions/export-manager/index.js +1 -0
  40. package/dist/_pkgs/affine-block-surface/extensions/interchange-import.js +1 -1
  41. package/dist/_pkgs/affine-block-surface/extensions/selection-pane/commands.js +1 -1
  42. package/dist/_pkgs/affine-block-surface/renderer/element-highlight-overlay.d.ts +8 -2
  43. package/dist/_pkgs/affine-block-surface/renderer/element-highlight-overlay.js +11 -13
  44. package/dist/_pkgs/affine-block-surface/view.js +11 -2
  45. package/dist/_pkgs/affine-block-surface-ref/translations.js +8 -8
  46. package/dist/_pkgs/affine-components/translations.js +6 -6
  47. package/dist/_pkgs/affine-fragment-doc-title/doc-title.d.ts +8 -0
  48. package/dist/_pkgs/affine-fragment-doc-title/doc-title.js +65 -4
  49. package/dist/_pkgs/affine-fragment-doc-title/effects.d.ts +2 -0
  50. package/dist/_pkgs/affine-fragment-doc-title/effects.js +2 -0
  51. package/dist/_pkgs/affine-fragment-doc-title/index.d.ts +2 -0
  52. package/dist/_pkgs/affine-fragment-doc-title/index.js +2 -0
  53. package/dist/_pkgs/affine-fragment-doc-title/title-band/format.d.ts +69 -0
  54. package/dist/_pkgs/affine-fragment-doc-title/title-band/format.js +182 -0
  55. package/dist/_pkgs/affine-fragment-doc-title/title-band/title-band.d.ts +62 -0
  56. package/dist/_pkgs/affine-fragment-doc-title/title-band/title-band.js +649 -0
  57. package/dist/_pkgs/affine-fragment-doc-title/translations.d.ts +21 -0
  58. package/dist/_pkgs/affine-fragment-doc-title/translations.js +58 -0
  59. package/dist/_pkgs/affine-gfx-group/toolbar/config.d.ts +2 -2
  60. package/dist/_pkgs/affine-gfx-group/toolbar/config.js +2 -2
  61. package/dist/_pkgs/affine-gfx-group/translations.js +1 -1
  62. package/dist/_pkgs/affine-gfx-note/translations.js +2 -2
  63. package/dist/_pkgs/affine-shared/services/index.d.ts +2 -0
  64. package/dist/_pkgs/affine-shared/services/index.js +2 -0
  65. package/dist/_pkgs/affine-shared/services/linked-doc-creation-service.d.ts +1 -1
  66. package/dist/_pkgs/affine-shared/services/properties-panel-service.d.ts +50 -0
  67. package/dist/_pkgs/affine-shared/services/properties-panel-service.js +17 -0
  68. package/dist/_pkgs/affine-shared/services/title-band-service.d.ts +124 -0
  69. package/dist/_pkgs/affine-shared/services/title-band-service.js +17 -0
  70. package/dist/_pkgs/affine-shared/services/translation-service/chrome.d.ts +3 -3
  71. package/dist/_pkgs/affine-shared/services/translation-service/chrome.js +17 -17
  72. package/dist/_pkgs/affine-widget-edgeless-auto-connect/translations.js +2 -2
  73. package/dist/_pkgs/affine-widget-edgeless-selected-rect/translations.js +1 -1
  74. package/dist/_pkgs/affine-widget-linked-doc/translations.js +3 -3
  75. package/dist/_pkgs/affine-widget-slash-menu/translations.js +1 -1
  76. package/dist/_pkgs/data-view/property-presets/converts.d.ts +2 -2
  77. package/dist/_pkgs/data-view/property-presets/index.d.ts +1 -1
  78. package/dist/_pkgs/data-view/property-presets/number/cell-renderer.d.ts +1 -1
  79. package/dist/_pkgs/data-view/property-presets/number/define.d.ts +1 -1
  80. package/dist/_pkgs/data-view/property-presets/number/types.d.ts +2 -2
  81. package/dist/_pkgs/data-view/property-presets/pure-index.d.ts +2 -1
  82. package/dist/_pkgs/data-view/property-presets/pure-index.js +4 -0
  83. package/dist/_pkgs/std/extension/command-registry.d.ts +50 -1
  84. package/dist/_pkgs/std/extension/command-registry.js +26 -7
  85. package/dist/commands.js +2 -1
  86. package/dist/host-panels.d.ts +83 -2
  87. package/dist/host-panels.js +82 -1
  88. package/dist/translations.js +1 -1
  89. package/package.json +1 -1
  90. package/dist/_pkgs/affine-block-surface/extensions/export-manager/file-exporter.d.ts +0 -26
  91. package/dist/_pkgs/affine-block-surface/extensions/export-manager/file-exporter.js +0 -73
@@ -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)
@@ -51,13 +58,20 @@ const saveGridForEveryone = {
51
58
  labelKey: 'com.labre.command.canvas.grid.save-for-everyone',
52
59
  labelFallback: 'Save the grid for everyone',
53
60
  descriptionKey: 'com.labre.command.canvas.grid.save-for-everyone.description',
54
- descriptionFallback: 'Make the grid as you see it now the setting of this document, for everyone who opens it.',
61
+ descriptionFallback: 'Make the grid as you see it now the setting of this page, 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';
@@ -292,7 +292,7 @@ const IMPORT_DEFAULT_NAME_FALLBACK = 'Imported diagram';
292
292
  * because nothing is wrong with the FILE.
293
293
  */
294
294
  const IMPORT_READONLY_KEY = 'com.labre.interchange.import.readonly';
295
- const IMPORT_READONLY_FALLBACK = 'This document became read-only while the file was being read, so nothing was imported.';
295
+ const IMPORT_READONLY_FALLBACK = 'This page became read-only while the file was being read, so nothing was imported.';
296
296
  const IMPORT_REMARKS_KEY = 'com.labre.interchange.import.remarks';
297
297
  const IMPORT_REMARKS_FALLBACK = 'What the import could not keep as it was';
298
298
  const IMPORT_CONSOLE_KEY = 'com.labre.interchange.import.console';
@@ -293,7 +293,7 @@ const hideForEveryone = {
293
293
  labelKey: 'com.labre.command.canvas.visibility.hide-for-everyone',
294
294
  labelFallback: 'Hide for everyone',
295
295
  descriptionKey: 'com.labre.command.canvas.visibility.hide-for-everyone.description',
296
- descriptionFallback: 'Hide the selected elements for everyone who opens this document. They stay in the document and in the selection pane.',
296
+ descriptionFallback: 'Hide the selected elements for everyone who opens this page. They stay in the page and in the selection pane.',
297
297
  surfaces: ['palette', 'agent'],
298
298
  scope: 'edgeless',
299
299
  defaultKeys: { mac: [], other: [] },
@@ -1,8 +1,14 @@
1
1
  import type { GfxController } from '../../std/gfx/index.js';
2
2
  import { Overlay } from './overlay.js';
3
3
  /**
4
- * Draws the transient emphasis ring around the elements held by
5
- * `gfx.highlight` (`ElementHighlightManager`).
4
+ * Draws the transient emphasis around the elements held by `gfx.highlight`
5
+ * (`ElementHighlightManager`).
6
+ *
7
+ * The emphasis is the canvas's existing vocabulary, a square border on the
8
+ * element's bounds like the selection frame (`edgeless-selected-rect`), not a
9
+ * second, detached ring: the product owner asked for the border alone after
10
+ * the P1 recette, so a selection-pane hover reads as « this one » and not as a
11
+ * new kind of mark.
6
12
  *
7
13
  * The overlay is a pure renderer: it owns no state, reads nothing from the
8
14
  * store and mutates nothing, which makes it safe on a read-only editor.
@@ -2,15 +2,17 @@ import { getChromeAccentColor } from '../../affine-shared/services/index.js';
2
2
  import { DisposableGroup } from '../../global/disposable/index.js';
3
3
  import { effect } from '@preact/signals-core';
4
4
  import { Overlay } from './overlay.js';
5
- /** Ring thickness, in screen pixels. */
5
+ /** Border thickness, in screen pixels: the selection frame's. */
6
6
  const HIGHLIGHT_STROKE_WIDTH = 2;
7
- /** Gap between the element bounds and the ring, in screen pixels. */
8
- const HIGHLIGHT_GAP = 4;
9
- /** Ring corner radius, in screen pixels. */
10
- const HIGHLIGHT_RADIUS = 4;
11
7
  /**
12
- * Draws the transient emphasis ring around the elements held by
13
- * `gfx.highlight` (`ElementHighlightManager`).
8
+ * Draws the transient emphasis around the elements held by `gfx.highlight`
9
+ * (`ElementHighlightManager`).
10
+ *
11
+ * The emphasis is the canvas's existing vocabulary, a square border on the
12
+ * element's bounds like the selection frame (`edgeless-selected-rect`), not a
13
+ * second, detached ring: the product owner asked for the border alone after
14
+ * the P1 recette, so a selection-pane hover reads as « this one » and not as a
15
+ * new kind of mark.
14
16
  *
15
17
  * The overlay is a pure renderer: it owns no state, reads nothing from the
16
18
  * store and mutates nothing, which makes it safe on a read-only editor.
@@ -37,8 +39,6 @@ export class ElementHighlightOverlay extends Overlay {
37
39
  if (elements.length === 0)
38
40
  return;
39
41
  const { zoom } = this.gfx.viewport;
40
- const gap = HIGHLIGHT_GAP / zoom;
41
- const radius = HIGHLIGHT_RADIUS / zoom;
42
42
  ctx.save();
43
43
  ctx.strokeStyle = this._strokeColor;
44
44
  ctx.lineWidth = HIGHLIGHT_STROKE_WIDTH / zoom;
@@ -46,14 +46,12 @@ export class ElementHighlightOverlay extends Overlay {
46
46
  const [x, y, w, h] = element.deserializedXYWH;
47
47
  ctx.save();
48
48
  // Rotate around the element center so rotated elements keep a tight
49
- // ring; connectors and other non-rotatable elements use their bound box.
49
+ // border; connectors and other non-rotatable elements use their bound box.
50
50
  ctx.translate(x + w / 2, y + h / 2);
51
51
  if (element.rotate) {
52
52
  ctx.rotate((element.rotate * Math.PI) / 180);
53
53
  }
54
- ctx.beginPath();
55
- ctx.roundRect(-w / 2 - gap, -h / 2 - gap, w + gap * 2, h + gap * 2, radius);
56
- ctx.stroke();
54
+ ctx.strokeRect(-w / 2, -h / 2, w, h);
57
55
  ctx.restore();
58
56
  }
59
57
  ctx.restore();
@@ -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.
@@ -94,35 +94,35 @@ export const SURFACE_REF_PLACEHOLDER_NOT_AVAILABLE = [
94
94
  */
95
95
  export const SURFACE_REF_PLACEHOLDER_DELETED_FRAME = [
96
96
  'com.labre.surface-ref.placeholder.deleted.frame',
97
- 'The frame is deleted or not in this doc.',
97
+ 'The frame is deleted or not in this page.',
98
98
  ];
99
99
  export const SURFACE_REF_PLACEHOLDER_DELETED_GROUP = [
100
100
  'com.labre.surface-ref.placeholder.deleted.group',
101
- 'The group is deleted or not in this doc.',
101
+ 'The group is deleted or not in this page.',
102
102
  ];
103
103
  export const SURFACE_REF_PLACEHOLDER_DELETED_MINDMAP = [
104
104
  'com.labre.surface-ref.placeholder.deleted.mindmap',
105
- 'The mind map is deleted or not in this doc.',
105
+ 'The mind map is deleted or not in this page.',
106
106
  ];
107
107
  export const SURFACE_REF_PLACEHOLDER_DELETED_EDGELESS = [
108
108
  'com.labre.surface-ref.placeholder.deleted.edgeless',
109
- 'The canvas content is deleted or not in this doc.',
109
+ 'The canvas content is deleted or not in this page.',
110
110
  ];
111
111
  export const SURFACE_REF_PLACEHOLDER_CANNOT_DISPLAY_FRAME = [
112
112
  'com.labre.surface-ref.placeholder.cannot-display.frame',
113
- 'The frame is inserted but cannot display in canvas mode. Switch to document mode to view the block.',
113
+ 'The frame is inserted but cannot display in canvas mode. Switch to page mode to view the block.',
114
114
  ];
115
115
  export const SURFACE_REF_PLACEHOLDER_CANNOT_DISPLAY_GROUP = [
116
116
  'com.labre.surface-ref.placeholder.cannot-display.group',
117
- 'The group is inserted but cannot display in canvas mode. Switch to document mode to view the block.',
117
+ 'The group is inserted but cannot display in canvas mode. Switch to page mode to view the block.',
118
118
  ];
119
119
  export const SURFACE_REF_PLACEHOLDER_CANNOT_DISPLAY_MINDMAP = [
120
120
  'com.labre.surface-ref.placeholder.cannot-display.mindmap',
121
- 'The mind map is inserted but cannot display in canvas mode. Switch to document mode to view the block.',
121
+ 'The mind map is inserted but cannot display in canvas mode. Switch to page mode to view the block.',
122
122
  ];
123
123
  export const SURFACE_REF_PLACEHOLDER_CANNOT_DISPLAY_EDGELESS = [
124
124
  'com.labre.surface-ref.placeholder.cannot-display.edgeless',
125
- 'The canvas content is inserted but cannot display in canvas mode. Switch to document mode to view the block.',
125
+ 'The canvas content is inserted but cannot display in canvas mode. Switch to page mode to view the block.',
126
126
  ];
127
127
  /**
128
128
  * Per-kind table joining the two placeholder sentences above to the
@@ -117,7 +117,7 @@ export const EMBED_CARD_TITLE_ALIAS_PLACEHOLDER = [
117
117
  ];
118
118
  export const EMBED_CARD_DESCRIPTION_ALIAS_PLACEHOLDER = [
119
119
  'com.labre.embed.card.description-alias-placeholder',
120
- 'Add description alias (empty to inherit document content)',
120
+ 'Add description alias (empty to inherit page content)',
121
121
  ];
122
122
  export const EMBED_CARD_TITLE_PLACEHOLDER = [
123
123
  'com.labre.embed.card.title-placeholder',
@@ -292,7 +292,7 @@ export const EMOJI_GROUP_WORDINGS = {
292
292
  /* ── Linked doc title chip ───────────────────────────────────────────── */
293
293
  export const DOC_TITLE_ARIA = [
294
294
  'com.labre.linked-doc.title-aria',
295
- 'Doc title',
295
+ 'Page title',
296
296
  ];
297
297
  /* ── Linked-doc notifications ─────────────────────────────────────────── */
298
298
  export const NOTIFY_SWITCHED_TO_CARD_TITLE = [
@@ -309,7 +309,7 @@ export const NOTIFY_SWITCHED_TO_EMBED_TITLE = [
309
309
  ];
310
310
  export const NOTIFY_SWITCHED_TO_EMBED_MESSAGE = [
311
311
  'com.labre.notify.linked-doc.switched-to-embed.message',
312
- 'Custom alias removed. The linked doc now displays the original title and description.',
312
+ 'Custom alias removed. The linked page now displays the original title and description.',
313
313
  ];
314
314
  export const NOTIFY_CLEARED_ALIASES_TITLE = [
315
315
  'com.labre.notify.linked-doc.cleared-aliases.title',
@@ -317,17 +317,17 @@ export const NOTIFY_CLEARED_ALIASES_TITLE = [
317
317
  ];
318
318
  export const NOTIFY_CLEARED_ALIASES_MESSAGE = [
319
319
  'com.labre.notify.linked-doc.cleared-aliases.message',
320
- 'Card view has been restored to original doc title and description. All custom aliases have been removed.',
320
+ 'Card view has been restored to original page title and description. All custom aliases have been removed.',
321
321
  ];
322
322
  /* ── Open-doc dropdown ────────────────────────────────────────────────── */
323
323
  export const OPEN_LABEL = ['com.labre.open-doc.open', 'Open'];
324
324
  export const OPEN_DOC_MENU_ARIA = [
325
325
  'com.labre.open-doc.menu-aria',
326
- 'Open doc menu',
326
+ 'Open page menu',
327
327
  ];
328
328
  export const OPEN_DOC_WITH_ARIA = [
329
329
  'com.labre.open-doc.with-aria',
330
- 'Open doc with',
330
+ 'Open page with',
331
331
  ];
332
332
  /* ── Resource popover (attachment / image not-found or retrieve error) ─ */
333
333
  /**
@@ -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;
@@ -34,12 +34,12 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
34
34
  };
35
35
  import { CodeBlockModel, ListBlockModel, NoteBlockModel, NoteDisplayMode, ParagraphBlockModel, } from '../affine-model/index.js';
36
36
  import { focusTextModel } from '../affine-rich-text/index.js';
37
- import { translateKey } from '../affine-shared/services/index.js';
37
+ import { TitleBandProvider, translateKey } from '../affine-shared/services/index.js';
38
38
  import { matchModels } from '../affine-shared/utils/index.js';
39
39
  import { WithDisposable } from '../global/lit/index.js';
40
40
  import { ShadowlessElement } from '../std/index.js';
41
41
  import { effect } from '@preact/signals-core';
42
- import { css, html } from 'lit';
42
+ import { css, html, nothing } from 'lit';
43
43
  import { property, query, state } from 'lit/decorators.js';
44
44
  import { DOC_TITLE_PLACEHOLDER } from './translations.js';
45
45
  const DOC_BLOCK_CHILD_PADDING = 24;
@@ -76,9 +76,18 @@ let DocTitle = (() => {
76
76
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
77
77
  }
78
78
  static { this.styles = css `
79
+ /*
80
+ * The page title's family is a host's to choose (labre ADR-0049 §5: the
81
+ * Labre app sets Cabinet Grotesk); headings H1 to H6 stay in the body
82
+ * family. Unset, the title keeps the body family, DESIGN.md's One Family
83
+ * Rule.
84
+ */
79
85
  .doc-title-container {
80
86
  box-sizing: border-box;
81
- font-family: var(--affine-font-family);
87
+ font-family: var(
88
+ --labre-doc-title-font-family,
89
+ var(--affine-font-family)
90
+ );
82
91
  font-size: var(--affine-font-base);
83
92
  line-height: var(--affine-line-height);
84
93
  color: var(--affine-text-primary-color);
@@ -112,6 +121,27 @@ let DocTitle = (() => {
112
121
  }
113
122
  }
114
123
 
124
+ .doc-title-container.doc-title-container-with-band {
125
+ padding-bottom: 8px;
126
+ }
127
+
128
+ .doc-title-band {
129
+ box-sizing: border-box;
130
+ width: 100%;
131
+ max-width: var(--affine-editor-width);
132
+ margin-left: auto;
133
+ margin-right: auto;
134
+ padding: 0 var(--affine-editor-side-padding, ${DOC_BLOCK_CHILD_PADDING}px)
135
+ 24px;
136
+ }
137
+
138
+ @container viewport (width <= 640px) {
139
+ .doc-title-band {
140
+ padding-left: ${DOC_BLOCK_CHILD_PADDING}px;
141
+ padding-right: ${DOC_BLOCK_CHILD_PADDING}px;
142
+ }
143
+ }
144
+
115
145
  .doc-title-container-empty::before {
116
146
  content: attr(data-placeholder);
117
147
  color: var(--affine-placeholder-color);
@@ -178,13 +208,35 @@ let DocTitle = (() => {
178
208
  });
179
209
  }
180
210
  }
211
+ /**
212
+ * The `std` the title band reads its seams from, when the band belongs
213
+ * here: in the page view, never inside a note on the canvas, where
214
+ * `edgeless-page-block-title` draws the band itself (ADR 0035 §11), so a
215
+ * host that renders this title in the note header does not get two.
216
+ */
217
+ get _bandStd() {
218
+ if (this.closest('edgeless-page-block-title'))
219
+ return null;
220
+ const std = this._std;
221
+ return std?.getOptional(TitleBandProvider) ? std : null;
222
+ }
223
+ firstUpdated() {
224
+ // `<doc-title>` sits beside the editor host, not inside it: on the first
225
+ // frame the host may not carry its `std` yet, and without one the band
226
+ // cannot read its seam. One more render once it has mounted.
227
+ if (this._std)
228
+ return;
229
+ const frame = requestAnimationFrame(() => this.requestUpdate());
230
+ this._disposables.add(() => cancelAnimationFrame(frame));
231
+ }
181
232
  render() {
182
233
  const isEmpty = !this._rootModel?.props.title.length && !this._isComposing;
234
+ const bandStd = this._bandStd;
183
235
  return html `
184
236
  <div
185
237
  class="doc-title-container ${isEmpty
186
238
  ? 'doc-title-container-empty'
187
- : ''}"
239
+ : ''} ${bandStd ? 'doc-title-container-with-band' : ''}"
188
240
  data-block-is-title="true"
189
241
  data-placeholder=${this._placeholder}
190
242
  >
@@ -197,6 +249,15 @@ let DocTitle = (() => {
197
249
  .wrapText=${this.wrapText}
198
250
  ></rich-text>
199
251
  </div>
252
+ ${bandStd
253
+ ? html `<div class="doc-title-band">
254
+ <labre-title-band
255
+ .std=${bandStd}
256
+ .docId=${this.doc.id}
257
+ .surface=${'page'}
258
+ ></labre-title-band>
259
+ </div>`
260
+ : nothing}
200
261
  `;
201
262
  }
202
263
  #_isComposing_accessor_storage;
@@ -1,7 +1,9 @@
1
1
  import { DocTitle } from './doc-title.js';
2
+ import { LABRE_TITLE_BAND, TitleBand } from './title-band/title-band.js';
2
3
  export declare function effects(): void;
3
4
  declare global {
4
5
  interface HTMLElementTagNameMap {
5
6
  'doc-title': DocTitle;
7
+ [LABRE_TITLE_BAND]: TitleBand;
6
8
  }
7
9
  }