@hypen-space/web 0.4.45 → 0.4.81
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/README.md +4 -4
- package/dist/canvas/dirty.d.ts +40 -0
- package/dist/canvas/dirty.js +100 -0
- package/dist/canvas/dirty.js.map +1 -0
- package/dist/canvas/events.d.ts +5 -0
- package/dist/canvas/events.js +30 -3
- package/dist/canvas/events.js.map +1 -1
- package/dist/canvas/index.d.ts +5 -1
- package/dist/canvas/index.js +4 -0
- package/dist/canvas/index.js.map +1 -1
- package/dist/canvas/layout.d.ts +12 -2
- package/dist/canvas/layout.js +593 -63
- package/dist/canvas/layout.js.map +1 -1
- package/dist/canvas/paint.d.ts +2 -0
- package/dist/canvas/paint.js +88 -11
- package/dist/canvas/paint.js.map +1 -1
- package/dist/canvas/renderer.d.ts +16 -0
- package/dist/canvas/renderer.js +154 -15
- package/dist/canvas/renderer.js.map +1 -1
- package/dist/canvas/scroll.d.ts +82 -0
- package/dist/canvas/scroll.js +639 -0
- package/dist/canvas/scroll.js.map +1 -0
- package/dist/canvas/selection.d.ts +63 -0
- package/dist/canvas/selection.js +466 -0
- package/dist/canvas/selection.js.map +1 -0
- package/dist/canvas/text.d.ts +6 -3
- package/dist/canvas/text.js +82 -43
- package/dist/canvas/text.js.map +1 -1
- package/dist/canvas/types.d.ts +18 -0
- package/dist/canvas/utils.d.ts +8 -2
- package/dist/canvas/utils.js +42 -16
- package/dist/canvas/utils.js.map +1 -1
- package/dist/canvas/virtualize.d.ts +25 -0
- package/dist/canvas/virtualize.js +65 -0
- package/dist/canvas/virtualize.js.map +1 -0
- package/dist/dom/applicators/index.js +0 -1
- package/dist/dom/applicators/index.js.map +1 -1
- package/dist/dom/applicators/margin.js +58 -17
- package/dist/dom/applicators/margin.js.map +1 -1
- package/dist/dom/applicators/padding.js +42 -17
- package/dist/dom/applicators/padding.js.map +1 -1
- package/dist/dom/components/icon.d.ts +9 -0
- package/dist/dom/components/icon.js +67 -0
- package/dist/dom/components/icon.js.map +1 -0
- package/dist/dom/components/index.js +2 -0
- package/dist/dom/components/index.js.map +1 -1
- package/dist/dom/renderer.d.ts +19 -5
- package/dist/dom/renderer.js +147 -17
- package/dist/dom/renderer.js.map +1 -1
- package/package.json +4 -2
- package/src/canvas/QUICKSTART.md +5 -5
- package/src/canvas/dirty.ts +116 -0
- package/src/canvas/events.ts +34 -3
- package/src/canvas/index.ts +5 -0
- package/src/canvas/layout.ts +653 -100
- package/src/canvas/paint.ts +109 -11
- package/src/canvas/renderer.ts +189 -16
- package/src/canvas/scroll.ts +729 -0
- package/src/canvas/selection.ts +560 -0
- package/src/canvas/text.ts +97 -54
- package/src/canvas/types.ts +26 -0
- package/src/canvas/utils.ts +47 -17
- package/src/canvas/virtualize.ts +81 -0
- package/src/dom/applicators/index.ts +1 -1
- package/src/dom/applicators/margin.ts +57 -11
- package/src/dom/applicators/padding.ts +45 -11
- package/src/dom/components/icon.ts +84 -0
- package/src/dom/components/index.ts +2 -0
- package/src/dom/renderer.ts +155 -17
package/src/dom/renderer.ts
CHANGED
|
@@ -11,9 +11,14 @@ import type { HypenGlobalContext } from "@hypen-space/core/context";
|
|
|
11
11
|
import { frameworkLoggers } from "@hypen-space/core/logger";
|
|
12
12
|
|
|
13
13
|
const log = frameworkLoggers.renderer;
|
|
14
|
+
|
|
15
|
+
/** Element types that treat the "action" prop as an onClick handler */
|
|
16
|
+
const ACTIONABLE_TYPES = new Set(["button", "link", "card"]);
|
|
17
|
+
|
|
14
18
|
import { ComponentRegistry } from "./components/index.js";
|
|
15
19
|
import { ApplicatorRegistry } from "./applicators/index.js";
|
|
16
20
|
import { canvasHandler, canvasApplicators } from "./canvas/index.js";
|
|
21
|
+
import { CanvasRenderer } from "../canvas/renderer.js";
|
|
17
22
|
import { RerenderTracker, type DebugConfig, defaultDebugConfig } from "./debug.js";
|
|
18
23
|
import { setEngine, disposeHypenElement } from "./element-data.js";
|
|
19
24
|
|
|
@@ -35,6 +40,14 @@ export class DOMRenderer {
|
|
|
35
40
|
private componentInstances = new Map<string, HypenModuleInstance>();
|
|
36
41
|
private debugTracker: RerenderTracker;
|
|
37
42
|
|
|
43
|
+
// Canvas subtree routing — Canvas components get their own CanvasRenderer
|
|
44
|
+
// and all descendant patches are forwarded to it instead of the DOM.
|
|
45
|
+
private canvasRenderers = new Map<string, CanvasRenderer>();
|
|
46
|
+
private canvasSubtreeMap = new Map<string, string>(); // nodeId → canvasRootId
|
|
47
|
+
private canvasElements = new Map<string, HTMLCanvasElement>();
|
|
48
|
+
/** Props stashed at create-time so we can forward them to CanvasRenderer */
|
|
49
|
+
private pendingCreateProps = new Map<string, { elementType: string; props: Record<string, any> }>();
|
|
50
|
+
|
|
38
51
|
constructor(container: HTMLElement, engine: IEngine, debugConfig?: Partial<DebugConfig>) {
|
|
39
52
|
this.container = container;
|
|
40
53
|
this.engine = engine;
|
|
@@ -58,12 +71,86 @@ export class DOMRenderer {
|
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
/**
|
|
61
|
-
* Apply a batch of patches to the DOM
|
|
74
|
+
* Apply a batch of patches to the DOM.
|
|
75
|
+
*
|
|
76
|
+
* Uses two-pass routing: first discovers which nodes belong to a Canvas
|
|
77
|
+
* subtree (via insert/move parents), then routes each patch to either
|
|
78
|
+
* the DOMRenderer or the owning CanvasRenderer.
|
|
62
79
|
*/
|
|
63
80
|
applyPatches(patches: Patch[]): void {
|
|
81
|
+
// Phase 1: scan inserts to discover new canvas-subtree members
|
|
64
82
|
for (const patch of patches) {
|
|
83
|
+
if (patch.type === "insert" || patch.type === "move") {
|
|
84
|
+
const parentId = patch.parentId;
|
|
85
|
+
const childId = patch.id;
|
|
86
|
+
if (!parentId || !childId) continue;
|
|
87
|
+
|
|
88
|
+
// Is the parent a canvas root or inside a canvas subtree?
|
|
89
|
+
const canvasRootId =
|
|
90
|
+
this.canvasRenderers.has(parentId) ? parentId
|
|
91
|
+
: this.canvasSubtreeMap.get(parentId);
|
|
92
|
+
if (canvasRootId) {
|
|
93
|
+
this.canvasSubtreeMap.set(childId, canvasRootId);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Phase 2: route patches
|
|
99
|
+
const canvasBatches = new Map<string, Patch[]>();
|
|
100
|
+
const domPatches: Patch[] = [];
|
|
101
|
+
|
|
102
|
+
for (const patch of patches) {
|
|
103
|
+
const canvasRootId = this.getCanvasRouteTarget(patch);
|
|
104
|
+
if (canvasRootId) {
|
|
105
|
+
let batch = canvasBatches.get(canvasRootId);
|
|
106
|
+
if (!batch) { batch = []; canvasBatches.set(canvasRootId, batch); }
|
|
107
|
+
batch.push(patch);
|
|
108
|
+
} else {
|
|
109
|
+
domPatches.push(patch);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Apply DOM patches normally
|
|
114
|
+
for (const patch of domPatches) {
|
|
65
115
|
this.applyPatch(patch);
|
|
66
116
|
}
|
|
117
|
+
|
|
118
|
+
// Forward canvas-subtree patches to their CanvasRenderer instances
|
|
119
|
+
for (const [rootId, batch] of canvasBatches) {
|
|
120
|
+
const renderer = this.canvasRenderers.get(rootId);
|
|
121
|
+
if (renderer) {
|
|
122
|
+
renderer.applyPatches(batch);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Determine which canvas root (if any) a patch should be routed to.
|
|
129
|
+
* Returns the canvas root node ID, or undefined for DOM patches.
|
|
130
|
+
*/
|
|
131
|
+
private getCanvasRouteTarget(patch: Patch): string | undefined {
|
|
132
|
+
const id = patch.id;
|
|
133
|
+
const parentId = patch.parentId;
|
|
134
|
+
|
|
135
|
+
switch (patch.type) {
|
|
136
|
+
case "insert":
|
|
137
|
+
case "move": {
|
|
138
|
+
// Route by parent — if parent is a canvas root or inside one
|
|
139
|
+
if (!parentId) return undefined;
|
|
140
|
+
if (this.canvasRenderers.has(parentId)) return parentId;
|
|
141
|
+
return this.canvasSubtreeMap.get(parentId);
|
|
142
|
+
}
|
|
143
|
+
case "create": {
|
|
144
|
+
// create patches are routed if the node was pre-registered in phase 1
|
|
145
|
+
if (!id) return undefined;
|
|
146
|
+
return this.canvasSubtreeMap.get(id);
|
|
147
|
+
}
|
|
148
|
+
default: {
|
|
149
|
+
// setProp, removeProp, setText, remove — route by node id
|
|
150
|
+
if (!id) return undefined;
|
|
151
|
+
return this.canvasSubtreeMap.get(id);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
67
154
|
}
|
|
68
155
|
|
|
69
156
|
/**
|
|
@@ -105,10 +192,12 @@ export class DOMRenderer {
|
|
|
105
192
|
}
|
|
106
193
|
|
|
107
194
|
/**
|
|
108
|
-
* Interpolate state values in text template
|
|
195
|
+
* Interpolate state values in text template.
|
|
196
|
+
*
|
|
197
|
+
* Bindings use the `@{state.path}` syntax (matching the engine's parser).
|
|
109
198
|
*/
|
|
110
199
|
private interpolateText(template: string, state: Record<string, any>): string {
|
|
111
|
-
return template.replace(
|
|
200
|
+
return template.replace(/@\{([^}]+)\}/g, (match, path) => {
|
|
112
201
|
try {
|
|
113
202
|
const value = path.split('.').reduce((obj: any, key: string) => {
|
|
114
203
|
if (key === 'state') return state;
|
|
@@ -122,20 +211,13 @@ export class DOMRenderer {
|
|
|
122
211
|
}
|
|
123
212
|
|
|
124
213
|
/**
|
|
125
|
-
* Apply a single patch
|
|
126
|
-
*
|
|
127
|
-
* Handles both camelCase (direct WASM) and snake_case (WebSocket JSON) field names.
|
|
214
|
+
* Apply a single patch.
|
|
128
215
|
*/
|
|
129
216
|
private applyPatch(patch: Patch): void {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
const id: string | undefined = patch.id ?? p.id;
|
|
135
|
-
const elementType: string | undefined = patch.elementType ?? p.element_type;
|
|
136
|
-
const parentId: string | undefined = patch.parentId ?? p.parent_id;
|
|
137
|
-
const beforeId: string | undefined = patch.beforeId ?? p.before_id;
|
|
138
|
-
const eventName: string | undefined = patch.eventName ?? p.event_name;
|
|
217
|
+
const id = patch.id;
|
|
218
|
+
const elementType = patch.elementType;
|
|
219
|
+
const parentId = patch.parentId;
|
|
220
|
+
const beforeId = patch.beforeId;
|
|
139
221
|
|
|
140
222
|
switch (patch.type) {
|
|
141
223
|
case "create":
|
|
@@ -186,9 +268,34 @@ export class DOMRenderer {
|
|
|
186
268
|
setEngine(element, this.engine);
|
|
187
269
|
|
|
188
270
|
this.applicators.applyAll(element, propsObj);
|
|
271
|
+
|
|
272
|
+
// Actionable components: wire "action" prop as onClick
|
|
273
|
+
if (propsObj.action && ACTIONABLE_TYPES.has(elementType.toLowerCase())) {
|
|
274
|
+
this.applicators.apply(element, "onClick", propsObj.action);
|
|
275
|
+
}
|
|
276
|
+
|
|
189
277
|
this.nodes.set(id, element);
|
|
190
278
|
this.debugTracker.trackRerender(id, element, `create:${elementType}`);
|
|
191
279
|
|
|
280
|
+
// Canvas component: create a CanvasRenderer for its subtree
|
|
281
|
+
if (elementType.toLowerCase() === "canvas") {
|
|
282
|
+
const canvasEl = element as unknown as HTMLCanvasElement;
|
|
283
|
+
// Apply sensible defaults if no explicit size
|
|
284
|
+
if (!canvasEl.width || canvasEl.width === 300) canvasEl.width = propsObj.width ? Number(propsObj.width) : 800;
|
|
285
|
+
if (!canvasEl.height || canvasEl.height === 150) canvasEl.height = propsObj.height ? Number(propsObj.height) : 600;
|
|
286
|
+
|
|
287
|
+
const canvasRenderer = new CanvasRenderer(canvasEl, this.engine);
|
|
288
|
+
this.canvasRenderers.set(id, canvasRenderer);
|
|
289
|
+
this.canvasElements.set(id, canvasEl);
|
|
290
|
+
|
|
291
|
+
// Bootstrap a root container inside the CanvasRenderer so child
|
|
292
|
+
// inserts with parentId === this canvas node find a parent.
|
|
293
|
+
canvasRenderer.applyPatches([
|
|
294
|
+
{ type: "create", id, elementType: "container", props: {} },
|
|
295
|
+
{ type: "insert", parentId: "root", id },
|
|
296
|
+
]);
|
|
297
|
+
}
|
|
298
|
+
|
|
192
299
|
if (!this.rootId) {
|
|
193
300
|
this.rootId = id;
|
|
194
301
|
if (!this.container.contains(element)) {
|
|
@@ -223,8 +330,8 @@ export class DOMRenderer {
|
|
|
223
330
|
// Engine patches may send interpolated strings; if we overwrite the template,
|
|
224
331
|
// future state updates won't be able to re-interpolate.
|
|
225
332
|
const currentTemplate = element.dataset.textTemplate;
|
|
226
|
-
const nextLooksLikeTemplate = nextText.includes("
|
|
227
|
-
const currentLooksLikeTemplate = typeof currentTemplate === "string" && currentTemplate.includes("
|
|
333
|
+
const nextLooksLikeTemplate = nextText.includes("@{");
|
|
334
|
+
const currentLooksLikeTemplate = typeof currentTemplate === "string" && currentTemplate.includes("@{");
|
|
228
335
|
|
|
229
336
|
if (nextLooksLikeTemplate) {
|
|
230
337
|
element.dataset.textTemplate = nextText;
|
|
@@ -239,7 +346,19 @@ export class DOMRenderer {
|
|
|
239
346
|
return;
|
|
240
347
|
}
|
|
241
348
|
|
|
349
|
+
// Actionable components: wire "action" prop as onClick
|
|
350
|
+
if (name === "action" && ACTIONABLE_TYPES.has(element.dataset.hypenType || "")) {
|
|
351
|
+
this.applicators.apply(element, "onClick", value);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
242
355
|
this.applicators.apply(element, name, value);
|
|
356
|
+
|
|
357
|
+
// Forward canvas dimension changes to its CanvasRenderer
|
|
358
|
+
if ((name === "width" || name === "height") && this.canvasRenderers.has(id)) {
|
|
359
|
+
const canvasEl = this.canvasElements.get(id)!;
|
|
360
|
+
this.canvasRenderers.get(id)!.resize(canvasEl.width, canvasEl.height);
|
|
361
|
+
}
|
|
243
362
|
}
|
|
244
363
|
|
|
245
364
|
/**
|
|
@@ -311,6 +430,17 @@ export class DOMRenderer {
|
|
|
311
430
|
const element = this.nodes.get(id);
|
|
312
431
|
if (!element) return;
|
|
313
432
|
|
|
433
|
+
// Clean up canvas renderer if this is a canvas root
|
|
434
|
+
if (this.canvasRenderers.has(id)) {
|
|
435
|
+
this.canvasRenderers.get(id)!.destroy();
|
|
436
|
+
this.canvasRenderers.delete(id);
|
|
437
|
+
this.canvasElements.delete(id);
|
|
438
|
+
// Remove all subtree entries pointing to this canvas root
|
|
439
|
+
for (const [nodeId, rootId] of this.canvasSubtreeMap) {
|
|
440
|
+
if (rootId === id) this.canvasSubtreeMap.delete(nodeId);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
314
444
|
// Dispose event listeners and other resources before removing from DOM
|
|
315
445
|
disposeHypenElement(element);
|
|
316
446
|
|
|
@@ -336,6 +466,14 @@ export class DOMRenderer {
|
|
|
336
466
|
* Clear all nodes
|
|
337
467
|
*/
|
|
338
468
|
clear(): void {
|
|
469
|
+
// Destroy all canvas renderers
|
|
470
|
+
for (const renderer of this.canvasRenderers.values()) {
|
|
471
|
+
renderer.destroy();
|
|
472
|
+
}
|
|
473
|
+
this.canvasRenderers.clear();
|
|
474
|
+
this.canvasElements.clear();
|
|
475
|
+
this.canvasSubtreeMap.clear();
|
|
476
|
+
|
|
339
477
|
// Dispose all element resources before clearing
|
|
340
478
|
for (const element of this.nodes.values()) {
|
|
341
479
|
disposeHypenElement(element);
|