@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/react.cjs CHANGED
@@ -6,27 +6,11 @@ var t = require('@nuforge/types');
6
6
  var react = require('react');
7
7
  var reactDom = require('react-dom');
8
8
 
9
- /**
10
- * Wire protocol for `RemoteCanvasHost` <-> `createCanvasReceiver`: a genuinely
11
- * navigated `/canvas/[id]`-style iframe has its own JS realm, so state and
12
- * interaction can't cross as direct object references the way `IframeCanvas`'s
13
- * portal allows — everything here is a structured-clone-safe plain message.
14
- *
15
- * Deliberately generic: only the shapes the editor primitive itself needs
16
- * (state sync, ready handshake, the core interaction events) are typed. Host
17
- * concerns like theme/preview-mode/overrides ride through `CanvasUiMessage`'s
18
- * opaque `payload` — this package relays it, never interprets it.
19
- */
20
9
  const CANVAS_CHANNEL = 'nuforge-canvas';
21
10
  const CANVAS_PROTOCOL_VERSION = 1;
22
- /** Namespace the `BroadcastChannel` per canvas instance (derive from the
23
- * iframe's own `src`, so a host never has to invent/track a separate id). */
24
11
  function canvasChannelName(src) {
25
12
  return `${CANVAS_CHANNEL}:${src}`;
26
13
  }
27
- /** Narrow an arbitrary `BroadcastChannel` payload to a message from this
28
- * protocol — guards against unrelated traffic on the same channel name and
29
- * against a version mismatch across a stale cached bundle. */
30
14
  function isCanvasMessage(data) {
31
15
  return (!!data &&
32
16
  typeof data === 'object' &&
@@ -34,13 +18,6 @@ function isCanvasMessage(data) {
34
18
  data.v === CANVAS_PROTOCOL_VERSION);
35
19
  }
36
20
 
37
- /**
38
- * Compute a drop position from the pointer's Y against a target's bounding rect.
39
- * When `allowInside` is set, the middle band is `inside` (drop as a child) and
40
- * the outer bands are `before`/`after`; otherwise it's a simple before/after
41
- * split at the midpoint. Pure — use it from an `onDragOver`/`onPointerMove`
42
- * handler to drive a drop indicator and the eventual `moveNode`/`pasteNode`.
43
- */
44
21
  function dropPositionFromPointer(clientY, rect, opts = {}) {
45
22
  const ratio = rect.height > 0 ? (clientY - rect.top) / rect.height : 0;
46
23
  if (opts.allowInside) {
@@ -53,9 +30,6 @@ function dropPositionFromPointer(clientY, rect, opts = {}) {
53
30
  return ratio < 0.5 ? 'before' : 'after';
54
31
  }
55
32
 
56
- // Tag every rendered element with its template id so a canvas can map a clicked
57
- // DOM node back to its source node. Covers external components too, so they're
58
- // independently selectable/highlightable in the editor canvas.
59
33
  const decorate = (view) => ({
60
34
  'data-nu-id': view.template?.id,
61
35
  });
@@ -64,18 +38,6 @@ const AMBIENT_CANVAS_WINDOW = {
64
38
  window: typeof window === 'undefined' ? null : window,
65
39
  };
66
40
  const CanvasWindowContext = react.createContext(AMBIENT_CANVAS_WINDOW);
67
- /**
68
- * The real `window`/`document` a canvas-rendered element visually lives in.
69
- * `IframeCanvas` portals its content into an `<iframe>`, but a portal only
70
- * relocates DOM nodes — the JS building them still runs in the host page's
71
- * realm. Any library that reads a bare `window`/`document` global (scroll
72
- * listeners, `matchMedia`, `ResizeObserver`/`IntersectionObserver` defaults)
73
- * silently ends up watching the HOST page, not the iframe the user actually
74
- * sees and scrolls. Build refs/options from `useCanvasWindow()` instead of the
75
- * ambient global so they resolve to the right realm — this works identically
76
- * outside `IframeCanvas` too (e.g. `NuFrame`'s direct, non-iframe render),
77
- * where it's just the ambient `window`/`document`.
78
- */
79
41
  function useCanvasWindow() {
80
42
  return react.useContext(CanvasWindowContext);
81
43
  }
@@ -83,17 +45,14 @@ const EditorView = react.memo(function EditorView({ view, }) {
83
45
  const renderChildren = (views) => views.map((child) => react.createElement(EditorView, { key: child.key, view: child }));
84
46
  return react$1.createViewElement(view, renderChildren, decorate);
85
47
  });
86
- /** Reactive renderer for a frame, decorating each element with `data-nu-id`. */
87
48
  const EditorFrame = react$1.observer(function EditorFrame({ frame }) {
88
49
  return react.createElement(react.Fragment, null, frame.view.children.map((view) => react.createElement(EditorView, { key: view.key, view })));
89
50
  });
90
- /** Map a DOM node (or event target) back to its template id. */
91
51
  function templateIdFromElement(target) {
92
52
  return (target
93
53
  ?.closest('[data-nu-id]')
94
54
  ?.getAttribute('data-nu-id') ?? null);
95
55
  }
96
- /** Sensible default styling for the isolated canvas document. */
97
56
  const DEFAULT_CANVAS_CSS = `
98
57
  *, *::before, *::after { box-sizing: border-box; }
99
58
  html, body { margin: 0; }
@@ -112,13 +71,6 @@ const DEFAULT_CANVAS_CSS = `
112
71
  img { max-width: 100%; }
113
72
  [data-nu-id] { cursor: default; }
114
73
  `;
115
- /**
116
- * A style-isolated editor canvas: the frame is portaled into an `<iframe>` so
117
- * the host's CSS can't leak into the preview (and vice-versa). Selection/hover
118
- * use native capture listeners on the iframe document, so element handlers stay
119
- * inert while editing. Selection/hover rects are computed (in a layout effect,
120
- * after the iframe commits) and handed to `renderBox` for the overlay.
121
- */
122
74
  function IframeCanvas({ frame, selectedId = null, hoveredId = null, revision = 0, previewMode = false, onSelect, onHover, onDropOnNode, baseCss = DEFAULT_CANVAS_CSS, srcDoc, className, containerClassName, renderBox, }) {
123
75
  const iframeRef = react.useRef(null);
124
76
  const [doc, setDoc] = react.useState(null);
@@ -153,9 +105,6 @@ function IframeCanvas({ frame, selectedId = null, hoveredId = null, revision = 0
153
105
  if (!doc) {
154
106
  return;
155
107
  }
156
- // In preview mode, only keep the scroll listener alive (so an overlay, if any
157
- // lingers during a toggle, stays aligned). Skip the capturing click handler
158
- // so element `onClick`s actually fire, and skip hover highlighting.
159
108
  const onScroll = () => setTick((x) => x + 1);
160
109
  doc.addEventListener('scroll', onScroll, true);
161
110
  if (previewMode) {
@@ -178,7 +127,6 @@ function IframeCanvas({ frame, selectedId = null, hoveredId = null, revision = 0
178
127
  doc.removeEventListener('scroll', onScroll, true);
179
128
  };
180
129
  }, [doc, previewMode, onSelect, onHover]);
181
- // Drop-on-canvas: compute the drop target node + position from the pointer.
182
130
  react.useEffect(() => {
183
131
  if (!doc || !onDropOnNode || previewMode) {
184
132
  return;
@@ -195,7 +143,7 @@ function IframeCanvas({ frame, selectedId = null, hoveredId = null, revision = 0
195
143
  setDrop(null);
196
144
  return;
197
145
  }
198
- e.preventDefault(); // allow the drop
146
+ e.preventDefault();
199
147
  setDrop({
200
148
  rect: { top: r.top, left: r.left, width: r.width, height: r.height },
201
149
  position: dropPositionFromPointer(e.clientY, r, { allowInside: true }),
@@ -221,8 +169,6 @@ function IframeCanvas({ frame, selectedId = null, hoveredId = null, revision = 0
221
169
  doc.removeEventListener('dragleave', onDragLeave);
222
170
  };
223
171
  }, [doc, onDropOnNode]);
224
- // Recompute overlay rects when the iframe is resized (panel/device resize) or
225
- // its content reflows — otherwise the selection box keeps a stale size.
226
172
  react.useEffect(() => {
227
173
  const iframe = iframeRef.current;
228
174
  if (!iframe) {
@@ -235,30 +181,19 @@ function IframeCanvas({ frame, selectedId = null, hoveredId = null, revision = 0
235
181
  }
236
182
  return () => observer.disconnect();
237
183
  }, [doc]);
238
- // Drop-on-canvas indicator has no meaning while previewing.
239
184
  react.useEffect(() => {
240
185
  if (previewMode) {
241
186
  setDrop(null);
242
187
  }
243
188
  }, [previewMode]);
244
- const rectOf = (id) => {
245
- if (!doc || !id) {
246
- return null;
247
- }
248
- const el = doc.querySelector(`[data-nu-id="${id}"]`);
189
+ const elementFor = (id) => doc && id ? doc.querySelector(`[data-nu-id="${id}"]`) : null;
190
+ const rectOfEl = (el) => {
249
191
  if (!el) {
250
192
  return null;
251
193
  }
252
194
  const r = el.getBoundingClientRect();
253
195
  return { top: r.top, left: r.left, width: r.width, height: r.height };
254
196
  };
255
- const labelOf = (id) => {
256
- if (!doc || !id) {
257
- return null;
258
- }
259
- const el = doc.querySelector(`[data-nu-id="${id}"]`);
260
- return el ? el.tagName.toLowerCase() : null;
261
- };
262
197
  react.useLayoutEffect(() => {
263
198
  if (previewMode) {
264
199
  setSelected(null);
@@ -266,18 +201,14 @@ function IframeCanvas({ frame, selectedId = null, hoveredId = null, revision = 0
266
201
  setSelectedLabel(null);
267
202
  return;
268
203
  }
269
- setSelected(rectOf(selectedId));
270
- setHovered(hoveredId && hoveredId !== selectedId ? rectOf(hoveredId) : null);
271
- setSelectedLabel(labelOf(selectedId));
204
+ const selectedEl = elementFor(selectedId);
205
+ setSelected(rectOfEl(selectedEl));
206
+ setHovered(hoveredId && hoveredId !== selectedId ? rectOfEl(elementFor(hoveredId)) : null);
207
+ setSelectedLabel(selectedEl ? selectedEl.tagName.toLowerCase() : null);
272
208
  }, [previewMode, selectedId, hoveredId, revision, tick, doc]);
273
209
  return react.createElement('div', { className: containerClassName, style: { position: 'relative' } }, react.createElement('iframe', { ref: iframeRef, title: 'nuforge preview', className, srcDoc }, doc?.body
274
210
  ? reactDom.createPortal(react.createElement(CanvasWindowContext.Provider, { value: { doc, window: doc.defaultView } }, react.createElement(EditorFrame, { frame })), doc.body)
275
- : null),
276
- // The overlay is fully yours: omit `renderBox` for none, or build borders,
277
- // labels and action toolbars from the computed rects + selectedLabel. The
278
- // container is pointer-events:none; set pointerEvents:'auto' on interactive
279
- // bits (e.g. an action toolbar) so clicks land.
280
- renderBox && !previewMode
211
+ : null), renderBox && !previewMode
281
212
  ? react.createElement('div', {
282
213
  style: {
283
214
  position: 'absolute',
@@ -288,22 +219,6 @@ function IframeCanvas({ frame, selectedId = null, hoveredId = null, revision = 0
288
219
  }, renderBox({ selected, hovered, selectedLabel, drop }))
289
220
  : null);
290
221
  }
291
- /**
292
- * The `IframeCanvas` alternative for when a canvas needs its OWN genuine JS
293
- * realm — e.g. so a third-party library reading bare `window`/`document` at
294
- * module scope (an animation library's scroll-linked internals, a gesture
295
- * library, ...) resolves against the canvas's real globals instead of
296
- * silently binding to the host page's. A portal can't fix this (see
297
- * `useCanvasWindow`'s docs for that half of the problem) because it only
298
- * relocates DOM nodes, not the JS module graph building them.
299
- *
300
- * Renders a genuinely-`src`-navigated `<iframe>` — no `createPortal`, so
301
- * nothing here shares object identity with the canvas's own React tree. State
302
- * flows one-way, host -> canvas, over a `BroadcastChannel` as `t.flatten`'d
303
- * JSON (see `./canvas-protocol`); interaction (select/hover/drop/keydown/
304
- * link-click) relays back the other way as small typed messages. Pair with
305
- * `createCanvasReceiver` (`./canvas-client`) on the canvas route's own end.
306
- */
307
222
  function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hoveredId = null, revision = 0, previewMode = false, uiPayload, onSelect, onHover, onDropOnNode, onKeydownRelay, onLinkClick, className, containerClassName, renderBox, }) {
308
223
  const iframeRef = react.useRef(null);
309
224
  const [doc, setDoc] = react.useState(null);
@@ -332,10 +247,6 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
332
247
  onLinkClick,
333
248
  };
334
249
  const connectionRef = react.useRef(null);
335
- // A same-origin `src` navigation still allows the host to read the iframe's
336
- // own document for layout geometry — that's a plain DOM read, not blocked
337
- // by realm separation (only shared JS object identity is). Used purely to
338
- // size/position the overlay; no sync-protocol concern.
339
250
  react.useEffect(() => {
340
251
  const iframe = iframeRef.current;
341
252
  if (!iframe) {
@@ -419,14 +330,6 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
419
330
  mountGen = message.mountGen;
420
331
  ready = true;
421
332
  sendInit();
422
- // The `uiPayload` effect below only fires again when the payload
423
- // itself changes — on a fresh mount (or a remount after an error
424
- // boundary), that push happens before the canvas has attached its
425
- // listener (it hasn't navigated/mounted yet) and is silently lost,
426
- // since BroadcastChannel doesn't buffer for late listeners. Resend
427
- // the CURRENT payload here too, gated on the same readiness signal
428
- // as the state bootstrap, so theme/overrides never depend on some
429
- // unrelated prop changing later to "unstick" them.
430
333
  postUi(props.current.uiPayload);
431
334
  break;
432
335
  case 'select':
@@ -462,12 +365,7 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
462
365
  }
463
366
  connectionRef.current = null;
464
367
  };
465
- // `nu` is stable for the app's lifetime; `src` identifies the channel.
466
- // Everything else is read fresh off `props.current` inside the handlers.
467
368
  }, [src, nu]);
468
- // A page switch or preview-mode transition needs a fresh `init`, not just an
469
- // incremental `patch` — see `previewMode`'s doc above for why preview mode
470
- // specifically must never let a local-only mutation linger into edit mode.
471
369
  const resyncKey = `${activeComponent}:${previewMode}`;
472
370
  const lastResyncKey = react.useRef(null);
473
371
  react.useEffect(() => {
@@ -480,24 +378,14 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
480
378
  react.useEffect(() => {
481
379
  connectionRef.current?.postUi(uiPayload);
482
380
  }, [uiPayload]);
483
- const rectOf = (id) => {
484
- if (!doc || !id) {
485
- return null;
486
- }
487
- const el = doc.querySelector(`[data-nu-id="${id}"]`);
381
+ const elementFor = (id) => doc && id ? doc.querySelector(`[data-nu-id="${id}"]`) : null;
382
+ const rectOfEl = (el) => {
488
383
  if (!el) {
489
384
  return null;
490
385
  }
491
386
  const r = el.getBoundingClientRect();
492
387
  return { top: r.top, left: r.left, width: r.width, height: r.height };
493
388
  };
494
- const labelOf = (id) => {
495
- if (!doc || !id) {
496
- return null;
497
- }
498
- const el = doc.querySelector(`[data-nu-id="${id}"]`);
499
- return el ? el.tagName.toLowerCase() : null;
500
- };
501
389
  react.useLayoutEffect(() => {
502
390
  if (previewMode) {
503
391
  setSelected(null);
@@ -505,9 +393,10 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
505
393
  setSelectedLabel(null);
506
394
  return;
507
395
  }
508
- setSelected(rectOf(selectedId));
509
- setHovered(hoveredId && hoveredId !== selectedId ? rectOf(hoveredId) : null);
510
- setSelectedLabel(labelOf(selectedId));
396
+ const selectedEl = elementFor(selectedId);
397
+ setSelected(rectOfEl(selectedEl));
398
+ setHovered(hoveredId && hoveredId !== selectedId ? rectOfEl(elementFor(hoveredId)) : null);
399
+ setSelectedLabel(selectedEl ? selectedEl.tagName.toLowerCase() : null);
511
400
  }, [previewMode, selectedId, hoveredId, revision, tick, doc]);
512
401
  return react.createElement('div', { className: containerClassName, style: { position: 'relative' } }, react.createElement('iframe', {
513
402
  ref: iframeRef,
@@ -525,26 +414,16 @@ function RemoteCanvasHost({ nu, activeComponent, src, selectedId = null, hovered
525
414
  }, renderBox({ selected, hovered, selectedLabel }))
526
415
  : null);
527
416
  }
528
- // Structural param types (not the nominal `Editor` class), so these hooks accept
529
- // an Editor imported from the package's main entry without a dual-bundle clash.
530
- /** Re-render only when the tree shape changes (add/remove/move). */
531
417
  function useStructureRevision(editor) {
532
418
  const [rev, setRev] = react.useState(0);
533
419
  react.useEffect(() => editor.onStructureChange(() => setRev((r) => r + 1)), [editor]);
534
420
  return rev;
535
421
  }
536
- /** Re-render on every committed change (for undo state, inspectors, …). */
537
422
  function useEditorRevision(editor) {
538
423
  const [rev, setRev] = react.useState(0);
539
424
  react.useEffect(() => editor.onChange(() => setRev((r) => r + 1)), [editor]);
540
425
  return rev;
541
426
  }
542
- /**
543
- * A headless inspector renderer: builds a control per {@link PropSchemaField}
544
- * (text / number / checkbox / select / expression) so you don't hand-write a
545
- * form per block. Style via `className` and your own CSS; wire `onChange` to the
546
- * editor's mutation methods.
547
- */
548
427
  function PropFields({ fields, values, onChange, className, }) {
549
428
  return react.createElement('div', { className, 'data-nu-propfields': '' }, fields.map((field) => react.createElement(PropFieldControl, {
550
429
  key: field.name,
@@ -582,7 +461,6 @@ function PropFieldControl({ field, value, onChange, }) {
582
461
  }, (field.options ?? []).map((opt) => react.createElement('option', { key: opt, value: opt }, opt)));
583
462
  break;
584
463
  default:
585
- // 'string' | 'expression'
586
464
  control = react.createElement('input', {
587
465
  id,
588
466
  type: 'text',
@@ -603,4 +481,3 @@ exports.templateIdFromElement = templateIdFromElement;
603
481
  exports.useCanvasWindow = useCanvasWindow;
604
482
  exports.useEditorRevision = useEditorRevision;
605
483
  exports.useStructureRevision = useStructureRevision;
606
- //# sourceMappingURL=react.cjs.map
package/dist/react.d.ts CHANGED
@@ -3,7 +3,6 @@ import { ReactNode } from 'react';
3
3
  import * as t from '@nuforge/types';
4
4
  import { MutationEntry } from '@nuforge/reactivity';
5
5
 
6
- /** A control hint for auto-generating inspector fields from a block's props. */
7
6
  interface PropSchemaField {
8
7
  name: string;
9
8
  type: 'string' | 'number' | 'boolean' | 'expression' | 'enum';
@@ -11,11 +10,6 @@ interface PropSchemaField {
11
10
  default?: string | number | boolean;
12
11
  options?: string[];
13
12
  }
14
- /**
15
- * A reusable element/block in the palette. `create()` returns a fresh template
16
- * subtree to insert. `icon` is a name (not a component) so the registry stays
17
- * framework-agnostic; the UI maps the name to its own icon set.
18
- */
19
13
  interface Block {
20
14
  id: string;
21
15
  label: string;
@@ -33,20 +27,8 @@ declare class BlockRegistry {
33
27
  byCategory(): Record<string, Block[]>;
34
28
  }
35
29
 
36
- /** Where a dragged node is dropped relative to a target. */
37
30
  type DropPosition = 'before' | 'after' | 'inside';
38
31
 
39
- /**
40
- * Wire protocol for `RemoteCanvasHost` <-> `createCanvasReceiver`: a genuinely
41
- * navigated `/canvas/[id]`-style iframe has its own JS realm, so state and
42
- * interaction can't cross as direct object references the way `IframeCanvas`'s
43
- * portal allows — everything here is a structured-clone-safe plain message.
44
- *
45
- * Deliberately generic: only the shapes the editor primitive itself needs
46
- * (state sync, ready handshake, the core interaction events) are typed. Host
47
- * concerns like theme/preview-mode/overrides ride through `CanvasUiMessage`'s
48
- * opaque `payload` — this package relays it, never interprets it.
49
- */
50
32
  declare const CANVAS_CHANNEL = "nuforge-canvas";
51
33
  declare const CANVAS_PROTOCOL_VERSION = 1;
52
34
  interface CanvasKeydownMessage {
@@ -61,7 +43,6 @@ interface CanvasKeydownMessage {
61
43
  altKey: boolean;
62
44
  }
63
45
 
64
- /** A named action a UI can discover and bind to (toolbar buttons, shortcuts). */
65
46
  interface Command {
66
47
  id: string;
67
48
  label: string;
@@ -79,67 +60,37 @@ declare class CommandRegistry {
79
60
  }
80
61
 
81
62
  interface CreateEditorOptions {
82
- /** Extra blocks to add to the palette. */
83
63
  blocks?: Block[];
84
- /** Include the built-in DEFAULT_BLOCKS (default true). */
85
64
  defaultBlocks?: boolean;
86
65
  }
87
- /**
88
- * A high-level facade over a {@link Nu} runtime for building visual page
89
- * editors. Every mutation goes through `nu.change`, so it is transactional and
90
- * undoable. Reach for these instead of hand-splicing the AST.
91
- */
92
66
  declare class Editor {
93
67
  readonly nu: Nu;
94
68
  readonly blocks: BlockRegistry;
95
69
  readonly commands: CommandRegistry;
96
70
  constructor(nu: Nu, opts?: CreateEditorOptions);
97
- /** Append (or insert at `index`) `child` into `parent`'s children. */
98
71
  insertNode(parent: t.Template, child: t.Template, index?: number): void;
99
- /** Remove a node from its parent. Returns false if it has no parent. */
100
72
  removeNode(node: t.Template): boolean;
101
- /** Move `dragged` before/after/inside `target`. Refuses to nest into self. */
102
73
  moveNode(dragged: t.Template, target: t.Template, position: DropPosition): boolean;
103
74
  private clipboardNode;
104
- /** Put a node on the editor clipboard (a clone is made on paste). */
105
75
  copyNode(node: t.Template): void;
106
- /** Copy a node, then remove it. */
107
76
  cutNode(node: t.Template): boolean;
108
- /** Whether there is something on the clipboard to paste. */
109
77
  canPaste(): boolean;
110
- /**
111
- * Paste a fresh copy (new ids) of the clipboard node before/after/inside
112
- * `target`. Returns the inserted node, or null if nothing was pasted.
113
- */
114
78
  pasteNode(target: t.Template, position?: DropPosition): t.Template | null;
115
- /** Duplicate a node in place (inserted right after it). Returns the copy. */
116
79
  duplicateNode(node: t.Template): t.Template | null;
117
- /**
118
- * Insert a detached node before/after/inside a target (undoable). The core of
119
- * canvas drag-and-drop: a drop computes a target + position, then calls this.
120
- * Returns the inserted node.
121
- */
122
80
  insertNodeAt(node: t.Template, target: t.Template, position: DropPosition): t.Template | null;
123
- /** Create a block and insert it before/after/inside a target. */
124
81
  insertBlockAt(blockId: string, target: t.Template, position: DropPosition): t.Template | null;
125
82
  private insertCloneRelativeTo;
126
- /** Set several props at once. */
127
83
  updateProps(node: t.Template, props: Record<string, t.Expression>): void;
128
84
  setProp(node: t.Template, key: string, expr: t.Expression): void;
129
85
  removeProp(node: t.Template, key: string): void;
130
- /** Parse `source` as a nuforge expression and assign it to `key`. */
131
86
  setExpression(node: t.Template, key: string, source: string): boolean;
132
- /** Update a text node's literal value. */
133
87
  setText(node: t.Template, value: string): boolean;
134
88
  setTag(node: t.TagTemplate, tag: string): void;
135
89
  addClass(node: t.Template, name: string): void;
136
- /** Parse `source` and set the condition under which class `name` applies. */
137
90
  setClassCondition(node: t.Template, name: string, source: string): boolean;
138
91
  removeClass(node: t.Template, name: string): void;
139
- /** Set the `@if` condition from source. */
140
92
  setCondition(node: t.Template, source: string): boolean;
141
93
  clearCondition(node: t.Template): void;
142
- /** Repeat a node over `iterator` (a list expression), binding each `alias`. */
143
94
  setEach(node: t.Template, iterator: string, alias?: string, indexName?: string): boolean;
144
95
  clearEach(node: t.Template): void;
145
96
  addState(component: t.NuComponent, name?: string, init?: string): t.Val;
@@ -148,12 +99,9 @@ declare class Editor {
148
99
  addComponentProp(component: t.NuComponent, name?: string, init?: string): t.ComponentProp;
149
100
  removeComponentProp(component: t.NuComponent, prop: t.ComponentProp): void;
150
101
  setComponentPropInit(prop: t.ComponentProp, source: string): boolean;
151
- /** Create a block's template and insert it into `parent`. */
152
102
  insertBlock(blockId: string, parent: t.Template, index?: number): t.Template | null;
153
103
  private uniqueName;
154
- /** Add a new component (a "page"). */
155
104
  addComponent(name?: string): t.NuComponent;
156
- /** Rename a component, updating `<Name/>` references across the program. */
157
105
  renameComponent(component: t.NuComponent, name: string): boolean;
158
106
  removeComponent(component: t.NuComponent): boolean;
159
107
  duplicateComponent(component: t.NuComponent): t.NuComponent;
@@ -164,15 +112,11 @@ declare class Editor {
164
112
  getChildren(node: t.Template): t.Template[];
165
113
  getSiblings(node: t.Template): t.Template[];
166
114
  getNodeIndex(node: t.Template): number;
167
- /** Full path from the State root to this node (for selections/serialization). */
168
115
  getNodePath(node: t.Type): Array<string | number>;
169
- /** Fires with the raw field-level writes of every change (incl. undo/redo). */
170
116
  onChange(cb: (entries: MutationEntry[]) => void): () => void;
171
- /** Fires only when the tree shape changes (a child array was mutated). */
172
117
  onStructureChange(cb: () => void): () => void;
173
118
  private spliceFromParent;
174
119
  private applyMove;
175
- /** Insert an already-detached node relative to `target` (no removal). */
176
120
  private insertRelativeTo;
177
121
  }
178
122
 
@@ -183,76 +127,31 @@ interface Rect {
183
127
  height: number;
184
128
  }
185
129
  interface CanvasWindow {
186
- /** The canvas's own document — the iframe's, inside `IframeCanvas`. */
187
130
  doc: Document | null;
188
- /** The canvas's own window — the iframe's, inside `IframeCanvas`. */
189
131
  window: Window | null;
190
132
  }
191
- /**
192
- * The real `window`/`document` a canvas-rendered element visually lives in.
193
- * `IframeCanvas` portals its content into an `<iframe>`, but a portal only
194
- * relocates DOM nodes — the JS building them still runs in the host page's
195
- * realm. Any library that reads a bare `window`/`document` global (scroll
196
- * listeners, `matchMedia`, `ResizeObserver`/`IntersectionObserver` defaults)
197
- * silently ends up watching the HOST page, not the iframe the user actually
198
- * sees and scrolls. Build refs/options from `useCanvasWindow()` instead of the
199
- * ambient global so they resolve to the right realm — this works identically
200
- * outside `IframeCanvas` too (e.g. `NuFrame`'s direct, non-iframe render),
201
- * where it's just the ambient `window`/`document`.
202
- */
203
133
  declare function useCanvasWindow(): CanvasWindow;
204
- /** Reactive renderer for a frame, decorating each element with `data-nu-id`. */
205
134
  declare const EditorFrame: (props: {
206
135
  frame: Frame;
207
136
  }) => ReactNode;
208
- /** Map a DOM node (or event target) back to its template id. */
209
137
  declare function templateIdFromElement(target: EventTarget | null): string | null;
210
- /** Sensible default styling for the isolated canvas document. */
211
138
  declare const DEFAULT_CANVAS_CSS = "\n *, *::before, *::after { box-sizing: border-box; }\n html, body { margin: 0; }\n body { font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; color: #111; line-height: 1.5; padding: 28px; }\n h1 { font-size: 2rem; font-weight: 700; margin: .5em 0; }\n h2 { font-size: 1.5rem; font-weight: 600; margin: .6em 0; }\n h3 { font-size: 1.2rem; font-weight: 600; margin: .7em 0; }\n p { margin: .8em 0; }\n ul { list-style: disc; padding-left: 1.5em; margin: .8em 0; }\n ol { list-style: decimal; padding-left: 1.5em; margin: .8em 0; }\n a { color: #2563eb; text-decoration: underline; }\n button { border: 1px solid #d4d4d8; border-radius: 6px; padding: 4px 12px; background: #fafafa; cursor: pointer; font: inherit; }\n button:hover { background: #f4f4f5; }\n input, textarea, select { border: 1px solid #d4d4d8; border-radius: 6px; padding: 4px 8px; font: inherit; }\n section { display: flex; align-items: center; gap: .75rem; margin: .8em 0; }\n img { max-width: 100%; }\n [data-nu-id] { cursor: default; }\n";
212
139
  interface IframeCanvasProps {
213
140
  frame: Frame;
214
141
  selectedId?: string | null;
215
142
  hoveredId?: string | null;
216
- /** Bump to force the overlay to recompute (e.g. on every committed change). */
217
143
  revision?: number;
218
- /**
219
- * Turn the canvas into a live, interactive preview. When `true`, the editing
220
- * affordances are switched off: clicks and hovers pass through to the real
221
- * elements (buttons fire, links work, inputs focus), drag-and-drop editing is
222
- * disabled, and no overlay is drawn. When `false` (the default) the canvas is
223
- * in edit mode — clicks select instead of activating, hover highlights, and
224
- * `onDropOnNode` / `renderBox` are live. The same reactive `frame` is shown in
225
- * both modes, so toggling is instant and state is preserved.
226
- */
227
144
  previewMode?: boolean;
228
145
  onSelect?: (id: string | null) => void;
229
146
  onHover?: (id: string | null) => void;
230
- /**
231
- * Enable drop-on-canvas: fired when something is dropped onto a node. Compute
232
- * a block to insert and call `editor.insertBlockAt(blockId, target, position)`.
233
- */
234
147
  onDropOnNode?: (targetId: string, position: DropPosition, event: DragEvent) => void;
235
- /** CSS injected into the isolated document (defaults to DEFAULT_CANVAS_CSS). */
236
148
  baseCss?: string;
237
- /**
238
- * Initial HTML for the iframe document — put anything in the `<head>` (CDN
239
- * stylesheets, web fonts, `<script>` tags, a `<base>`, meta). The frame is
240
- * portaled into its `<body>`. `baseCss` is still appended on top; pass
241
- * `baseCss=""` to fully control styling from here.
242
- */
243
149
  srcDoc?: string;
244
150
  className?: string;
245
151
  containerClassName?: string;
246
- /**
247
- * Build the overlay from the computed selection / hover / drop rects and the
248
- * selected element's tag. Omit it for no overlay. The overlay container is
249
- * pointer-events:none — set `pointerEvents: 'auto'` on interactive bits (e.g.
250
- * an action toolbar) so they receive clicks.
251
- */
252
152
  renderBox?: (rects: {
253
153
  selected: Rect | null;
254
154
  hovered: Rect | null;
255
- /** Tag name of the selected element, e.g. `"div"`. */
256
155
  selectedLabel: string | null;
257
156
  drop: {
258
157
  rect: Rect;
@@ -260,41 +159,15 @@ interface IframeCanvasProps {
260
159
  } | null;
261
160
  }) => ReactNode;
262
161
  }
263
- /**
264
- * A style-isolated editor canvas: the frame is portaled into an `<iframe>` so
265
- * the host's CSS can't leak into the preview (and vice-versa). Selection/hover
266
- * use native capture listeners on the iframe document, so element handlers stay
267
- * inert while editing. Selection/hover rects are computed (in a layout effect,
268
- * after the iframe commits) and handed to `renderBox` for the overlay.
269
- */
270
162
  declare function IframeCanvas({ frame, selectedId, hoveredId, revision, previewMode, onSelect, onHover, onDropOnNode, baseCss, srcDoc, className, containerClassName, renderBox, }: IframeCanvasProps): ReactNode;
271
163
  interface RemoteCanvasHostProps {
272
- /** The host's `Nu` instance — flattened and pushed to the canvas realm on
273
- * every committed change. Never referenced directly by the canvas realm: a
274
- * real `src` navigation puts it in a separate JS realm with no shared
275
- * object identity, unlike `IframeCanvas`'s portal. */
276
164
  nu: Nu;
277
- /** Which component the canvas should render (`Nu.createFrame`'s `component.name`). */
278
165
  activeComponent: string;
279
- /** The canvas route's URL, e.g. `/canvas/my-project`. The `BroadcastChannel`
280
- * name is derived from it, so `createCanvasReceiver` on the other end just
281
- * needs the SAME `src` — no separate id to keep in sync. */
282
166
  src: string;
283
167
  selectedId?: string | null;
284
168
  hoveredId?: string | null;
285
- /** Bump to force the overlay to recompute (e.g. on every committed change). */
286
169
  revision?: number;
287
- /**
288
- * Mirrors `IframeCanvas`'s `previewMode`: when `true`, the canvas is a live,
289
- * interactive preview (its own DSL `onClick` handlers run against the
290
- * canvas's local `Nu`, since that instance is genuinely loaded and mutable —
291
- * see `createCanvasReceiver`'s docs). Toggling this (either direction) forces
292
- * a fresh `init` instead of an incremental `patch`, so any preview-only
293
- * local mutation never lingers once you're back in edit mode.
294
- */
295
170
  previewMode?: boolean;
296
- /** Opaque host-defined payload (theme CSS, hide/lock overrides, ...) pushed
297
- * to the canvas via a `ui` message whenever this reference changes. */
298
171
  uiPayload?: unknown;
299
172
  onSelect?: (id: string | null) => void;
300
173
  onHover?: (id: string | null) => void;
@@ -303,59 +176,22 @@ interface RemoteCanvasHostProps {
303
176
  onLinkClick?: (href: string) => void;
304
177
  className?: string;
305
178
  containerClassName?: string;
306
- /**
307
- * Build the overlay from the computed selection / hover rects and the
308
- * selected element's tag — same contract as `IframeCanvas`'s `renderBox`
309
- * (no `drop` rect: the canvas realm renders its own drop indicator locally,
310
- * since it already has the DOM geometry and relaying it every `dragover`
311
- * tick would add a message-per-frame the local realm doesn't need). Reading
312
- * these rects is a same-origin DOM read (`iframe.contentDocument`), which a
313
- * real `src` navigation doesn't block — only shared JS object identity is
314
- * blocked, not DOM geometry reads.
315
- */
316
179
  renderBox?: (rects: {
317
180
  selected: Rect | null;
318
181
  hovered: Rect | null;
319
182
  selectedLabel: string | null;
320
183
  }) => ReactNode;
321
184
  }
322
- /**
323
- * The `IframeCanvas` alternative for when a canvas needs its OWN genuine JS
324
- * realm — e.g. so a third-party library reading bare `window`/`document` at
325
- * module scope (an animation library's scroll-linked internals, a gesture
326
- * library, ...) resolves against the canvas's real globals instead of
327
- * silently binding to the host page's. A portal can't fix this (see
328
- * `useCanvasWindow`'s docs for that half of the problem) because it only
329
- * relocates DOM nodes, not the JS module graph building them.
330
- *
331
- * Renders a genuinely-`src`-navigated `<iframe>` — no `createPortal`, so
332
- * nothing here shares object identity with the canvas's own React tree. State
333
- * flows one-way, host -> canvas, over a `BroadcastChannel` as `t.flatten`'d
334
- * JSON (see `./canvas-protocol`); interaction (select/hover/drop/keydown/
335
- * link-click) relays back the other way as small typed messages. Pair with
336
- * `createCanvasReceiver` (`./canvas-client`) on the canvas route's own end.
337
- */
338
185
  declare function RemoteCanvasHost({ nu, activeComponent, src, selectedId, hoveredId, revision, previewMode, uiPayload, onSelect, onHover, onDropOnNode, onKeydownRelay, onLinkClick, className, containerClassName, renderBox, }: RemoteCanvasHostProps): ReactNode;
339
- /** Re-render only when the tree shape changes (add/remove/move). */
340
186
  declare function useStructureRevision(editor: Pick<Editor, 'onStructureChange'>): number;
341
- /** Re-render on every committed change (for undo state, inspectors, …). */
342
187
  declare function useEditorRevision(editor: Pick<Editor, 'onChange'>): number;
343
188
  type PropFieldValue = string | number | boolean | undefined;
344
189
  interface PropFieldsProps {
345
- /** The control schema (e.g. a block's `props`). */
346
190
  fields: PropSchemaField[];
347
- /** Current value per field name. */
348
191
  values: Record<string, PropFieldValue>;
349
- /** Called when a control changes. Wire to `editor.setProp`/`setExpression`. */
350
192
  onChange: (name: string, value: string | number | boolean) => void;
351
193
  className?: string;
352
194
  }
353
- /**
354
- * A headless inspector renderer: builds a control per {@link PropSchemaField}
355
- * (text / number / checkbox / select / expression) so you don't hand-write a
356
- * form per block. Style via `className` and your own CSS; wire `onChange` to the
357
- * editor's mutation methods.
358
- */
359
195
  declare function PropFields({ fields, values, onChange, className, }: PropFieldsProps): ReactNode;
360
196
 
361
197
  export { DEFAULT_CANVAS_CSS, EditorFrame, IframeCanvas, PropFields, RemoteCanvasHost, templateIdFromElement, useCanvasWindow, useEditorRevision, useStructureRevision };