@formicoidea/labre-core 0.37.0 → 0.38.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 (53) hide show
  1. package/dist/_pkgs/affine-block-frame/frame-manager.d.ts +31 -0
  2. package/dist/_pkgs/affine-block-frame/frame-manager.js +62 -2
  3. package/dist/_pkgs/affine-block-frame/frame-tool.js +6 -2
  4. package/dist/_pkgs/affine-block-surface/extensions/audit.js +12 -34
  5. package/dist/_pkgs/affine-block-surface/extensions/reading-toolbar.js +12 -12
  6. package/dist/_pkgs/affine-block-surface/extensions/reading.js +10 -28
  7. package/dist/_pkgs/affine-block-surface/extensions/validation.d.ts +321 -16
  8. package/dist/_pkgs/affine-block-surface/extensions/validation.js +945 -62
  9. package/dist/_pkgs/affine-block-surface/framework-background/facts.d.ts +24 -0
  10. package/dist/_pkgs/affine-block-surface/framework-background/facts.js +31 -0
  11. package/dist/_pkgs/affine-block-surface/framework-background/index.d.ts +1 -0
  12. package/dist/_pkgs/affine-block-surface/framework-background/index.js +1 -0
  13. package/dist/_pkgs/affine-block-surface/framework-background/stacking.d.ts +131 -0
  14. package/dist/_pkgs/affine-block-surface/framework-background/stacking.js +184 -0
  15. package/dist/_pkgs/affine-block-surface/utils/sort.d.ts +15 -1
  16. package/dist/_pkgs/affine-block-surface/utils/sort.js +30 -31
  17. package/dist/_pkgs/affine-block-surface/view.js +5 -0
  18. package/dist/_pkgs/affine-components/highlight-dropdown-menu/highlight-duotone-icon.js +8 -8
  19. package/dist/_pkgs/affine-components/highlight-dropdown-menu/text-duotone-icon.js +11 -11
  20. package/dist/_pkgs/affine-components/icons/text.js +426 -426
  21. package/dist/_pkgs/affine-gfx-group/element-renderer/utils.js +3 -1
  22. package/dist/_pkgs/affine-gfx-template/index.d.ts +1 -0
  23. package/dist/_pkgs/affine-gfx-template/index.js +1 -0
  24. package/dist/_pkgs/affine-gfx-template/make-snapshot.d.ts +16 -2
  25. package/dist/_pkgs/affine-gfx-template/make-snapshot.js +43 -4
  26. package/dist/_pkgs/affine-gfx-template/services/template-middlewares.js +1 -0
  27. package/dist/_pkgs/affine-gfx-template/services/template.d.ts +6 -0
  28. package/dist/_pkgs/affine-gfx-template/services/template.js +6 -0
  29. package/dist/_pkgs/affine-gfx-template/snapshot-from-action.d.ts +20 -0
  30. package/dist/_pkgs/affine-gfx-template/snapshot-from-action.js +205 -0
  31. package/dist/_pkgs/affine-gfx-template/toolbar/template-panel.js +4 -0
  32. package/dist/_pkgs/affine-gfx-template/toolbar/template-type.d.ts +13 -0
  33. package/dist/_pkgs/affine-inline-link/link-node/configs/toolbar.d.ts +11 -17
  34. package/dist/_pkgs/affine-inline-link/link-node/configs/toolbar.js +55 -14
  35. package/dist/_pkgs/affine-model/elements/cynefin/cynefin.d.ts +9 -20
  36. package/dist/_pkgs/affine-model/elements/cynefin/cynefin.js +7 -28
  37. package/dist/_pkgs/affine-model/elements/edgy/board.d.ts +9 -21
  38. package/dist/_pkgs/affine-model/elements/edgy/board.js +8 -29
  39. package/dist/_pkgs/affine-model/elements/edgy/facets.d.ts +14 -28
  40. package/dist/_pkgs/affine-model/elements/edgy/facets.js +12 -36
  41. package/dist/_pkgs/affine-model/elements/estuarine/estuarine.d.ts +9 -19
  42. package/dist/_pkgs/affine-model/elements/estuarine/estuarine.js +7 -27
  43. package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-service.d.ts +28 -0
  44. package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-service.js +20 -3
  45. package/dist/_pkgs/affine-widget-edgeless-auto-connect/index.js +124 -124
  46. package/dist/_pkgs/affine-widget-edgeless-zoom-toolbar/index.js +26 -26
  47. package/dist/_pkgs/affine-widget-keyboard-toolbar/icons.js +98 -98
  48. package/dist/_pkgs/affine-widget-page-dragging-area/index.js +10 -10
  49. package/dist/_pkgs/affine-widget-viewport-overlay/index.js +13 -13
  50. package/dist/_pkgs/data-view/core/group-by/renderer/boolean-group.js +11 -11
  51. package/dist/_pkgs/data-view/core/utils/menu-title.js +17 -17
  52. package/dist/_pkgs/data-view/view-presets/table/pc-virtual/group/top/group-title.js +18 -18
  53. package/package.json +1 -1
@@ -3,7 +3,9 @@ import { FontWeight } from '../../affine-model/index.js';
3
3
  import { Bound } from '../../global/gfx/index.js';
4
4
  import { GROUP_TITLE_FONT, GROUP_TITLE_FONT_SIZE, GROUP_TITLE_OFFSET, GROUP_TITLE_PADDING, } from './consts.js';
5
5
  export function titleRenderParams(group, zoom) {
6
- let text = group.title.toString().trim();
6
+ // A group loaded without a title (older documents, hand-written templates)
7
+ // must not take the whole canvas down with it.
8
+ let text = (group.title?.toString() ?? '').trim();
7
9
  const font = getGroupTitleFont(zoom);
8
10
  const lineWidth = getLineWidth(text, font);
9
11
  const lineHeight = getLineHeight(GROUP_TITLE_FONT, GROUP_TITLE_FONT_SIZE / zoom, 'normal');
@@ -1,6 +1,7 @@
1
1
  export * from './contribute.js';
2
2
  export * from './make-snapshot.js';
3
3
  export * from './services/template.js';
4
+ export * from './snapshot-from-action.js';
4
5
  export * from './template-tool.js';
5
6
  export * from './toolbar/senior-tool.js';
6
7
  export * from './toolbar/template-panel.js';
@@ -1,6 +1,7 @@
1
1
  export * from './contribute.js';
2
2
  export * from './make-snapshot.js';
3
3
  export * from './services/template.js';
4
+ export * from './snapshot-from-action.js';
4
5
  export * from './template-tool.js';
5
6
  export * from './toolbar/senior-tool.js';
6
7
  export * from './toolbar/template-panel.js';
@@ -12,11 +12,25 @@ export declare function surfaceText(text: string): {
12
12
  insert: string;
13
13
  }[];
14
14
  };
15
+ /**
16
+ * Serialize a plain record into the surface `Y.Map` JSON form used inside
17
+ * snapshots — a group's `children`, a mindmap's node table. Same seam as
18
+ * {@link surfaceText}: what `SurfaceBlockTransformer.toSnapshot` emits.
19
+ */
20
+ export declare function surfaceYMap(json: Record<string, unknown>): {
21
+ "affine:surface:ymap": boolean;
22
+ json: Record<string, unknown>;
23
+ };
15
24
  /**
16
25
  * Wrap a surface-elements map into the minimal `page → surface` DocSnapshot a
17
26
  * template's `content` expects. Block/element ids are remapped on insert by the
18
- * template job's `replaceIdMiddleware`, and z-order indices are regenerated, so
19
- * callers only need stable-within-snapshot ids and meaningful element props.
27
+ * template job's `replaceIdMiddleware`.
28
+ *
29
+ * Z-order indices are regenerated on insert too, but IN THE SNAPSHOT'S OWN
30
+ * ORDER: `createRegenerateIndexMiddleware` sorts the elements by their `index`
31
+ * (group-aware) and only then mints fresh keys — and it skips any element that
32
+ * carries no `index` at all. So a missing `index` is filled in here, in key
33
+ * (insertion) order, which is the order a creation action added the elements.
20
34
  */
21
35
  export declare function makeTemplateSnapshot(elements: SurfaceElementsJSON, title?: string): {
22
36
  type: string;
@@ -1,4 +1,4 @@
1
- import { SURFACE_TEXT_UNIQ_IDENTIFIER } from '../std/gfx/index.js';
1
+ import { generateKeyBetween, SURFACE_TEXT_UNIQ_IDENTIFIER, SURFACE_YMAP_UNIQ_IDENTIFIER, } from '../std/gfx/index.js';
2
2
  /**
3
3
  * Serialize a plain string into the surface `Y.Text` JSON form used inside
4
4
  * snapshots (the shape inner-text / label field). Mirrors what
@@ -11,13 +11,52 @@ export function surfaceText(text) {
11
11
  delta: text ? [{ insert: text }] : [],
12
12
  };
13
13
  }
14
+ /**
15
+ * Serialize a plain record into the surface `Y.Map` JSON form used inside
16
+ * snapshots — a group's `children`, a mindmap's node table. Same seam as
17
+ * {@link surfaceText}: what `SurfaceBlockTransformer.toSnapshot` emits.
18
+ */
19
+ export function surfaceYMap(json) {
20
+ return {
21
+ [SURFACE_YMAP_UNIQ_IDENTIFIER]: true,
22
+ json,
23
+ };
24
+ }
14
25
  /**
15
26
  * Wrap a surface-elements map into the minimal `page → surface` DocSnapshot a
16
27
  * template's `content` expects. Block/element ids are remapped on insert by the
17
- * template job's `replaceIdMiddleware`, and z-order indices are regenerated, so
18
- * callers only need stable-within-snapshot ids and meaningful element props.
28
+ * template job's `replaceIdMiddleware`.
29
+ *
30
+ * Z-order indices are regenerated on insert too, but IN THE SNAPSHOT'S OWN
31
+ * ORDER: `createRegenerateIndexMiddleware` sorts the elements by their `index`
32
+ * (group-aware) and only then mints fresh keys — and it skips any element that
33
+ * carries no `index` at all. So a missing `index` is filled in here, in key
34
+ * (insertion) order, which is the order a creation action added the elements.
19
35
  */
20
36
  export function makeTemplateSnapshot(elements, title = 'Template') {
37
+ const indexed = {};
38
+ let last = null;
39
+ for (const [id, element] of Object.entries(elements)) {
40
+ const copy = { ...element };
41
+ // The middleware keys its fresh indexes by the element's OWN `id` field,
42
+ // not by the map key: a hand-written element without one would collect
43
+ // every other element's key under `undefined` and land at one shared depth.
44
+ if (copy['id'] === undefined)
45
+ copy['id'] = id;
46
+ // A group's title is a Y.Text the model initialises at creation and reads
47
+ // back unguarded when it paints (`group.title.toString()`): a group written
48
+ // without one lands with no key at all, and the renderer threw on every
49
+ // frame — the inserted map showed its background and nothing else
50
+ // (recette of 09/09/2026).
51
+ if (copy['type'] === 'group' && copy['title'] === undefined) {
52
+ copy['title'] = surfaceText('');
53
+ }
54
+ if (copy['index'] === undefined) {
55
+ last = generateKeyBetween(last, null);
56
+ copy['index'] = last;
57
+ }
58
+ indexed[id] = copy;
59
+ }
21
60
  return {
22
61
  type: 'page',
23
62
  meta: {
@@ -41,7 +80,7 @@ export function makeTemplateSnapshot(elements, title = 'Template') {
41
80
  type: 'block',
42
81
  id: 'block:template-surface',
43
82
  flavour: 'affine:surface',
44
- props: { elements },
83
+ props: { elements: indexed },
45
84
  children: [],
46
85
  },
47
86
  ],
@@ -35,6 +35,7 @@ export const replaceIdMiddleware = (job) => {
35
35
  regeneratedIdMap.set(id, newId);
36
36
  val.id = newId;
37
37
  elements[newId] = val;
38
+ job.insertedElementIds.push(newId);
38
39
  if (['connector', 'group', 'mindmap'].includes(val['type'])) {
39
40
  defered.push(newId);
40
41
  }
@@ -30,6 +30,12 @@ export declare class TemplateJob {
30
30
  static middlewares: ((job: TemplateJob) => void)[];
31
31
  private _template;
32
32
  job: Transformer;
33
+ /**
34
+ * The surface-element ids minted for this insertion, in the snapshot's own
35
+ * key order. Filled by `replaceIdMiddleware`; read by a template's
36
+ * `afterInsert`, which needs the FRESH ids of what it just dropped.
37
+ */
38
+ insertedElementIds: string[];
33
39
  model: SurfaceBlockModel;
34
40
  slots: {
35
41
  beforeInsert: Subject<SlotBlockPayload | {
@@ -28,6 +28,12 @@ export class TemplateJob {
28
28
  static { this.middlewares = []; }
29
29
  constructor({ model, type, middlewares }) {
30
30
  this._template = null;
31
+ /**
32
+ * The surface-element ids minted for this insertion, in the snapshot's own
33
+ * key order. Filled by `replaceIdMiddleware`; read by a template's
34
+ * `afterInsert`, which needs the FRESH ids of what it just dropped.
35
+ */
36
+ this.insertedElementIds = [];
31
37
  this.slots = {
32
38
  beforeInsert: new Subject(),
33
39
  };
@@ -0,0 +1,20 @@
1
+ import type { BlockStdScope, CommandDescriptor } from '../std/index.js';
2
+ import { makeTemplateSnapshot } from './make-snapshot.js';
3
+ import type { Template } from './toolbar/template-type.js';
4
+ /**
5
+ * Record what a creation action asks the surface for, and hand it back as a
6
+ * template snapshot — so a framework template is the command it derives from
7
+ * rather than a hand-written copy of it that drifts.
8
+ *
9
+ * The `std` handed to `run` is a plain recording fake: it answers the members
10
+ * placement actions actually touch and throws on anything else, so an
11
+ * unsupported action fails loudly at build time instead of producing a
12
+ * half-empty template.
13
+ */
14
+ export declare function snapshotFromAction(run: (std: BlockStdScope) => void, name: string): ReturnType<typeof makeTemplateSnapshot>;
15
+ /**
16
+ * Derive a template from the command a user would have run — the template IS
17
+ * the command, so the two can never say different things about what a
18
+ * framework's artefact looks like.
19
+ */
20
+ export declare function templateFromCommand(command: CommandDescriptor, preview: string, name?: string): Template;
@@ -0,0 +1,205 @@
1
+ import { EdgelessCRUDIdentifier } from '../affine-block-surface/index.js';
2
+ import { Bound } from '../global/gfx/index.js';
3
+ import { generateKeyBetween, GfxControllerIdentifier } from '../std/gfx/index.js';
4
+ import * as Y from 'yjs';
5
+ import { makeTemplateSnapshot, surfaceText, surfaceYMap, } from './make-snapshot.js';
6
+ /**
7
+ * The invocation every derived template runs under. Any legal literal does:
8
+ * telemetry is emitted by `runCommand`, never by a command body, so nothing is
9
+ * reported when the descriptor's `run` is called directly like this.
10
+ */
11
+ const INVOCATION = {
12
+ surface: 'senior-menu',
13
+ source: 'internal',
14
+ };
15
+ /** A standalone Y.Text reads empty until integrated; integrate to read it. */
16
+ function materialize(text) {
17
+ if (!text.doc)
18
+ new Y.Doc().getMap('m').set('t', text);
19
+ return text.toString();
20
+ }
21
+ /**
22
+ * Record what a creation action asks the surface for, and hand it back as a
23
+ * template snapshot — so a framework template is the command it derives from
24
+ * rather than a hand-written copy of it that drifts.
25
+ *
26
+ * The `std` handed to `run` is a plain recording fake: it answers the members
27
+ * placement actions actually touch and throws on anything else, so an
28
+ * unsupported action fails loudly at build time instead of producing a
29
+ * half-empty template.
30
+ */
31
+ export function snapshotFromAction(run, name) {
32
+ const elements = new Map();
33
+ let n = 0;
34
+ let lastIndex = null;
35
+ const nextIndex = () => (lastIndex = generateKeyBetween(lastIndex, null));
36
+ const addElement = (props) => {
37
+ const id = `el-${n++}`;
38
+ const record = {};
39
+ for (const [key, value] of Object.entries(props)) {
40
+ if (value !== undefined)
41
+ record[key] = value;
42
+ }
43
+ record['id'] = id;
44
+ // ponytail: the Y conversions are hard-coded (`text`, and a group's
45
+ // `children` / `title`) instead of asking each element class for its own
46
+ // `propsToY`. Ceiling: it covers every prop the framework actions pass
47
+ // today. Upgrade path: run the real `propsToY` once the element-ctor map is
48
+ // reachable without a live surface.
49
+ const text = record['text'];
50
+ if (typeof text === 'string')
51
+ record['text'] = surfaceText(text);
52
+ else if (text instanceof Y.Text)
53
+ record['text'] = surfaceText(materialize(text));
54
+ if (record['type'] === 'group') {
55
+ const children = record['children'];
56
+ if (children && typeof children === 'object') {
57
+ record['children'] = surfaceYMap(Object.fromEntries(Object.keys(children).map(key => [key, true])));
58
+ }
59
+ const title = record['title'];
60
+ if (typeof title === 'string')
61
+ record['title'] = surfaceText(title);
62
+ else if (title instanceof Y.Text)
63
+ record['title'] = surfaceText(materialize(title));
64
+ }
65
+ if (record['index'] === undefined)
66
+ record['index'] = nextIndex();
67
+ // The model members an action reads back off `getElementById`, kept
68
+ // non-enumerable so they never leak into the snapshot.
69
+ Object.defineProperties(record, {
70
+ group: { value: null },
71
+ deserializedXYWH: {
72
+ get: () => typeof record['xywh'] === 'string'
73
+ ? Bound.deserialize(record['xywh'])
74
+ : null,
75
+ },
76
+ });
77
+ elements.set(id, record);
78
+ return id;
79
+ };
80
+ const records = () => [...elements.values()];
81
+ const surface = {
82
+ addElement,
83
+ getElementById: (id) => elements.get(id) ?? null,
84
+ getElementsByType: (type) => records().filter(el => el['type'] === type),
85
+ get elementModels() {
86
+ return records();
87
+ },
88
+ };
89
+ const gfx = {
90
+ surface,
91
+ viewport: { centerX: 0, centerY: 0, zoom: 1, center: { x: 0, y: 0 } },
92
+ doc: { captureSync: () => { } },
93
+ selection: { set: () => { }, selectedElements: [] },
94
+ tool: { setTool: () => { } },
95
+ layer: {
96
+ generateIndex: () => nextIndex(),
97
+ getReorderedIndex: (_model, direction) => {
98
+ const sorted = records()
99
+ .map(el => String(el['index']))
100
+ .sort();
101
+ return direction === 'back'
102
+ ? generateKeyBetween(null, sorted[0] ?? null)
103
+ : generateKeyBetween(sorted[sorted.length - 1] ?? null, null);
104
+ },
105
+ get canvasElements() {
106
+ return records();
107
+ },
108
+ },
109
+ std: null,
110
+ };
111
+ const crud = {
112
+ addElement: (type, props) => addElement({ ...props, type }),
113
+ };
114
+ const std = {
115
+ get: (identifier) => {
116
+ if (identifier === GfxControllerIdentifier)
117
+ return gfx;
118
+ if (identifier === EdgelessCRUDIdentifier)
119
+ return crud;
120
+ throw new Error(`snapshotFromAction: unsupported service "${identifier?.identifierName ?? String(identifier)}"`);
121
+ },
122
+ getOptional: () => undefined,
123
+ command: {
124
+ // Really run the command, so a group built by `createGroupCommand` is
125
+ // whatever that command says a group is — no second definition here.
126
+ exec: (cmd, payload) => {
127
+ let out;
128
+ cmd({ std: gfx.std, ...payload }, r => {
129
+ out = r;
130
+ });
131
+ return [{}, out ?? {}];
132
+ },
133
+ },
134
+ store: {
135
+ readonly: false,
136
+ captureSync: () => { },
137
+ id: 'doc:template',
138
+ workspace: { meta: { getDocMeta: () => undefined } },
139
+ },
140
+ };
141
+ gfx.std = std;
142
+ run(gfx.std);
143
+ if (elements.size === 0) {
144
+ throw new Error('snapshotFromAction: the action added no element (a tool-arming command?)');
145
+ }
146
+ normalizeToOrigin(records());
147
+ return makeTemplateSnapshot(Object.fromEntries(elements), name);
148
+ }
149
+ /** The connector endpoints that carry a free position rather than an anchor. */
150
+ function freeEndpoints(el) {
151
+ const out = [];
152
+ for (const key of ['source', 'target']) {
153
+ const end = el[key];
154
+ if (end && Array.isArray(end.position)) {
155
+ out.push(end);
156
+ }
157
+ }
158
+ return out;
159
+ }
160
+ /** Move the whole drawing so its top-left corner sits at (0, 0). */
161
+ function normalizeToOrigin(records) {
162
+ let minX = Infinity;
163
+ let minY = Infinity;
164
+ for (const el of records) {
165
+ if (typeof el['xywh'] === 'string') {
166
+ const bound = Bound.deserialize(el['xywh']);
167
+ minX = Math.min(minX, bound.x);
168
+ minY = Math.min(minY, bound.y);
169
+ }
170
+ for (const end of freeEndpoints(el)) {
171
+ minX = Math.min(minX, end.position[0]);
172
+ minY = Math.min(minY, end.position[1]);
173
+ }
174
+ }
175
+ if (!Number.isFinite(minX) || (minX === 0 && minY === 0))
176
+ return;
177
+ for (const el of records) {
178
+ if (typeof el['xywh'] === 'string') {
179
+ const bound = Bound.deserialize(el['xywh']);
180
+ bound.x -= minX;
181
+ bound.y -= minY;
182
+ el['xywh'] = bound.serialize();
183
+ }
184
+ for (const end of freeEndpoints(el)) {
185
+ // `vertices` are left alone on purpose: they are already normalised 0-1.
186
+ end.position = [end.position[0] - minX, end.position[1] - minY];
187
+ }
188
+ }
189
+ }
190
+ /**
191
+ * Derive a template from the command a user would have run — the template IS
192
+ * the command, so the two can never say different things about what a
193
+ * framework's artefact looks like.
194
+ */
195
+ export function templateFromCommand(command, preview, name = command.labelFallback ?? command.id) {
196
+ return {
197
+ name,
198
+ type: 'template',
199
+ preview,
200
+ commandId: command.id,
201
+ content: snapshotFromAction(std => {
202
+ void command.run(std, INVOCATION);
203
+ }, name),
204
+ };
205
+ }
@@ -330,6 +330,9 @@ let EdgelessTemplatePanel = (() => {
330
330
  }
331
331
  async _insertTemplate(template, bound) {
332
332
  this._loadingTemplate = template;
333
+ // Kept before the clone: `afterInsert` is a closure the template author
334
+ // wrote, and `cloneDeep` does not survive a function.
335
+ const original = template;
333
336
  template = cloneDeep(template);
334
337
  const center = {
335
338
  x: bound.x + bound.w / 2,
@@ -344,6 +347,7 @@ let EdgelessTemplatePanel = (() => {
344
347
  .then(blob => templateJob.job.assets.set(key, blob))));
345
348
  }
346
349
  const insertedBound = await templateJob.insertTemplate(template.content);
350
+ original.afterInsert?.(this.edgeless.std, templateJob.insertedElementIds);
347
351
  if (insertedBound && template.type === 'template') {
348
352
  const padding = 20 / this.gfx.viewport.zoom;
349
353
  this.gfx.viewport.setViewportByBound(insertedBound, [padding, padding, padding, padding], true);
@@ -1,3 +1,4 @@
1
+ import type { BlockStdScope } from '../../std/index.js';
1
2
  export type Template = {
2
3
  /**
3
4
  * name of the sticker
@@ -20,6 +21,18 @@ export type Template = {
20
21
  * `sticker`: sticker template, only contains one image block under surface block
21
22
  */
22
23
  type: 'template' | 'sticker';
24
+ /**
25
+ * Id of the command this template was derived from, when it was derived
26
+ * rather than hand-written (see `snapshotFromAction`). Lets a coverage test
27
+ * assert that every framework artefact command has a template.
28
+ */
29
+ commandId?: string;
30
+ /**
31
+ * Run once the template's elements are on the surface, with the FRESH
32
+ * element ids in snapshot order — for a template whose final shape depends
33
+ * on what is already on the board.
34
+ */
35
+ afterInsert?: (std: BlockStdScope, insertedIds: string[]) => void;
23
36
  };
24
37
  export type TemplateCategory = {
25
38
  name: string;
@@ -1,20 +1,20 @@
1
- import { ActionPlacement } from '../../../affine-shared/services/index.js';
1
+ import { ActionPlacement, type ToolbarContext } from '../../../affine-shared/services/index.js';
2
2
  export declare const builtinInlineLinkToolbarConfig: {
3
3
  readonly actions: [{
4
4
  readonly id: "a.preview";
5
- readonly content: (cx: import("../../../affine-shared/services/index.js").ToolbarContext) => import("lit-html").TemplateResult<1> | null;
5
+ readonly content: (cx: ToolbarContext) => import("lit-html").TemplateResult<1> | null;
6
6
  }, {
7
7
  readonly id: "b.copy-link-and-edit";
8
8
  readonly actions: [{
9
9
  readonly id: "copy-link";
10
10
  readonly tooltip: "Copy link";
11
11
  readonly icon: import("lit-html").TemplateResult<1>;
12
- readonly run: (ctx: import("../../../affine-shared/services/index.js").ToolbarContext) => void;
12
+ readonly run: (ctx: ToolbarContext) => void;
13
13
  }, {
14
14
  readonly id: "edit";
15
15
  readonly tooltip: "Edit";
16
16
  readonly icon: import("lit-html").TemplateResult<1>;
17
- readonly run: (ctx: import("../../../affine-shared/services/index.js").ToolbarContext) => void;
17
+ readonly run: (ctx: ToolbarContext) => void;
18
18
  }];
19
19
  }, {
20
20
  id: string;
@@ -22,35 +22,29 @@ export declare const builtinInlineLinkToolbarConfig: {
22
22
  id: string;
23
23
  labelWording: import("../../../affine-shared/services/index.js").ChromeWording;
24
24
  disabled: true;
25
- run?: undefined;
26
- when?: undefined;
27
- } | {
28
- id: string;
29
- labelWording: import("../../../affine-shared/services/index.js").ChromeWording;
30
- run(ctx: import("../../../affine-shared/services/index.js").ToolbarContext): void;
31
- disabled?: undefined;
32
25
  when?: undefined;
26
+ run?: undefined;
33
27
  } | {
34
28
  id: string;
35
29
  labelWording: import("../../../affine-shared/services/index.js").ChromeWording;
36
- when(ctx: import("../../../affine-shared/services/index.js").ToolbarContext): boolean;
37
- run(ctx: import("../../../affine-shared/services/index.js").ToolbarContext): void;
30
+ when(ctx: ToolbarContext): boolean;
31
+ run(ctx: ToolbarContext): void;
38
32
  disabled?: undefined;
39
33
  })[];
40
- content(ctx: import("../../../affine-shared/services/index.js").ToolbarContext): import("lit-html").TemplateResult<1> | null;
41
- when(ctx: import("../../../affine-shared/services/index.js").ToolbarContext): boolean;
34
+ content(ctx: ToolbarContext): import("lit-html").TemplateResult<1> | null;
35
+ when(ctx: ToolbarContext): boolean;
42
36
  }, {
43
37
  readonly placement: ActionPlacement.More;
44
38
  readonly id: "b.remove-link";
45
39
  readonly label: "Remove link";
46
40
  readonly icon: import("lit-html").TemplateResult<1>;
47
- readonly run: (ctx: import("../../../affine-shared/services/index.js").ToolbarContext) => void;
41
+ readonly run: (ctx: ToolbarContext) => void;
48
42
  }, {
49
43
  readonly placement: ActionPlacement.More;
50
44
  readonly id: "c.delete";
51
45
  readonly labelWording: import("../../../affine-shared/services/index.js").ChromeWording;
52
46
  readonly icon: import("lit-html").TemplateResult<1>;
53
47
  readonly variant: "destructive";
54
- readonly run: (ctx: import("../../../affine-shared/services/index.js").ToolbarContext) => void;
48
+ readonly run: (ctx: ToolbarContext) => void;
55
49
  }];
56
50
  };
@@ -14,6 +14,47 @@ const trackBaseProps = {
14
14
  category: 'link',
15
15
  type: 'inline view',
16
16
  };
17
+ /**
18
+ * Flavour the "Card view" conversion would create for `url`.
19
+ */
20
+ function cardFlavour(ctx, url) {
21
+ const options = ctx.std.get(EmbedOptionProvider).getEmbedBlockOptions(url);
22
+ return options?.viewType === 'card' ? options.flavour : 'affine:bookmark';
23
+ }
24
+ /**
25
+ * Flavour the "Embed view" conversion would create for `url`, or `null` when
26
+ * nothing can embed it.
27
+ */
28
+ function embedFlavour(ctx, url) {
29
+ const options = ctx.std.get(EmbedOptionProvider).getEmbedBlockOptions(url);
30
+ if (options?.viewType === 'embed')
31
+ return options.flavour;
32
+ if (ctx.std.get(EmbedIframeService).canEmbed(url)) {
33
+ return 'affine:embed-iframe';
34
+ }
35
+ return null;
36
+ }
37
+ /**
38
+ * A conversion is a creation tool: it may only be offered when the block it
39
+ * creates can be rendered here. Block flags gate view extensions (ADR 0009),
40
+ * so a flavour without a registered view is one the host switched off —
41
+ * converting would replace the link with a block that paints as nothing.
42
+ *
43
+ * ponytail: runtime probe of the flag's EFFECT, not the flag. Holds while a
44
+ * block's renderer and tooling share one view extension. The day bookmark or
45
+ * embed gets the render/tooling split of ADR 0009, the view stays registered
46
+ * with the flag off and this gate goes blind. Upgrade path: a link-conversion
47
+ * contribution identifier that the gated bookmark/embed extensions register,
48
+ * so the action disappears by absence like every other gated tool.
49
+ */
50
+ function canConvertTo(ctx, flavour) {
51
+ return !!flavour && !!ctx.std.getView(flavour);
52
+ }
53
+ function isOffered(ctx, action) {
54
+ return typeof action.when === 'function'
55
+ ? action.when(ctx)
56
+ : (action.when ?? true);
57
+ }
17
58
  export const builtinInlineLinkToolbarConfig = {
18
59
  actions: [
19
60
  {
@@ -85,6 +126,15 @@ export const builtinInlineLinkToolbarConfig = {
85
126
  {
86
127
  id: 'card',
87
128
  labelWording: TOOLBAR_CARD_VIEW,
129
+ when(ctx) {
130
+ const target = ctx.message$.peek()?.element;
131
+ if (!(target instanceof AffineLink))
132
+ return false;
133
+ const url = target.link;
134
+ if (!url)
135
+ return false;
136
+ return canConvertTo(ctx, cardFlavour(ctx, url));
137
+ },
88
138
  run(ctx) {
89
139
  const target = ctx.message$.peek()?.element;
90
140
  if (!(target instanceof AffineLink))
@@ -101,12 +151,7 @@ export const builtinInlineLinkToolbarConfig = {
101
151
  // Clears
102
152
  ctx.reset();
103
153
  const title = inlineEditor.yTextString.slice(selfInlineRange.index, selfInlineRange.index + selfInlineRange.length);
104
- const options = ctx.std
105
- .get(EmbedOptionProvider)
106
- .getEmbedBlockOptions(url);
107
- const flavour = options?.viewType === 'card'
108
- ? options.flavour
109
- : 'affine:bookmark';
154
+ const flavour = cardFlavour(ctx, url);
110
155
  const index = parent.children.indexOf(model);
111
156
  const props = {
112
157
  url,
@@ -147,13 +192,7 @@ export const builtinInlineLinkToolbarConfig = {
147
192
  const { parent } = model;
148
193
  if (!inlineEditor || !selfInlineRange || !parent)
149
194
  return false;
150
- // check if the url can be embedded as iframe block
151
- const embedIframeService = ctx.std.get(EmbedIframeService);
152
- const canEmbedAsIframe = embedIframeService.canEmbed(url);
153
- const options = ctx.std
154
- .get(EmbedOptionProvider)
155
- .getEmbedBlockOptions(url);
156
- return canEmbedAsIframe || options?.viewType === 'embed';
195
+ return canConvertTo(ctx, embedFlavour(ctx, url));
157
196
  },
158
197
  run(ctx) {
159
198
  const target = ctx.message$.peek()?.element;
@@ -257,7 +296,9 @@ export const builtinInlineLinkToolbarConfig = {
257
296
  catch {
258
297
  return false;
259
298
  }
260
- return true;
299
+ // "Inline view" is the current state; without another target the
300
+ // dropdown would offer nothing.
301
+ return this.actions.some(action => action.id !== 'inline' && isOffered(ctx, action));
261
302
  },
262
303
  },
263
304
  {
@@ -1,10 +1,7 @@
1
- import type { IVec, SerializedXYWH } from '../../../global/gfx/index.js';
2
- import { Bound } from '../../../global/gfx/index.js';
3
- import type { BaseElementProps, PointTestOptions } from '../../../std/gfx/index.js';
4
- import { GfxPrimitiveElementModel } from '../../../std/gfx/index.js';
5
- export type CynefinProps = BaseElementProps & {
6
- /** When false the resize handles are hidden — toggled from the toolbar. */
7
- resizeEnabled?: boolean;
1
+ import type { SerializedXYWH } from '../../../global/gfx/index.js';
2
+ import { FrameworkBackgroundElementModel } from '../framework-background/index.js';
3
+ import type { FrameworkBackgroundProps } from '../framework-background/index.js';
4
+ export type CynefinProps = FrameworkBackgroundProps & {
8
5
  /** When false the domain titles + the A/C marker letters and names are hidden. */
9
6
  showTitles?: boolean;
10
7
  /** When false the explanatory text (subheadings, decisions, annotations, notes) is hidden. */
@@ -19,21 +16,13 @@ export type CynefinProps = BaseElementProps & {
19
16
  * boundary — reproduced from the official SVG paths. The user places regular
20
17
  * edgeless elements on top of it.
21
18
  *
22
- * Mirrors the Wardley / EDGY backgrounds: extends {@link GfxPrimitiveElementModel}
23
- * so it inherits selection, move, copy/paste, duplicate, align and undo/redo.
19
+ * An INSTANCE of the framework-background primitive
20
+ * ({@link FrameworkBackgroundElementModel}), like the Wardley / EDGY
21
+ * backgrounds: the passive-canvas geometry comes from the primitive, the
22
+ * fields below are the persisted document.
24
23
  */
25
- export declare class CynefinElementModel extends GfxPrimitiveElementModel<CynefinProps> {
24
+ export declare class CynefinElementModel extends FrameworkBackgroundElementModel<CynefinProps> {
26
25
  get type(): string;
27
- get connectable(): boolean;
28
- containsBound(bounds: Bound): boolean;
29
- getLineIntersections(start: IVec, end: IVec): import("../../../global/gfx/index.js").PointLocation[] | null;
30
- getNearestPoint(point: IVec): IVec;
31
- /**
32
- * Picked by its BORDER band — see {@link backgroundIncludesPoint}. The five
33
- * domains are a picture the user drops elements onto, so the clicks inside
34
- * the frame belong to those elements.
35
- */
36
- includesPoint(x: number, y: number, options?: PointTestOptions): boolean;
37
26
  accessor resizeEnabled: boolean;
38
27
  accessor showTitles: boolean;
39
28
  accessor showDescriptions: boolean;