@nuforge/editor 0.3.0 → 0.3.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/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 ||
@@ -702,7 +599,9 @@ class Editor {
702
599
  component.name = clean;
703
600
  for (const c of this.getComponents()) {
704
601
  this.walkTemplates(c.template, (tpl) => {
705
- if (tpl instanceof t__namespace.ComponentTemplate && tpl.component.name === old) {
602
+ if (tpl instanceof t__namespace.ComponentTemplate &&
603
+ !tpl.component.external &&
604
+ tpl.component.name === old) {
706
605
  tpl.component.name = clean;
707
606
  }
708
607
  });
@@ -740,7 +639,6 @@ class Editor {
740
639
  this.walkTemplates(child, fn);
741
640
  }
742
641
  }
743
- // --- query ----------------------------------------------------------------
744
642
  getComponents() {
745
643
  return this.nu.state.program.components.filter((c) => c instanceof t__namespace.NuComponent);
746
644
  }
@@ -760,7 +658,6 @@ class Editor {
760
658
  getNodeIndex(node) {
761
659
  return this.getSiblings(node).indexOf(node);
762
660
  }
763
- /** Full path from the State root to this node (for selections/serialization). */
764
661
  getNodePath(node) {
765
662
  const out = [];
766
663
  let current = node;
@@ -772,12 +669,9 @@ class Editor {
772
669
  }
773
670
  return out;
774
671
  }
775
- // --- change subscription --------------------------------------------------
776
- /** Fires with the raw field-level writes of every change (incl. undo/redo). */
777
672
  onChange(cb) {
778
673
  return this.nu.listenToMutations(cb);
779
674
  }
780
- /** Fires only when the tree shape changes (a child array was mutated). */
781
675
  onStructureChange(cb) {
782
676
  return this.nu.listenToMutations((entries) => {
783
677
  if (entries.some((e) => Array.isArray(e.target))) {
@@ -785,7 +679,6 @@ class Editor {
785
679
  }
786
680
  });
787
681
  }
788
- // --- internals (run inside nu.change) -------------------------------------
789
682
  spliceFromParent(node) {
790
683
  const parent = this.nu.getParentNode(node);
791
684
  const kids = parent?.children;
@@ -833,7 +726,6 @@ class Editor {
833
726
  destKids.splice(position === 'after' ? ti + 1 : ti, 0, dragged);
834
727
  return true;
835
728
  }
836
- /** Insert an already-detached node relative to `target` (no removal). */
837
729
  insertRelativeTo(node, target, position) {
838
730
  if (position === 'inside') {
839
731
  if (!canHaveChildren(target)) {
@@ -880,4 +772,3 @@ exports.templateChildren = templateChildren;
880
772
  exports.templateLabel = templateLabel;
881
773
  exports.text = text;
882
774
  exports.textPreview = textPreview;
883
- //# sourceMappingURL=index.cjs.map