@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.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;