@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.mjs CHANGED
@@ -3,11 +3,9 @@ import { Nu } from '@nuforge/core';
3
3
  import { reactive } from '@nuforge/reactivity';
4
4
  import { Parser } from '@nuforge/parser';
5
5
 
6
- /** A literal expression (`"x"`, `5`, `true`). */
7
6
  function lit(value) {
8
7
  return t.literal({ value });
9
8
  }
10
- /** A text node: `<text text={value} />`, rendered as its string value. */
11
9
  function text(value) {
12
10
  return t.tagTemplate({
13
11
  tag: 'text',
@@ -15,10 +13,6 @@ function text(value) {
15
13
  children: [],
16
14
  });
17
15
  }
18
- /**
19
- * Concise element builder — `el('div', { class: lit('box') }, [text('hi')])`
20
- * instead of the verbose `t.tagTemplate({ tag, props, children })`.
21
- */
22
16
  function el(tag, props = {}, children = []) {
23
17
  return t.tagTemplate({ tag, props, children });
24
18
  }
@@ -51,7 +45,6 @@ class BlockRegistry {
51
45
  return out;
52
46
  }
53
47
  }
54
- /** The built-in palette — the common HTML primitives. */
55
48
  const DEFAULT_BLOCKS = [
56
49
  { id: 'box', label: 'Box', icon: 'square', create: () => el('div') },
57
50
  { id: 'text', label: 'Text', icon: 'type', create: () => text('Text') },
@@ -81,27 +74,11 @@ const DEFAULT_BLOCKS = [
81
74
  },
82
75
  ];
83
76
 
84
- /**
85
- * Wire protocol for `RemoteCanvasHost` <-> `createCanvasReceiver`: a genuinely
86
- * navigated `/canvas/[id]`-style iframe has its own JS realm, so state and
87
- * interaction can't cross as direct object references the way `IframeCanvas`'s
88
- * portal allows — everything here is a structured-clone-safe plain message.
89
- *
90
- * Deliberately generic: only the shapes the editor primitive itself needs
91
- * (state sync, ready handshake, the core interaction events) are typed. Host
92
- * concerns like theme/preview-mode/overrides ride through `CanvasUiMessage`'s
93
- * opaque `payload` — this package relays it, never interprets it.
94
- */
95
77
  const CANVAS_CHANNEL = 'nuforge-canvas';
96
78
  const CANVAS_PROTOCOL_VERSION = 1;
97
- /** Namespace the `BroadcastChannel` per canvas instance (derive from the
98
- * iframe's own `src`, so a host never has to invent/track a separate id). */
99
79
  function canvasChannelName(src) {
100
80
  return `${CANVAS_CHANNEL}:${src}`;
101
81
  }
102
- /** Narrow an arbitrary `BroadcastChannel` payload to a message from this
103
- * protocol — guards against unrelated traffic on the same channel name and
104
- * against a version mismatch across a stale cached bundle. */
105
82
  function isCanvasMessage(data) {
106
83
  return (!!data &&
107
84
  typeof data === 'object' &&
@@ -109,47 +86,18 @@ function isCanvasMessage(data) {
109
86
  data.v === CANVAS_PROTOCOL_VERSION);
110
87
  }
111
88
 
112
- /**
113
- * Canvas-side receiver for a genuinely-navigated `/canvas/[id]`-style route:
114
- * a `Nu` instance kept in sync with the host's one-way, via `BroadcastChannel`
115
- * + `t.unflatten`/`t.merge` (the same primitives `@nuforge/collaboration`'s
116
- * `LoroSyncProvider` uses for its state<->doc binding, without the CRDT layer
117
- * — there's exactly one writer here, so convergence machinery is unneeded).
118
- * No `createEditor()`: this instance is never meant to originate an edit the
119
- * host doesn't already know about, so it only needs `Nu`, not the mutation SDK.
120
- */
121
89
  function createCanvasReceiver(opts) {
122
90
  const nu = Nu.create({ externals: opts.externals });
123
91
  const volatile = reactive({ activeComponent: null, loadGeneration: 0 });
124
92
  const channel = new BroadcastChannel(canvasChannelName(opts.src));
125
93
  const uiListeners = new Set();
126
- /** Did the incoming flattened state add or remove any node id (a
127
- * structural change), vs. only edit fields on already-known nodes? */
128
- const isStructuralChange = (incoming) => {
129
- if (!nu.loaded) {
130
- return false;
131
- }
132
- const currentKeys = Object.keys(t.flatten(nu.state).types);
133
- const incomingKeys = Object.keys(incoming);
134
- if (currentKeys.length !== incomingKeys.length) {
135
- return true;
136
- }
137
- const incomingSet = new Set(incomingKeys);
138
- return currentKeys.some((key) => !incomingSet.has(key));
139
- };
140
94
  const applyState = (message) => {
141
- // A full reload — the very first bootstrap, any later resync (`init`
142
- // see RemoteCanvasHost's `previewMode`/`activeComponent`-change doc), OR
143
- // a `patch` that structurally changed the tree. See `volatile.loadGeneration`'s
144
- // doc for why structural patches are folded into this same path instead
145
- // of merging in place.
146
- const needsFullReload = message.type === 'init' || isStructuralChange(message.flat.types);
147
- if (needsFullReload) {
95
+ if (message.type === 'init') {
148
96
  nu.load(t.unflatten(message.flat));
149
97
  volatile.loadGeneration++;
150
98
  }
151
99
  else {
152
- nu.change(() => t.merge(nu.state, t.unflatten(message.flat)));
100
+ nu.change(() => t.merge(nu.state, t.unflatten(message.flat), { arrayReconcile: 'id' }));
153
101
  }
154
102
  volatile.activeComponent = message.activeComponent;
155
103
  };
@@ -218,13 +166,6 @@ class CommandRegistry {
218
166
  }
219
167
  }
220
168
 
221
- /**
222
- * Compute a drop position from the pointer's Y against a target's bounding rect.
223
- * When `allowInside` is set, the middle band is `inside` (drop as a child) and
224
- * the outer bands are `before`/`after`; otherwise it's a simple before/after
225
- * split at the midpoint. Pure — use it from an `onDragOver`/`onPointerMove`
226
- * handler to drive a drop indicator and the eventual `moveNode`/`pasteNode`.
227
- */
228
169
  function dropPositionFromPointer(clientY, rect, opts = {}) {
229
170
  const ratio = rect.height > 0 ? (clientY - rect.top) / rect.height : 0;
230
171
  if (opts.allowInside) {
@@ -236,7 +177,6 @@ function dropPositionFromPointer(clientY, rect, opts = {}) {
236
177
  }
237
178
  return ratio < 0.5 ? 'before' : 'after';
238
179
  }
239
- /** A human label for a template node (tag name, component name, slot, …). */
240
180
  function templateLabel(node) {
241
181
  if (node instanceof t.TagTemplate) {
242
182
  return node.tag;
@@ -252,15 +192,12 @@ function templateLabel(node) {
252
192
  }
253
193
  return 'node';
254
194
  }
255
- /** Child templates of a node (empty for leaves / text nodes). */
256
195
  function templateChildren(node) {
257
196
  return (node.children ?? []).filter((child) => child instanceof t.Type);
258
197
  }
259
- /** Is this a text node (`<text text=…>`), rendered as its string value? */
260
198
  function isTextTemplate(node) {
261
199
  return node instanceof t.TagTemplate && node.tag === 'text';
262
200
  }
263
- /** Preview the content of a text node, e.g. for a layers tree. */
264
201
  function textPreview(node) {
265
202
  if (!isTextTemplate(node)) {
266
203
  return null;
@@ -271,7 +208,6 @@ function textPreview(node) {
271
208
  }
272
209
  return '{…}';
273
210
  }
274
- /** Can this node hold appended children? */
275
211
  function canHaveChildren(node) {
276
212
  if (!node || isTextTemplate(node)) {
277
213
  return false;
@@ -280,7 +216,6 @@ function canHaveChildren(node) {
280
216
  node instanceof t.ComponentTemplate ||
281
217
  t.is(node, t.FragmentTemplate));
282
218
  }
283
- /** Is `ancestor` somewhere above `node` in the tree? */
284
219
  function isAncestorOf(nu, ancestor, node) {
285
220
  let parent = nu.getParentNode(node);
286
221
  while (parent) {
@@ -292,11 +227,6 @@ function isAncestorOf(nu, ancestor, node) {
292
227
  return false;
293
228
  }
294
229
 
295
- /**
296
- * A high-level facade over a {@link Nu} runtime for building visual page
297
- * editors. Every mutation goes through `nu.change`, so it is transactional and
298
- * undoable. Reach for these instead of hand-splicing the AST.
299
- */
300
230
  class Editor {
301
231
  nu;
302
232
  blocks;
@@ -319,8 +249,6 @@ class Editor {
319
249
  run: (e) => e.nu.redo(),
320
250
  });
321
251
  }
322
- // --- mutation -------------------------------------------------------------
323
- /** Append (or insert at `index`) `child` into `parent`'s children. */
324
252
  insertNode(parent, child, index) {
325
253
  const kids = parent.children;
326
254
  if (!Array.isArray(kids)) {
@@ -335,7 +263,6 @@ class Editor {
335
263
  }
336
264
  });
337
265
  }
338
- /** Remove a node from its parent. Returns false if it has no parent. */
339
266
  removeNode(node) {
340
267
  let ok = false;
341
268
  this.nu.change(() => {
@@ -343,7 +270,6 @@ class Editor {
343
270
  });
344
271
  return ok;
345
272
  }
346
- /** Move `dragged` before/after/inside `target`. Refuses to nest into self. */
347
273
  moveNode(dragged, target, position) {
348
274
  if (dragged === target || isAncestorOf(this.nu, dragged, target)) {
349
275
  return false;
@@ -354,40 +280,26 @@ class Editor {
354
280
  });
355
281
  return ok;
356
282
  }
357
- // --- clipboard ------------------------------------------------------------
358
283
  clipboardNode = null;
359
- /** Put a node on the editor clipboard (a clone is made on paste). */
360
284
  copyNode(node) {
361
285
  this.clipboardNode = node;
362
286
  }
363
- /** Copy a node, then remove it. */
364
287
  cutNode(node) {
365
288
  this.copyNode(node);
366
289
  return this.removeNode(node);
367
290
  }
368
- /** Whether there is something on the clipboard to paste. */
369
291
  canPaste() {
370
292
  return this.clipboardNode !== null;
371
293
  }
372
- /**
373
- * Paste a fresh copy (new ids) of the clipboard node before/after/inside
374
- * `target`. Returns the inserted node, or null if nothing was pasted.
375
- */
376
294
  pasteNode(target, position = 'after') {
377
295
  if (!this.clipboardNode) {
378
296
  return null;
379
297
  }
380
298
  return this.insertCloneRelativeTo(this.clipboardNode, target, position);
381
299
  }
382
- /** Duplicate a node in place (inserted right after it). Returns the copy. */
383
300
  duplicateNode(node) {
384
301
  return this.insertCloneRelativeTo(node, node, 'after');
385
302
  }
386
- /**
387
- * Insert a detached node before/after/inside a target (undoable). The core of
388
- * canvas drag-and-drop: a drop computes a target + position, then calls this.
389
- * Returns the inserted node.
390
- */
391
303
  insertNodeAt(node, target, position) {
392
304
  let ok = false;
393
305
  this.nu.change(() => {
@@ -395,7 +307,6 @@ class Editor {
395
307
  });
396
308
  return ok ? (this.nu.getNodeFromId(node.id) ?? null) : null;
397
309
  }
398
- /** Create a block and insert it before/after/inside a target. */
399
310
  insertBlockAt(blockId, target, position) {
400
311
  const block = this.blocks.get(blockId);
401
312
  if (!block) {
@@ -409,10 +320,8 @@ class Editor {
409
320
  this.nu.change(() => {
410
321
  ok = this.insertRelativeTo(clone, target, position);
411
322
  });
412
- // Return the reactive node from the tree (clone is wrapped on insert).
413
323
  return ok ? (this.nu.getNodeFromId(clone.id) ?? null) : null;
414
324
  }
415
- /** Set several props at once. */
416
325
  updateProps(node, props) {
417
326
  this.nu.change(() => {
418
327
  for (const [key, value] of Object.entries(props)) {
@@ -430,7 +339,6 @@ class Editor {
430
339
  delete node.props[key];
431
340
  });
432
341
  }
433
- /** Parse `source` as a nuforge expression and assign it to `key`. */
434
342
  setExpression(node, key, source) {
435
343
  let expr;
436
344
  try {
@@ -442,7 +350,6 @@ class Editor {
442
350
  this.setProp(node, key, expr);
443
351
  return true;
444
352
  }
445
- /** Update a text node's literal value. */
446
353
  setText(node, value) {
447
354
  const expr = node.props?.text;
448
355
  if (!(expr instanceof t.Literal)) {
@@ -475,7 +382,6 @@ class Editor {
475
382
  tag.classList.properties[clean] = t.literal({ value: true });
476
383
  });
477
384
  }
478
- /** Parse `source` and set the condition under which class `name` applies. */
479
385
  setClassCondition(node, name, source) {
480
386
  let expr;
481
387
  try {
@@ -504,7 +410,6 @@ class Editor {
504
410
  }
505
411
  });
506
412
  }
507
- /** Set the `@if` condition from source. */
508
413
  setCondition(node, source) {
509
414
  let expr;
510
415
  try {
@@ -523,8 +428,6 @@ class Editor {
523
428
  node.if = null;
524
429
  });
525
430
  }
526
- // --- loop (@each) ---------------------------------------------------------
527
- /** Repeat a node over `iterator` (a list expression), binding each `alias`. */
528
431
  setEach(node, iterator, alias = 'item', indexName) {
529
432
  let expr;
530
433
  try {
@@ -547,7 +450,6 @@ class Editor {
547
450
  node.each = null;
548
451
  });
549
452
  }
550
- // --- component state / props ----------------------------------------------
551
453
  addState(component, name = 'value', init = '0') {
552
454
  const taken = new Set(component.state.map((v) => v.name));
553
455
  let unique = name;
@@ -632,8 +534,6 @@ class Editor {
632
534
  });
633
535
  return true;
634
536
  }
635
- // --- blocks ---------------------------------------------------------------
636
- /** Create a block's template and insert it into `parent`. */
637
537
  insertBlock(blockId, parent, index) {
638
538
  const block = this.blocks.get(blockId);
639
539
  if (!block || !canHaveChildren(parent)) {
@@ -643,7 +543,6 @@ class Editor {
643
543
  this.insertNode(parent, created, index);
644
544
  return created;
645
545
  }
646
- // --- components / pages ---------------------------------------------------
647
546
  uniqueName(base) {
648
547
  const taken = new Set(this.getComponents().map((c) => c.name));
649
548
  if (!taken.has(base)) {
@@ -655,7 +554,6 @@ class Editor {
655
554
  }
656
555
  return `${base}${i}`;
657
556
  }
658
- /** Add a new component (a "page"). */
659
557
  addComponent(name = 'Page') {
660
558
  const unique = this.uniqueName(name);
661
559
  const component = t.nuComponent({
@@ -669,7 +567,6 @@ class Editor {
669
567
  });
670
568
  return this.getComponent(unique);
671
569
  }
672
- /** Rename a component, updating `<Name/>` references across the program. */
673
570
  renameComponent(component, name) {
674
571
  const clean = name.trim();
675
572
  if (!clean ||
@@ -719,7 +616,6 @@ class Editor {
719
616
  this.walkTemplates(child, fn);
720
617
  }
721
618
  }
722
- // --- query ----------------------------------------------------------------
723
619
  getComponents() {
724
620
  return this.nu.state.program.components.filter((c) => c instanceof t.NuComponent);
725
621
  }
@@ -739,7 +635,6 @@ class Editor {
739
635
  getNodeIndex(node) {
740
636
  return this.getSiblings(node).indexOf(node);
741
637
  }
742
- /** Full path from the State root to this node (for selections/serialization). */
743
638
  getNodePath(node) {
744
639
  const out = [];
745
640
  let current = node;
@@ -751,12 +646,9 @@ class Editor {
751
646
  }
752
647
  return out;
753
648
  }
754
- // --- change subscription --------------------------------------------------
755
- /** Fires with the raw field-level writes of every change (incl. undo/redo). */
756
649
  onChange(cb) {
757
650
  return this.nu.listenToMutations(cb);
758
651
  }
759
- /** Fires only when the tree shape changes (a child array was mutated). */
760
652
  onStructureChange(cb) {
761
653
  return this.nu.listenToMutations((entries) => {
762
654
  if (entries.some((e) => Array.isArray(e.target))) {
@@ -764,7 +656,6 @@ class Editor {
764
656
  }
765
657
  });
766
658
  }
767
- // --- internals (run inside nu.change) -------------------------------------
768
659
  spliceFromParent(node) {
769
660
  const parent = this.nu.getParentNode(node);
770
661
  const kids = parent?.children;
@@ -812,7 +703,6 @@ class Editor {
812
703
  destKids.splice(position === 'after' ? ti + 1 : ti, 0, dragged);
813
704
  return true;
814
705
  }
815
- /** Insert an already-detached node relative to `target` (no removal). */
816
706
  insertRelativeTo(node, target, position) {
817
707
  if (position === 'inside') {
818
708
  if (!canHaveChildren(target)) {
@@ -839,4 +729,3 @@ function createEditor(nu, opts) {
839
729
  }
840
730
 
841
731
  export { BlockRegistry, CANVAS_CHANNEL, CANVAS_PROTOCOL_VERSION, CommandRegistry, DEFAULT_BLOCKS, Editor, canHaveChildren, canvasChannelName, createCanvasReceiver, createEditor, defineBlock, dropPositionFromPointer, el, isAncestorOf, isCanvasMessage, isTextTemplate, lit, templateChildren, templateLabel, text, textPreview };
842
- //# sourceMappingURL=index.mjs.map