@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/react.d.ts CHANGED
@@ -3,6 +3,7 @@ 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. */
6
7
  interface PropSchemaField {
7
8
  name: string;
8
9
  type: 'string' | 'number' | 'boolean' | 'expression' | 'enum';
@@ -10,6 +11,11 @@ interface PropSchemaField {
10
11
  default?: string | number | boolean;
11
12
  options?: string[];
12
13
  }
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
+ */
13
19
  interface Block {
14
20
  id: string;
15
21
  label: string;
@@ -27,6 +33,35 @@ declare class BlockRegistry {
27
33
  byCategory(): Record<string, Block[]>;
28
34
  }
29
35
 
36
+ /** Where a dragged node is dropped relative to a target. */
37
+ type DropPosition = 'before' | 'after' | 'inside';
38
+
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
+ declare const CANVAS_CHANNEL = "nuforge-canvas";
51
+ declare const CANVAS_PROTOCOL_VERSION = 1;
52
+ interface CanvasKeydownMessage {
53
+ channel: typeof CANVAS_CHANNEL;
54
+ v: typeof CANVAS_PROTOCOL_VERSION;
55
+ type: 'keydown';
56
+ key: string;
57
+ code: string;
58
+ ctrlKey: boolean;
59
+ metaKey: boolean;
60
+ shiftKey: boolean;
61
+ altKey: boolean;
62
+ }
63
+
64
+ /** A named action a UI can discover and bind to (toolbar buttons, shortcuts). */
30
65
  interface Command {
31
66
  id: string;
32
67
  label: string;
@@ -43,40 +78,68 @@ declare class CommandRegistry {
43
78
  run(id: string): void;
44
79
  }
45
80
 
46
- type DropPosition = 'before' | 'after' | 'inside';
47
-
48
81
  interface CreateEditorOptions {
82
+ /** Extra blocks to add to the palette. */
49
83
  blocks?: Block[];
84
+ /** Include the built-in DEFAULT_BLOCKS (default true). */
50
85
  defaultBlocks?: boolean;
51
86
  }
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
+ */
52
92
  declare class Editor {
53
93
  readonly nu: Nu;
54
94
  readonly blocks: BlockRegistry;
55
95
  readonly commands: CommandRegistry;
56
96
  constructor(nu: Nu, opts?: CreateEditorOptions);
97
+ /** Append (or insert at `index`) `child` into `parent`'s children. */
57
98
  insertNode(parent: t.Template, child: t.Template, index?: number): void;
99
+ /** Remove a node from its parent. Returns false if it has no parent. */
58
100
  removeNode(node: t.Template): boolean;
101
+ /** Move `dragged` before/after/inside `target`. Refuses to nest into self. */
59
102
  moveNode(dragged: t.Template, target: t.Template, position: DropPosition): boolean;
60
103
  private clipboardNode;
104
+ /** Put a node on the editor clipboard (a clone is made on paste). */
61
105
  copyNode(node: t.Template): void;
106
+ /** Copy a node, then remove it. */
62
107
  cutNode(node: t.Template): boolean;
108
+ /** Whether there is something on the clipboard to paste. */
63
109
  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
+ */
64
114
  pasteNode(target: t.Template, position?: DropPosition): t.Template | null;
115
+ /** Duplicate a node in place (inserted right after it). Returns the copy. */
65
116
  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
+ */
66
122
  insertNodeAt(node: t.Template, target: t.Template, position: DropPosition): t.Template | null;
123
+ /** Create a block and insert it before/after/inside a target. */
67
124
  insertBlockAt(blockId: string, target: t.Template, position: DropPosition): t.Template | null;
68
125
  private insertCloneRelativeTo;
126
+ /** Set several props at once. */
69
127
  updateProps(node: t.Template, props: Record<string, t.Expression>): void;
70
128
  setProp(node: t.Template, key: string, expr: t.Expression): void;
71
129
  removeProp(node: t.Template, key: string): void;
130
+ /** Parse `source` as a nuforge expression and assign it to `key`. */
72
131
  setExpression(node: t.Template, key: string, source: string): boolean;
132
+ /** Update a text node's literal value. */
73
133
  setText(node: t.Template, value: string): boolean;
74
134
  setTag(node: t.TagTemplate, tag: string): void;
75
135
  addClass(node: t.Template, name: string): void;
136
+ /** Parse `source` and set the condition under which class `name` applies. */
76
137
  setClassCondition(node: t.Template, name: string, source: string): boolean;
77
138
  removeClass(node: t.Template, name: string): void;
139
+ /** Set the `@if` condition from source. */
78
140
  setCondition(node: t.Template, source: string): boolean;
79
141
  clearCondition(node: t.Template): void;
142
+ /** Repeat a node over `iterator` (a list expression), binding each `alias`. */
80
143
  setEach(node: t.Template, iterator: string, alias?: string, indexName?: string): boolean;
81
144
  clearEach(node: t.Template): void;
82
145
  addState(component: t.NuComponent, name?: string, init?: string): t.Val;
@@ -85,9 +148,12 @@ declare class Editor {
85
148
  addComponentProp(component: t.NuComponent, name?: string, init?: string): t.ComponentProp;
86
149
  removeComponentProp(component: t.NuComponent, prop: t.ComponentProp): void;
87
150
  setComponentPropInit(prop: t.ComponentProp, source: string): boolean;
151
+ /** Create a block's template and insert it into `parent`. */
88
152
  insertBlock(blockId: string, parent: t.Template, index?: number): t.Template | null;
89
153
  private uniqueName;
154
+ /** Add a new component (a "page"). */
90
155
  addComponent(name?: string): t.NuComponent;
156
+ /** Rename a component, updating `<Name/>` references across the program. */
91
157
  renameComponent(component: t.NuComponent, name: string): boolean;
92
158
  removeComponent(component: t.NuComponent): boolean;
93
159
  duplicateComponent(component: t.NuComponent): t.NuComponent;
@@ -98,11 +164,15 @@ declare class Editor {
98
164
  getChildren(node: t.Template): t.Template[];
99
165
  getSiblings(node: t.Template): t.Template[];
100
166
  getNodeIndex(node: t.Template): number;
167
+ /** Full path from the State root to this node (for selections/serialization). */
101
168
  getNodePath(node: t.Type): Array<string | number>;
169
+ /** Fires with the raw field-level writes of every change (incl. undo/redo). */
102
170
  onChange(cb: (entries: MutationEntry[]) => void): () => void;
171
+ /** Fires only when the tree shape changes (a child array was mutated). */
103
172
  onStructureChange(cb: () => void): () => void;
104
173
  private spliceFromParent;
105
174
  private applyMove;
175
+ /** Insert an already-detached node relative to `target` (no removal). */
106
176
  private insertRelativeTo;
107
177
  }
108
178
 
@@ -112,27 +182,77 @@ interface Rect {
112
182
  width: number;
113
183
  height: number;
114
184
  }
185
+ interface CanvasWindow {
186
+ /** The canvas's own document — the iframe's, inside `IframeCanvas`. */
187
+ doc: Document | null;
188
+ /** The canvas's own window — the iframe's, inside `IframeCanvas`. */
189
+ window: Window | null;
190
+ }
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
+ declare function useCanvasWindow(): CanvasWindow;
204
+ /** Reactive renderer for a frame, decorating each element with `data-nu-id`. */
115
205
  declare const EditorFrame: (props: {
116
206
  frame: Frame;
117
207
  }) => ReactNode;
208
+ /** Map a DOM node (or event target) back to its template id. */
118
209
  declare function templateIdFromElement(target: EventTarget | null): string | null;
210
+ /** Sensible default styling for the isolated canvas document. */
119
211
  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";
120
212
  interface IframeCanvasProps {
121
213
  frame: Frame;
122
214
  selectedId?: string | null;
123
215
  hoveredId?: string | null;
216
+ /** Bump to force the overlay to recompute (e.g. on every committed change). */
124
217
  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
+ */
125
227
  previewMode?: boolean;
126
228
  onSelect?: (id: string | null) => void;
127
229
  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
+ */
128
234
  onDropOnNode?: (targetId: string, position: DropPosition, event: DragEvent) => void;
235
+ /** CSS injected into the isolated document (defaults to DEFAULT_CANVAS_CSS). */
129
236
  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
+ */
130
243
  srcDoc?: string;
131
244
  className?: string;
132
245
  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
+ */
133
252
  renderBox?: (rects: {
134
253
  selected: Rect | null;
135
254
  hovered: Rect | null;
255
+ /** Tag name of the selected element, e.g. `"div"`. */
136
256
  selectedLabel: string | null;
137
257
  drop: {
138
258
  rect: Rect;
@@ -140,17 +260,103 @@ interface IframeCanvasProps {
140
260
  } | null;
141
261
  }) => ReactNode;
142
262
  }
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
+ */
143
270
  declare function IframeCanvas({ frame, selectedId, hoveredId, revision, previewMode, onSelect, onHover, onDropOnNode, baseCss, srcDoc, className, containerClassName, renderBox, }: IframeCanvasProps): ReactNode;
271
+ 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
+ nu: Nu;
277
+ /** Which component the canvas should render (`Nu.createFrame`'s `component.name`). */
278
+ 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
+ src: string;
283
+ selectedId?: string | null;
284
+ hoveredId?: string | null;
285
+ /** Bump to force the overlay to recompute (e.g. on every committed change). */
286
+ 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
+ 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
+ uiPayload?: unknown;
299
+ onSelect?: (id: string | null) => void;
300
+ onHover?: (id: string | null) => void;
301
+ onDropOnNode?: (targetId: string, position: DropPosition) => void;
302
+ onKeydownRelay?: (e: Omit<CanvasKeydownMessage, 'channel' | 'v' | 'type'>) => void;
303
+ onLinkClick?: (href: string) => void;
304
+ className?: string;
305
+ 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
+ renderBox?: (rects: {
317
+ selected: Rect | null;
318
+ hovered: Rect | null;
319
+ selectedLabel: string | null;
320
+ }) => ReactNode;
321
+ }
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
+ 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). */
144
340
  declare function useStructureRevision(editor: Pick<Editor, 'onStructureChange'>): number;
341
+ /** Re-render on every committed change (for undo state, inspectors, …). */
145
342
  declare function useEditorRevision(editor: Pick<Editor, 'onChange'>): number;
146
343
  type PropFieldValue = string | number | boolean | undefined;
147
344
  interface PropFieldsProps {
345
+ /** The control schema (e.g. a block's `props`). */
148
346
  fields: PropSchemaField[];
347
+ /** Current value per field name. */
149
348
  values: Record<string, PropFieldValue>;
349
+ /** Called when a control changes. Wire to `editor.setProp`/`setExpression`. */
150
350
  onChange: (name: string, value: string | number | boolean) => void;
151
351
  className?: string;
152
352
  }
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
+ */
153
359
  declare function PropFields({ fields, values, onChange, className, }: PropFieldsProps): ReactNode;
154
360
 
155
- export { DEFAULT_CANVAS_CSS, EditorFrame, IframeCanvas, PropFields, templateIdFromElement, useEditorRevision, useStructureRevision };
156
- export type { IframeCanvasProps, PropFieldValue, PropFieldsProps, Rect };
361
+ export { DEFAULT_CANVAS_CSS, EditorFrame, IframeCanvas, PropFields, RemoteCanvasHost, templateIdFromElement, useCanvasWindow, useEditorRevision, useStructureRevision };
362
+ export type { CanvasWindow, IframeCanvasProps, PropFieldValue, PropFieldsProps, Rect, RemoteCanvasHostProps };