@formicoidea/labre-core 0.45.0 → 0.46.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 (65) hide show
  1. package/dist/_pkgs/affine-block-callout/commands/turn-callout-into.d.ts +43 -0
  2. package/dist/_pkgs/affine-block-callout/commands/turn-callout-into.js +109 -0
  3. package/dist/_pkgs/affine-block-callout/configs/toolbar.d.ts +65 -0
  4. package/dist/_pkgs/affine-block-callout/configs/toolbar.js +178 -0
  5. package/dist/_pkgs/affine-block-callout/view.js +7 -1
  6. package/dist/_pkgs/affine-block-frame/commands.d.ts +27 -0
  7. package/dist/_pkgs/affine-block-frame/commands.js +49 -0
  8. package/dist/_pkgs/affine-block-frame/frame-manager.d.ts +11 -6
  9. package/dist/_pkgs/affine-block-frame/frame-manager.js +10 -5
  10. package/dist/_pkgs/affine-block-frame/frame-order.d.ts +43 -0
  11. package/dist/_pkgs/affine-block-frame/frame-order.js +93 -0
  12. package/dist/_pkgs/affine-block-frame/index.d.ts +2 -0
  13. package/dist/_pkgs/affine-block-frame/index.js +2 -0
  14. package/dist/_pkgs/affine-block-frame/present/frame-order-menu.d.ts +10 -2
  15. package/dist/_pkgs/affine-block-frame/present/frame-order-menu.js +32 -18
  16. package/dist/_pkgs/affine-block-frame/view.js +6 -0
  17. package/dist/_pkgs/affine-block-note/configs/toolbar.d.ts +19 -0
  18. package/dist/_pkgs/affine-block-note/configs/toolbar.js +38 -23
  19. package/dist/_pkgs/affine-block-note/translations.d.ts +2 -0
  20. package/dist/_pkgs/affine-block-note/translations.js +14 -1
  21. package/dist/_pkgs/affine-block-root/configs/toolbar.js +31 -2
  22. package/dist/_pkgs/affine-block-root/translations.d.ts +7 -2
  23. package/dist/_pkgs/affine-block-root/translations.js +10 -13
  24. package/dist/_pkgs/affine-block-surface/extensions/selection-pane/actions.d.ts +20 -9
  25. package/dist/_pkgs/affine-block-surface/extensions/selection-pane/actions.js +57 -20
  26. package/dist/_pkgs/affine-block-surface/extensions/selection-pane/commands.d.ts +6 -8
  27. package/dist/_pkgs/affine-block-surface/extensions/selection-pane/commands.js +21 -8
  28. package/dist/_pkgs/affine-block-surface/extensions/selection-pane/index.d.ts +1 -1
  29. package/dist/_pkgs/affine-block-surface/extensions/selection-pane/index.js +1 -1
  30. package/dist/_pkgs/affine-components/portal/portal.d.ts +1 -1
  31. package/dist/_pkgs/affine-foundation/view.d.ts +8 -8
  32. package/dist/_pkgs/affine-fragment-frame-panel/body/frame-panel-body.d.ts +34 -2
  33. package/dist/_pkgs/affine-fragment-frame-panel/body/frame-panel-body.js +194 -81
  34. package/dist/_pkgs/affine-fragment-frame-panel/card/frame-card.d.ts +7 -4
  35. package/dist/_pkgs/affine-fragment-frame-panel/card/frame-card.js +9 -24
  36. package/dist/_pkgs/affine-fragment-frame-panel/index.d.ts +1 -0
  37. package/dist/_pkgs/affine-fragment-frame-panel/index.js +1 -0
  38. package/dist/_pkgs/affine-model/utils/types.d.ts +2 -2
  39. package/dist/_pkgs/affine-shared/services/edit-props-store.d.ts +4 -4
  40. package/dist/_pkgs/affine-shared/services/font-loader/config.d.ts +2 -2
  41. package/dist/_pkgs/affine-shared/services/font-loader/font-loader-service.d.ts +3 -3
  42. package/dist/_pkgs/affine-shared/services/index.d.ts +1 -1
  43. package/dist/_pkgs/affine-shared/services/index.js +1 -1
  44. package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-storage.d.ts +4 -4
  45. package/dist/_pkgs/affine-shared/services/outline-panel-service.d.ts +31 -0
  46. package/dist/_pkgs/affine-shared/services/outline-panel-service.js +17 -0
  47. package/dist/_pkgs/affine-shared/services/translation-service/chrome.d.ts +11 -0
  48. package/dist/_pkgs/affine-shared/services/translation-service/chrome.js +19 -0
  49. package/dist/_pkgs/affine-shared/utils/index.d.ts +1 -0
  50. package/dist/_pkgs/affine-shared/utils/index.js +1 -0
  51. package/dist/_pkgs/affine-shared/utils/panel-reorder-drag.d.ts +85 -0
  52. package/dist/_pkgs/affine-shared/utils/panel-reorder-drag.js +162 -0
  53. package/dist/_pkgs/affine-widget-edgeless-toolbar/selection-pane/drop.d.ts +7 -6
  54. package/dist/_pkgs/affine-widget-edgeless-toolbar/selection-pane/drop.js +11 -8
  55. package/dist/_pkgs/affine-widget-edgeless-toolbar/selection-pane/selection-pane-widget.d.ts +31 -15
  56. package/dist/_pkgs/affine-widget-edgeless-toolbar/selection-pane/selection-pane-widget.js +178 -142
  57. package/dist/_pkgs/std/scope/std-scope.d.ts +1 -1
  58. package/dist/commands.js +2 -0
  59. package/dist/host-panels.d.ts +90 -0
  60. package/dist/host-panels.js +111 -0
  61. package/package.json +5 -1
  62. package/dist/_pkgs/affine-fragment-frame-panel/utils/drag.d.ts +0 -27
  63. package/dist/_pkgs/affine-fragment-frame-panel/utils/drag.js +0 -96
  64. package/dist/_pkgs/affine-shared/services/sidebar-service.d.ts +0 -8
  65. package/dist/_pkgs/affine-shared/services/sidebar-service.js +0 -7
@@ -0,0 +1,93 @@
1
+ import { generateKeyBetweenV2, GfxControllerIdentifier } from '../std/gfx/index.js';
2
+ import { EdgelessFrameManagerIdentifier } from './frame-manager.js';
3
+ /**
4
+ * The one write of the frames' PRESENTATION order (ADR 0034): the
5
+ * order a presentation walks the frames in, held by each frame's
6
+ * `presentationIndex` — a fractional key, never a position.
7
+ *
8
+ * Why one function: two sites wrote that order by hand — the frame panel's
9
+ * drag and the presentation toolbar's order menu — each its own way, neither
10
+ * refusing a read-only document, and the panel taking its undo checkpoint
11
+ * AFTER the write, so the reorder merged into whatever gesture came before.
12
+ * Both now reach this through `canvas.frame.reorder`, and so does a host's own
13
+ * slide panel: the guards below live here, once.
14
+ *
15
+ * Mirrors the selection pane's `reorderPaneElement`
16
+ * (`blocks/surface/src/extensions/selection-pane/actions.ts`):
17
+ *
18
+ * - refuses on a read-only document before reading anything else;
19
+ * - writes nothing when nothing would change — an unchanged write is an empty
20
+ * undo step, and an undo that "does nothing" reads as a broken undo;
21
+ * - takes ONE `captureSync()` first, so one gesture is one undo step, the
22
+ * legacy keys `refreshLegacyFrameOrder` may write included (it writes in
23
+ * the same capture window, so it joins the gesture's step);
24
+ * - writes through `gfx.updateElement`, which is `store.updateBlock` for a
25
+ * frame — not the CRUD `updateElement`, which would also record the moved
26
+ * frame's props as the flavour's last-used props.
27
+ */
28
+ /**
29
+ * Move the frames `ids` so they sit, as one block, directly BEFORE `before` in
30
+ * the presentation order — `null` puts them at the end. The moved frames keep
31
+ * their current relative order, whatever order `ids` lists them in. Answers
32
+ * whether anything was written.
33
+ *
34
+ * Refused, with nothing written: a read-only document; an id that is not a
35
+ * frame of this document; a `before` that is one of the moved frames, or not
36
+ * a frame; and a target that is where the frames already are.
37
+ *
38
+ * Each moved frame gets a key between its new neighbours, so only the moved
39
+ * frames are written and nothing else shifts. Two neighbours with equal (or
40
+ * inverted) keys cannot be split — `generateKeyBetweenV2` throws on them, and
41
+ * the model's default key is random per frame, so a pasted, imported or
42
+ * hand-made document can carry them: refusing beats throwing in a drop.
43
+ */
44
+ export function reorderFramePresentation(std, ids, before) {
45
+ if (std.store.readonly)
46
+ return false;
47
+ const manager = std.get(EdgelessFrameManagerIdentifier);
48
+ const frames = manager.frames;
49
+ const moving = new Set(ids);
50
+ if (moving.size === 0)
51
+ return false;
52
+ if (before !== null && moving.has(before))
53
+ return false;
54
+ const known = new Set(frames.map(frame => frame.id));
55
+ for (const id of moving)
56
+ if (!known.has(id))
57
+ return false;
58
+ if (before !== null && !known.has(before))
59
+ return false;
60
+ // `frames` is already in presentation order, so filtering keeps the moved
61
+ // frames' relative order.
62
+ const moved = frames.filter(frame => moving.has(frame.id));
63
+ const rest = frames.filter(frame => !moving.has(frame.id));
64
+ const position = before === null
65
+ ? rest.length
66
+ : rest.findIndex(frame => frame.id === before);
67
+ const target = [
68
+ ...rest.slice(0, position),
69
+ ...moved,
70
+ ...rest.slice(position),
71
+ ];
72
+ if (target.every((frame, i) => frame === frames[i]))
73
+ return false;
74
+ std.store.captureSync();
75
+ // Gives a key to legacy frames that have none, keeping their order, so the
76
+ // neighbours' keys below mean what the list shows. A no-op on any document
77
+ // where a frame already carries one.
78
+ manager.refreshLegacyFrameOrder();
79
+ const lower = rest[position - 1]?.props.presentationIndex || null;
80
+ const upper = rest[position]?.props.presentationIndex || null;
81
+ if (lower !== null && upper !== null && lower >= upper)
82
+ return false;
83
+ const gfx = std.get(GfxControllerIdentifier);
84
+ std.store.transact(() => {
85
+ let previous = lower;
86
+ for (const frame of moved) {
87
+ const presentationIndex = generateKeyBetweenV2(previous, upper);
88
+ gfx.updateElement(frame, { presentationIndex });
89
+ previous = presentationIndex;
90
+ }
91
+ });
92
+ return true;
93
+ }
@@ -1,8 +1,10 @@
1
+ export * from './commands.js';
1
2
  export * from './edgeless-clipboard-config.js';
2
3
  export * from './edgeless-toolbar/index.js';
3
4
  export * from './frame-block.js';
4
5
  export * from './frame-highlight-manager.js';
5
6
  export * from './frame-manager.js';
7
+ export * from './frame-order.js';
6
8
  export * from './frame-spec.js';
7
9
  export * from './frame-tool.js';
8
10
  export * from './frame-toolbar.js';
@@ -1,8 +1,10 @@
1
+ export * from './commands.js';
1
2
  export * from './edgeless-clipboard-config.js';
2
3
  export * from './edgeless-toolbar/index.js';
3
4
  export * from './frame-block.js';
4
5
  export * from './frame-highlight-manager.js';
5
6
  export * from './frame-manager.js';
7
+ export * from './frame-order.js';
6
8
  export * from './frame-spec.js';
7
9
  export * from './frame-tool.js';
8
10
  export * from './frame-toolbar.js';
@@ -1,9 +1,17 @@
1
- import type { BlockComponent } from '../../std/index.js';
1
+ import { type BlockComponent } from '../../std/index.js';
2
2
  import { LitElement } from 'lit';
3
3
  declare const EdgelessFrameOrderMenu_base: typeof LitElement & import("../../global/utils/types.js").Constructor<import("../../global/lit/with-disposable.js").DisposableClass>;
4
+ /**
5
+ * The presentation toolbar's order menu. Its WRITE is `canvas.frame.reorder`
6
+ * (ADR 0034); its drag gesture is its own.
7
+ *
8
+ * ponytail: a drag-to-reorder gesture of its own, beside the frame panel's and
9
+ * the selection pane's — its ceiling is a 256px popover, which the panels' one
10
+ * controller (`createPanelReorderDrag`, ADR 0034) is not built for. If the
11
+ * menu ever becomes a panel, move it onto that controller and delete this one.
12
+ */
4
13
  export declare class EdgelessFrameOrderMenu extends EdgelessFrameOrderMenu_base {
5
14
  static styles: import("lit").CSSResult;
6
- get crud(): import("../../affine-block-surface/index.js").EdgelessCRUDExtension;
7
15
  private get _frameMgr();
8
16
  private get _frames();
9
17
  private _bindEvent;
@@ -32,14 +32,37 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
32
32
  }
33
33
  return useValue ? value : void 0;
34
34
  };
35
- import { EdgelessCRUDIdentifier } from '../../affine-block-surface/index.js';
36
35
  import { DisposableGroup } from '../../global/disposable/index.js';
37
36
  import { SignalWatcher, WithDisposable } from '../../global/lit/index.js';
38
- import { generateKeyBetweenV2 } from '../../std/gfx/index.js';
37
+ import { runCommand, } from '../../std/index.js';
39
38
  import { css, html, LitElement, nothing } from 'lit';
40
39
  import { property, query, state } from 'lit/decorators.js';
41
40
  import { repeat } from 'lit/directives/repeat.js';
41
+ import { frameCommands } from '../commands.js';
42
42
  import { EdgelessFrameManagerIdentifier } from '../frame-manager.js';
43
+ /**
44
+ * How the menu invokes the registry: from the editor's chrome, on the frames
45
+ * it names, like the selection pane's rows.
46
+ */
47
+ const MENU_INVOCATION = {
48
+ surface: 'contextual-toolbar',
49
+ source: 'toolbar:general',
50
+ };
51
+ function reorderCommand() {
52
+ const command = frameCommands.find(c => c.id === 'canvas.frame.reorder');
53
+ if (!command)
54
+ throw new Error('frame order menu: no canvas.frame.reorder');
55
+ return command;
56
+ }
57
+ /**
58
+ * The presentation toolbar's order menu. Its WRITE is `canvas.frame.reorder`
59
+ * (ADR 0034); its drag gesture is its own.
60
+ *
61
+ * ponytail: a drag-to-reorder gesture of its own, beside the frame panel's and
62
+ * the selection pane's — its ceiling is a 256px popover, which the panels' one
63
+ * controller (`createPanelReorderDrag`, ADR 0034) is not built for. If the
64
+ * menu ever becomes a panel, move it onto that controller and delete this one.
65
+ */
43
66
  let EdgelessFrameOrderMenu = (() => {
44
67
  let _classSuper = SignalWatcher(WithDisposable(LitElement));
45
68
  let __clone_decorators;
@@ -161,9 +184,6 @@ let EdgelessFrameOrderMenu = (() => {
161
184
  width: 90%;
162
185
  }
163
186
  `; }
164
- get crud() {
165
- return this.edgeless.std.get(EdgelessCRUDIdentifier);
166
- }
167
187
  get _frameMgr() {
168
188
  return this.edgeless.std.get(EdgelessFrameManagerIdentifier);
169
189
  }
@@ -225,20 +245,14 @@ let EdgelessFrameOrderMenu = (() => {
225
245
  this._disposables.addFromEvent(document, 'pointerup', () => {
226
246
  clone.style.visibility = 'hidden';
227
247
  indicatorLine.style.visibility = 'hidden';
228
- if (newIndex >= 0 &&
229
- newIndex <= this._frames.length &&
230
- newIndex !== index &&
231
- newIndex !== index + 1) {
232
- const frameMgr = this._frameMgr;
233
- // Legacy compatibility
234
- frameMgr.refreshLegacyFrameOrder();
235
- const before = this._frames[newIndex - 1]?.props.presentationIndex || null;
236
- const after = this._frames[newIndex]?.props.presentationIndex || null;
237
- const frame = this._frames[index];
238
- this.crud.updateElement(frame.id, {
239
- presentationIndex: generateKeyBetweenV2(before, after),
248
+ const frames = this._frames;
249
+ // A drop on its own slot (before itself) or on the next one is refused
250
+ // by the command: nothing is written, no undo step is pushed.
251
+ if (newIndex >= 0 && newIndex <= frames.length && frames[index]) {
252
+ runCommand(this.edgeless.std, reorderCommand(), MENU_INVOCATION, {
253
+ ids: [frames[index].id],
254
+ before: frames[newIndex]?.id ?? null,
240
255
  });
241
- this.edgeless.store.captureSync();
242
256
  this.requestUpdate();
243
257
  }
244
258
  this._disposables.dispose();
@@ -1,4 +1,6 @@
1
1
  import { ViewExtensionProvider, } from '../affine-ext-loader/index.js';
2
+ import { CommandExtension } from '../std/index.js';
3
+ import { frameCommands } from './commands.js';
2
4
  import { EdgelessClipboardFrameConfig } from './edgeless-clipboard-config.js';
3
5
  import { frameQuickTool } from './edgeless-toolbar/index.js';
4
6
  import { effects } from './effects.js';
@@ -30,6 +32,10 @@ export class FrameViewExtension extends ViewExtensionProvider {
30
32
  context.register(edgelessNavigatorBgWidget);
31
33
  context.register(EdgelessClipboardFrameConfig);
32
34
  context.register(FrameBlockInteraction);
35
+ // The presentation order's one write (ADR 0034). The frame panel also
36
+ // 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));
33
39
  }
34
40
  }
35
41
  }
@@ -1,2 +1,21 @@
1
+ import { NoteDisplayMode } from '../../affine-model/index.js';
2
+ import { type BlockStdScope } from '../../std/index.js';
1
3
  import type { ExtensionType } from '../../store/index.js';
4
+ /**
5
+ * The toast shown after a note's display mode changes. Its "View in TOC" link
6
+ * is offered only when the host registered an outline panel
7
+ * (`OutlinePanelExtension`, ADR 0034 §1): a link that opens nothing is a lie,
8
+ * so a host with no outline, or one that passed `null`, gets a toast with no
9
+ * link, and a body without the "Find it in the TOC" sentence that points at
10
+ * it.
11
+ */
12
+ export declare function displayModeToast(std: BlockStdScope, newMode: NoteDisplayMode): {
13
+ title: string;
14
+ message: string;
15
+ actions: {
16
+ key: string;
17
+ label: string;
18
+ onClick: () => void;
19
+ }[];
20
+ };
2
21
  export declare const createBuiltinToolbarConfigExtension: (flavour: string) => ExtensionType[];
@@ -1,6 +1,6 @@
1
1
  import { EdgelessLegacySlotIdentifier } from '../../affine-block-surface/index.js';
2
2
  import { NoteBlockModel, NoteDisplayMode } from '../../affine-model/index.js';
3
- import { NotificationProvider, SidebarExtensionIdentifier, TOAST_NOTE_REMOVED_FROM_PAGE, ToolbarModuleExtension, translateKey, } from '../../affine-shared/services/index.js';
3
+ import { NotificationProvider, OutlinePanelProvider, TOAST_NOTE_REMOVED_FROM_PAGE, ToolbarModuleExtension, translateKey, } from '../../affine-shared/services/index.js';
4
4
  import { Bound } from '../../global/gfx/index.js';
5
5
  import { AutoHeightIcon, CustomizedHeightIcon, InsertIntoPageIcon, ScissorsIcon, } from '@blocksuite/icons/lit';
6
6
  import { BlockFlavourIdentifier } from '../../std/index.js';
@@ -8,7 +8,7 @@ import { computed } from '@preact/signals-core';
8
8
  import { html } from 'lit';
9
9
  import { changeNoteDisplayMode } from '../commands/index.js';
10
10
  import { NoteConfigExtension } from '../config.js';
11
- import { NOTE_TOAST_ADDED_TO_PAGE_BODY, NOTE_TOAST_DISPLAYED_IN_PAGE_MODE, NOTE_TOAST_REMOVED_FROM_PAGE_BODY, NOTE_TOAST_VIEW_IN_TOC, NOTE_TOOLBAR_AUTO_HEIGHT, NOTE_TOOLBAR_CUSTOMIZED_HEIGHT, NOTE_TOOLBAR_CUTTING_MODE, NOTE_TOOLBAR_DISPLAY_IN_PAGE, NOTE_TOOLBAR_DISPLAYED_IN_PAGE, NOTE_TOOLBAR_REMOVE_FROM_PAGE_TOOLTIP, NOTE_TOOLBAR_SIZE, NOTE_TOOLBAR_SLICER, } from '../translations.js';
11
+ import { NOTE_TOAST_ADDED_TO_PAGE_BODY, NOTE_TOAST_ADDED_TO_PAGE_BODY_NO_TOC, NOTE_TOAST_DISPLAYED_IN_PAGE_MODE, NOTE_TOAST_REMOVED_FROM_PAGE_BODY, NOTE_TOAST_REMOVED_FROM_PAGE_BODY_NO_TOC, NOTE_TOAST_VIEW_IN_TOC, NOTE_TOOLBAR_AUTO_HEIGHT, NOTE_TOOLBAR_CUSTOMIZED_HEIGHT, NOTE_TOOLBAR_CUTTING_MODE, NOTE_TOOLBAR_DISPLAY_IN_PAGE, NOTE_TOOLBAR_DISPLAYED_IN_PAGE, NOTE_TOOLBAR_REMOVE_FROM_PAGE_TOOLTIP, NOTE_TOOLBAR_SIZE, NOTE_TOOLBAR_SLICER, } from '../translations.js';
12
12
  const trackBaseProps = {
13
13
  category: 'note',
14
14
  };
@@ -238,6 +238,41 @@ const builtinSurfaceToolbarConfig = {
238
238
  ],
239
239
  when: ctx => ctx.getSurfaceModelsByType(NoteBlockModel).length > 0,
240
240
  };
241
+ /**
242
+ * The toast shown after a note's display mode changes. Its "View in TOC" link
243
+ * is offered only when the host registered an outline panel
244
+ * (`OutlinePanelExtension`, ADR 0034 §1): a link that opens nothing is a lie,
245
+ * so a host with no outline, or one that passed `null`, gets a toast with no
246
+ * link, and a body without the "Find it in the TOC" sentence that points at
247
+ * it.
248
+ */
249
+ export function displayModeToast(std, newMode) {
250
+ const outline = std.getOptional(OutlinePanelProvider);
251
+ const removed = newMode === NoteDisplayMode.EdgelessOnly;
252
+ const title = removed
253
+ ? TOAST_NOTE_REMOVED_FROM_PAGE
254
+ : NOTE_TOAST_DISPLAYED_IN_PAGE_MODE;
255
+ const body = removed
256
+ ? outline
257
+ ? NOTE_TOAST_REMOVED_FROM_PAGE_BODY
258
+ : NOTE_TOAST_REMOVED_FROM_PAGE_BODY_NO_TOC
259
+ : outline
260
+ ? NOTE_TOAST_ADDED_TO_PAGE_BODY
261
+ : NOTE_TOAST_ADDED_TO_PAGE_BODY_NO_TOC;
262
+ return {
263
+ title: translateKey(std, ...title),
264
+ message: translateKey(std, ...body),
265
+ actions: outline
266
+ ? [
267
+ {
268
+ key: 'view-in-toc',
269
+ label: translateKey(std, ...NOTE_TOAST_VIEW_IN_TOC),
270
+ onClick: () => outline.open(),
271
+ },
272
+ ]
273
+ : [],
274
+ };
275
+ }
241
276
  function setDisplayMode(ctx, model, newMode) {
242
277
  const displayMode = model.props.displayMode;
243
278
  ctx.command.exec(changeNoteDisplayMode, {
@@ -249,31 +284,11 @@ function setDisplayMode(ctx, model, newMode) {
249
284
  if (newMode === NoteDisplayMode.DocOnly) {
250
285
  ctx.selection.clear();
251
286
  }
252
- const data = newMode === NoteDisplayMode.EdgelessOnly
253
- ? {
254
- title: translateKey(ctx.std, ...TOAST_NOTE_REMOVED_FROM_PAGE),
255
- message: translateKey(ctx.std, ...NOTE_TOAST_REMOVED_FROM_PAGE_BODY),
256
- }
257
- : {
258
- title: translateKey(ctx.std, ...NOTE_TOAST_DISPLAYED_IN_PAGE_MODE),
259
- message: translateKey(ctx.std, ...NOTE_TOAST_ADDED_TO_PAGE_BODY),
260
- };
261
287
  const notification = ctx.std.getOptional(NotificationProvider);
262
288
  notification?.notifyWithUndoAction({
263
- title: data.title,
264
- message: data.message,
289
+ ...displayModeToast(ctx.std, newMode),
265
290
  accent: 'success',
266
291
  duration: 5 * 1000,
267
- actions: [
268
- {
269
- key: 'view-in-toc',
270
- label: translateKey(ctx.std, ...NOTE_TOAST_VIEW_IN_TOC),
271
- onClick: () => {
272
- const sidebar = ctx.std.getOptional(SidebarExtensionIdentifier);
273
- sidebar?.open('outline');
274
- },
275
- },
276
- ],
277
292
  });
278
293
  ctx.track('NoteDisplayModeChanged', {
279
294
  ...trackBaseProps,
@@ -66,6 +66,8 @@ export declare const NOTE_DISPLAY_MODE_PAGE: ChromeWording;
66
66
  export declare const NOTE_TOAST_DISPLAYED_IN_PAGE_MODE: ChromeWording;
67
67
  export declare const NOTE_TOAST_REMOVED_FROM_PAGE_BODY: ChromeWording;
68
68
  export declare const NOTE_TOAST_ADDED_TO_PAGE_BODY: ChromeWording;
69
+ export declare const NOTE_TOAST_REMOVED_FROM_PAGE_BODY_NO_TOC: ChromeWording;
70
+ export declare const NOTE_TOAST_ADDED_TO_PAGE_BODY_NO_TOC: ChromeWording;
69
71
  export declare const NOTE_TOAST_VIEW_IN_TOC: ChromeWording;
70
72
  export declare const NOTE_TOOLBAR_DISPLAY_IN_PAGE: ChromeWording;
71
73
  export declare const NOTE_TOOLBAR_DISPLAYED_IN_PAGE: ChromeWording;
@@ -154,7 +154,10 @@ export const NOTE_DISPLAY_MODE_PAGE = DISPLAY_MODE_PAGE;
154
154
  * `TOAST_NOTE_REMOVED_FROM_PAGE` (already in `chrome.ts`); its opposite gets
155
155
  * its own key here. Each toast's BODY bakes in the trailing "Find it in the
156
156
  * TOC…" sentence rather than composing it at the call site — one full
157
- * sentence per key, never a fragment plus a suffix.
157
+ * sentence per key, never a fragment plus a suffix. That sentence promises the
158
+ * "View in TOC" link, which exists only when the host registers an outline
159
+ * panel (`OutlinePanelExtension`, ADR 0034); each body therefore has a
160
+ * `body-no-toc` twin without it, picked when no outline answers.
158
161
  */
159
162
  export const NOTE_TOAST_DISPLAYED_IN_PAGE_MODE = [
160
163
  'com.labre.note.toast.displayed-in-page-mode',
@@ -168,6 +171,14 @@ export const NOTE_TOAST_ADDED_TO_PAGE_BODY = [
168
171
  'com.labre.note.toast.added-to-page.body',
169
172
  'Content added to your document. Find it in the TOC for quick navigation.',
170
173
  ];
174
+ export const NOTE_TOAST_REMOVED_FROM_PAGE_BODY_NO_TOC = [
175
+ 'com.labre.note.toast.removed-from-page.body-no-toc',
176
+ 'Content removed from your document.',
177
+ ];
178
+ export const NOTE_TOAST_ADDED_TO_PAGE_BODY_NO_TOC = [
179
+ 'com.labre.note.toast.added-to-page.body-no-toc',
180
+ 'Content added to your document.',
181
+ ];
171
182
  export const NOTE_TOAST_VIEW_IN_TOC = [
172
183
  'com.labre.note.toast.view-in-toc',
173
184
  'View in Toc',
@@ -236,6 +247,8 @@ export const NOTE_WORDINGS = [
236
247
  NOTE_TOAST_DISPLAYED_IN_PAGE_MODE,
237
248
  NOTE_TOAST_REMOVED_FROM_PAGE_BODY,
238
249
  NOTE_TOAST_ADDED_TO_PAGE_BODY,
250
+ NOTE_TOAST_REMOVED_FROM_PAGE_BODY_NO_TOC,
251
+ NOTE_TOAST_ADDED_TO_PAGE_BODY_NO_TOC,
239
252
  NOTE_TOAST_VIEW_IN_TOC,
240
253
  NOTE_TOOLBAR_DISPLAY_IN_PAGE,
241
254
  NOTE_TOOLBAR_DISPLAYED_IN_PAGE,
@@ -271,12 +271,41 @@ export const builtinToolbarConfig = {
271
271
  blockId: block.model.id,
272
272
  })));
273
273
  }
274
- chain
274
+ // The copy lands right after the last selected block that no other
275
+ // selected block contains. Said explicitly: the command's default
276
+ // anchors on the LAST selected model, which in `flat` mode below
277
+ // is a descendant — and `highest` keeps a block-selected child of
278
+ // a block-selected parent too, so with both selected the copy went
279
+ // INSIDE the parent, after the child.
280
+ const [, { selectedModels: highest = [] }] = chain
275
281
  .pipe(getSelectedModelsCommand, {
276
282
  types: ['block', 'image'],
277
283
  mode: 'highest',
278
284
  })
279
- .pipe(duplicateSelectedModelsCommand)
285
+ .run();
286
+ const selected = new Set(highest);
287
+ const hasSelectedAncestor = (model) => {
288
+ for (let at = store.getParent(model); at; at = store.getParent(at))
289
+ if (selected.has(at))
290
+ return true;
291
+ return false;
292
+ };
293
+ const last = highest.findLast(model => !hasSelectedAncestor(model));
294
+ const parentModel = last && store.getParent(last);
295
+ if (!last || !parentModel)
296
+ return;
297
+ // `flat`, not `highest`: `draftSelectedModelsCommand` keeps a
298
+ // drafted block's children only when they are selected too, so a
299
+ // paragraph duplicated alone lost its nested children.
300
+ chain
301
+ .pipe(getSelectedModelsCommand, {
302
+ types: ['block', 'image'],
303
+ mode: 'flat',
304
+ })
305
+ .pipe(duplicateSelectedModelsCommand, {
306
+ parentModel,
307
+ index: parentModel.children.indexOf(last) + 1,
308
+ })
280
309
  .run();
281
310
  },
282
311
  },
@@ -4,6 +4,10 @@ import { type ChromeWording } from '../affine-shared/services/index.js';
4
4
  * `PACKAGE_SEED_WORDINGS` in `@labre/affine/translations` — see that file and
5
5
  * `packages/affine/shared/src/services/translation-service/README.md`.
6
6
  */
7
+ /**
8
+ * Aliases (L7 dedupe): the callout's own row says the same two words (#468),
9
+ * so they live in `chrome.ts` under the keys they always had.
10
+ */
7
11
  export declare const ROOT_TOOLBAR_CONVERSIONS_ARIA: ChromeWording;
8
12
  export declare const ROOT_TOOLBAR_TURN_INTO: ChromeWording;
9
13
  export declare const ROOT_TOOLBAR_CREATE_TABLE: ChromeWording;
@@ -51,8 +55,9 @@ export declare const ROOT_TOAST_ZOOM_TO_SELECTION: ChromeWording;
51
55
  export declare const ROOT_MINDMAP_SEED_NEW_NODE: ChromeWording;
52
56
  /**
53
57
  * This package's own CHROME wordings — L7 dedupe aliased
54
- * `ROOT_TOOLBAR_CREATE_LINKED_DOC`, `ROOT_TOOLBAR_FRAME`, `ROOT_TOOLBAR_RELOAD`
55
- * and `ROOT_TOOLBAR_REMOVE_LINK` to `chrome.ts` wordings — not listed again
58
+ * `ROOT_TOOLBAR_CONVERSIONS_ARIA`, `ROOT_TOOLBAR_CREATE_LINKED_DOC`,
59
+ * `ROOT_TOOLBAR_FRAME`, `ROOT_TOOLBAR_RELOAD`, `ROOT_TOOLBAR_REMOVE_LINK` and
60
+ * `ROOT_TOOLBAR_TURN_INTO` to `chrome.ts` wordings — not listed again
56
61
  * here, same rule the note/slash-menu packages' own chrome aliases already
57
62
  * follow.
58
63
  */
@@ -1,18 +1,16 @@
1
- import { TOOL_NAME_FRAME, TOOLBAR_CREATE_LINKED_DOC_TITLE, TOOLBAR_RELOAD, TOOLBAR_REMOVE_LINK, } from '../affine-shared/services/index.js';
1
+ import { TOOL_NAME_FRAME, TOOLBAR_CONVERSIONS_ARIA, TOOLBAR_CREATE_LINKED_DOC_TITLE, TOOLBAR_RELOAD, TOOLBAR_REMOVE_LINK, TOOLBAR_TURN_INTO, } from '../affine-shared/services/index.js';
2
2
  /**
3
3
  * This package's own wordings, joined into `PACKAGE_WORDINGS` /
4
4
  * `PACKAGE_SEED_WORDINGS` in `@labre/affine/translations` — see that file and
5
5
  * `packages/affine/shared/src/services/translation-service/README.md`.
6
6
  */
7
7
  /* ── The block contextual toolbar (`configs/toolbar.ts`) ──────────────── */
8
- export const ROOT_TOOLBAR_CONVERSIONS_ARIA = [
9
- 'com.labre.root.toolbar.conversions',
10
- 'Conversions',
11
- ];
12
- export const ROOT_TOOLBAR_TURN_INTO = [
13
- 'com.labre.root.toolbar.turn-into',
14
- 'Turn into',
15
- ];
8
+ /**
9
+ * Aliases (L7 dedupe): the callout's own row says the same two words (#468),
10
+ * so they live in `chrome.ts` under the keys they always had.
11
+ */
12
+ export const ROOT_TOOLBAR_CONVERSIONS_ARIA = TOOLBAR_CONVERSIONS_ARIA;
13
+ export const ROOT_TOOLBAR_TURN_INTO = TOOLBAR_TURN_INTO;
16
14
  export const ROOT_TOOLBAR_CREATE_TABLE = [
17
15
  'com.labre.root.toolbar.create-table',
18
16
  'Create Table',
@@ -134,14 +132,13 @@ export const ROOT_MINDMAP_SEED_NEW_NODE = [
134
132
  ];
135
133
  /**
136
134
  * This package's own CHROME wordings — L7 dedupe aliased
137
- * `ROOT_TOOLBAR_CREATE_LINKED_DOC`, `ROOT_TOOLBAR_FRAME`, `ROOT_TOOLBAR_RELOAD`
138
- * and `ROOT_TOOLBAR_REMOVE_LINK` to `chrome.ts` wordings — not listed again
135
+ * `ROOT_TOOLBAR_CONVERSIONS_ARIA`, `ROOT_TOOLBAR_CREATE_LINKED_DOC`,
136
+ * `ROOT_TOOLBAR_FRAME`, `ROOT_TOOLBAR_RELOAD`, `ROOT_TOOLBAR_REMOVE_LINK` and
137
+ * `ROOT_TOOLBAR_TURN_INTO` to `chrome.ts` wordings — not listed again
139
138
  * here, same rule the note/slash-menu packages' own chrome aliases already
140
139
  * follow.
141
140
  */
142
141
  export const ROOT_CHROME_WORDINGS = [
143
- ROOT_TOOLBAR_CONVERSIONS_ARIA,
144
- ROOT_TOOLBAR_TURN_INTO,
145
142
  ROOT_TOOLBAR_CREATE_TABLE,
146
143
  ROOT_ALIGNMENT_MENU_ARIA,
147
144
  ROOT_ALIGN_LEFT,
@@ -4,17 +4,28 @@ import type { SelectionPaneNode } from './tree.js';
4
4
  * Move `id` so it sits directly ABOVE `above` in the stack — `null` puts it at
5
5
  * the bottom of its stack. Answers whether anything was written.
6
6
  *
7
- * The drop target of a drag in a top-first list: dropping a row just over row
8
- * X means "directly above X". The new `index` is a key between the two
9
- * neighbours (the same `fractional-indexing` keys `LayerManager` generates), so
10
- * one field of one model is written and nothing else moves.
11
- *
12
- * Refused, with nothing written: a read-only document; a mindmap node (it has
13
- * no index of its own — the whole mindmap moves as one, as the toolbar's
14
- * reorder already says); an `above` that is not stacked with `id`; and a
15
- * target that is where the model already is.
7
+ * The one-row case of {@link reorderPaneElements}, kept for its callers.
16
8
  */
17
9
  export declare function reorderPaneElement(std: BlockStdScope, id: string, above: string | null): boolean;
10
+ /**
11
+ * Move the models `ids`, as one block, so they sit directly ABOVE `above` in
12
+ * their stack — `null` puts them at the bottom of it. They keep their current
13
+ * relative z-order, whatever order `ids` lists them in. Answers whether
14
+ * anything was written.
15
+ *
16
+ * The drop target of a drag in a top-first list: dropping rows just over row
17
+ * X means "directly above X". Each moved model gets a new `index` between its
18
+ * new neighbours (the same `fractional-indexing` keys `LayerManager`
19
+ * generates), so only the moved models are written and nothing else shifts.
20
+ *
21
+ * Refused, with nothing written: a read-only document; an unknown id; a
22
+ * mindmap node (it has no index of its own — the whole mindmap moves as one,
23
+ * as the toolbar's reorder already says); models that are not all stacked
24
+ * together (a selection spanning two stacks has no one place to land, ADR
25
+ * 0034); an `above` that is one of them or not stacked with them; and a
26
+ * target that is where the models already are.
27
+ */
28
+ export declare function reorderPaneElements(std: BlockStdScope, ids: readonly string[], above: string | null): boolean;
18
29
  /**
19
30
  * Lock or unlock each model ON ITS OWN. Answers whether anything was written.
20
31
  *
@@ -48,26 +48,53 @@ function stackSiblings(std, model) {
48
48
  * Move `id` so it sits directly ABOVE `above` in the stack — `null` puts it at
49
49
  * the bottom of its stack. Answers whether anything was written.
50
50
  *
51
- * The drop target of a drag in a top-first list: dropping a row just over row
52
- * X means "directly above X". The new `index` is a key between the two
53
- * neighbours (the same `fractional-indexing` keys `LayerManager` generates), so
54
- * one field of one model is written and nothing else moves.
55
- *
56
- * Refused, with nothing written: a read-only document; a mindmap node (it has
57
- * no index of its own — the whole mindmap moves as one, as the toolbar's
58
- * reorder already says); an `above` that is not stacked with `id`; and a
59
- * target that is where the model already is.
51
+ * The one-row case of {@link reorderPaneElements}, kept for its callers.
60
52
  */
61
53
  export function reorderPaneElement(std, id, above) {
54
+ return reorderPaneElements(std, [id], above);
55
+ }
56
+ /**
57
+ * Move the models `ids`, as one block, so they sit directly ABOVE `above` in
58
+ * their stack — `null` puts them at the bottom of it. They keep their current
59
+ * relative z-order, whatever order `ids` lists them in. Answers whether
60
+ * anything was written.
61
+ *
62
+ * The drop target of a drag in a top-first list: dropping rows just over row
63
+ * X means "directly above X". Each moved model gets a new `index` between its
64
+ * new neighbours (the same `fractional-indexing` keys `LayerManager`
65
+ * generates), so only the moved models are written and nothing else shifts.
66
+ *
67
+ * Refused, with nothing written: a read-only document; an unknown id; a
68
+ * mindmap node (it has no index of its own — the whole mindmap moves as one,
69
+ * as the toolbar's reorder already says); models that are not all stacked
70
+ * together (a selection spanning two stacks has no one place to land, ADR
71
+ * 0034); an `above` that is one of them or not stacked with them; and a
72
+ * target that is where the models already are.
73
+ */
74
+ export function reorderPaneElements(std, ids, above) {
62
75
  if (std.store.readonly)
63
76
  return false;
64
- const model = gfxModel(std, id);
65
- if (!model || id === above)
77
+ const moving = new Set(ids);
78
+ if (moving.size === 0)
66
79
  return false;
67
- if (model.group instanceof MindmapElementModel)
80
+ if (above !== null && moving.has(above))
68
81
  return false;
69
- const siblings = stackSiblings(std, model);
70
- const others = siblings.filter(other => other !== model);
82
+ const models = [];
83
+ for (const id of moving) {
84
+ const model = gfxModel(std, id);
85
+ if (!model)
86
+ return false;
87
+ if (model.group instanceof MindmapElementModel)
88
+ return false;
89
+ models.push(model);
90
+ }
91
+ const siblings = stackSiblings(std, models[0]);
92
+ if (models.some(model => !siblings.includes(model)))
93
+ return false;
94
+ // `siblings` is in stacking order, bottom first, so filtering keeps the
95
+ // moved models' relative order.
96
+ const moved = siblings.filter(model => moving.has(model.id));
97
+ const others = siblings.filter(model => !moving.has(model.id));
71
98
  let position = 0;
72
99
  if (above !== null) {
73
100
  const target = others.findIndex(other => other.id === above);
@@ -75,10 +102,13 @@ export function reorderPaneElement(std, id, above) {
75
102
  return false;
76
103
  position = target + 1;
77
104
  }
78
- // Already there: the model's current lower neighbour is the target.
79
- const current = siblings.indexOf(model);
80
- const lowerNow = current > 0 ? siblings[current - 1] : null;
81
- if ((lowerNow?.id ?? null) === above)
105
+ // Already there: the stack would read the same after the move.
106
+ const after = [
107
+ ...others.slice(0, position),
108
+ ...moved,
109
+ ...others.slice(position),
110
+ ];
111
+ if (after.every((model, i) => model === siblings[i]))
82
112
  return false;
83
113
  const lower = others[position - 1]?.index ?? null;
84
114
  const upper = others[position]?.index ?? null;
@@ -86,9 +116,16 @@ export function reorderPaneElement(std, id, above) {
86
116
  // this only guards a hand-made document; refusing beats throwing in a drop.
87
117
  if (lower !== null && upper !== null && lower >= upper)
88
118
  return false;
89
- const index = generateKeyBetween(lower, upper);
119
+ const gfx = std.get(GfxControllerIdentifier);
90
120
  std.store.captureSync();
91
- std.get(GfxControllerIdentifier).updateElement(model, { index });
121
+ std.store.transact(() => {
122
+ let previous = lower;
123
+ for (const model of moved) {
124
+ const index = generateKeyBetween(previous, upper);
125
+ gfx.updateElement(model, { index });
126
+ previous = index;
127
+ }
128
+ });
92
129
  return true;
93
130
  }
94
131
  /**