@formicoidea/labre-core 0.45.0 → 0.46.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 (73) 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/commands/turn-into-callout.d.ts +1 -1
  4. package/dist/_pkgs/affine-block-callout/commands/turn-into-callout.js +1 -1
  5. package/dist/_pkgs/affine-block-callout/configs/slash-menu.js +4 -2
  6. package/dist/_pkgs/affine-block-callout/configs/toolbar.d.ts +65 -0
  7. package/dist/_pkgs/affine-block-callout/configs/toolbar.js +178 -0
  8. package/dist/_pkgs/affine-block-callout/view.d.ts +30 -1
  9. package/dist/_pkgs/affine-block-callout/view.js +45 -3
  10. package/dist/_pkgs/affine-block-code/code-toolbar/components/lang-button.js +1 -1
  11. package/dist/_pkgs/affine-block-frame/commands.d.ts +27 -0
  12. package/dist/_pkgs/affine-block-frame/commands.js +49 -0
  13. package/dist/_pkgs/affine-block-frame/frame-manager.d.ts +11 -6
  14. package/dist/_pkgs/affine-block-frame/frame-manager.js +10 -5
  15. package/dist/_pkgs/affine-block-frame/frame-order.d.ts +43 -0
  16. package/dist/_pkgs/affine-block-frame/frame-order.js +93 -0
  17. package/dist/_pkgs/affine-block-frame/index.d.ts +2 -0
  18. package/dist/_pkgs/affine-block-frame/index.js +2 -0
  19. package/dist/_pkgs/affine-block-frame/present/frame-order-menu.d.ts +10 -2
  20. package/dist/_pkgs/affine-block-frame/present/frame-order-menu.js +32 -18
  21. package/dist/_pkgs/affine-block-frame/view.js +6 -0
  22. package/dist/_pkgs/affine-block-note/configs/toolbar.d.ts +19 -0
  23. package/dist/_pkgs/affine-block-note/configs/toolbar.js +38 -23
  24. package/dist/_pkgs/affine-block-note/translations.d.ts +2 -0
  25. package/dist/_pkgs/affine-block-note/translations.js +14 -1
  26. package/dist/_pkgs/affine-block-root/configs/toolbar.js +31 -2
  27. package/dist/_pkgs/affine-block-root/translations.d.ts +7 -2
  28. package/dist/_pkgs/affine-block-root/translations.js +10 -13
  29. package/dist/_pkgs/affine-block-surface/extensions/selection-pane/actions.d.ts +20 -9
  30. package/dist/_pkgs/affine-block-surface/extensions/selection-pane/actions.js +57 -20
  31. package/dist/_pkgs/affine-block-surface/extensions/selection-pane/commands.d.ts +6 -8
  32. package/dist/_pkgs/affine-block-surface/extensions/selection-pane/commands.js +21 -8
  33. package/dist/_pkgs/affine-block-surface/extensions/selection-pane/index.d.ts +1 -1
  34. package/dist/_pkgs/affine-block-surface/extensions/selection-pane/index.js +1 -1
  35. package/dist/_pkgs/affine-components/portal/portal.d.ts +1 -1
  36. package/dist/_pkgs/affine-foundation/view.d.ts +8 -8
  37. package/dist/_pkgs/affine-fragment-frame-panel/body/frame-panel-body.d.ts +34 -2
  38. package/dist/_pkgs/affine-fragment-frame-panel/body/frame-panel-body.js +194 -81
  39. package/dist/_pkgs/affine-fragment-frame-panel/card/frame-card.d.ts +7 -4
  40. package/dist/_pkgs/affine-fragment-frame-panel/card/frame-card.js +9 -24
  41. package/dist/_pkgs/affine-fragment-frame-panel/index.d.ts +1 -0
  42. package/dist/_pkgs/affine-fragment-frame-panel/index.js +1 -0
  43. package/dist/_pkgs/affine-model/utils/types.d.ts +2 -2
  44. package/dist/_pkgs/affine-shared/services/edit-props-store.d.ts +4 -4
  45. package/dist/_pkgs/affine-shared/services/font-loader/config.d.ts +2 -2
  46. package/dist/_pkgs/affine-shared/services/font-loader/font-loader-service.d.ts +3 -3
  47. package/dist/_pkgs/affine-shared/services/index.d.ts +1 -1
  48. package/dist/_pkgs/affine-shared/services/index.js +1 -1
  49. package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-storage.d.ts +4 -4
  50. package/dist/_pkgs/affine-shared/services/outline-panel-service.d.ts +31 -0
  51. package/dist/_pkgs/affine-shared/services/outline-panel-service.js +17 -0
  52. package/dist/_pkgs/affine-shared/services/translation-service/chrome.d.ts +11 -0
  53. package/dist/_pkgs/affine-shared/services/translation-service/chrome.js +19 -0
  54. package/dist/_pkgs/affine-shared/utils/index.d.ts +1 -0
  55. package/dist/_pkgs/affine-shared/utils/index.js +1 -0
  56. package/dist/_pkgs/affine-shared/utils/panel-reorder-drag.d.ts +85 -0
  57. package/dist/_pkgs/affine-shared/utils/panel-reorder-drag.js +162 -0
  58. package/dist/_pkgs/affine-widget-edgeless-toolbar/selection-pane/drop.d.ts +7 -6
  59. package/dist/_pkgs/affine-widget-edgeless-toolbar/selection-pane/drop.js +11 -8
  60. package/dist/_pkgs/affine-widget-edgeless-toolbar/selection-pane/selection-pane-widget.d.ts +31 -15
  61. package/dist/_pkgs/affine-widget-edgeless-toolbar/selection-pane/selection-pane-widget.js +178 -142
  62. package/dist/_pkgs/std/scope/std-scope.d.ts +1 -1
  63. package/dist/commands.js +2 -0
  64. package/dist/extensions/view.js +5 -1
  65. package/dist/flags.d.ts +3 -0
  66. package/dist/flags.js +3 -0
  67. package/dist/host-panels.d.ts +90 -0
  68. package/dist/host-panels.js +111 -0
  69. package/package.json +5 -1
  70. package/dist/_pkgs/affine-fragment-frame-panel/utils/drag.d.ts +0 -27
  71. package/dist/_pkgs/affine-fragment-frame-panel/utils/drag.js +0 -96
  72. package/dist/_pkgs/affine-shared/services/sidebar-service.d.ts +0 -8
  73. package/dist/_pkgs/affine-shared/services/sidebar-service.js +0 -7
@@ -0,0 +1,43 @@
1
+ import { CalloutBlockModel } from '../../affine-model/index.js';
2
+ import { type Command } from '../../std/index.js';
3
+ import { type BlockModel, type Store } from '../../store/index.js';
4
+ /**
5
+ * The selected callouts "Turn into → <text kind>" would unwrap, or none at all.
6
+ *
7
+ * Decided BEFORE anything is written, and all-or-nothing: if one callout
8
+ * cannot be converted, none is (#468). A callout cannot be when its parent
9
+ * does not accept the target flavour (a code block under a database row),
10
+ * or when a grandchild could not live under the block its own parent becomes
11
+ * — and a code block keeps no children at all, so a callout whose content
12
+ * has nested blocks is not turned into code (the merge would drop them).
13
+ * Anything that is not a callout is skipped, and a callout nested in another
14
+ * selected callout moves with it rather than being converted twice.
15
+ */
16
+ export declare function calloutUnwrapTargets(store: Store, models: BlockModel[], flavour: string): CalloutBlockModel[];
17
+ /**
18
+ * "Turn into → <text kind>" on a selected callout (#468): the exact inverse of
19
+ * `turnIntoCalloutCommand`. The callout is a HUB whose own `text` is unused
20
+ * and whose words live in its children, so the conversion UNWRAPS: each child
21
+ * becomes a block of the target kind at the callout's place, in order, its
22
+ * own children moved under it; a target of `affine:code` merges a callout's
23
+ * children into one code block, the way `mergeToCodeModel` merges a
24
+ * selection; an empty callout yields one empty block. The callout's emoji is
25
+ * dropped with it — it belongs to the frame being removed.
26
+ *
27
+ * Add first, prove with `getModelById`, delete only after (#418, #423): every
28
+ * new block is built before anything moves, and if one cannot be, the ones
29
+ * already built are deleted and every callout stays as it was. Only then are
30
+ * the grandchildren moved (validated up front by `calloutUnwrapTargets`) and
31
+ * the callouts deleted. The lifted blocks are therefore NEW blocks carrying
32
+ * the text, like the paragraph `turnIntoCalloutCommand` creates, while the
33
+ * grandchildren keep their identity.
34
+ */
35
+ export declare const turnCalloutIntoCommand: Command<{
36
+ models: BlockModel[];
37
+ flavour: string;
38
+ props?: {
39
+ type?: string;
40
+ };
41
+ }, {
42
+ updatedBlocks: BlockModel[];
43
+ }>;
@@ -0,0 +1,109 @@
1
+ import { CalloutBlockModel } from '../../affine-model/index.js';
2
+ import { matchModels } from '../../affine-shared/utils/index.js';
3
+ import { BlockSelection } from '../../std/index.js';
4
+ import { Text } from '../../store/index.js';
5
+ const isAncestor = (store, ancestor, block) => {
6
+ for (let at = store.getParent(block); at; at = store.getParent(at)) {
7
+ if (at === ancestor)
8
+ return true;
9
+ }
10
+ return false;
11
+ };
12
+ /**
13
+ * The selected callouts "Turn into → <text kind>" would unwrap, or none at all.
14
+ *
15
+ * Decided BEFORE anything is written, and all-or-nothing: if one callout
16
+ * cannot be converted, none is (#468). A callout cannot be when its parent
17
+ * does not accept the target flavour (a code block under a database row),
18
+ * or when a grandchild could not live under the block its own parent becomes
19
+ * — and a code block keeps no children at all, so a callout whose content
20
+ * has nested blocks is not turned into code (the merge would drop them).
21
+ * Anything that is not a callout is skipped, and a callout nested in another
22
+ * selected callout moves with it rather than being converted twice.
23
+ */
24
+ export function calloutUnwrapTargets(store, models, flavour) {
25
+ const { schema } = store;
26
+ const callouts = models.filter((model) => matchModels(model, [CalloutBlockModel]));
27
+ const outermost = callouts.filter(callout => !callouts.some(other => isAncestor(store, other, callout)));
28
+ const feasible = outermost.every(callout => {
29
+ const parent = store.getParent(callout);
30
+ if (!parent || !schema.isValid(flavour, parent.flavour))
31
+ return false;
32
+ return callout.children.every(child => flavour === 'affine:code'
33
+ ? child.children.length === 0
34
+ : child.children.every(grandchild => schema.isValid(grandchild.flavour, flavour)));
35
+ });
36
+ return feasible ? outermost : [];
37
+ }
38
+ /**
39
+ * "Turn into → <text kind>" on a selected callout (#468): the exact inverse of
40
+ * `turnIntoCalloutCommand`. The callout is a HUB whose own `text` is unused
41
+ * and whose words live in its children, so the conversion UNWRAPS: each child
42
+ * becomes a block of the target kind at the callout's place, in order, its
43
+ * own children moved under it; a target of `affine:code` merges a callout's
44
+ * children into one code block, the way `mergeToCodeModel` merges a
45
+ * selection; an empty callout yields one empty block. The callout's emoji is
46
+ * dropped with it — it belongs to the frame being removed.
47
+ *
48
+ * Add first, prove with `getModelById`, delete only after (#418, #423): every
49
+ * new block is built before anything moves, and if one cannot be, the ones
50
+ * already built are deleted and every callout stays as it was. Only then are
51
+ * the grandchildren moved (validated up front by `calloutUnwrapTargets`) and
52
+ * the callouts deleted. The lifted blocks are therefore NEW blocks carrying
53
+ * the text, like the paragraph `turnIntoCalloutCommand` creates, while the
54
+ * grandchildren keep their identity.
55
+ */
56
+ export const turnCalloutIntoCommand = (ctx, next) => {
57
+ const { std, models, flavour, props } = ctx;
58
+ const store = std.store;
59
+ if (store.readonly)
60
+ return;
61
+ const callouts = calloutUnwrapTargets(store, models, flavour);
62
+ if (callouts.length === 0)
63
+ return;
64
+ store.captureSync();
65
+ const created = [];
66
+ const adoptions = [];
67
+ const add = (callout, text) => {
68
+ const parent = store.getParent(callout);
69
+ // Inserted right before the callout, so the blocks keep their order.
70
+ const block = parent
71
+ ? store.getModelById(store.addBlock(flavour, { ...props, ...(text && { text }) }, parent, parent.children.indexOf(callout)))
72
+ : null;
73
+ if (block)
74
+ created.push(block);
75
+ return block;
76
+ };
77
+ // `addBlock` returns an id even for a block the store refused, so only
78
+ // `getModelById` proves the block exists (`add` above).
79
+ const build = (callout) => {
80
+ const sources = callout.children;
81
+ if (flavour === 'affine:code') {
82
+ const joined = sources
83
+ .map(source => source.text?.toString())
84
+ .filter(Boolean)
85
+ .join('\n');
86
+ return add(callout, new Text(joined)) !== null;
87
+ }
88
+ if (sources.length === 0)
89
+ return add(callout) !== null;
90
+ return sources.every(source => {
91
+ const block = add(callout, source.text?.clone());
92
+ if (block && source.children.length > 0)
93
+ adoptions.push([source.children, block]);
94
+ return block !== null;
95
+ });
96
+ };
97
+ if (!callouts.every(build)) {
98
+ created.forEach(block => store.deleteBlock(block));
99
+ return;
100
+ }
101
+ adoptions.forEach(([children, block]) => store.moveBlocks(children, block));
102
+ callouts.forEach(callout => store.deleteBlock(callout));
103
+ // The gesture started from a block selection (the callout's handle), so it
104
+ // ends on one; deferred a frame so the new blocks have rendered.
105
+ requestAnimationFrame(() => {
106
+ std.selection.setGroup('note', created.map(block => std.selection.create(BlockSelection, { blockId: block.id })));
107
+ });
108
+ return next({ updatedBlocks: created });
109
+ };
@@ -14,7 +14,7 @@ import type { BlockModel, Store } from '../../store/index.js';
14
14
  */
15
15
  export declare function calloutConversionTargets(store: Store, models: BlockModel[]): BlockModel[];
16
16
  /**
17
- * Wraps each selected paragraph, list item or code block in a callout (#418).
17
+ * Turns each selected paragraph, list item or code block into a callout (#418).
18
18
  *
19
19
  * A callout is a hub with no text of its own, so this cannot be
20
20
  * `transformModel`: the callout takes the block's place, a paragraph inside it
@@ -37,7 +37,7 @@ export function calloutConversionTargets(store, models) {
37
37
  return convertible.filter(model => !convertible.some(other => isAncestor(store, other, model)));
38
38
  }
39
39
  /**
40
- * Wraps each selected paragraph, list item or code block in a callout (#418).
40
+ * Turns each selected paragraph, list item or code block into a callout (#418).
41
41
  *
42
42
  * A callout is a hub with no text of its own, so this cannot be
43
43
  * `transformModel`: the callout takes the block's place, a paragraph inside it
@@ -42,10 +42,12 @@ export const calloutSlashMenuConfig = {
42
42
  if (index === -1)
43
43
  return;
44
44
  const calloutId = store.addBlock('affine:callout', {}, parent, index + 1);
45
- if (!calloutId)
45
+ // `addBlock` returns an id even when the schema refused the block
46
+ // (`transact` swallows the throw, #418): only the model proves it.
47
+ if (!store.getModelById(calloutId))
46
48
  return;
47
49
  const paragraphId = store.addBlock('affine:paragraph', {}, calloutId);
48
- if (!paragraphId)
50
+ if (!store.getModelById(paragraphId))
49
51
  return;
50
52
  std.host.updateComplete
51
53
  .then(() => {
@@ -0,0 +1,65 @@
1
+ import { ActionPlacement, type ToolbarContext } from '../../affine-shared/services/index.js';
2
+ /**
3
+ * The callout's block toolbar, opened by its drag handle (#468). Without a
4
+ * module for `affine:callout` the toolbar widget shows no row for a selected
5
+ * callout at all, so a callout could not be converted back. Copy, duplicate
6
+ * and delete are the note row's, restricted to the block selection — except
7
+ * that they take the selection in `flat` mode, descendants included, where
8
+ * the note row's Duplicate takes `highest`: a callout is a hub, and drafted
9
+ * without its children it would copy as an empty frame.
10
+ *
11
+ * Centred above the block (`top`) like the note row a paragraph or a code
12
+ * block opens; without a placement of its own the widget sets a block's row
13
+ * at `top-start`, which left the callout's row hanging off its left edge.
14
+ */
15
+ export declare const calloutToolbarConfig: {
16
+ readonly placement: "top";
17
+ readonly actions: [{
18
+ readonly id: "a.conversions";
19
+ readonly when: (ctx: ToolbarContext) => boolean;
20
+ readonly content: (ctx: ToolbarContext) => import("lit-html").TemplateResult<1> | null;
21
+ }, {
22
+ readonly variant?: "destructive";
23
+ readonly content?: ((cx: ToolbarContext) => import("lit-html").TemplateResult | null) | (import("lit-html").TemplateResult | null);
24
+ readonly label?: string;
25
+ readonly icon?: import("lit-html").TemplateResult;
26
+ readonly active?: ((cx: ToolbarContext) => boolean) | boolean;
27
+ readonly disabled?: ((cx: ToolbarContext) => boolean) | boolean;
28
+ readonly placement?: ActionPlacement;
29
+ readonly tooltip?: string | import("lit-html").TemplateResult;
30
+ readonly score?: number;
31
+ readonly when?: ((cx: ToolbarContext) => boolean) | boolean;
32
+ readonly priority?: number;
33
+ readonly labelWording?: import("../../affine-shared/services/index.js").ChromeWording;
34
+ readonly showLabel?: boolean;
35
+ readonly tooltipWording?: import("../../affine-shared/services/index.js").ChromeWording;
36
+ readonly run?: (cx: ToolbarContext) => void;
37
+ readonly id: "g.comment";
38
+ }, {
39
+ readonly placement: ActionPlacement.More;
40
+ readonly id: "a.clipboard";
41
+ readonly actions: [{
42
+ readonly id: "copy";
43
+ readonly labelWording: import("../../affine-shared/services/index.js").ChromeWording;
44
+ readonly icon: import("lit-html").TemplateResult<1>;
45
+ readonly run: ({ chain, host, std }: ToolbarContext) => void;
46
+ }, {
47
+ readonly id: "duplicate";
48
+ readonly labelWording: import("../../affine-shared/services/index.js").ChromeWording;
49
+ readonly icon: import("lit-html").TemplateResult<1>;
50
+ readonly run: (ctx: ToolbarContext) => void;
51
+ }];
52
+ readonly when: (ctx: ToolbarContext) => boolean;
53
+ }, {
54
+ readonly placement: ActionPlacement.More;
55
+ readonly id: "c.delete";
56
+ readonly actions: [{
57
+ readonly id: "delete";
58
+ readonly labelWording: import("../../affine-shared/services/index.js").ChromeWording;
59
+ readonly icon: import("lit-html").TemplateResult<1>;
60
+ readonly variant: "destructive";
61
+ readonly run: ({ chain }: ToolbarContext) => void;
62
+ }];
63
+ readonly when: (ctx: ToolbarContext) => boolean;
64
+ }];
65
+ };
@@ -0,0 +1,178 @@
1
+ import { toast } from '../../affine-components/toast/index.js';
2
+ import { EditorChevronDown } from '../../affine-components/toolbar/index.js';
3
+ import { CalloutBlockModel } from '../../affine-model/index.js';
4
+ import { textConversionConfigs } from '../../affine-rich-text/index.js';
5
+ import { copySelectedModelsCommand, deleteSelectedModelsCommand, draftSelectedModelsCommand, duplicateSelectedModelsCommand, getSelectedModelsCommand, } from '../../affine-shared/commands/index.js';
6
+ import { ActionPlacement, blockCommentToolbarButton, TOAST_COPIED_TO_CLIPBOARD, TOOLBAR_CONVERSIONS_ARIA, TOOLBAR_COPY, TOOLBAR_DELETE, TOOLBAR_DUPLICATE, TOOLBAR_TURN_INTO, translateKey, } from '../../affine-shared/services/index.js';
7
+ import { matchModels } from '../../affine-shared/utils/index.js';
8
+ import { CopyIcon, DeleteIcon, DuplicateIcon, FontIcon, } from '@blocksuite/icons/lit';
9
+ import { html } from 'lit';
10
+ import { repeat } from 'lit/directives/repeat.js';
11
+ import { calloutUnwrapTargets, turnCalloutIntoCommand, } from '../commands/turn-callout-into.js';
12
+ /** The block-selected callouts — the row only opens on a block selection. */
13
+ const selectedCallouts = ({ chain }) => {
14
+ const [ok, { selectedModels = [] }] = chain
15
+ .pipe(getSelectedModelsCommand, { types: ['block'], mode: 'highest' })
16
+ .run();
17
+ return ok
18
+ ? selectedModels.filter(model => matchModels(model, [CalloutBlockModel]))
19
+ : [];
20
+ };
21
+ /**
22
+ * "Turn into" on a callout (#468): the text kinds of the note's own row, minus
23
+ * the divider (which is not a conversion of a block's words), each offered
24
+ * only when `calloutUnwrapTargets` says the whole conversion can complete —
25
+ * a refusal is an absent entry, never a click that does nothing.
26
+ *
27
+ * Like the note row, which offers "Turn into" on a paragraph or a code block
28
+ * only once it holds words (`isFormatSupported`), the entry waits for the
29
+ * callout to hold some: an empty callout shows the "⋮" menu alone (PO
30
+ * recette of #468, for one behaviour across blocks).
31
+ */
32
+ const conversionsAction = {
33
+ id: 'a.conversions',
34
+ when: ctx => !ctx.readonly,
35
+ content(ctx) {
36
+ const { std, store } = ctx;
37
+ const callouts = selectedCallouts(ctx);
38
+ if (callouts.length === 0)
39
+ return null;
40
+ const holdsWords = callouts.some(callout => callout.children.some(child => (child.text?.length ?? 0) > 0));
41
+ if (!holdsWords)
42
+ return null;
43
+ const conversions = textConversionConfigs.filter(({ flavour }) => flavour !== 'affine:divider' &&
44
+ calloutUnwrapTargets(store, callouts, flavour).length > 0);
45
+ if (conversions.length === 0)
46
+ return null;
47
+ return html `
48
+ <editor-menu-button
49
+ .contentPadding="${'8px'}"
50
+ .button=${html `
51
+ <editor-icon-button
52
+ aria-label="${translateKey(std, ...TOOLBAR_CONVERSIONS_ARIA)}"
53
+ .tooltip="${translateKey(std, ...TOOLBAR_TURN_INTO)}"
54
+ >
55
+ ${FontIcon()} ${EditorChevronDown}
56
+ </editor-icon-button>
57
+ `}
58
+ >
59
+ <div data-size="large" data-orientation="vertical">
60
+ ${repeat(conversions, item => item.name, ({ flavour, type, name, nameWording, icon }) => {
61
+ const label = nameWording
62
+ ? translateKey(std, ...nameWording)
63
+ : name;
64
+ return html `
65
+ <editor-menu-action
66
+ aria-label=${label}
67
+ @click=${() => std.command.exec(turnCalloutIntoCommand, {
68
+ models: callouts,
69
+ flavour,
70
+ ...(type && { props: { type } }),
71
+ })}
72
+ >
73
+ ${icon}<span class="label">${label}</span>
74
+ </editor-menu-action>
75
+ `;
76
+ })}
77
+ </div>
78
+ </editor-menu-button>
79
+ `;
80
+ },
81
+ };
82
+ /**
83
+ * The callout's block toolbar, opened by its drag handle (#468). Without a
84
+ * module for `affine:callout` the toolbar widget shows no row for a selected
85
+ * callout at all, so a callout could not be converted back. Copy, duplicate
86
+ * and delete are the note row's, restricted to the block selection — except
87
+ * that they take the selection in `flat` mode, descendants included, where
88
+ * the note row's Duplicate takes `highest`: a callout is a hub, and drafted
89
+ * without its children it would copy as an empty frame.
90
+ *
91
+ * Centred above the block (`top`) like the note row a paragraph or a code
92
+ * block opens; without a placement of its own the widget sets a block's row
93
+ * at `top-start`, which left the callout's row hanging off its left edge.
94
+ */
95
+ export const calloutToolbarConfig = {
96
+ placement: 'top',
97
+ actions: [
98
+ conversionsAction,
99
+ {
100
+ id: 'g.comment',
101
+ ...blockCommentToolbarButton,
102
+ },
103
+ {
104
+ placement: ActionPlacement.More,
105
+ id: 'a.clipboard',
106
+ actions: [
107
+ {
108
+ id: 'copy',
109
+ labelWording: TOOLBAR_COPY,
110
+ icon: CopyIcon(),
111
+ run({ chain, host, std }) {
112
+ // `flat`, not `highest`: the callout's words are its children,
113
+ // and the draft keeps only the children that are selected too.
114
+ const [ok] = chain
115
+ .pipe(getSelectedModelsCommand, {
116
+ types: ['block'],
117
+ mode: 'flat',
118
+ })
119
+ .pipe(draftSelectedModelsCommand)
120
+ .pipe(copySelectedModelsCommand)
121
+ .run();
122
+ if (!ok)
123
+ return;
124
+ toast(host, translateKey(std, ...TOAST_COPIED_TO_CLIPBOARD));
125
+ },
126
+ },
127
+ {
128
+ id: 'duplicate',
129
+ labelWording: TOOLBAR_DUPLICATE,
130
+ icon: DuplicateIcon(),
131
+ run(ctx) {
132
+ const { chain, store } = ctx;
133
+ // The copy lands right after the (last) callout. Said explicitly:
134
+ // the command's default anchors on the LAST selected model, which
135
+ // in `flat` mode is one of the callout's descendants.
136
+ const last = selectedCallouts(ctx).at(-1);
137
+ const parentModel = last && store.getParent(last);
138
+ if (!last || !parentModel)
139
+ return;
140
+ store.captureSync();
141
+ // `flat`, not `highest`, for the same reason as Copy: a callout
142
+ // drafted without its children duplicates as an empty frame.
143
+ chain
144
+ .pipe(getSelectedModelsCommand, {
145
+ types: ['block'],
146
+ mode: 'flat',
147
+ })
148
+ .pipe(duplicateSelectedModelsCommand, {
149
+ parentModel,
150
+ index: parentModel.children.indexOf(last) + 1,
151
+ })
152
+ .run();
153
+ },
154
+ },
155
+ ],
156
+ when: ctx => !ctx.flags.isNative(),
157
+ },
158
+ {
159
+ placement: ActionPlacement.More,
160
+ id: 'c.delete',
161
+ actions: [
162
+ {
163
+ id: 'delete',
164
+ labelWording: TOOLBAR_DELETE,
165
+ icon: DeleteIcon(),
166
+ variant: 'destructive',
167
+ run({ chain }) {
168
+ chain
169
+ .pipe(getSelectedModelsCommand, { types: ['block'] })
170
+ .pipe(deleteSelectedModelsCommand)
171
+ .run();
172
+ },
173
+ },
174
+ ],
175
+ when: ctx => !ctx.flags.isNative(),
176
+ },
177
+ ],
178
+ };
@@ -1,6 +1,35 @@
1
1
  import { type ViewExtensionContext, ViewExtensionProvider } from '../affine-ext-loader/index.js';
2
- export declare class CalloutViewExtension extends ViewExtensionProvider {
2
+ /**
3
+ * Callout rendering — ALWAYS registered, independent of the `callout` flag
4
+ * (ADR 0009, #418). A document that already holds callouts must paint them,
5
+ * let them be edited and let them be turned back into text whatever the flag
6
+ * says: until 2026-10-11 the whole view sat behind the flag, and
7
+ * `{ callout: false }` left every stored callout rendered as nothing.
8
+ *
9
+ * What lives here is what an EXISTING callout needs, the mirror of
10
+ * `WardleyRenderViewExtension`:
11
+ * - `effects()` whole: `affine-callout` paints the block, and
12
+ * `affine-emoji-menu` opens from a rendered callout's icon — both are
13
+ * render-side, so unlike the frameworks' this `effects()` is not tooling;
14
+ * - the flavour and the block view, the lookup `lit-host` performs;
15
+ * - the keymap: Enter and Backspace inside a callout edit stored content, they
16
+ * create nothing;
17
+ * - the block toolbar (#468): Turn into text, copy, duplicate, delete — the
18
+ * way OUT of a stored callout, as the Wardley contextual toolbar stays with
19
+ * a stored map.
20
+ */
21
+ export declare class CalloutRenderViewExtension extends ViewExtensionProvider {
3
22
  name: string;
4
23
  effect(): void;
5
24
  setup(context: ViewExtensionContext): void;
6
25
  }
26
+ /**
27
+ * Callout creation tooling — flag-gated (`callout`, ADR 0009): the slash-menu
28
+ * item and the "Turn into → Callout" entry, the two ways to CREATE a callout.
29
+ * With the flag off no new callout can be made; the ones already written keep
30
+ * rendering through {@link CalloutRenderViewExtension}.
31
+ */
32
+ export declare class CalloutViewExtension extends ViewExtensionProvider {
33
+ name: string;
34
+ setup(context: ViewExtensionContext): void;
35
+ }
@@ -1,16 +1,37 @@
1
1
  import { ViewExtensionProvider, } from '../affine-ext-loader/index.js';
2
2
  import { TextConversionEntryExtension } from '../affine-rich-text/index.js';
3
+ import { ToolbarModuleExtension } from '../affine-shared/services/index.js';
3
4
  import { SlashMenuConfigExtension } from '../affine-widget-slash-menu/index.js';
4
- import { BlockViewExtension, FlavourExtension } from '../std/index.js';
5
+ import { BlockFlavourIdentifier, BlockViewExtension, FlavourExtension, } from '../std/index.js';
5
6
  import { literal } from 'lit/static-html.js';
6
7
  import { CalloutKeymapExtension } from './callout-keymap.js';
7
8
  import { calloutSlashMenuConfig } from './configs/slash-menu.js';
9
+ import { calloutToolbarConfig } from './configs/toolbar.js';
8
10
  import { calloutTurnIntoEntry } from './configs/turn-into.js';
9
11
  import { effects } from './effects.js';
10
- export class CalloutViewExtension extends ViewExtensionProvider {
12
+ /**
13
+ * Callout rendering — ALWAYS registered, independent of the `callout` flag
14
+ * (ADR 0009, #418). A document that already holds callouts must paint them,
15
+ * let them be edited and let them be turned back into text whatever the flag
16
+ * says: until 2026-10-11 the whole view sat behind the flag, and
17
+ * `{ callout: false }` left every stored callout rendered as nothing.
18
+ *
19
+ * What lives here is what an EXISTING callout needs, the mirror of
20
+ * `WardleyRenderViewExtension`:
21
+ * - `effects()` whole: `affine-callout` paints the block, and
22
+ * `affine-emoji-menu` opens from a rendered callout's icon — both are
23
+ * render-side, so unlike the frameworks' this `effects()` is not tooling;
24
+ * - the flavour and the block view, the lookup `lit-host` performs;
25
+ * - the keymap: Enter and Backspace inside a callout edit stored content, they
26
+ * create nothing;
27
+ * - the block toolbar (#468): Turn into text, copy, duplicate, delete — the
28
+ * way OUT of a stored callout, as the Wardley contextual toolbar stays with
29
+ * a stored map.
30
+ */
31
+ export class CalloutRenderViewExtension extends ViewExtensionProvider {
11
32
  constructor() {
12
33
  super(...arguments);
13
- this.name = 'affine-callout-block';
34
+ this.name = 'affine-callout-render-block';
14
35
  }
15
36
  effect() {
16
37
  super.effect();
@@ -22,6 +43,27 @@ export class CalloutViewExtension extends ViewExtensionProvider {
22
43
  FlavourExtension('affine:callout'),
23
44
  BlockViewExtension('affine:callout', literal `affine-callout`),
24
45
  CalloutKeymapExtension,
46
+ ToolbarModuleExtension({
47
+ id: BlockFlavourIdentifier('affine:callout'),
48
+ config: calloutToolbarConfig,
49
+ }),
50
+ ]);
51
+ }
52
+ }
53
+ /**
54
+ * Callout creation tooling — flag-gated (`callout`, ADR 0009): the slash-menu
55
+ * item and the "Turn into → Callout" entry, the two ways to CREATE a callout.
56
+ * With the flag off no new callout can be made; the ones already written keep
57
+ * rendering through {@link CalloutRenderViewExtension}.
58
+ */
59
+ export class CalloutViewExtension extends ViewExtensionProvider {
60
+ constructor() {
61
+ super(...arguments);
62
+ this.name = 'affine-callout-block';
63
+ }
64
+ setup(context) {
65
+ super.setup(context);
66
+ context.register([
25
67
  SlashMenuConfigExtension('affine:callout', calloutSlashMenuConfig),
26
68
  TextConversionEntryExtension(calloutTurnIntoEntry),
27
69
  ]);
@@ -118,7 +118,7 @@ let LanguageListButton = (() => {
118
118
  const plainText = {
119
119
  label: translateKey(this.blockComponent.std, ...CODE_PLAIN_TEXT_LANGUAGE),
120
120
  name: PLAIN_TEXT_ITEM_NAME,
121
- aliases: ['plain', 'text', 'none'],
121
+ aliases: ['plain', 'text', 'plaintext', 'txt', 'none'],
122
122
  };
123
123
  const index = languages.findIndex(item => item.name === PLAIN_TEXT_ITEM_NAME);
124
124
  if (index === -1) {
@@ -0,0 +1,27 @@
1
+ import type { AnyCommandDescriptor } from '../std/index.js';
2
+ import { z } from 'zod';
3
+ /**
4
+ * The frames' commands, `owner: 'core'`: a frame belongs to no framework, and
5
+ * `'core'` owners are exempt from the id-prefix rule (ADR 0008).
6
+ *
7
+ * They live here and not beside the selection pane's: `blocks/surface` cannot
8
+ * import `blocks/frame` (the dependency runs the other way), and the action
9
+ * reads the frame manager's presentation order.
10
+ */
11
+ export declare const reorderFramesParams: z.ZodObject<{
12
+ /** The frames to move, as one block, keeping their relative order. */
13
+ ids: z.ZodArray<z.ZodString, "many">;
14
+ /**
15
+ * The frame they land directly BEFORE in the presentation order; `null`
16
+ * puts them at the end.
17
+ */
18
+ before: z.ZodNullable<z.ZodString>;
19
+ }, "strip", z.ZodTypeAny, {
20
+ before: string | null;
21
+ ids: string[];
22
+ }, {
23
+ before: string | null;
24
+ ids: string[];
25
+ }>;
26
+ export type ReorderFramesParams = z.infer<typeof reorderFramesParams>;
27
+ export declare const frameCommands: AnyCommandDescriptor[];
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+ import { reorderFramePresentation } from './frame-order.js';
3
+ /**
4
+ * The frames' commands, `owner: 'core'`: a frame belongs to no framework, and
5
+ * `'core'` owners are exempt from the id-prefix rule (ADR 0008).
6
+ *
7
+ * They live here and not beside the selection pane's: `blocks/surface` cannot
8
+ * import `blocks/frame` (the dependency runs the other way), and the action
9
+ * reads the frame manager's presentation order.
10
+ */
11
+ export const reorderFramesParams = z.object({
12
+ /** The frames to move, as one block, keeping their relative order. */
13
+ ids: z.array(z.string().min(1)).min(1),
14
+ /**
15
+ * The frame they land directly BEFORE in the presentation order; `null`
16
+ * puts them at the end.
17
+ */
18
+ before: z.string().min(1).nullable(),
19
+ });
20
+ /**
21
+ * Reorder the presentation (ADR 0034). The frame panel's drag, the
22
+ * presentation toolbar's order menu and a host's own slide panel all run it
23
+ * through `runCommand`, so the read-only refusal, the no-op check and the one
24
+ * undo step live in the action, once.
25
+ */
26
+ const reorderFrames = {
27
+ id: 'canvas.frame.reorder',
28
+ owner: 'core',
29
+ kind: 'action',
30
+ labelKey: 'com.labre.command.canvas.frame.reorder',
31
+ labelFallback: 'Reorder frames',
32
+ descriptionKey: 'com.labre.command.canvas.frame.reorder.description',
33
+ descriptionFallback: 'Move frames before another frame in the presentation order, or to its end.',
34
+ // Agent only: without an explicit target there is nothing to say where.
35
+ surfaces: ['agent'],
36
+ scope: 'edgeless',
37
+ defaultKeys: { mac: [], other: [] },
38
+ availability: 'editable',
39
+ params: reorderFramesParams,
40
+ run: (std, _invocation, params) => {
41
+ const parsed = reorderFramesParams.safeParse(params);
42
+ if (!parsed.success) {
43
+ console.error('canvas.frame.reorder: invalid params', parsed.error);
44
+ return;
45
+ }
46
+ reorderFramePresentation(std, parsed.data.ids, parsed.data.before);
47
+ },
48
+ };
49
+ export const frameCommands = [reorderFrames];