@formicoidea/labre-core 0.38.2 → 0.39.3

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 (36) hide show
  1. package/dist/_pkgs/affine-block-database/properties/rich-text/cell-renderer.js +14 -9
  2. package/dist/_pkgs/affine-block-frame/frame-manager.js +10 -9
  3. package/dist/_pkgs/affine-block-list/list-block.d.ts +8 -1
  4. package/dist/_pkgs/affine-block-list/list-block.js +27 -17
  5. package/dist/_pkgs/affine-block-paragraph/paragraph-block.d.ts +8 -1
  6. package/dist/_pkgs/affine-block-paragraph/paragraph-block.js +30 -15
  7. package/dist/_pkgs/affine-block-root/edgeless/clipboard/clipboard.js +3 -3
  8. package/dist/_pkgs/affine-block-root/keyboard/keymap.js +20 -0
  9. package/dist/_pkgs/affine-block-surface/extensions/export-manager/export-manager.d.ts +6 -4
  10. package/dist/_pkgs/affine-block-surface/extensions/export-manager/export-manager.js +15 -10
  11. package/dist/_pkgs/affine-block-surface/utils/priority-queue.js +2 -2
  12. package/dist/_pkgs/affine-components/toggle-button/toggle-button.d.ts +5 -0
  13. package/dist/_pkgs/affine-components/toggle-button/toggle-button.js +31 -18
  14. package/dist/_pkgs/affine-gfx-connector/connector-manager.d.ts +13 -1
  15. package/dist/_pkgs/affine-gfx-connector/connector-manager.js +9 -5
  16. package/dist/_pkgs/affine-gfx-connector/connector-watcher.js +48 -17
  17. package/dist/_pkgs/affine-gfx-connector/view/view.js +4 -0
  18. package/dist/_pkgs/affine-gfx-mindmap/view/view.js +9 -1
  19. package/dist/_pkgs/affine-gfx-mindmap/view.js +7 -2
  20. package/dist/_pkgs/affine-gfx-template/contribute.d.ts +32 -7
  21. package/dist/_pkgs/affine-gfx-template/contribute.js +66 -22
  22. package/dist/_pkgs/affine-gfx-template/toolbar/template-panel.d.ts +7 -0
  23. package/dist/_pkgs/affine-gfx-template/toolbar/template-panel.js +19 -4
  24. package/dist/_pkgs/affine-model/elements/mindmap/mindmap.js +10 -1
  25. package/dist/_pkgs/affine-shared/adapters/middlewares/proxy.d.ts +10 -0
  26. package/dist/_pkgs/affine-shared/adapters/middlewares/proxy.js +19 -12
  27. package/dist/_pkgs/affine-shared/adapters/utils/fetch.js +4 -0
  28. package/dist/_pkgs/affine-widget-keyboard-toolbar/widget.js +18 -3
  29. package/dist/_pkgs/affine-widget-linked-doc/mobile-linked-doc-menu.d.ts +7 -1
  30. package/dist/_pkgs/affine-widget-linked-doc/mobile-linked-doc-menu.js +8 -2
  31. package/dist/_pkgs/std/gfx/index.d.ts +1 -1
  32. package/dist/_pkgs/std/gfx/model/surface/element-model.d.ts +2 -2
  33. package/dist/_pkgs/std/gfx/model/surface/element-model.js +2 -2
  34. package/dist/_pkgs/std/gfx/model/surface/surface-model.d.ts +1 -1
  35. package/dist/_pkgs/std/gfx/model/surface/surface-model.js +1 -1
  36. package/package.json +1 -1
@@ -84,7 +84,19 @@ export declare class ConnectorPathGenerator extends PathGenerator {
84
84
  constructor(options: {
85
85
  getElementById: (id: string) => GfxModel | null;
86
86
  });
87
- static updatePath(connector: ConnectorElementModel | LocalConnectorElementModel, path: PointLocation[] | null, elementGetter?: (id: string) => GfxModel | null): void;
87
+ static updatePath(connector: ConnectorElementModel | LocalConnectorElementModel, path: PointLocation[] | null, elementGetter?: (id: string) => GfxModel | null, options?: {
88
+ /**
89
+ * Whether this peer may persist the derived label box. `labelXYWH` is a
90
+ * `@field()`: unlike `path` / `xywh`, which are `@local()` and must be
91
+ * recomputed everywhere to repaint, writing it makes the CRDT grow on
92
+ * every peer that merely WATCHES a connector move — and throws on a
93
+ * readonly viewer. The authoring peer's value arrives with the sync, so
94
+ * a peer reacting to a remote change passes `false`. Defaults to `true`
95
+ * for the direct callers (paste, mindmap layout, shape resize), which
96
+ * are local gestures by construction.
97
+ */
98
+ persistLabelXYWH?: boolean;
99
+ }): void;
88
100
  private _computeStartEndPoint;
89
101
  private _generateConnectorPath;
90
102
  private _generateCurveConnectorPath;
@@ -978,7 +978,7 @@ export class ConnectorPathGenerator extends PathGenerator {
978
978
  super();
979
979
  this.options = options;
980
980
  }
981
- static updatePath(connector, path, elementGetter) {
981
+ static updatePath(connector, path, elementGetter, options) {
982
982
  const instance = new ConnectorPathGenerator({
983
983
  getElementById: elementGetter ?? (() => null),
984
984
  });
@@ -1004,12 +1004,16 @@ export class ConnectorPathGenerator extends PathGenerator {
1004
1004
  // the property assignment order matters
1005
1005
  connector.xywh = bound.serialize();
1006
1006
  connector.path = relativePoints;
1007
- // Updates Connector's Label position.
1007
+ // Updates Connector's Label position — a PERSISTED write, unlike
1008
+ // everything above it.
1008
1009
  if (isConnectorWithLabel(connector)) {
1009
1010
  const model = connector;
1010
- const [cx, cy] = model.getPointByOffsetDistance(model.labelOffset.distance);
1011
- const [, , w, h] = model.labelXYWH;
1012
- model.labelXYWH = [cx - w / 2, cy - h / 2, w, h];
1011
+ const mayPersist = (options?.persistLabelXYWH ?? true) && !model.surface?.store?.readonly;
1012
+ if (mayPersist) {
1013
+ const [cx, cy] = model.getPointByOffsetDistance(model.labelOffset.distance);
1014
+ const [, , w, h] = model.labelXYWH;
1015
+ model.labelXYWH = [cx - w / 2, cy - h / 2, w, h];
1016
+ }
1013
1017
  }
1014
1018
  connector.updatingPath = false;
1015
1019
  }
@@ -62,49 +62,74 @@ function reanchorConnectorsForPolygon(surface, polygonId, elementGetter) {
62
62
  export const connectorWatcher = (surface) => {
63
63
  const hasElementById = (id) => surface.hasElementById(id) || surface.store.hasBlock(id);
64
64
  const elementGetter = (id) => surface.getElementById(id) ?? surface.store.getModelById(id);
65
- const updateConnectorPath = (connector) => {
65
+ const updateConnectorPath = (connector, local) => {
66
66
  if (((connector.source?.id && hasElementById(connector.source.id)) ||
67
67
  (!connector.source?.id && connector.source?.position)) &&
68
68
  ((connector.target?.id && hasElementById(connector.target.id)) ||
69
69
  (!connector.target?.id && connector.target?.position))) {
70
- ConnectorPathGenerator.updatePath(connector, null, elementGetter);
70
+ // The path itself is `@local()` — every peer recomputes it to repaint.
71
+ // The label box is not: only the peer that authored the change persists
72
+ // it, the others read the value that arrives with the sync.
73
+ ConnectorPathGenerator.updatePath(connector, null, elementGetter, {
74
+ persistLabelXYWH: local,
75
+ });
71
76
  }
72
77
  };
73
- const pendingList = new Set();
78
+ /**
79
+ * Connectors whose path must be recomputed in the next microtask, mapped to
80
+ * whether ANY of the changes that queued them was local — a single local
81
+ * gesture is enough to let the peer persist the derived label box.
82
+ */
83
+ const pendingList = new Map();
74
84
  let pendingFlag = false;
75
- const addToUpdateList = (connector) => {
76
- pendingList.add(connector);
85
+ const addToUpdateList = (connector, local) => {
86
+ pendingList.set(connector, (pendingList.get(connector) ?? false) || local);
77
87
  if (!pendingFlag) {
78
88
  pendingFlag = true;
79
89
  queueMicrotask(() => {
80
- pendingList.forEach(updateConnectorPath);
90
+ pendingList.forEach((isLocal, connector) => updateConnectorPath(connector, isLocal));
81
91
  pendingList.clear();
82
92
  pendingFlag = false;
83
93
  });
84
94
  }
85
95
  };
86
96
  const disposables = [
87
- surface.elementAdded.subscribe(({ id }) => {
97
+ surface.elementAdded.subscribe(({ id, local }) => {
88
98
  const element = elementGetter(id);
89
99
  if (!element)
90
100
  return;
91
101
  if ('type' in element && element.type === 'connector') {
92
- addToUpdateList(element);
102
+ addToUpdateList(element, local);
93
103
  }
94
104
  else {
95
- surface.getConnectors(id).forEach(addToUpdateList);
105
+ surface
106
+ .getConnectors(id)
107
+ .forEach(connector => addToUpdateList(connector, local));
96
108
  }
97
109
  }),
98
- surface.elementUpdated.subscribe(({ id, props }) => {
110
+ surface.elementUpdated.subscribe(({ id, props, local }) => {
99
111
  const element = elementGetter(id);
100
- if (props['vertices']) {
112
+ // The element may already be gone by the time the update is delivered
113
+ // (a remote peer that moved and then deleted it, an undo…).
114
+ if (!element)
115
+ return;
116
+ if (local && props['vertices']) {
101
117
  // When polygon vertices change, re-anchor connected connectors to the
102
118
  // nearest valid boundary point BEFORE scheduling the path update, so
103
119
  // the path generator uses the corrected anchor positions.
120
+ //
121
+ // LOCAL ONLY: re-anchoring writes `source`/`target`, both persisted
122
+ // `@field()`s. The peer that edited the vertices re-anchors and syncs
123
+ // the result, so doing it again on a receiving peer is a duplicate
124
+ // write — and an illegal one on a readonly viewer (see #242).
104
125
  reanchorConnectorsForPolygon(surface, id, elementGetter);
105
126
  }
106
127
  if (props['xywh'] || props['rotate'] || props['vertices']) {
107
- surface.getConnectors(id).forEach(addToUpdateList);
128
+ // Not gated on `local`: `path`, `absolutePath` and the connector's own
129
+ // `xywh` are `@local()`, so every peer must recompute them to repaint.
130
+ surface
131
+ .getConnectors(id)
132
+ .forEach(connector => addToUpdateList(connector, local));
108
133
  }
109
134
  if ('type' in element &&
110
135
  element.type === 'connector' &&
@@ -113,25 +138,31 @@ export const connectorWatcher = (surface) => {
113
138
  props['source'] ||
114
139
  props['curveControlPoint'] !== undefined)) {
115
140
  const connector = element;
116
- // Clear custom handle data when connector mode changes
117
- if (props['mode'] !== undefined) {
141
+ // Clear custom handle data when connector mode changes. `local` only:
142
+ // `curveControlPoint` is persisted, and the peer that changed the mode
143
+ // cleared it in the same gesture.
144
+ if (local && props['mode'] !== undefined) {
118
145
  if (connector.curveControlPoint !== null) {
119
146
  connector.curveControlPoint = null;
120
147
  }
121
148
  }
122
- addToUpdateList(connector);
149
+ addToUpdateList(connector, local);
123
150
  }
124
151
  }),
125
152
  surface.store.slots.blockUpdated.subscribe(payload => {
126
153
  if (payload.type === 'add' ||
127
154
  (payload.type === 'update' && payload.props.key === 'xywh')) {
128
- surface.getConnectors(payload.id).forEach(addToUpdateList);
155
+ surface
156
+ .getConnectors(payload.id)
157
+ .forEach(connector => addToUpdateList(connector, payload.isLocal));
129
158
  }
130
159
  }),
131
160
  ];
161
+ // Mount-time recompute of the `@local()` geometry. Not a local gesture:
162
+ // merely OPENING a document must not rewrite every connector's label box.
132
163
  surface
133
164
  .getElementsByType('connector')
134
- .forEach(connector => updateConnectorPath(connector));
165
+ .forEach(connector => updateConnectorPath(connector, false));
135
166
  return () => {
136
167
  disposables.forEach(d => d.unsubscribe());
137
168
  };
@@ -44,7 +44,11 @@ export class ConnectorElementView extends GfxElementModelView {
44
44
  };
45
45
  };
46
46
  const watchEvent = (labelModel) => {
47
+ // `ViewManager.get` returns null for an element that has no view yet —
48
+ // the local label element is registered a beat before its view exists.
47
49
  const view = this.gfx.view.get(labelModel);
50
+ if (!view)
51
+ return;
48
52
  const connectorModel = this.model;
49
53
  let labelBound = null;
50
54
  let startPoint = {
@@ -173,12 +173,18 @@ export class MindMapView extends GfxElementModelView {
173
173
  button: false,
174
174
  node: false,
175
175
  };
176
+ this._hoveredState = this._hoveredState.set(node.id, hoveredState);
177
+ // `ViewManager.get` returns null for an element whose view is not
178
+ // mounted yet — the button model exists a beat before its view does.
179
+ // The button is created and tracked either way; only the hover wiring
180
+ // is skipped.
176
181
  const buttonView = this.gfx.view.get(id);
182
+ if (!buttonView)
183
+ return collapseButton;
177
184
  const isOnElementBound = (evt) => {
178
185
  const [x, y] = this.gfx.viewport.toModelCoord(evt.x, evt.y);
179
186
  return buttonView.model.includesPoint(x, y, { useElementBound: true }, this.gfx.std.host);
180
187
  };
181
- this._hoveredState = this._hoveredState.set(node.id, hoveredState);
182
188
  buttonView.on('pointerenter', () => {
183
189
  hoveredState.button = true;
184
190
  this._updateButtonVisibility(node.id);
@@ -216,6 +222,8 @@ export class MindMapView extends GfxElementModelView {
216
222
  }
217
223
  });
218
224
  const nodeView = this.gfx.view.get(node.id);
225
+ if (!nodeView)
226
+ return collapseButton;
219
227
  nodeView.on('pointerenter', () => {
220
228
  hoveredState.node = true;
221
229
  this._updateButtonVisibility(node.id);
@@ -1,5 +1,5 @@
1
1
  import { ViewExtensionProvider, } from '../affine-ext-loader/index.js';
2
- import { extendTemplateCategory } from '../affine-gfx-template/index.js';
2
+ import { TemplateCategoryExtension } from '../affine-gfx-template/index.js';
3
3
  import { effects } from './effects.js';
4
4
  import { MindmapElementRendererExtension } from './element-renderer.js';
5
5
  import { MindMapIndicatorOverlay } from './indicator-overlay/index.js';
@@ -22,7 +22,6 @@ export class MindmapRenderViewExtension extends ViewExtensionProvider {
22
22
  effect() {
23
23
  super.effect();
24
24
  effects();
25
- extendTemplateCategory(mindmapTemplateCategory);
26
25
  }
27
26
  setup(context) {
28
27
  super.setup(context);
@@ -33,6 +32,12 @@ export class MindmapRenderViewExtension extends ViewExtensionProvider {
33
32
  context.register(MindMapDragExtension);
34
33
  context.register(MindMapIndicatorOverlay);
35
34
  context.register(MindMapInteraction);
35
+ if (this.isEdgeless(context.scope)) {
36
+ // The Mind Map Templates-panel category stays on the ALWAYS-ON half, on
37
+ // purpose: it must survive the `mindmap` flag, which only hides the
38
+ // senior button ({@link MindmapToolViewExtension}).
39
+ context.register(TemplateCategoryExtension(mindmapTemplateCategory));
40
+ }
36
41
  }
37
42
  }
38
43
  /** The dedicated "Mind Map" senior button — flag-gated (`mindmap`). */
@@ -1,9 +1,34 @@
1
- import type { TemplateCategory } from './toolbar/template-type.js';
1
+ import type { BlockStdScope } from '../std/index.js';
2
+ import type { ExtensionType } from '../store/index.js';
3
+ import type { TemplateCategory, TemplateManager } from './toolbar/template-type.js';
2
4
  /**
3
- * Contribute a template category to the edgeless template panel. Idempotent per
4
- * category name (a framework's ViewExtension `effect()` may run more than once).
5
- * Lets each framework package own its template definitions while the panel
6
- * aggregates every contributed category. The future favorites feature reads the
7
- * same registry to decide which framework toolbars to show.
5
+ * A framework registers its Templates-panel categories here; the panel reads
6
+ * them back from the std of the edgeless it opens on. One identifier per
7
+ * category NAME, so two extensions claiming the same category collide at
8
+ * setup (`DuplicateServiceDefinitionError`, caught by `di-mount.unit.spec.ts`)
9
+ * rather than listing it twice.
8
10
  */
9
- export declare function extendTemplateCategory(category: TemplateCategory): void;
11
+ export declare const TemplateCategoryIdentifier: import("../global/di/types.js").ServiceIdentifier<TemplateCategory> & (<U extends TemplateCategory = TemplateCategory>(variant: import("../global/di/types.js").ServiceVariant) => import("../global/di/types.js").ServiceIdentifier<U>);
12
+ /**
13
+ * Register a framework's template categories. Call it from `setup()` of the
14
+ * FLAG-GATED view extension, beside the senior tool — a category is creation
15
+ * tooling, so a disabled framework must contribute none (`docs/adr/0009`):
16
+ *
17
+ * ```ts
18
+ * context.register(TemplateCategoryExtension(wardleyTemplateCategory));
19
+ * ```
20
+ *
21
+ * Replaces the module-level registry of 0.38.0–0.38.2 (`extendTemplateCategory`)
22
+ * that appended from `effect()` and never removed: a category outlived the
23
+ * editor whose flags had admitted it, so a framework switched off in the host
24
+ * kept its category until a full reload (#244). The DI container is per editor;
25
+ * nothing has to be un-registered.
26
+ */
27
+ export declare function TemplateCategoryExtension(...categories: TemplateCategory[]): ExtensionType;
28
+ /**
29
+ * The catalogue ONE editor sees: the built-in categories (`Other`, plus
30
+ * whatever a host appended through `EdgelessTemplatePanel.templates.extend`)
31
+ * followed by the categories the view extensions mounted on this `std`
32
+ * registered — i.e. exactly the frameworks whose tooling is on.
33
+ */
34
+ export declare function templateManagerFor(std: BlockStdScope): TemplateManager;
@@ -1,32 +1,76 @@
1
- import { EdgelessTemplatePanel } from './toolbar/template-panel.js';
1
+ import { createIdentifier } from '../global/di/index.js';
2
+ import { builtInTemplates } from './toolbar/builtin-templates.js';
3
+ /**
4
+ * A framework registers its Templates-panel categories here; the panel reads
5
+ * them back from the std of the edgeless it opens on. One identifier per
6
+ * category NAME, so two extensions claiming the same category collide at
7
+ * setup (`DuplicateServiceDefinitionError`, caught by `di-mount.unit.spec.ts`)
8
+ * rather than listing it twice.
9
+ */
10
+ export const TemplateCategoryIdentifier = createIdentifier('TemplateCategory');
11
+ /**
12
+ * Register a framework's template categories. Call it from `setup()` of the
13
+ * FLAG-GATED view extension, beside the senior tool — a category is creation
14
+ * tooling, so a disabled framework must contribute none (`docs/adr/0009`):
15
+ *
16
+ * ```ts
17
+ * context.register(TemplateCategoryExtension(wardleyTemplateCategory));
18
+ * ```
19
+ *
20
+ * Replaces the module-level registry of 0.38.0–0.38.2 (`extendTemplateCategory`)
21
+ * that appended from `effect()` and never removed: a category outlived the
22
+ * editor whose flags had admitted it, so a framework switched off in the host
23
+ * kept its category until a full reload (#244). The DI container is per editor;
24
+ * nothing has to be un-registered.
25
+ */
26
+ export function TemplateCategoryExtension(...categories) {
27
+ return {
28
+ setup: di => {
29
+ for (const category of categories) {
30
+ di.addImpl(TemplateCategoryIdentifier(category.name), () => category);
31
+ }
32
+ },
33
+ };
34
+ }
2
35
  function loadCategory(category) {
3
36
  return Array.isArray(category.templates)
4
37
  ? Promise.resolve(category.templates)
5
38
  : category.templates();
6
39
  }
7
- /** Wrap a single {@link TemplateCategory} as a {@link TemplateManager}. */
8
- function categoryManager(category) {
40
+ /**
41
+ * The catalogue ONE editor sees: the built-in categories (`Other`, plus
42
+ * whatever a host appended through `EdgelessTemplatePanel.templates.extend`)
43
+ * followed by the categories the view extensions mounted on this `std`
44
+ * registered — i.e. exactly the frameworks whose tooling is on.
45
+ */
46
+ export function templateManagerFor(std) {
47
+ const contributed = [
48
+ ...std.provider.getAll(TemplateCategoryIdentifier).values(),
49
+ ];
50
+ const byName = (name) => contributed.find(c => c.name === name);
9
51
  return {
10
- categories: () => [category.name],
11
- list: async (cate) => cate === category.name ? await loadCategory(category) : [],
12
- search: async (keyword) => {
13
- const all = await loadCategory(category);
52
+ categories: async () => [
53
+ ...new Set([
54
+ ...(await builtInTemplates.categories()),
55
+ ...contributed.map(c => c.name),
56
+ ]),
57
+ ],
58
+ // Both halves, so a host's `extend(...)` can still append to a framework's
59
+ // own category (the global half answers `[]` for a name it does not know).
60
+ list: async (name) => {
61
+ const category = byName(name);
62
+ return [
63
+ ...(category ? await loadCategory(category) : []),
64
+ ...(await builtInTemplates.list(name)),
65
+ ];
66
+ },
67
+ search: async (keyword, name) => {
14
68
  const k = keyword.trim().toLocaleLowerCase();
15
- return all.filter(t => t.name?.toLocaleLowerCase().includes(k));
69
+ const pool = name ? [byName(name)].filter(c => !!c) : contributed;
70
+ const own = (await Promise.all(pool.map(loadCategory)))
71
+ .flat()
72
+ .filter(t => t.name?.toLocaleLowerCase().includes(k));
73
+ return [...(await builtInTemplates.search(keyword, name)), ...own];
16
74
  },
17
75
  };
18
76
  }
19
- const registered = new Set();
20
- /**
21
- * Contribute a template category to the edgeless template panel. Idempotent per
22
- * category name (a framework's ViewExtension `effect()` may run more than once).
23
- * Lets each framework package own its template definitions while the panel
24
- * aggregates every contributed category. The future favorites feature reads the
25
- * same registry to decide which framework toolbars to show.
26
- */
27
- export function extendTemplateCategory(category) {
28
- if (registered.has(category.name))
29
- return;
30
- registered.add(category.name);
31
- EdgelessTemplatePanel.templates.extend(categoryManager(category));
32
- }
@@ -5,12 +5,19 @@ import type { Template } from './template-type.js';
5
5
  declare const EdgelessTemplatePanel_base: typeof LitElement & import("../../global/utils/types.js").Constructor<import("../../global/lit/with-disposable.js").DisposableClass>;
6
6
  export declare class EdgelessTemplatePanel extends EdgelessTemplatePanel_base {
7
7
  static styles: import("lit").CSSResult;
8
+ /**
9
+ * The GLOBAL half of the catalogue — `Other` and a host's `extend(...)`.
10
+ * Framework categories are not here: they live in the editor's DI container
11
+ * (`TemplateCategoryExtension`) and are read through {@link catalogue}.
12
+ */
8
13
  static templates: {
9
14
  list: (category: string) => Promise<Template[]>;
10
15
  categories: () => Promise<string[]>;
11
16
  search: (keyword: string, cateName?: string) => Promise<Template[]>;
12
17
  extend(manager: import("./template-type.js").TemplateManager): void;
13
18
  };
19
+ /** What THIS edgeless offers: the global half plus its std's categories. */
20
+ get catalogue(): import("./template-type.js").TemplateManager;
14
21
  private _fetchJob;
15
22
  draggableController: EdgelessDraggableElementController<Template>;
16
23
  private _closePanel;
@@ -45,6 +45,7 @@ import { property, state } from 'lit/decorators.js';
45
45
  import { repeat } from 'lit/directives/repeat.js';
46
46
  import { styleMap } from 'lit/directives/style-map.js';
47
47
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
48
+ import { templateManagerFor } from '../contribute.js';
48
49
  import { createTemplateJob } from '../services/template.js';
49
50
  import { builtInTemplates } from './builtin-templates.js';
50
51
  import { defaultPreview, Triangle } from './cards.js';
@@ -261,7 +262,16 @@ let EdgelessTemplatePanel = (() => {
261
262
  color: var(--affine-background-overlay-panel-color);
262
263
  }
263
264
  `; }
265
+ /**
266
+ * The GLOBAL half of the catalogue — `Other` and a host's `extend(...)`.
267
+ * Framework categories are not here: they live in the editor's DI container
268
+ * (`TemplateCategoryExtension`) and are read through {@link catalogue}.
269
+ */
264
270
  static { this.templates = builtInTemplates; }
271
+ /** What THIS edgeless offers: the global half plus its std's categories. */
272
+ get catalogue() {
273
+ return templateManagerFor(this.edgeless.std);
274
+ }
265
275
  _closePanel() {
266
276
  if (this.isDragging)
267
277
  return;
@@ -293,9 +303,14 @@ let EdgelessTemplatePanel = (() => {
293
303
  }
294
304
  async _initCategory() {
295
305
  try {
296
- this._categories = await EdgelessTemplatePanel.templates.categories();
306
+ this._categories = await this.catalogue.categories();
307
+ // The remembered tab may belong to a framework this editor no longer
308
+ // registers (switched off since): fall back rather than show an empty list.
309
+ const remembered = this._getLocalSelectedCategory();
297
310
  this._currentCategory =
298
- this._getLocalSelectedCategory() ?? this._categories[0];
311
+ remembered && this._categories.includes(remembered)
312
+ ? remembered
313
+ : this._categories[0];
299
314
  this._updateTemplates();
300
315
  }
301
316
  catch (e) {
@@ -366,8 +381,8 @@ let EdgelessTemplatePanel = (() => {
366
381
  this._fetch(async (state) => {
367
382
  try {
368
383
  const templates = this._searchKeyword
369
- ? await EdgelessTemplatePanel.templates.search(this._searchKeyword)
370
- : await EdgelessTemplatePanel.templates.list(this._currentCategory);
384
+ ? await this.catalogue.search(this._searchKeyword)
385
+ : await this.catalogue.list(this._currentCategory);
371
386
  if (state.canceled)
372
387
  return;
373
388
  this._templates = templates;
@@ -57,7 +57,16 @@ function isNodeType(node) {
57
57
  }
58
58
  function observeChildren(_, instance, transaction) {
59
59
  if (instance.children.doc) {
60
- instance.setChildIds(Array.from(instance.children.keys()), transaction?.local ?? true);
60
+ // `?? false`, like every other group-like element (see `group.ts`): a
61
+ // missing transaction is NOT a local gesture. `startObserve` re-runs this
62
+ // observer with `transaction === null` whenever the `children` key itself
63
+ // is rewritten — by a remote peer, a snapshot import or an undo — and
64
+ // reporting that as local makes the group watchers treat a received change
65
+ // as one this peer just made: they delete an emptied mindmap locally,
66
+ // which a readonly viewer cannot do (#251, rule from #242).
67
+ instance.setChildIds(Array.from(instance.children.keys()), transaction?.local ?? false);
68
+ // Pure local recompute, no CRDT write: unconditional on purpose, every
69
+ // peer must rebuild its tree to repaint.
61
70
  instance.buildTree();
62
71
  instance.connectors.clear();
63
72
  }
@@ -2,7 +2,17 @@ import type { TransformerMiddleware } from '../../../store/index.js';
2
2
  import { StoreExtension } from '../../../store/index.js';
3
3
  export declare const customImageProxyMiddleware: (imageProxyURL: string) => TransformerMiddleware;
4
4
  export declare const setImageProxyMiddlewareURL: (url: string) => void;
5
+ /**
6
+ * Reads the URL at run time: a `setImageProxyURL` made after module load
7
+ * reaches every job using this middleware.
8
+ */
5
9
  export declare const defaultImageProxyMiddleware: TransformerMiddleware;
10
+ /**
11
+ * The ONE seam governing every remote-image fetch the library performs on the
12
+ * host's behalf: link-card favicons and og:images, image import, canvas
13
+ * PNG/PDF export, copy-as-image. An empty URL means « fetch direct ».
14
+ * See `docs/integrate/04-host-seams.md`.
15
+ */
6
16
  export declare class ImageProxyService extends StoreExtension {
7
17
  static key: string;
8
18
  private _imageProxyURL;
@@ -5,18 +5,25 @@ export const customImageProxyMiddleware = (imageProxyURL) => {
5
5
  adapterConfigs.set('imageProxy', imageProxyURL);
6
6
  };
7
7
  };
8
- const imageProxyMiddlewareBuilder = () => {
9
- let middleware = customImageProxyMiddleware(DEFAULT_IMAGE_PROXY_ENDPOINT);
10
- return {
11
- get: () => middleware,
12
- set: (url) => {
13
- middleware = customImageProxyMiddleware(url);
14
- },
15
- };
8
+ // Module-wide so that transformer jobs built outside a store (playground
9
+ // panels, host-side importers) follow `setImageProxyURL` too.
10
+ let imageProxyMiddlewareURL = DEFAULT_IMAGE_PROXY_ENDPOINT;
11
+ export const setImageProxyMiddlewareURL = (url) => {
12
+ imageProxyMiddlewareURL = url;
13
+ };
14
+ /**
15
+ * Reads the URL at run time: a `setImageProxyURL` made after module load
16
+ * reaches every job using this middleware.
17
+ */
18
+ export const defaultImageProxyMiddleware = ({ adapterConfigs, }) => {
19
+ adapterConfigs.set('imageProxy', imageProxyMiddlewareURL);
16
20
  };
17
- const defaultImageProxyMiddlewarBuilder = imageProxyMiddlewareBuilder();
18
- export const setImageProxyMiddlewareURL = defaultImageProxyMiddlewarBuilder.set;
19
- export const defaultImageProxyMiddleware = defaultImageProxyMiddlewarBuilder.get();
21
+ /**
22
+ * The ONE seam governing every remote-image fetch the library performs on the
23
+ * host's behalf: link-card favicons and og:images, image import, canvas
24
+ * PNG/PDF export, copy-as-image. An empty URL means « fetch direct ».
25
+ * See `docs/integrate/04-host-seams.md`.
26
+ */
20
27
  // TODO(@mirone): this should be configured when setup instead of runtime
21
28
  export class ImageProxyService extends StoreExtension {
22
29
  constructor() {
@@ -29,7 +36,7 @@ export class ImageProxyService extends StoreExtension {
29
36
  setImageProxyMiddlewareURL(url);
30
37
  }
31
38
  buildUrl(imageUrl) {
32
- if (imageUrl.startsWith(this.imageProxyURL)) {
39
+ if (!this.imageProxyURL || imageUrl.startsWith(this.imageProxyURL)) {
33
40
  return imageUrl;
34
41
  }
35
42
  return `${this.imageProxyURL}?url=${encodeURIComponent(imageUrl)}`;
@@ -12,6 +12,10 @@ const fetchImage = async (url, init, proxy) => {
12
12
  if (url.startsWith(window.location.origin)) {
13
13
  return await fetch(url, init);
14
14
  }
15
+ // Already proxied (a link card renders `buildUrl(...)`): do not wrap twice.
16
+ if (url.startsWith(proxy)) {
17
+ return await fetch(url, init);
18
+ }
15
19
  return await fetch(proxy + '?url=' + encodeURIComponent(url), init)
16
20
  .then(res => {
17
21
  if (!res.ok) {
@@ -7,6 +7,13 @@ import { html, nothing } from 'lit';
7
7
  import { literal, unsafeStatic } from 'lit/static-html.js';
8
8
  import { defaultKeyboardToolbarConfig, KeyboardToolbarConfigExtension, } from './config.js';
9
9
  export const AFFINE_KEYBOARD_TOOLBAR_WIDGET = 'affine-keyboard-toolbar-widget';
10
+ /**
11
+ * Signals backing the fallback keyboard, at module level on purpose: the
12
+ * `keyboard` getter is read from `render()`, so a fresh `signal()` per call
13
+ * would hand `PositionController`'s effect a brand new object every frame.
14
+ */
15
+ const fallbackKeyboardVisible$ = signal(false);
16
+ const fallbackKeyboardHeight$ = signal(0);
10
17
  export class AffineKeyboardToolbarWidget extends WidgetComponent {
11
18
  constructor() {
12
19
  super(...arguments);
@@ -26,12 +33,20 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent {
26
33
  this._initialInputMode = '';
27
34
  }
28
35
  get keyboard() {
29
- const provider = this.std.get(VirtualKeyboardProvider);
30
- if (isVirtualKeyboardProviderWithAction(provider))
36
+ // The provider is host-supplied — a native mobile shell reports the
37
+ // on-screen keyboard. Labre ships no such shell, so a plain web host
38
+ // registers nothing and this lookup must stay optional; a hard `get` throws
39
+ // and takes the whole document open down with it (issue #247).
40
+ const provider = this.std.getOptional(VirtualKeyboardProvider);
41
+ if (provider && isVirtualKeyboardProviderWithAction(provider))
31
42
  return provider;
32
43
  return {
33
- // fallback keyboard actions
44
+ // Fallback keyboard, complete WITHOUT a provider: the signals come first
45
+ // so that a provider without actions still wins on them through the
46
+ // spread below, while its `show`/`hide` stay ours.
34
47
  fallback: true,
48
+ visible$: fallbackKeyboardVisible$,
49
+ height$: fallbackKeyboardHeight$,
35
50
  show: () => {
36
51
  const rootComponent = this.block?.rootComponent;
37
52
  if (rootComponent && rootComponent === document.activeElement) {
@@ -13,7 +13,13 @@ export declare class AffineMobileLinkedDocMenu extends AffineMobileLinkedDocMenu
13
13
  private readonly _updateLinkedDocGroup;
14
14
  private _updateLinkedDocGroupAbortController;
15
15
  private get _query();
16
- get keyboard(): VirtualKeyboardProvider | import("../affine-shared/services/virtual-keyboard-service.js").VirtualKeyboardProviderWithAction;
16
+ /**
17
+ * The virtual keyboard provider is host-supplied — a native mobile shell
18
+ * reports the on-screen keyboard. Labre ships no such shell, so a plain web
19
+ * host registers nothing and this lookup must stay optional; a hard `get`
20
+ * throws and takes the whole document open down with it (issue #247).
21
+ */
22
+ get keyboard(): VirtualKeyboardProvider | null;
17
23
  connectedCallback(): void;
18
24
  firstUpdated(): void;
19
25
  render(): import("lit-html").TemplateResult<1> | typeof nothing;
@@ -71,8 +71,14 @@ let AffineMobileLinkedDocMenu = (() => {
71
71
  get _query() {
72
72
  return getTextContentFromInlineRange(this.context.inlineEditor, this.context.startRange);
73
73
  }
74
+ /**
75
+ * The virtual keyboard provider is host-supplied — a native mobile shell
76
+ * reports the on-screen keyboard. Labre ships no such shell, so a plain web
77
+ * host registers nothing and this lookup must stay optional; a hard `get`
78
+ * throws and takes the whole document open down with it (issue #247).
79
+ */
74
80
  get keyboard() {
75
- return this.context.std.get(VirtualKeyboardProvider);
81
+ return this.context.std.getOptional(VirtualKeyboardProvider) ?? null;
76
82
  }
77
83
  connectedCallback() {
78
84
  super.connectedCallback();
@@ -114,7 +120,7 @@ let AffineMobileLinkedDocMenu = (() => {
114
120
  if (groups.length === 0) {
115
121
  return nothing;
116
122
  }
117
- this.style.bottom = `${this.keyboard.height$.value}px`;
123
+ this.style.bottom = `${this.keyboard?.height$.value ?? 0}px`;
118
124
  return html `
119
125
  ${join(groups.map(this._renderGroup), html `<div class="divider"></div>`)}
120
126
  `;