@formicoidea/labre-core 0.37.0 → 0.38.2
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.
- package/dist/_pkgs/affine-block-frame/frame-manager.d.ts +31 -0
- package/dist/_pkgs/affine-block-frame/frame-manager.js +62 -2
- package/dist/_pkgs/affine-block-frame/frame-tool.js +6 -2
- package/dist/_pkgs/affine-block-surface/extensions/audit.js +12 -34
- package/dist/_pkgs/affine-block-surface/extensions/reading-toolbar.js +12 -12
- package/dist/_pkgs/affine-block-surface/extensions/reading.js +10 -28
- package/dist/_pkgs/affine-block-surface/extensions/validation.d.ts +321 -16
- package/dist/_pkgs/affine-block-surface/extensions/validation.js +945 -62
- package/dist/_pkgs/affine-block-surface/framework-background/facts.d.ts +24 -0
- package/dist/_pkgs/affine-block-surface/framework-background/facts.js +31 -0
- package/dist/_pkgs/affine-block-surface/framework-background/index.d.ts +1 -0
- package/dist/_pkgs/affine-block-surface/framework-background/index.js +1 -0
- package/dist/_pkgs/affine-block-surface/framework-background/stacking.d.ts +131 -0
- package/dist/_pkgs/affine-block-surface/framework-background/stacking.js +184 -0
- package/dist/_pkgs/affine-block-surface/utils/sort.d.ts +15 -1
- package/dist/_pkgs/affine-block-surface/utils/sort.js +30 -31
- package/dist/_pkgs/affine-block-surface/view.js +5 -0
- package/dist/_pkgs/affine-components/highlight-dropdown-menu/highlight-duotone-icon.js +8 -8
- package/dist/_pkgs/affine-components/highlight-dropdown-menu/text-duotone-icon.js +11 -11
- package/dist/_pkgs/affine-components/icons/text.js +426 -426
- package/dist/_pkgs/affine-gfx-group/element-renderer/utils.js +3 -1
- package/dist/_pkgs/affine-gfx-template/index.d.ts +1 -0
- package/dist/_pkgs/affine-gfx-template/index.js +1 -0
- package/dist/_pkgs/affine-gfx-template/make-snapshot.d.ts +16 -2
- package/dist/_pkgs/affine-gfx-template/make-snapshot.js +43 -4
- package/dist/_pkgs/affine-gfx-template/services/template-middlewares.js +1 -0
- package/dist/_pkgs/affine-gfx-template/services/template.d.ts +6 -0
- package/dist/_pkgs/affine-gfx-template/services/template.js +6 -0
- package/dist/_pkgs/affine-gfx-template/snapshot-from-action.d.ts +20 -0
- package/dist/_pkgs/affine-gfx-template/snapshot-from-action.js +205 -0
- package/dist/_pkgs/affine-gfx-template/toolbar/template-panel.js +4 -0
- package/dist/_pkgs/affine-gfx-template/toolbar/template-type.d.ts +13 -0
- package/dist/_pkgs/affine-inline-link/link-node/configs/toolbar.d.ts +11 -17
- package/dist/_pkgs/affine-inline-link/link-node/configs/toolbar.js +55 -14
- package/dist/_pkgs/affine-model/elements/cynefin/cynefin.d.ts +9 -20
- package/dist/_pkgs/affine-model/elements/cynefin/cynefin.js +7 -28
- package/dist/_pkgs/affine-model/elements/edgy/board.d.ts +9 -21
- package/dist/_pkgs/affine-model/elements/edgy/board.js +8 -29
- package/dist/_pkgs/affine-model/elements/edgy/facets.d.ts +14 -28
- package/dist/_pkgs/affine-model/elements/edgy/facets.js +12 -36
- package/dist/_pkgs/affine-model/elements/estuarine/estuarine.d.ts +9 -19
- package/dist/_pkgs/affine-model/elements/estuarine/estuarine.js +7 -27
- package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-service.d.ts +28 -0
- package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-service.js +20 -3
- package/dist/_pkgs/affine-widget-edgeless-auto-connect/index.js +124 -124
- package/dist/_pkgs/affine-widget-edgeless-zoom-toolbar/index.js +26 -26
- package/dist/_pkgs/affine-widget-keyboard-toolbar/icons.js +98 -98
- package/dist/_pkgs/affine-widget-page-dragging-area/index.js +10 -10
- package/dist/_pkgs/affine-widget-viewport-overlay/index.js +13 -13
- package/dist/_pkgs/data-view/core/group-by/renderer/boolean-group.js +11 -11
- package/dist/_pkgs/data-view/core/utils/menu-title.js +17 -17
- package/dist/_pkgs/data-view/view-presets/table/pc-virtual/group/top/group-title.js +18 -18
- package/dist/_pkgs/std/gfx/model/surface/surface-model.js +12 -2
- 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
|
-
|
|
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
|
|
19
|
-
*
|
|
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
|
|
18
|
-
*
|
|
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
|
],
|
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
37
|
-
run(ctx:
|
|
30
|
+
when(ctx: ToolbarContext): boolean;
|
|
31
|
+
run(ctx: ToolbarContext): void;
|
|
38
32
|
disabled?: undefined;
|
|
39
33
|
})[];
|
|
40
|
-
content(ctx:
|
|
41
|
-
when(ctx:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
|
|
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
|
-
*
|
|
23
|
-
*
|
|
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
|
|
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;
|