@nuforge/editor 0.1.3 → 0.3.0

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
@@ -1,7 +1,8 @@
1
1
  import * as t from '@nuforge/types';
2
- import { Nu } from '@nuforge/core';
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. */
5
6
  interface PropSchemaField {
6
7
  name: string;
7
8
  type: 'string' | 'number' | 'boolean' | 'expression' | 'enum';
@@ -9,6 +10,11 @@ interface PropSchemaField {
9
10
  default?: string | number | boolean;
10
11
  options?: string[];
11
12
  }
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
+ */
12
18
  interface Block {
13
19
  id: string;
14
20
  label: string;
@@ -26,58 +32,293 @@ declare class BlockRegistry {
26
32
  all(): Block[];
27
33
  byCategory(): Record<string, Block[]>;
28
34
  }
35
+ /** The built-in palette — the common HTML primitives. */
29
36
  declare const DEFAULT_BLOCKS: Block[];
30
37
 
38
+ /** A literal expression (`"x"`, `5`, `true`). */
31
39
  declare function lit(value: string | number | boolean): t.Literal;
40
+ /** A text node: `<text text={value} />`, rendered as its string value. */
32
41
  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
+ */
33
46
  declare function el(tag: string, props?: Record<string, t.Expression>, children?: t.Template[]): t.TagTemplate;
34
47
 
48
+ /** Where a dragged node is dropped relative to a target. */
35
49
  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
+ */
36
57
  declare function dropPositionFromPointer(clientY: number, rect: {
37
58
  top: number;
38
59
  height: number;
39
60
  }, opts?: {
40
61
  allowInside?: boolean;
41
62
  }): DropPosition;
63
+ /** A human label for a template node (tag name, component name, slot, …). */
42
64
  declare function templateLabel(node: t.Template): string;
65
+ /** Child templates of a node (empty for leaves / text nodes). */
43
66
  declare function templateChildren(node: t.Template): t.Template[];
67
+ /** Is this a text node (`<text text=…>`), rendered as its string value? */
44
68
  declare function isTextTemplate(node: t.Template): boolean;
69
+ /** Preview the content of a text node, e.g. for a layers tree. */
45
70
  declare function textPreview(node: t.Template): string | null;
71
+ /** Can this node hold appended children? */
46
72
  declare function canHaveChildren(node: t.Template | null): boolean;
73
+ /** Is `ancestor` somewhere above `node` in the tree? */
47
74
  declare function isAncestorOf(nu: Nu, ancestor: t.Template, node: t.Template): boolean;
48
75
 
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
+ declare const CANVAS_CHANNEL = "nuforge-canvas";
88
+ 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
+ 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
+ interface FlattenedState {
95
+ root: {
96
+ $$typeId: string;
97
+ };
98
+ types: Record<string, {
99
+ type: string;
100
+ [key: string]: unknown;
101
+ }>;
102
+ }
103
+ interface CanvasInitMessage {
104
+ channel: typeof CANVAS_CHANNEL;
105
+ v: typeof CANVAS_PROTOCOL_VERSION;
106
+ 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
+ activeComponent: string;
111
+ flat: FlattenedState;
112
+ }
113
+ interface CanvasPatchMessage {
114
+ channel: typeof CANVAS_CHANNEL;
115
+ v: typeof CANVAS_PROTOCOL_VERSION;
116
+ type: 'patch';
117
+ activeComponent: string;
118
+ flat: FlattenedState;
119
+ rev: number;
120
+ }
121
+ /** Host-defined payload (theme CSS, preview-mode toggle, hide/lock overrides,
122
+ * ...) — nucode doesn't interpret this, just relays it verbatim. */
123
+ interface CanvasUiMessage {
124
+ channel: typeof CANVAS_CHANNEL;
125
+ v: typeof CANVAS_PROTOCOL_VERSION;
126
+ type: 'ui';
127
+ payload: unknown;
128
+ }
129
+ type CanvasHostMessage = CanvasInitMessage | CanvasPatchMessage | CanvasUiMessage;
130
+ interface CanvasReadyMessage {
131
+ channel: typeof CANVAS_CHANNEL;
132
+ v: typeof CANVAS_PROTOCOL_VERSION;
133
+ 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
+ mountGen: number;
138
+ }
139
+ interface CanvasSelectMessage {
140
+ channel: typeof CANVAS_CHANNEL;
141
+ v: typeof CANVAS_PROTOCOL_VERSION;
142
+ type: 'select';
143
+ nodeId: string | null;
144
+ }
145
+ interface CanvasHoverMessage {
146
+ channel: typeof CANVAS_CHANNEL;
147
+ v: typeof CANVAS_PROTOCOL_VERSION;
148
+ type: 'hover';
149
+ nodeId: string | null;
150
+ }
151
+ interface CanvasDropMessage {
152
+ channel: typeof CANVAS_CHANNEL;
153
+ v: typeof CANVAS_PROTOCOL_VERSION;
154
+ type: 'drop';
155
+ targetId: string;
156
+ position: DropPosition;
157
+ }
158
+ interface CanvasKeydownMessage {
159
+ channel: typeof CANVAS_CHANNEL;
160
+ v: typeof CANVAS_PROTOCOL_VERSION;
161
+ type: 'keydown';
162
+ key: string;
163
+ code: string;
164
+ ctrlKey: boolean;
165
+ metaKey: boolean;
166
+ shiftKey: boolean;
167
+ altKey: boolean;
168
+ }
169
+ interface CanvasLinkClickMessage {
170
+ channel: typeof CANVAS_CHANNEL;
171
+ v: typeof CANVAS_PROTOCOL_VERSION;
172
+ type: 'link-click';
173
+ href: string;
174
+ }
175
+ type CanvasClientMessage = CanvasReadyMessage | CanvasSelectMessage | CanvasHoverMessage | CanvasDropMessage | CanvasKeydownMessage | CanvasLinkClickMessage;
176
+ 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
+ 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
+ declare function isCanvasMessage(data: unknown): data is CanvasMessage;
186
+
187
+ 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
+ src: string;
191
+ externals?: NuExternalsFactory;
192
+ }
193
+ 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
+ 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
+ volatile: {
211
+ 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
+ loadGeneration: number;
236
+ };
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
+ 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
+ onUi(cb: (payload: unknown) => void): () => void;
245
+ /** Send an interaction event up to the host (select/hover/drop/keydown/link-click). */
246
+ postEvent(message: DistributiveOmit<CanvasClientMessage, 'channel' | 'v'>): void;
247
+ dispose(): void;
248
+ }
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
+ declare function createCanvasReceiver(opts: CanvasReceiverOpts): CanvasReceiver;
259
+
49
260
  interface CreateEditorOptions {
261
+ /** Extra blocks to add to the palette. */
50
262
  blocks?: Block[];
263
+ /** Include the built-in DEFAULT_BLOCKS (default true). */
51
264
  defaultBlocks?: boolean;
52
265
  }
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
+ */
53
271
  declare class Editor {
54
272
  readonly nu: Nu;
55
273
  readonly blocks: BlockRegistry;
56
274
  readonly commands: CommandRegistry;
57
275
  constructor(nu: Nu, opts?: CreateEditorOptions);
276
+ /** Append (or insert at `index`) `child` into `parent`'s children. */
58
277
  insertNode(parent: t.Template, child: t.Template, index?: number): void;
278
+ /** Remove a node from its parent. Returns false if it has no parent. */
59
279
  removeNode(node: t.Template): boolean;
280
+ /** Move `dragged` before/after/inside `target`. Refuses to nest into self. */
60
281
  moveNode(dragged: t.Template, target: t.Template, position: DropPosition): boolean;
61
282
  private clipboardNode;
283
+ /** Put a node on the editor clipboard (a clone is made on paste). */
62
284
  copyNode(node: t.Template): void;
285
+ /** Copy a node, then remove it. */
63
286
  cutNode(node: t.Template): boolean;
287
+ /** Whether there is something on the clipboard to paste. */
64
288
  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
+ */
65
293
  pasteNode(target: t.Template, position?: DropPosition): t.Template | null;
294
+ /** Duplicate a node in place (inserted right after it). Returns the copy. */
66
295
  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
+ */
67
301
  insertNodeAt(node: t.Template, target: t.Template, position: DropPosition): t.Template | null;
302
+ /** Create a block and insert it before/after/inside a target. */
68
303
  insertBlockAt(blockId: string, target: t.Template, position: DropPosition): t.Template | null;
69
304
  private insertCloneRelativeTo;
305
+ /** Set several props at once. */
70
306
  updateProps(node: t.Template, props: Record<string, t.Expression>): void;
71
307
  setProp(node: t.Template, key: string, expr: t.Expression): void;
72
308
  removeProp(node: t.Template, key: string): void;
309
+ /** Parse `source` as a nuforge expression and assign it to `key`. */
73
310
  setExpression(node: t.Template, key: string, source: string): boolean;
311
+ /** Update a text node's literal value. */
74
312
  setText(node: t.Template, value: string): boolean;
75
313
  setTag(node: t.TagTemplate, tag: string): void;
76
314
  addClass(node: t.Template, name: string): void;
315
+ /** Parse `source` and set the condition under which class `name` applies. */
77
316
  setClassCondition(node: t.Template, name: string, source: string): boolean;
78
317
  removeClass(node: t.Template, name: string): void;
318
+ /** Set the `@if` condition from source. */
79
319
  setCondition(node: t.Template, source: string): boolean;
80
320
  clearCondition(node: t.Template): void;
321
+ /** Repeat a node over `iterator` (a list expression), binding each `alias`. */
81
322
  setEach(node: t.Template, iterator: string, alias?: string, indexName?: string): boolean;
82
323
  clearEach(node: t.Template): void;
83
324
  addState(component: t.NuComponent, name?: string, init?: string): t.Val;
@@ -86,9 +327,12 @@ declare class Editor {
86
327
  addComponentProp(component: t.NuComponent, name?: string, init?: string): t.ComponentProp;
87
328
  removeComponentProp(component: t.NuComponent, prop: t.ComponentProp): void;
88
329
  setComponentPropInit(prop: t.ComponentProp, source: string): boolean;
330
+ /** Create a block's template and insert it into `parent`. */
89
331
  insertBlock(blockId: string, parent: t.Template, index?: number): t.Template | null;
90
332
  private uniqueName;
333
+ /** Add a new component (a "page"). */
91
334
  addComponent(name?: string): t.NuComponent;
335
+ /** Rename a component, updating `<Name/>` references across the program. */
92
336
  renameComponent(component: t.NuComponent, name: string): boolean;
93
337
  removeComponent(component: t.NuComponent): boolean;
94
338
  duplicateComponent(component: t.NuComponent): t.NuComponent;
@@ -99,15 +343,20 @@ declare class Editor {
99
343
  getChildren(node: t.Template): t.Template[];
100
344
  getSiblings(node: t.Template): t.Template[];
101
345
  getNodeIndex(node: t.Template): number;
346
+ /** Full path from the State root to this node (for selections/serialization). */
102
347
  getNodePath(node: t.Type): Array<string | number>;
348
+ /** Fires with the raw field-level writes of every change (incl. undo/redo). */
103
349
  onChange(cb: (entries: MutationEntry[]) => void): () => void;
350
+ /** Fires only when the tree shape changes (a child array was mutated). */
104
351
  onStructureChange(cb: () => void): () => void;
105
352
  private spliceFromParent;
106
353
  private applyMove;
354
+ /** Insert an already-detached node relative to `target` (no removal). */
107
355
  private insertRelativeTo;
108
356
  }
109
357
  declare function createEditor(nu: Nu, opts?: CreateEditorOptions): Editor;
110
358
 
359
+ /** A named action a UI can discover and bind to (toolbar buttons, shortcuts). */
111
360
  interface Command {
112
361
  id: string;
113
362
  label: string;
@@ -124,5 +373,5 @@ declare class CommandRegistry {
124
373
  run(id: string): void;
125
374
  }
126
375
 
127
- export { BlockRegistry, CommandRegistry, DEFAULT_BLOCKS, Editor, canHaveChildren, createEditor, defineBlock, dropPositionFromPointer, el, isAncestorOf, isTextTemplate, lit, templateChildren, templateLabel, text, textPreview };
128
- export type { Block, Command, CreateEditorOptions, DropPosition, PropSchemaField };
376
+ 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 };
377
+ export type { Block, CanvasClientMessage, CanvasDropMessage, CanvasHostMessage, CanvasHoverMessage, CanvasInitMessage, CanvasKeydownMessage, CanvasLinkClickMessage, CanvasMessage, CanvasPatchMessage, CanvasReadyMessage, CanvasReceiver, CanvasReceiverOpts, CanvasSelectMessage, CanvasUiMessage, Command, CreateEditorOptions, DistributiveOmit, DropPosition, FlattenedState, PropSchemaField };