@hypen-space/web 0.2.11 → 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/src/dom/applicators/effects.js +38 -2
- package/dist/src/dom/applicators/effects.js.map +3 -3
- package/dist/src/dom/applicators/events.js +280 -397
- package/dist/src/dom/applicators/events.js.map +5 -4
- package/dist/src/dom/applicators/font.js +94 -5
- package/dist/src/dom/applicators/font.js.map +3 -3
- package/dist/src/dom/applicators/index.js +590 -425
- package/dist/src/dom/applicators/index.js.map +10 -9
- package/dist/src/dom/applicators/layout.js +33 -5
- package/dist/src/dom/applicators/layout.js.map +3 -3
- package/dist/src/dom/applicators/size.js +81 -16
- package/dist/src/dom/applicators/size.js.map +3 -3
- package/dist/src/dom/components/hypenapp.js +296 -0
- package/dist/src/dom/components/hypenapp.js.map +10 -0
- package/dist/src/dom/components/index.js +263 -1
- package/dist/src/dom/components/index.js.map +5 -4
- package/dist/src/dom/element-data.js +140 -0
- package/dist/src/dom/element-data.js.map +10 -0
- package/dist/src/dom/index.js +857 -430
- package/dist/src/dom/index.js.map +13 -11
- package/dist/src/dom/renderer.js +857 -430
- package/dist/src/dom/renderer.js.map +13 -11
- package/dist/src/hypen.js +857 -430
- package/dist/src/hypen.js.map +13 -11
- package/dist/src/index.js +862 -430
- package/dist/src/index.js.map +15 -12
- package/package.json +3 -3
- package/src/canvas/QUICKSTART.md +2 -4
- package/src/dom/applicators/effects.ts +45 -1
- package/src/dom/applicators/events.ts +348 -537
- package/src/dom/applicators/font.ts +127 -2
- package/src/dom/applicators/index.ts +117 -7
- package/src/dom/applicators/layout.ts +40 -4
- package/src/dom/applicators/size.ts +101 -16
- package/src/dom/components/hypenapp.ts +348 -0
- package/src/dom/components/index.ts +2 -0
- package/src/dom/element-data.ts +234 -0
- package/src/dom/renderer.ts +8 -5
- package/src/index.ts +3 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HypenApp Component
|
|
3
|
+
*
|
|
4
|
+
* Embeds a remote Hypen app via WebSocket
|
|
5
|
+
*
|
|
6
|
+
* Usage in Hypen DSL:
|
|
7
|
+
* ```hypen
|
|
8
|
+
* HypenApp("ws://localhost:3000")
|
|
9
|
+
*
|
|
10
|
+
* // Or with named prop:
|
|
11
|
+
* HypenApp(url: "ws://localhost:3000")
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ComponentHandler } from "./index.js";
|
|
16
|
+
import { RemoteEngine } from "@hypen-space/core/remote/client";
|
|
17
|
+
import type { Patch } from "@hypen-space/core/remote";
|
|
18
|
+
|
|
19
|
+
// Store active HypenApp instances for cleanup
|
|
20
|
+
const activeInstances = new WeakMap<
|
|
21
|
+
HTMLElement,
|
|
22
|
+
{
|
|
23
|
+
engine: RemoteEngine;
|
|
24
|
+
nodes: Map<string, HTMLElement>;
|
|
25
|
+
}
|
|
26
|
+
>();
|
|
27
|
+
|
|
28
|
+
export const hypenAppHandler: ComponentHandler = {
|
|
29
|
+
create(): HTMLElement {
|
|
30
|
+
const el = document.createElement("div");
|
|
31
|
+
el.dataset.hypenType = "hypenapp";
|
|
32
|
+
el.style.display = "contents"; // Don't affect layout
|
|
33
|
+
return el;
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
applyProps(element: HTMLElement, props: Record<string, any>): void {
|
|
37
|
+
// Get URL from props (can be positional "0" or named "url")
|
|
38
|
+
const url = props["0"] || props.url;
|
|
39
|
+
|
|
40
|
+
if (!url || typeof url !== "string") {
|
|
41
|
+
console.error("[HypenApp] URL is required");
|
|
42
|
+
element.innerHTML = '<div style="color: red;">HypenApp: URL required</div>';
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check if already connected
|
|
47
|
+
const existing = activeInstances.get(element);
|
|
48
|
+
if (existing) {
|
|
49
|
+
// Already connected, don't reconnect
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Create the remote engine
|
|
54
|
+
const engine = new RemoteEngine(url, {
|
|
55
|
+
autoReconnect: props.autoReconnect ?? true,
|
|
56
|
+
reconnectInterval: props.reconnectInterval ?? 3000,
|
|
57
|
+
maxReconnectAttempts: props.maxReconnectAttempts ?? 10,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Map to track created nodes
|
|
61
|
+
const nodes = new Map<string, HTMLElement>();
|
|
62
|
+
let rootId: string | null = null;
|
|
63
|
+
|
|
64
|
+
// Store instance for cleanup
|
|
65
|
+
activeInstances.set(element, { engine, nodes });
|
|
66
|
+
|
|
67
|
+
// Set up patch handling
|
|
68
|
+
engine.onPatches((patches) => {
|
|
69
|
+
applyPatches(element, nodes, patches, engine, (id) => {
|
|
70
|
+
if (!rootId) rootId = id;
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Show loading state
|
|
75
|
+
element.innerHTML = '<div class="hypen-app-loading">Connecting...</div>';
|
|
76
|
+
|
|
77
|
+
// Connect
|
|
78
|
+
engine
|
|
79
|
+
.connect()
|
|
80
|
+
.then(() => {
|
|
81
|
+
// Clear loading state - patches will populate content
|
|
82
|
+
element.innerHTML = "";
|
|
83
|
+
console.log(`[HypenApp] Connected to ${url}`);
|
|
84
|
+
})
|
|
85
|
+
.catch((error) => {
|
|
86
|
+
element.innerHTML = `<div style="color: red;">HypenApp: Connection failed - ${error.message}</div>`;
|
|
87
|
+
console.error("[HypenApp] Connection failed:", error);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Handle disconnection
|
|
91
|
+
engine.onDisconnect(() => {
|
|
92
|
+
console.log("[HypenApp] Disconnected");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
engine.onError((error) => {
|
|
96
|
+
console.error("[HypenApp] Error:", error);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Cleanup on element removal
|
|
100
|
+
const observer = new MutationObserver((mutations) => {
|
|
101
|
+
for (const mutation of mutations) {
|
|
102
|
+
for (const removedNode of mutation.removedNodes) {
|
|
103
|
+
if (removedNode === element || (removedNode as Element).contains?.(element)) {
|
|
104
|
+
engine.disconnect();
|
|
105
|
+
activeInstances.delete(element);
|
|
106
|
+
observer.disconnect();
|
|
107
|
+
console.log("[HypenApp] Cleaned up");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Observe parent for removal
|
|
115
|
+
if (element.parentNode) {
|
|
116
|
+
observer.observe(element.parentNode, { childList: true, subtree: true });
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Minimal patch application for HypenApp container
|
|
123
|
+
*/
|
|
124
|
+
function applyPatches(
|
|
125
|
+
container: HTMLElement,
|
|
126
|
+
nodes: Map<string, HTMLElement>,
|
|
127
|
+
patches: Patch[],
|
|
128
|
+
engine: RemoteEngine,
|
|
129
|
+
onRoot: (id: string) => void
|
|
130
|
+
): void {
|
|
131
|
+
for (const patch of patches) {
|
|
132
|
+
switch (patch.type) {
|
|
133
|
+
case "create": {
|
|
134
|
+
const el = createElement((patch as any).element_type, patch.props || {});
|
|
135
|
+
el.dataset.hypenId = patch.id!;
|
|
136
|
+
(el as any).__hypenEngine = engine;
|
|
137
|
+
nodes.set(patch.id!, el);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case "setProp": {
|
|
142
|
+
const el = nodes.get(patch.id!);
|
|
143
|
+
if (el) {
|
|
144
|
+
applyProp(el, patch.name!, patch.value);
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
case "setText": {
|
|
150
|
+
const el = nodes.get(patch.id!);
|
|
151
|
+
if (el) {
|
|
152
|
+
el.textContent = patch.text!;
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case "insert": {
|
|
158
|
+
const parentId = (patch as any).parent_id;
|
|
159
|
+
const parent = parentId === "root" ? container : nodes.get(parentId);
|
|
160
|
+
const child = nodes.get(patch.id!);
|
|
161
|
+
const beforeId = (patch as any).before_id;
|
|
162
|
+
|
|
163
|
+
if (parent && child) {
|
|
164
|
+
if (parentId === "root") {
|
|
165
|
+
onRoot(patch.id!);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (beforeId) {
|
|
169
|
+
const before = nodes.get(beforeId);
|
|
170
|
+
if (before && before.parentNode === parent) {
|
|
171
|
+
parent.insertBefore(child, before);
|
|
172
|
+
} else if (!parent.contains(child)) {
|
|
173
|
+
parent.appendChild(child);
|
|
174
|
+
}
|
|
175
|
+
} else if (!parent.contains(child)) {
|
|
176
|
+
parent.appendChild(child);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case "move": {
|
|
183
|
+
const parentId = (patch as any).parent_id;
|
|
184
|
+
const parent = parentId === "root" ? container : nodes.get(parentId);
|
|
185
|
+
const child = nodes.get(patch.id!);
|
|
186
|
+
const beforeId = (patch as any).before_id;
|
|
187
|
+
|
|
188
|
+
if (parent && child) {
|
|
189
|
+
if (beforeId) {
|
|
190
|
+
const before = nodes.get(beforeId);
|
|
191
|
+
if (before && before.parentNode === parent) {
|
|
192
|
+
parent.insertBefore(child, before);
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
parent.appendChild(child);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
case "remove": {
|
|
202
|
+
const el = nodes.get(patch.id!);
|
|
203
|
+
if (el && el.parentNode) {
|
|
204
|
+
el.parentNode.removeChild(el);
|
|
205
|
+
}
|
|
206
|
+
nodes.delete(patch.id!);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create element by type
|
|
215
|
+
*/
|
|
216
|
+
function createElement(type: string, props: Record<string, any>): HTMLElement {
|
|
217
|
+
const normalizedType = type.toLowerCase();
|
|
218
|
+
|
|
219
|
+
// Map Hypen types to HTML elements
|
|
220
|
+
const tagMap: Record<string, string> = {
|
|
221
|
+
column: "div",
|
|
222
|
+
row: "div",
|
|
223
|
+
text: "span",
|
|
224
|
+
button: "button",
|
|
225
|
+
input: "input",
|
|
226
|
+
image: "img",
|
|
227
|
+
container: "div",
|
|
228
|
+
box: "div",
|
|
229
|
+
center: "div",
|
|
230
|
+
list: "div",
|
|
231
|
+
spacer: "div",
|
|
232
|
+
stack: "div",
|
|
233
|
+
divider: "hr",
|
|
234
|
+
grid: "div",
|
|
235
|
+
card: "div",
|
|
236
|
+
heading: "h2",
|
|
237
|
+
link: "a",
|
|
238
|
+
textarea: "textarea",
|
|
239
|
+
checkbox: "input",
|
|
240
|
+
select: "select",
|
|
241
|
+
slider: "input",
|
|
242
|
+
switch: "input",
|
|
243
|
+
spinner: "div",
|
|
244
|
+
badge: "span",
|
|
245
|
+
avatar: "img",
|
|
246
|
+
progressbar: "div",
|
|
247
|
+
video: "video",
|
|
248
|
+
audio: "audio",
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const tag = tagMap[normalizedType] || "div";
|
|
252
|
+
const el = document.createElement(tag);
|
|
253
|
+
el.dataset.hypenType = normalizedType;
|
|
254
|
+
|
|
255
|
+
// Apply basic styles
|
|
256
|
+
if (normalizedType === "column") {
|
|
257
|
+
el.style.display = "flex";
|
|
258
|
+
el.style.flexDirection = "column";
|
|
259
|
+
} else if (normalizedType === "row") {
|
|
260
|
+
el.style.display = "flex";
|
|
261
|
+
el.style.flexDirection = "row";
|
|
262
|
+
} else if (normalizedType === "center") {
|
|
263
|
+
el.style.display = "flex";
|
|
264
|
+
el.style.alignItems = "center";
|
|
265
|
+
el.style.justifyContent = "center";
|
|
266
|
+
} else if (normalizedType === "text") {
|
|
267
|
+
// Text content from props
|
|
268
|
+
if (props["0"]) {
|
|
269
|
+
el.textContent = String(props["0"]);
|
|
270
|
+
}
|
|
271
|
+
} else if (normalizedType === "button") {
|
|
272
|
+
el.style.cursor = "pointer";
|
|
273
|
+
} else if (normalizedType === "checkbox" || normalizedType === "switch") {
|
|
274
|
+
(el as HTMLInputElement).type = "checkbox";
|
|
275
|
+
} else if (normalizedType === "slider") {
|
|
276
|
+
(el as HTMLInputElement).type = "range";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return el;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Apply a prop to an element
|
|
284
|
+
*/
|
|
285
|
+
function applyProp(el: HTMLElement, name: string, value: any): void {
|
|
286
|
+
// Text content
|
|
287
|
+
if (name === "0" || name === "text") {
|
|
288
|
+
el.textContent = String(value);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Style props
|
|
293
|
+
const styleProps: Record<string, string> = {
|
|
294
|
+
padding: "padding",
|
|
295
|
+
margin: "margin",
|
|
296
|
+
backgroundColor: "backgroundColor",
|
|
297
|
+
background: "background",
|
|
298
|
+
color: "color",
|
|
299
|
+
fontSize: "fontSize",
|
|
300
|
+
fontWeight: "fontWeight",
|
|
301
|
+
width: "width",
|
|
302
|
+
height: "height",
|
|
303
|
+
minWidth: "minWidth",
|
|
304
|
+
minHeight: "minHeight",
|
|
305
|
+
maxWidth: "maxWidth",
|
|
306
|
+
maxHeight: "maxHeight",
|
|
307
|
+
borderRadius: "borderRadius",
|
|
308
|
+
border: "border",
|
|
309
|
+
gap: "gap",
|
|
310
|
+
flex: "flex",
|
|
311
|
+
alignItems: "alignItems",
|
|
312
|
+
justifyContent: "justifyContent",
|
|
313
|
+
opacity: "opacity",
|
|
314
|
+
overflow: "overflow",
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
if (styleProps[name]) {
|
|
318
|
+
const cssValue = typeof value === "number" ? `${value}px` : String(value);
|
|
319
|
+
(el.style as any)[styleProps[name]] = cssValue;
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Event handlers
|
|
324
|
+
if (name === "onClick" || name === "onclick") {
|
|
325
|
+
el.onclick = () => {
|
|
326
|
+
const engine = (el as any).__hypenEngine as RemoteEngine;
|
|
327
|
+
if (engine && typeof value === "string" && value.startsWith("@actions.")) {
|
|
328
|
+
const action = value.replace("@actions.", "");
|
|
329
|
+
engine.dispatchAction(action);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Other attributes
|
|
336
|
+
el.setAttribute(name, String(value));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Disconnect a HypenApp instance
|
|
341
|
+
*/
|
|
342
|
+
export function disconnectHypenApp(element: HTMLElement): void {
|
|
343
|
+
const instance = activeInstances.get(element);
|
|
344
|
+
if (instance) {
|
|
345
|
+
instance.engine.disconnect();
|
|
346
|
+
activeInstances.delete(element);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
@@ -79,6 +79,7 @@ export class ComponentRegistry {
|
|
|
79
79
|
const { paragraphHandler } = require("./paragraph.js");
|
|
80
80
|
const { routerHandler } = require("./router.js");
|
|
81
81
|
const { routeHandler } = require("./route.js");
|
|
82
|
+
const { hypenAppHandler } = require("./hypenapp.js");
|
|
82
83
|
|
|
83
84
|
this.register("column", columnHandler);
|
|
84
85
|
this.register("row", rowHandler);
|
|
@@ -111,5 +112,6 @@ export class ComponentRegistry {
|
|
|
111
112
|
this.register("paragraph", paragraphHandler);
|
|
112
113
|
this.register("router", routerHandler);
|
|
113
114
|
this.register("route", routeHandler);
|
|
115
|
+
this.register("hypenapp", hypenAppHandler);
|
|
114
116
|
}
|
|
115
117
|
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-Safe Element Data
|
|
3
|
+
*
|
|
4
|
+
* Provides strongly-typed access to Hypen-specific data attached to DOM elements.
|
|
5
|
+
* Eliminates `as any` casts throughout the codebase.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type DisposableStack, getElementDisposables } from "@hypen/core";
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Types
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Engine interface for dispatching actions
|
|
16
|
+
*/
|
|
17
|
+
export interface IEngine {
|
|
18
|
+
dispatchAction(name: string, payload?: unknown): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* All Hypen-specific data attached to an element
|
|
23
|
+
*/
|
|
24
|
+
export interface HypenElementData {
|
|
25
|
+
/** Engine reference for action dispatch */
|
|
26
|
+
engine?: IEngine;
|
|
27
|
+
/** Target key for keyboard events (e.g., "Enter", "Escape") */
|
|
28
|
+
keyTarget?: string;
|
|
29
|
+
/** Set of registered event type:action pairs */
|
|
30
|
+
registeredEvents?: Set<string>;
|
|
31
|
+
/** Custom element metadata */
|
|
32
|
+
meta?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Private Storage
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* WeakMap to store Hypen data without polluting the element
|
|
41
|
+
*/
|
|
42
|
+
const elementDataMap = new WeakMap<HTMLElement, HypenElementData>();
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Core API
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get Hypen data for an element, creating if needed
|
|
50
|
+
*/
|
|
51
|
+
export function getHypenData(element: HTMLElement): HypenElementData {
|
|
52
|
+
let data = elementDataMap.get(element);
|
|
53
|
+
if (!data) {
|
|
54
|
+
data = {};
|
|
55
|
+
elementDataMap.set(element, data);
|
|
56
|
+
}
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if element has Hypen data
|
|
62
|
+
*/
|
|
63
|
+
export function hasHypenData(element: HTMLElement): boolean {
|
|
64
|
+
return elementDataMap.has(element);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Clear Hypen data from an element
|
|
69
|
+
*/
|
|
70
|
+
export function clearHypenData(element: HTMLElement): void {
|
|
71
|
+
elementDataMap.delete(element);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// Engine Access
|
|
76
|
+
// ============================================================================
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get the engine attached to an element
|
|
80
|
+
*/
|
|
81
|
+
export function getEngine(element: HTMLElement): IEngine | undefined {
|
|
82
|
+
return getHypenData(element).engine;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Set the engine on an element
|
|
87
|
+
*/
|
|
88
|
+
export function setEngine(element: HTMLElement, engine: IEngine): void {
|
|
89
|
+
getHypenData(element).engine = engine;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Find engine by walking up the DOM tree
|
|
94
|
+
*/
|
|
95
|
+
export function findEngine(element: HTMLElement): IEngine | undefined {
|
|
96
|
+
let current: HTMLElement | null = element;
|
|
97
|
+
while (current) {
|
|
98
|
+
const engine = getEngine(current);
|
|
99
|
+
if (engine) return engine;
|
|
100
|
+
current = current.parentElement;
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// Event Registration Tracking
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get registered events set for an element
|
|
111
|
+
*/
|
|
112
|
+
export function getRegisteredEvents(element: HTMLElement): Set<string> {
|
|
113
|
+
const data = getHypenData(element);
|
|
114
|
+
if (!data.registeredEvents) {
|
|
115
|
+
data.registeredEvents = new Set();
|
|
116
|
+
}
|
|
117
|
+
return data.registeredEvents;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if an event handler is registered
|
|
122
|
+
*/
|
|
123
|
+
export function isEventRegistered(element: HTMLElement, eventKey: string): boolean {
|
|
124
|
+
return getRegisteredEvents(element).has(eventKey);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Mark an event handler as registered
|
|
129
|
+
*/
|
|
130
|
+
export function registerEvent(element: HTMLElement, eventKey: string): void {
|
|
131
|
+
getRegisteredEvents(element).add(eventKey);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Unregister an event handler
|
|
136
|
+
*/
|
|
137
|
+
export function unregisterEvent(element: HTMLElement, eventKey: string): void {
|
|
138
|
+
getRegisteredEvents(element).delete(eventKey);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// Keyboard Event Key Target
|
|
143
|
+
// ============================================================================
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get the target key for keyboard events
|
|
147
|
+
*/
|
|
148
|
+
export function getKeyTarget(element: HTMLElement): string | undefined {
|
|
149
|
+
return getHypenData(element).keyTarget;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Set the target key for keyboard events
|
|
154
|
+
*/
|
|
155
|
+
export function setKeyTarget(element: HTMLElement, key: string): void {
|
|
156
|
+
getHypenData(element).keyTarget = key;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ============================================================================
|
|
160
|
+
// Metadata
|
|
161
|
+
// ============================================================================
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get custom metadata value
|
|
165
|
+
*/
|
|
166
|
+
export function getMeta<T>(element: HTMLElement, key: string): T | undefined {
|
|
167
|
+
return getHypenData(element).meta?.[key] as T | undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Set custom metadata value
|
|
172
|
+
*/
|
|
173
|
+
export function setMeta<T>(element: HTMLElement, key: string, value: T): void {
|
|
174
|
+
const data = getHypenData(element);
|
|
175
|
+
if (!data.meta) {
|
|
176
|
+
data.meta = {};
|
|
177
|
+
}
|
|
178
|
+
data.meta[key] = value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// Cleanup
|
|
183
|
+
// ============================================================================
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Dispose all resources attached to an element and clear data
|
|
187
|
+
* Call this when removing an element from the DOM
|
|
188
|
+
*/
|
|
189
|
+
export function disposeHypenElement(element: HTMLElement): void {
|
|
190
|
+
// Dispose any registered disposables
|
|
191
|
+
try {
|
|
192
|
+
const disposables = getElementDisposables(element);
|
|
193
|
+
disposables.dispose();
|
|
194
|
+
} catch {
|
|
195
|
+
// Ignore if no disposables
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Clear Hypen data
|
|
199
|
+
clearHypenData(element);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ============================================================================
|
|
203
|
+
// Legacy Compatibility Layer
|
|
204
|
+
// ============================================================================
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Symbol-based legacy accessor (for backwards compatibility)
|
|
208
|
+
* Use the typed functions above instead when possible
|
|
209
|
+
*/
|
|
210
|
+
const HYPEN_ENGINE_SYMBOL = Symbol.for("hypen.engine");
|
|
211
|
+
const REGISTERED_EVENTS_SYMBOL = Symbol.for("hypen.registeredEvents");
|
|
212
|
+
const KEY_TARGET_SYMBOL = Symbol.for("hypen.keyTarget");
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Legacy accessor for engine (backwards compatible with existing code)
|
|
216
|
+
*/
|
|
217
|
+
export function getLegacyEngine(element: HTMLElement): IEngine | undefined {
|
|
218
|
+
// Try new storage first
|
|
219
|
+
const engine = getEngine(element);
|
|
220
|
+
if (engine) return engine;
|
|
221
|
+
|
|
222
|
+
// Fall back to legacy storage
|
|
223
|
+
return (element as any)[HYPEN_ENGINE_SYMBOL] ??
|
|
224
|
+
(element as any).__hypenEngine;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Legacy setter for engine (backwards compatible)
|
|
229
|
+
*/
|
|
230
|
+
export function setLegacyEngine(element: HTMLElement, engine: IEngine): void {
|
|
231
|
+
// Set in both locations for compatibility
|
|
232
|
+
setEngine(element, engine);
|
|
233
|
+
(element as any).__hypenEngine = engine;
|
|
234
|
+
}
|
package/src/dom/renderer.ts
CHANGED
|
@@ -149,14 +149,17 @@ export class DOMRenderer {
|
|
|
149
149
|
private onCreate(id: string, elementType: string, props: Record<string, any> | Map<string, any>): void {
|
|
150
150
|
const propsObj = props instanceof Map ? Object.fromEntries(props) : props;
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
let element = this.components.createElement(elementType, propsObj);
|
|
153
153
|
|
|
154
154
|
if (!element) {
|
|
155
|
+
// For unknown component types, create a transparent container (div).
|
|
156
|
+
// This handles module wrappers (like "App") and custom components
|
|
157
|
+
// that aren't registered but should act as layout containers.
|
|
155
158
|
const fallback = document.createElement("div");
|
|
156
|
-
fallback.dataset.hypenType = elementType;
|
|
157
|
-
fallback.
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
fallback.dataset.hypenType = elementType.toLowerCase();
|
|
160
|
+
fallback.style.display = "contents"; // Make container transparent in layout
|
|
161
|
+
element = fallback;
|
|
162
|
+
console.log(`[Renderer] Unknown component "${elementType}" - using transparent container`);
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
element.dataset.hypenType = elementType.toLowerCase();
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,9 @@ export { ApplicatorRegistry } from "./dom/applicators/index.js";
|
|
|
38
38
|
export { EventManager } from "./dom/events.js";
|
|
39
39
|
export { RerenderTracker, type DebugConfig, defaultDebugConfig } from "./dom/debug.js";
|
|
40
40
|
|
|
41
|
+
// HypenApp - Embed remote Hypen apps
|
|
42
|
+
export { hypenAppHandler, disconnectHypenApp } from "./dom/components/hypenapp.js";
|
|
43
|
+
|
|
41
44
|
// ============================================================================
|
|
42
45
|
// CANVAS RENDERER
|
|
43
46
|
// ============================================================================
|