@nuforge/editor 0.3.0 → 0.3.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.
package/dist/index.cjs CHANGED
@@ -24,11 +24,9 @@ function _interopNamespaceDefault(e) {
24
24
 
25
25
  var t__namespace = /*#__PURE__*/_interopNamespaceDefault(t);
26
26
 
27
- /** A literal expression (`"x"`, `5`, `true`). */
28
27
  function lit(value) {
29
28
  return t__namespace.literal({ value });
30
29
  }
31
- /** A text node: `<text text={value} />`, rendered as its string value. */
32
30
  function text(value) {
33
31
  return t__namespace.tagTemplate({
34
32
  tag: 'text',
@@ -36,10 +34,6 @@ function text(value) {
36
34
  children: [],
37
35
  });
38
36
  }
39
- /**
40
- * Concise element builder — `el('div', { class: lit('box') }, [text('hi')])`
41
- * instead of the verbose `t.tagTemplate({ tag, props, children })`.
42
- */
43
37
  function el(tag, props = {}, children = []) {
44
38
  return t__namespace.tagTemplate({ tag, props, children });
45
39
  }
@@ -72,7 +66,6 @@ class BlockRegistry {
72
66
  return out;
73
67
  }
74
68
  }
75
- /** The built-in palette — the common HTML primitives. */
76
69
  const DEFAULT_BLOCKS = [
77
70
  { id: 'box', label: 'Box', icon: 'square', create: () => el('div') },
78
71
  { id: 'text', label: 'Text', icon: 'type', create: () => text('Text') },
@@ -102,27 +95,11 @@ const DEFAULT_BLOCKS = [
102
95
  },
103
96
  ];
104
97
 
105
- /**
106
- * Wire protocol for `RemoteCanvasHost` <-> `createCanvasReceiver`: a genuinely
107
- * navigated `/canvas/[id]`-style iframe has its own JS realm, so state and
108
- * interaction can't cross as direct object references the way `IframeCanvas`'s
109
- * portal allows — everything here is a structured-clone-safe plain message.
110
- *
111
- * Deliberately generic: only the shapes the editor primitive itself needs
112
- * (state sync, ready handshake, the core interaction events) are typed. Host
113
- * concerns like theme/preview-mode/overrides ride through `CanvasUiMessage`'s
114
- * opaque `payload` — this package relays it, never interprets it.
115
- */
116
98
  const CANVAS_CHANNEL = 'nuforge-canvas';
117
99
  const CANVAS_PROTOCOL_VERSION = 1;
118
- /** Namespace the `BroadcastChannel` per canvas instance (derive from the
119
- * iframe's own `src`, so a host never has to invent/track a separate id). */
120
100
  function canvasChannelName(src) {
121
101
  return `${CANVAS_CHANNEL}:${src}`;
122
102
  }
123
- /** Narrow an arbitrary `BroadcastChannel` payload to a message from this
124
- * protocol — guards against unrelated traffic on the same channel name and
125
- * against a version mismatch across a stale cached bundle. */
126
103
  function isCanvasMessage(data) {
127
104
  return (!!data &&
128
105
  typeof data === 'object' &&
@@ -130,47 +107,18 @@ function isCanvasMessage(data) {
130
107
  data.v === CANVAS_PROTOCOL_VERSION);
131
108
  }
132
109
 
133
- /**
134
- * Canvas-side receiver for a genuinely-navigated `/canvas/[id]`-style route:
135
- * a `Nu` instance kept in sync with the host's one-way, via `BroadcastChannel`
136
- * + `t.unflatten`/`t.merge` (the same primitives `@nuforge/collaboration`'s
137
- * `LoroSyncProvider` uses for its state<->doc binding, without the CRDT layer
138
- * — there's exactly one writer here, so convergence machinery is unneeded).
139
- * No `createEditor()`: this instance is never meant to originate an edit the
140
- * host doesn't already know about, so it only needs `Nu`, not the mutation SDK.
141
- */
142
110
  function createCanvasReceiver(opts) {
143
111
  const nu = core.Nu.create({ externals: opts.externals });
144
112
  const volatile = reactivity.reactive({ activeComponent: null, loadGeneration: 0 });
145
113
  const channel = new BroadcastChannel(canvasChannelName(opts.src));
146
114
  const uiListeners = new Set();
147
- /** Did the incoming flattened state add or remove any node id (a
148
- * structural change), vs. only edit fields on already-known nodes? */
149
- const isStructuralChange = (incoming) => {
150
- if (!nu.loaded) {
151
- return false;
152
- }
153
- const currentKeys = Object.keys(t__namespace.flatten(nu.state).types);
154
- const incomingKeys = Object.keys(incoming);
155
- if (currentKeys.length !== incomingKeys.length) {
156
- return true;
157
- }
158
- const incomingSet = new Set(incomingKeys);
159
- return currentKeys.some((key) => !incomingSet.has(key));
160
- };
161
115
  const applyState = (message) => {
162
- // A full reload — the very first bootstrap, any later resync (`init`
163
- // see RemoteCanvasHost's `previewMode`/`activeComponent`-change doc), OR
164
- // a `patch` that structurally changed the tree. See `volatile.loadGeneration`'s
165
- // doc for why structural patches are folded into this same path instead
166
- // of merging in place.
167
- const needsFullReload = message.type === 'init' || isStructuralChange(message.flat.types);
168
- if (needsFullReload) {
116
+ if (message.type === 'init') {
169
117
  nu.load(t__namespace.unflatten(message.flat));
170
118
  volatile.loadGeneration++;
171
119
  }
172
120
  else {
173
- nu.change(() => t__namespace.merge(nu.state, t__namespace.unflatten(message.flat)));
121
+ nu.change(() => t__namespace.merge(nu.state, t__namespace.unflatten(message.flat), { arrayReconcile: 'id' }));
174
122
  }
175
123
  volatile.activeComponent = message.activeComponent;
176
124
  };
@@ -239,13 +187,6 @@ class CommandRegistry {
239
187
  }
240
188
  }
241
189
 
242
- /**
243
- * Compute a drop position from the pointer's Y against a target's bounding rect.
244
- * When `allowInside` is set, the middle band is `inside` (drop as a child) and
245
- * the outer bands are `before`/`after`; otherwise it's a simple before/after
246
- * split at the midpoint. Pure — use it from an `onDragOver`/`onPointerMove`
247
- * handler to drive a drop indicator and the eventual `moveNode`/`pasteNode`.
248
- */
249
190
  function dropPositionFromPointer(clientY, rect, opts = {}) {
250
191
  const ratio = rect.height > 0 ? (clientY - rect.top) / rect.height : 0;
251
192
  if (opts.allowInside) {
@@ -257,7 +198,6 @@ function dropPositionFromPointer(clientY, rect, opts = {}) {
257
198
  }
258
199
  return ratio < 0.5 ? 'before' : 'after';
259
200
  }
260
- /** A human label for a template node (tag name, component name, slot, …). */
261
201
  function templateLabel(node) {
262
202
  if (node instanceof t__namespace.TagTemplate) {
263
203
  return node.tag;
@@ -273,15 +213,12 @@ function templateLabel(node) {
273
213
  }
274
214
  return 'node';
275
215
  }
276
- /** Child templates of a node (empty for leaves / text nodes). */
277
216
  function templateChildren(node) {
278
217
  return (node.children ?? []).filter((child) => child instanceof t__namespace.Type);
279
218
  }
280
- /** Is this a text node (`<text text=…>`), rendered as its string value? */
281
219
  function isTextTemplate(node) {
282
220
  return node instanceof t__namespace.TagTemplate && node.tag === 'text';
283
221
  }
284
- /** Preview the content of a text node, e.g. for a layers tree. */
285
222
  function textPreview(node) {
286
223
  if (!isTextTemplate(node)) {
287
224
  return null;
@@ -292,7 +229,6 @@ function textPreview(node) {
292
229
  }
293
230
  return '{…}';
294
231
  }
295
- /** Can this node hold appended children? */
296
232
  function canHaveChildren(node) {
297
233
  if (!node || isTextTemplate(node)) {
298
234
  return false;
@@ -301,7 +237,6 @@ function canHaveChildren(node) {
301
237
  node instanceof t__namespace.ComponentTemplate ||
302
238
  t__namespace.is(node, t__namespace.FragmentTemplate));
303
239
  }
304
- /** Is `ancestor` somewhere above `node` in the tree? */
305
240
  function isAncestorOf(nu, ancestor, node) {
306
241
  let parent = nu.getParentNode(node);
307
242
  while (parent) {
@@ -313,11 +248,6 @@ function isAncestorOf(nu, ancestor, node) {
313
248
  return false;
314
249
  }
315
250
 
316
- /**
317
- * A high-level facade over a {@link Nu} runtime for building visual page
318
- * editors. Every mutation goes through `nu.change`, so it is transactional and
319
- * undoable. Reach for these instead of hand-splicing the AST.
320
- */
321
251
  class Editor {
322
252
  nu;
323
253
  blocks;
@@ -340,8 +270,6 @@ class Editor {
340
270
  run: (e) => e.nu.redo(),
341
271
  });
342
272
  }
343
- // --- mutation -------------------------------------------------------------
344
- /** Append (or insert at `index`) `child` into `parent`'s children. */
345
273
  insertNode(parent, child, index) {
346
274
  const kids = parent.children;
347
275
  if (!Array.isArray(kids)) {
@@ -356,7 +284,6 @@ class Editor {
356
284
  }
357
285
  });
358
286
  }
359
- /** Remove a node from its parent. Returns false if it has no parent. */
360
287
  removeNode(node) {
361
288
  let ok = false;
362
289
  this.nu.change(() => {
@@ -364,7 +291,6 @@ class Editor {
364
291
  });
365
292
  return ok;
366
293
  }
367
- /** Move `dragged` before/after/inside `target`. Refuses to nest into self. */
368
294
  moveNode(dragged, target, position) {
369
295
  if (dragged === target || isAncestorOf(this.nu, dragged, target)) {
370
296
  return false;
@@ -375,40 +301,26 @@ class Editor {
375
301
  });
376
302
  return ok;
377
303
  }
378
- // --- clipboard ------------------------------------------------------------
379
304
  clipboardNode = null;
380
- /** Put a node on the editor clipboard (a clone is made on paste). */
381
305
  copyNode(node) {
382
306
  this.clipboardNode = node;
383
307
  }
384
- /** Copy a node, then remove it. */
385
308
  cutNode(node) {
386
309
  this.copyNode(node);
387
310
  return this.removeNode(node);
388
311
  }
389
- /** Whether there is something on the clipboard to paste. */
390
312
  canPaste() {
391
313
  return this.clipboardNode !== null;
392
314
  }
393
- /**
394
- * Paste a fresh copy (new ids) of the clipboard node before/after/inside
395
- * `target`. Returns the inserted node, or null if nothing was pasted.
396
- */
397
315
  pasteNode(target, position = 'after') {
398
316
  if (!this.clipboardNode) {
399
317
  return null;
400
318
  }
401
319
  return this.insertCloneRelativeTo(this.clipboardNode, target, position);
402
320
  }
403
- /** Duplicate a node in place (inserted right after it). Returns the copy. */
404
321
  duplicateNode(node) {
405
322
  return this.insertCloneRelativeTo(node, node, 'after');
406
323
  }
407
- /**
408
- * Insert a detached node before/after/inside a target (undoable). The core of
409
- * canvas drag-and-drop: a drop computes a target + position, then calls this.
410
- * Returns the inserted node.
411
- */
412
324
  insertNodeAt(node, target, position) {
413
325
  let ok = false;
414
326
  this.nu.change(() => {
@@ -416,7 +328,6 @@ class Editor {
416
328
  });
417
329
  return ok ? (this.nu.getNodeFromId(node.id) ?? null) : null;
418
330
  }
419
- /** Create a block and insert it before/after/inside a target. */
420
331
  insertBlockAt(blockId, target, position) {
421
332
  const block = this.blocks.get(blockId);
422
333
  if (!block) {
@@ -430,10 +341,8 @@ class Editor {
430
341
  this.nu.change(() => {
431
342
  ok = this.insertRelativeTo(clone, target, position);
432
343
  });
433
- // Return the reactive node from the tree (clone is wrapped on insert).
434
344
  return ok ? (this.nu.getNodeFromId(clone.id) ?? null) : null;
435
345
  }
436
- /** Set several props at once. */
437
346
  updateProps(node, props) {
438
347
  this.nu.change(() => {
439
348
  for (const [key, value] of Object.entries(props)) {
@@ -451,7 +360,6 @@ class Editor {
451
360
  delete node.props[key];
452
361
  });
453
362
  }
454
- /** Parse `source` as a nuforge expression and assign it to `key`. */
455
363
  setExpression(node, key, source) {
456
364
  let expr;
457
365
  try {
@@ -463,7 +371,6 @@ class Editor {
463
371
  this.setProp(node, key, expr);
464
372
  return true;
465
373
  }
466
- /** Update a text node's literal value. */
467
374
  setText(node, value) {
468
375
  const expr = node.props?.text;
469
376
  if (!(expr instanceof t__namespace.Literal)) {
@@ -496,7 +403,6 @@ class Editor {
496
403
  tag.classList.properties[clean] = t__namespace.literal({ value: true });
497
404
  });
498
405
  }
499
- /** Parse `source` and set the condition under which class `name` applies. */
500
406
  setClassCondition(node, name, source) {
501
407
  let expr;
502
408
  try {
@@ -525,7 +431,6 @@ class Editor {
525
431
  }
526
432
  });
527
433
  }
528
- /** Set the `@if` condition from source. */
529
434
  setCondition(node, source) {
530
435
  let expr;
531
436
  try {
@@ -544,8 +449,6 @@ class Editor {
544
449
  node.if = null;
545
450
  });
546
451
  }
547
- // --- loop (@each) ---------------------------------------------------------
548
- /** Repeat a node over `iterator` (a list expression), binding each `alias`. */
549
452
  setEach(node, iterator, alias = 'item', indexName) {
550
453
  let expr;
551
454
  try {
@@ -568,7 +471,6 @@ class Editor {
568
471
  node.each = null;
569
472
  });
570
473
  }
571
- // --- component state / props ----------------------------------------------
572
474
  addState(component, name = 'value', init = '0') {
573
475
  const taken = new Set(component.state.map((v) => v.name));
574
476
  let unique = name;
@@ -653,8 +555,6 @@ class Editor {
653
555
  });
654
556
  return true;
655
557
  }
656
- // --- blocks ---------------------------------------------------------------
657
- /** Create a block's template and insert it into `parent`. */
658
558
  insertBlock(blockId, parent, index) {
659
559
  const block = this.blocks.get(blockId);
660
560
  if (!block || !canHaveChildren(parent)) {
@@ -664,7 +564,6 @@ class Editor {
664
564
  this.insertNode(parent, created, index);
665
565
  return created;
666
566
  }
667
- // --- components / pages ---------------------------------------------------
668
567
  uniqueName(base) {
669
568
  const taken = new Set(this.getComponents().map((c) => c.name));
670
569
  if (!taken.has(base)) {
@@ -676,7 +575,6 @@ class Editor {
676
575
  }
677
576
  return `${base}${i}`;
678
577
  }
679
- /** Add a new component (a "page"). */
680
578
  addComponent(name = 'Page') {
681
579
  const unique = this.uniqueName(name);
682
580
  const component = t__namespace.nuComponent({
@@ -690,7 +588,6 @@ class Editor {
690
588
  });
691
589
  return this.getComponent(unique);
692
590
  }
693
- /** Rename a component, updating `<Name/>` references across the program. */
694
591
  renameComponent(component, name) {
695
592
  const clean = name.trim();
696
593
  if (!clean ||
@@ -740,7 +637,6 @@ class Editor {
740
637
  this.walkTemplates(child, fn);
741
638
  }
742
639
  }
743
- // --- query ----------------------------------------------------------------
744
640
  getComponents() {
745
641
  return this.nu.state.program.components.filter((c) => c instanceof t__namespace.NuComponent);
746
642
  }
@@ -760,7 +656,6 @@ class Editor {
760
656
  getNodeIndex(node) {
761
657
  return this.getSiblings(node).indexOf(node);
762
658
  }
763
- /** Full path from the State root to this node (for selections/serialization). */
764
659
  getNodePath(node) {
765
660
  const out = [];
766
661
  let current = node;
@@ -772,12 +667,9 @@ class Editor {
772
667
  }
773
668
  return out;
774
669
  }
775
- // --- change subscription --------------------------------------------------
776
- /** Fires with the raw field-level writes of every change (incl. undo/redo). */
777
670
  onChange(cb) {
778
671
  return this.nu.listenToMutations(cb);
779
672
  }
780
- /** Fires only when the tree shape changes (a child array was mutated). */
781
673
  onStructureChange(cb) {
782
674
  return this.nu.listenToMutations((entries) => {
783
675
  if (entries.some((e) => Array.isArray(e.target))) {
@@ -785,7 +677,6 @@ class Editor {
785
677
  }
786
678
  });
787
679
  }
788
- // --- internals (run inside nu.change) -------------------------------------
789
680
  spliceFromParent(node) {
790
681
  const parent = this.nu.getParentNode(node);
791
682
  const kids = parent?.children;
@@ -833,7 +724,6 @@ class Editor {
833
724
  destKids.splice(position === 'after' ? ti + 1 : ti, 0, dragged);
834
725
  return true;
835
726
  }
836
- /** Insert an already-detached node relative to `target` (no removal). */
837
727
  insertRelativeTo(node, target, position) {
838
728
  if (position === 'inside') {
839
729
  if (!canHaveChildren(target)) {
@@ -880,4 +770,3 @@ exports.templateChildren = templateChildren;
880
770
  exports.templateLabel = templateLabel;
881
771
  exports.text = text;
882
772
  exports.textPreview = textPreview;
883
- //# sourceMappingURL=index.cjs.map
package/dist/index.d.ts CHANGED
@@ -2,7 +2,6 @@ import * as t from '@nuforge/types';
2
2
  import { Nu, NuExternalsFactory } from '@nuforge/core';
3
3
  import { MutationEntry } from '@nuforge/reactivity';
4
4
 
5
- /** A control hint for auto-generating inspector fields from a block's props. */
6
5
  interface PropSchemaField {
7
6
  name: string;
8
7
  type: 'string' | 'number' | 'boolean' | 'expression' | 'enum';
@@ -10,11 +9,6 @@ interface PropSchemaField {
10
9
  default?: string | number | boolean;
11
10
  options?: string[];
12
11
  }
13
- /**
14
- * A reusable element/block in the palette. `create()` returns a fresh template
15
- * subtree to insert. `icon` is a name (not a component) so the registry stays
16
- * framework-agnostic; the UI maps the name to its own icon set.
17
- */
18
12
  interface Block {
19
13
  id: string;
20
14
  label: string;
@@ -32,65 +26,29 @@ declare class BlockRegistry {
32
26
  all(): Block[];
33
27
  byCategory(): Record<string, Block[]>;
34
28
  }
35
- /** The built-in palette — the common HTML primitives. */
36
29
  declare const DEFAULT_BLOCKS: Block[];
37
30
 
38
- /** A literal expression (`"x"`, `5`, `true`). */
39
31
  declare function lit(value: string | number | boolean): t.Literal;
40
- /** A text node: `<text text={value} />`, rendered as its string value. */
41
32
  declare function text(value: string): t.TagTemplate;
42
- /**
43
- * Concise element builder — `el('div', { class: lit('box') }, [text('hi')])`
44
- * instead of the verbose `t.tagTemplate({ tag, props, children })`.
45
- */
46
33
  declare function el(tag: string, props?: Record<string, t.Expression>, children?: t.Template[]): t.TagTemplate;
47
34
 
48
- /** Where a dragged node is dropped relative to a target. */
49
35
  type DropPosition = 'before' | 'after' | 'inside';
50
- /**
51
- * Compute a drop position from the pointer's Y against a target's bounding rect.
52
- * When `allowInside` is set, the middle band is `inside` (drop as a child) and
53
- * the outer bands are `before`/`after`; otherwise it's a simple before/after
54
- * split at the midpoint. Pure — use it from an `onDragOver`/`onPointerMove`
55
- * handler to drive a drop indicator and the eventual `moveNode`/`pasteNode`.
56
- */
57
36
  declare function dropPositionFromPointer(clientY: number, rect: {
58
37
  top: number;
59
38
  height: number;
60
39
  }, opts?: {
61
40
  allowInside?: boolean;
62
41
  }): DropPosition;
63
- /** A human label for a template node (tag name, component name, slot, …). */
64
42
  declare function templateLabel(node: t.Template): string;
65
- /** Child templates of a node (empty for leaves / text nodes). */
66
43
  declare function templateChildren(node: t.Template): t.Template[];
67
- /** Is this a text node (`<text text=…>`), rendered as its string value? */
68
44
  declare function isTextTemplate(node: t.Template): boolean;
69
- /** Preview the content of a text node, e.g. for a layers tree. */
70
45
  declare function textPreview(node: t.Template): string | null;
71
- /** Can this node hold appended children? */
72
46
  declare function canHaveChildren(node: t.Template | null): boolean;
73
- /** Is `ancestor` somewhere above `node` in the tree? */
74
47
  declare function isAncestorOf(nu: Nu, ancestor: t.Template, node: t.Template): boolean;
75
48
 
76
- /**
77
- * Wire protocol for `RemoteCanvasHost` <-> `createCanvasReceiver`: a genuinely
78
- * navigated `/canvas/[id]`-style iframe has its own JS realm, so state and
79
- * interaction can't cross as direct object references the way `IframeCanvas`'s
80
- * portal allows — everything here is a structured-clone-safe plain message.
81
- *
82
- * Deliberately generic: only the shapes the editor primitive itself needs
83
- * (state sync, ready handshake, the core interaction events) are typed. Host
84
- * concerns like theme/preview-mode/overrides ride through `CanvasUiMessage`'s
85
- * opaque `payload` — this package relays it, never interprets it.
86
- */
87
49
  declare const CANVAS_CHANNEL = "nuforge-canvas";
88
50
  declare const CANVAS_PROTOCOL_VERSION = 1;
89
- /** Namespace the `BroadcastChannel` per canvas instance (derive from the
90
- * iframe's own `src`, so a host never has to invent/track a separate id). */
91
51
  declare function canvasChannelName(src: string): string;
92
- /** The `{ root, types }` shape from `@nuforge/types`' `flatten` — plain JSON,
93
- * safe across `postMessage`/`BroadcastChannel`'s structured clone. */
94
52
  interface FlattenedState {
95
53
  root: {
96
54
  $$typeId: string;
@@ -104,9 +62,6 @@ interface CanvasInitMessage {
104
62
  channel: typeof CANVAS_CHANNEL;
105
63
  v: typeof CANVAS_PROTOCOL_VERSION;
106
64
  type: 'init';
107
- /** Which component the canvas should render (matches `Nu.createFrame`'s
108
- * `component.name`) — lets the SAME canvas realm switch pages without a
109
- * full iframe reload; only the rendered Frame changes. */
110
65
  activeComponent: string;
111
66
  flat: FlattenedState;
112
67
  }
@@ -118,8 +73,6 @@ interface CanvasPatchMessage {
118
73
  flat: FlattenedState;
119
74
  rev: number;
120
75
  }
121
- /** Host-defined payload (theme CSS, preview-mode toggle, hide/lock overrides,
122
- * ...) — nucode doesn't interpret this, just relays it verbatim. */
123
76
  interface CanvasUiMessage {
124
77
  channel: typeof CANVAS_CHANNEL;
125
78
  v: typeof CANVAS_PROTOCOL_VERSION;
@@ -131,9 +84,6 @@ interface CanvasReadyMessage {
131
84
  channel: typeof CANVAS_CHANNEL;
132
85
  v: typeof CANVAS_PROTOCOL_VERSION;
133
86
  type: 'ready';
134
- /** Distinguishes a fresh mount from a stale one still catching up (e.g. a
135
- * rapid remount) — the host ignores a `ready` whose `mountGen` is not newer
136
- * than the last one it accepted. */
137
87
  mountGen: number;
138
88
  }
139
89
  interface CanvasSelectMessage {
@@ -174,151 +124,58 @@ interface CanvasLinkClickMessage {
174
124
  }
175
125
  type CanvasClientMessage = CanvasReadyMessage | CanvasSelectMessage | CanvasHoverMessage | CanvasDropMessage | CanvasKeydownMessage | CanvasLinkClickMessage;
176
126
  type CanvasMessage = CanvasHostMessage | CanvasClientMessage;
177
- /** Plain `Omit` collapses a union to only its shared keys (`keyof` over a
178
- * union intersects, not unions) — this distributes per member instead, so
179
- * e.g. `DistributiveOmit<CanvasClientMessage, 'channel' | 'v'>` still keeps
180
- * each variant's own fields (`nodeId`, `targetId`, `key`, ...). */
181
127
  type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
182
- /** Narrow an arbitrary `BroadcastChannel` payload to a message from this
183
- * protocol — guards against unrelated traffic on the same channel name and
184
- * against a version mismatch across a stale cached bundle. */
185
128
  declare function isCanvasMessage(data: unknown): data is CanvasMessage;
186
129
 
187
130
  interface CanvasReceiverOpts {
188
- /** Must match the `src` `RemoteCanvasHost` was given — the channel name is
189
- * derived from it, so both sides agree without a separate id to track. */
190
131
  src: string;
191
132
  externals?: NuExternalsFactory;
192
133
  }
193
134
  interface CanvasReceiver {
194
- /**
195
- * A `Nu` instance mirroring the host's, kept in sync one-way (host -> here)
196
- * via `init`/`patch` messages. It's a genuinely mutable, loaded `Nu` — DSL
197
- * `onClick` handlers rendered from it run and mutate it locally exactly like
198
- * any other `Nu` (e.g. a counter's `val inc = () => count = count + 1`, used
199
- * by an in-editor "preview mode" toggle to stay truly interactive). That
200
- * local mutation is intentionally NOT relayed back to the host: the next
201
- * `init`/`patch` from the host simply overwrites/merges over it, so nothing
202
- * here needs to enforce read-only-ness — only `RemoteCanvasHost`'s own
203
- * resync-on-transition behavior needs to guarantee a clean return to the
204
- * host's authoritative state (see its docs).
205
- */
206
135
  nu: Nu;
207
- /** Which component `nu` should currently be rendered as (via
208
- * `nu.createFrame({ component: { name } })`) — reactive, so a component
209
- * wrapped in `@nuforge/react`'s `observer()` re-renders when it changes. */
210
136
  volatile: {
211
137
  activeComponent: string | null;
212
- /**
213
- * Bumped every time `nu` gets a full `nu.load()` — a bootstrap, a resync
214
- * (previewMode/activeComponent change), OR a `patch` whose flattened
215
- * node-id set differs from the current one (nodes added/removed, not
216
- * just field edits — see `applyState`). Callers building a Frame (e.g.
217
- * via `useNuFrame`) should key their memo/`key` on this, alongside
218
- * `activeComponent`, so React remounts and gets a fresh Frame instead of
219
- * reading a disposed one.
220
- *
221
- * Structural patches are folded into the SAME full-reload path as
222
- * `init`, not merged in place, even though that means a `val`'s live
223
- * runtime value (e.g. a preview-mode counter's `count` — lives in a
224
- * Frame's `Environment` bindings, not `nu.state`'s AST) gets reset too.
225
- * An earlier version tried to preserve that by merging state first and
226
- * bumping this counter as a second, separate step — that let React
227
- * render the OLD (soon-stale) Frame against the ALREADY-merged,
228
- * structurally-different `nu.state` in between the two updates
229
- * (`Frame`'s template/component caches are keyed by structural PATH, not
230
- * object identity, and aren't evicted on removal), producing duplicate
231
- * React keys or a briefly-missing node before the remount caught up.
232
- * Doing both as one atomic `nu.load()` + bump — the same already-proven
233
- * resync path — closes that window entirely.
234
- */
235
138
  loadGeneration: number;
236
139
  };
237
- /** Announce this receiver is mounted and ready for the host's first `init`.
238
- * Call once, after registering any `onUi`/message handling — the host may
239
- * reply synchronously-ish (same task queue) once it sees `ready`. */
240
140
  postReady(): void;
241
- /** Register a callback for host->iframe UI payloads (`ui` messages) —
242
- * interpretation (theme, preview mode, overrides, ...) is entirely up to
243
- * the caller; this SDK only relays the envelope. Returns an unsubscribe. */
244
141
  onUi(cb: (payload: unknown) => void): () => void;
245
- /** Send an interaction event up to the host (select/hover/drop/keydown/link-click). */
246
142
  postEvent(message: DistributiveOmit<CanvasClientMessage, 'channel' | 'v'>): void;
247
143
  dispose(): void;
248
144
  }
249
- /**
250
- * Canvas-side receiver for a genuinely-navigated `/canvas/[id]`-style route:
251
- * a `Nu` instance kept in sync with the host's one-way, via `BroadcastChannel`
252
- * + `t.unflatten`/`t.merge` (the same primitives `@nuforge/collaboration`'s
253
- * `LoroSyncProvider` uses for its state<->doc binding, without the CRDT layer
254
- * — there's exactly one writer here, so convergence machinery is unneeded).
255
- * No `createEditor()`: this instance is never meant to originate an edit the
256
- * host doesn't already know about, so it only needs `Nu`, not the mutation SDK.
257
- */
258
145
  declare function createCanvasReceiver(opts: CanvasReceiverOpts): CanvasReceiver;
259
146
 
260
147
  interface CreateEditorOptions {
261
- /** Extra blocks to add to the palette. */
262
148
  blocks?: Block[];
263
- /** Include the built-in DEFAULT_BLOCKS (default true). */
264
149
  defaultBlocks?: boolean;
265
150
  }
266
- /**
267
- * A high-level facade over a {@link Nu} runtime for building visual page
268
- * editors. Every mutation goes through `nu.change`, so it is transactional and
269
- * undoable. Reach for these instead of hand-splicing the AST.
270
- */
271
151
  declare class Editor {
272
152
  readonly nu: Nu;
273
153
  readonly blocks: BlockRegistry;
274
154
  readonly commands: CommandRegistry;
275
155
  constructor(nu: Nu, opts?: CreateEditorOptions);
276
- /** Append (or insert at `index`) `child` into `parent`'s children. */
277
156
  insertNode(parent: t.Template, child: t.Template, index?: number): void;
278
- /** Remove a node from its parent. Returns false if it has no parent. */
279
157
  removeNode(node: t.Template): boolean;
280
- /** Move `dragged` before/after/inside `target`. Refuses to nest into self. */
281
158
  moveNode(dragged: t.Template, target: t.Template, position: DropPosition): boolean;
282
159
  private clipboardNode;
283
- /** Put a node on the editor clipboard (a clone is made on paste). */
284
160
  copyNode(node: t.Template): void;
285
- /** Copy a node, then remove it. */
286
161
  cutNode(node: t.Template): boolean;
287
- /** Whether there is something on the clipboard to paste. */
288
162
  canPaste(): boolean;
289
- /**
290
- * Paste a fresh copy (new ids) of the clipboard node before/after/inside
291
- * `target`. Returns the inserted node, or null if nothing was pasted.
292
- */
293
163
  pasteNode(target: t.Template, position?: DropPosition): t.Template | null;
294
- /** Duplicate a node in place (inserted right after it). Returns the copy. */
295
164
  duplicateNode(node: t.Template): t.Template | null;
296
- /**
297
- * Insert a detached node before/after/inside a target (undoable). The core of
298
- * canvas drag-and-drop: a drop computes a target + position, then calls this.
299
- * Returns the inserted node.
300
- */
301
165
  insertNodeAt(node: t.Template, target: t.Template, position: DropPosition): t.Template | null;
302
- /** Create a block and insert it before/after/inside a target. */
303
166
  insertBlockAt(blockId: string, target: t.Template, position: DropPosition): t.Template | null;
304
167
  private insertCloneRelativeTo;
305
- /** Set several props at once. */
306
168
  updateProps(node: t.Template, props: Record<string, t.Expression>): void;
307
169
  setProp(node: t.Template, key: string, expr: t.Expression): void;
308
170
  removeProp(node: t.Template, key: string): void;
309
- /** Parse `source` as a nuforge expression and assign it to `key`. */
310
171
  setExpression(node: t.Template, key: string, source: string): boolean;
311
- /** Update a text node's literal value. */
312
172
  setText(node: t.Template, value: string): boolean;
313
173
  setTag(node: t.TagTemplate, tag: string): void;
314
174
  addClass(node: t.Template, name: string): void;
315
- /** Parse `source` and set the condition under which class `name` applies. */
316
175
  setClassCondition(node: t.Template, name: string, source: string): boolean;
317
176
  removeClass(node: t.Template, name: string): void;
318
- /** Set the `@if` condition from source. */
319
177
  setCondition(node: t.Template, source: string): boolean;
320
178
  clearCondition(node: t.Template): void;
321
- /** Repeat a node over `iterator` (a list expression), binding each `alias`. */
322
179
  setEach(node: t.Template, iterator: string, alias?: string, indexName?: string): boolean;
323
180
  clearEach(node: t.Template): void;
324
181
  addState(component: t.NuComponent, name?: string, init?: string): t.Val;
@@ -327,12 +184,9 @@ declare class Editor {
327
184
  addComponentProp(component: t.NuComponent, name?: string, init?: string): t.ComponentProp;
328
185
  removeComponentProp(component: t.NuComponent, prop: t.ComponentProp): void;
329
186
  setComponentPropInit(prop: t.ComponentProp, source: string): boolean;
330
- /** Create a block's template and insert it into `parent`. */
331
187
  insertBlock(blockId: string, parent: t.Template, index?: number): t.Template | null;
332
188
  private uniqueName;
333
- /** Add a new component (a "page"). */
334
189
  addComponent(name?: string): t.NuComponent;
335
- /** Rename a component, updating `<Name/>` references across the program. */
336
190
  renameComponent(component: t.NuComponent, name: string): boolean;
337
191
  removeComponent(component: t.NuComponent): boolean;
338
192
  duplicateComponent(component: t.NuComponent): t.NuComponent;
@@ -343,20 +197,15 @@ declare class Editor {
343
197
  getChildren(node: t.Template): t.Template[];
344
198
  getSiblings(node: t.Template): t.Template[];
345
199
  getNodeIndex(node: t.Template): number;
346
- /** Full path from the State root to this node (for selections/serialization). */
347
200
  getNodePath(node: t.Type): Array<string | number>;
348
- /** Fires with the raw field-level writes of every change (incl. undo/redo). */
349
201
  onChange(cb: (entries: MutationEntry[]) => void): () => void;
350
- /** Fires only when the tree shape changes (a child array was mutated). */
351
202
  onStructureChange(cb: () => void): () => void;
352
203
  private spliceFromParent;
353
204
  private applyMove;
354
- /** Insert an already-detached node relative to `target` (no removal). */
355
205
  private insertRelativeTo;
356
206
  }
357
207
  declare function createEditor(nu: Nu, opts?: CreateEditorOptions): Editor;
358
208
 
359
- /** A named action a UI can discover and bind to (toolbar buttons, shortcuts). */
360
209
  interface Command {
361
210
  id: string;
362
211
  label: string;