@hypen-space/web 0.4.980 → 0.5.3
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 +20 -13
- package/dist/canvas/accessibility.d.ts +118 -11
- package/dist/canvas/accessibility.js +470 -108
- package/dist/canvas/accessibility.js.map +1 -1
- package/dist/canvas/dispatch.d.ts +24 -0
- package/dist/canvas/dispatch.js +62 -0
- package/dist/canvas/dispatch.js.map +1 -0
- package/dist/canvas/editing.d.ts +178 -0
- package/dist/canvas/editing.js +590 -0
- package/dist/canvas/editing.js.map +1 -0
- package/dist/canvas/events.d.ts +28 -23
- package/dist/canvas/events.js +90 -103
- package/dist/canvas/events.js.map +1 -1
- package/dist/canvas/focus.d.ts +78 -0
- package/dist/canvas/focus.js +182 -0
- package/dist/canvas/focus.js.map +1 -0
- package/dist/canvas/index.d.ts +3 -2
- package/dist/canvas/index.js +3 -2
- package/dist/canvas/index.js.map +1 -1
- package/dist/canvas/layout.js +68 -98
- package/dist/canvas/layout.js.map +1 -1
- package/dist/canvas/paint.d.ts +16 -3
- package/dist/canvas/paint.js +449 -110
- package/dist/canvas/paint.js.map +1 -1
- package/dist/canvas/renderer.d.ts +28 -4
- package/dist/canvas/renderer.js +201 -81
- package/dist/canvas/renderer.js.map +1 -1
- package/dist/canvas/selection.js +31 -177
- package/dist/canvas/selection.js.map +1 -1
- package/dist/canvas/text-geometry.d.ts +72 -0
- package/dist/canvas/text-geometry.js +244 -0
- package/dist/canvas/text-geometry.js.map +1 -0
- package/dist/canvas/text.js +44 -18
- package/dist/canvas/text.js.map +1 -1
- package/dist/canvas/types.d.ts +10 -1
- package/dist/canvas/utils.d.ts +0 -23
- package/dist/canvas/utils.js +3 -67
- package/dist/canvas/utils.js.map +1 -1
- package/dist/canvas/variants.d.ts +52 -0
- package/dist/canvas/variants.js +156 -0
- package/dist/canvas/variants.js.map +1 -0
- package/dist/dom/a11y-styles.d.ts +20 -0
- package/dist/dom/a11y-styles.js +61 -0
- package/dist/dom/a11y-styles.js.map +1 -0
- package/dist/dom/applicators/aria.d.ts +19 -0
- package/dist/dom/applicators/aria.js +36 -0
- package/dist/dom/applicators/aria.js.map +1 -0
- package/dist/dom/applicators/events.d.ts +7 -0
- package/dist/dom/applicators/events.js +36 -8
- package/dist/dom/applicators/events.js.map +1 -1
- package/dist/dom/applicators/font.d.ts +11 -6
- package/dist/dom/applicators/font.js +16 -27
- package/dist/dom/applicators/font.js.map +1 -1
- package/dist/dom/applicators/index.js +51 -45
- package/dist/dom/applicators/index.js.map +1 -1
- package/dist/dom/canvas/index.js +2 -2
- package/dist/dom/canvas/index.js.map +1 -1
- package/dist/dom/components/app.js +2 -2
- package/dist/dom/components/app.js.map +1 -1
- package/dist/dom/components/audio.js +2 -2
- package/dist/dom/components/audio.js.map +1 -1
- package/dist/dom/components/avatar.js +3 -4
- package/dist/dom/components/avatar.js.map +1 -1
- package/dist/dom/components/badge.js +3 -4
- package/dist/dom/components/badge.js.map +1 -1
- package/dist/dom/components/button.js +2 -2
- package/dist/dom/components/button.js.map +1 -1
- package/dist/dom/components/card.js +2 -2
- package/dist/dom/components/card.js.map +1 -1
- package/dist/dom/components/center.js +2 -2
- package/dist/dom/components/center.js.map +1 -1
- package/dist/dom/components/checkbox.js +4 -5
- package/dist/dom/components/checkbox.js.map +1 -1
- package/dist/dom/components/column.js +2 -2
- package/dist/dom/components/column.js.map +1 -1
- package/dist/dom/components/container.js +2 -2
- package/dist/dom/components/container.js.map +1 -1
- package/dist/dom/components/divider.js +2 -2
- package/dist/dom/components/divider.js.map +1 -1
- package/dist/dom/components/grid.js +10 -10
- package/dist/dom/components/grid.js.map +1 -1
- package/dist/dom/components/heading.js +4 -6
- package/dist/dom/components/heading.js.map +1 -1
- package/dist/dom/components/hypenapp.d.ts +25 -1
- package/dist/dom/components/hypenapp.js +215 -247
- package/dist/dom/components/hypenapp.js.map +1 -1
- package/dist/dom/components/icon.js +4 -5
- package/dist/dom/components/icon.js.map +1 -1
- package/dist/dom/components/image.js +2 -2
- package/dist/dom/components/image.js.map +1 -1
- package/dist/dom/components/index.d.ts +2 -2
- package/dist/dom/components/index.js +9 -2
- package/dist/dom/components/index.js.map +1 -1
- package/dist/dom/components/input.js +2 -2
- package/dist/dom/components/input.js.map +1 -1
- package/dist/dom/components/link.js +2 -2
- package/dist/dom/components/link.js.map +1 -1
- package/dist/dom/components/list.js +2 -2
- package/dist/dom/components/list.js.map +1 -1
- package/dist/dom/components/paragraph.js +3 -4
- package/dist/dom/components/paragraph.js.map +1 -1
- package/dist/dom/components/progressbar.js +3 -3
- package/dist/dom/components/progressbar.js.map +1 -1
- package/dist/dom/components/route.js +2 -2
- package/dist/dom/components/route.js.map +1 -1
- package/dist/dom/components/router.js +2 -2
- package/dist/dom/components/router.js.map +1 -1
- package/dist/dom/components/row.js +10 -10
- package/dist/dom/components/row.js.map +1 -1
- package/dist/dom/components/select.js +3 -4
- package/dist/dom/components/select.js.map +1 -1
- package/dist/dom/components/slider.js +2 -2
- package/dist/dom/components/slider.js.map +1 -1
- package/dist/dom/components/spacer.js +2 -2
- package/dist/dom/components/spacer.js.map +1 -1
- package/dist/dom/components/spinner.js +27 -13
- package/dist/dom/components/spinner.js.map +1 -1
- package/dist/dom/components/stack.js +10 -10
- package/dist/dom/components/stack.js.map +1 -1
- package/dist/dom/components/switch.js +5 -6
- package/dist/dom/components/switch.js.map +1 -1
- package/dist/dom/components/tabs.d.ts +27 -0
- package/dist/dom/components/tabs.js +69 -0
- package/dist/dom/components/tabs.js.map +1 -0
- package/dist/dom/components/text.js +4 -7
- package/dist/dom/components/text.js.map +1 -1
- package/dist/dom/components/textarea.js +2 -2
- package/dist/dom/components/textarea.js.map +1 -1
- package/dist/dom/components/video.js +2 -2
- package/dist/dom/components/video.js.map +1 -1
- package/dist/dom/components/visuallyhidden.d.ts +9 -0
- package/dist/dom/components/visuallyhidden.js +26 -0
- package/dist/dom/components/visuallyhidden.js.map +1 -0
- package/dist/dom/debug.js +6 -8
- package/dist/dom/debug.js.map +1 -1
- package/dist/dom/index.d.ts +0 -1
- package/dist/dom/index.js +0 -1
- package/dist/dom/index.js.map +1 -1
- package/dist/dom/operability.d.ts +100 -0
- package/dist/dom/operability.js +298 -0
- package/dist/dom/operability.js.map +1 -0
- package/dist/dom/renderer.d.ts +98 -5
- package/dist/dom/renderer.js +400 -66
- package/dist/dom/renderer.js.map +1 -1
- package/dist/dom/route-focus.d.ts +42 -0
- package/dist/dom/route-focus.js +88 -0
- package/dist/dom/route-focus.js.map +1 -0
- package/dist/dom/semantics.d.ts +35 -0
- package/dist/dom/semantics.js +184 -0
- package/dist/dom/semantics.js.map +1 -0
- package/dist/variants.d.ts +90 -0
- package/dist/variants.js +206 -0
- package/dist/variants.js.map +1 -0
- package/package.json +2 -2
- package/src/canvas/QUICKSTART.md +16 -11
- package/src/canvas/README.md +63 -34
- package/src/canvas/accessibility.ts +507 -115
- package/src/canvas/dispatch.ts +78 -0
- package/src/canvas/editing.ts +697 -0
- package/src/canvas/events.ts +104 -115
- package/src/canvas/focus.ts +216 -0
- package/src/canvas/index.ts +8 -9
- package/src/canvas/layout.ts +68 -103
- package/src/canvas/paint.ts +509 -109
- package/src/canvas/renderer.ts +239 -99
- package/src/canvas/selection.ts +52 -210
- package/src/canvas/text-geometry.ts +311 -0
- package/src/canvas/text.ts +45 -18
- package/src/canvas/types.ts +28 -1
- package/src/canvas/utils.ts +3 -69
- package/src/canvas/variants.ts +172 -0
- package/src/dom/README.md +8 -7
- package/src/dom/a11y-styles.ts +65 -0
- package/src/dom/applicators/aria.ts +41 -0
- package/src/dom/applicators/events.ts +39 -7
- package/src/dom/applicators/font.ts +16 -29
- package/src/dom/applicators/index.ts +61 -46
- package/src/dom/canvas/index.ts +2 -2
- package/src/dom/components/app.ts +2 -2
- package/src/dom/components/audio.ts +2 -2
- package/src/dom/components/avatar.ts +3 -4
- package/src/dom/components/badge.ts +3 -4
- package/src/dom/components/button.ts +2 -2
- package/src/dom/components/card.ts +2 -2
- package/src/dom/components/center.ts +2 -2
- package/src/dom/components/checkbox.ts +4 -5
- package/src/dom/components/column.ts +2 -2
- package/src/dom/components/container.ts +2 -2
- package/src/dom/components/divider.ts +2 -2
- package/src/dom/components/grid.ts +10 -10
- package/src/dom/components/heading.ts +4 -6
- package/src/dom/components/hypenapp.ts +246 -274
- package/src/dom/components/icon.ts +4 -5
- package/src/dom/components/image.ts +2 -2
- package/src/dom/components/index.ts +10 -3
- package/src/dom/components/input.ts +2 -2
- package/src/dom/components/link.ts +2 -2
- package/src/dom/components/list.ts +2 -2
- package/src/dom/components/paragraph.ts +3 -4
- package/src/dom/components/progressbar.ts +3 -3
- package/src/dom/components/route.ts +2 -2
- package/src/dom/components/router.ts +2 -2
- package/src/dom/components/row.ts +10 -10
- package/src/dom/components/select.ts +3 -4
- package/src/dom/components/slider.ts +2 -2
- package/src/dom/components/spacer.ts +2 -2
- package/src/dom/components/spinner.ts +34 -16
- package/src/dom/components/stack.ts +10 -10
- package/src/dom/components/switch.ts +5 -6
- package/src/dom/components/tabs.ts +76 -0
- package/src/dom/components/text.ts +4 -7
- package/src/dom/components/textarea.ts +2 -2
- package/src/dom/components/video.ts +2 -2
- package/src/dom/components/visuallyhidden.ts +30 -0
- package/src/dom/debug.ts +6 -8
- package/src/dom/index.ts +0 -8
- package/src/dom/operability.ts +312 -0
- package/src/dom/renderer.ts +457 -66
- package/src/dom/route-focus.ts +98 -0
- package/src/dom/semantics.ts +199 -0
- package/src/variants.ts +249 -0
- package/dist/canvas/input.d.ts +0 -76
- package/dist/canvas/input.js +0 -207
- package/dist/canvas/input.js.map +0 -1
- package/dist/client.d.ts +0 -36
- package/dist/client.js +0 -47
- package/dist/client.js.map +0 -1
- package/src/canvas/input.ts +0 -251
- package/src/client.ts +0 -88
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HypenApp Component
|
|
3
3
|
*
|
|
4
|
-
* Embeds a remote Hypen app via WebSocket
|
|
4
|
+
* Embeds a remote Hypen app via WebSocket.
|
|
5
5
|
*
|
|
6
6
|
* Usage in Hypen DSL:
|
|
7
7
|
* ```hypen
|
|
@@ -9,28 +9,182 @@
|
|
|
9
9
|
*
|
|
10
10
|
* // Or with named prop:
|
|
11
11
|
* HypenApp(url: "ws://localhost:3000")
|
|
12
|
+
*
|
|
13
|
+
* // With custom loading / error UI via slot children:
|
|
14
|
+
* HypenApp("ws://localhost:3000") {
|
|
15
|
+
* Column { Spinner() Text("Connecting to app...") }.slot("loading")
|
|
16
|
+
* Column { Text("Couldn't reach the app") }.slot("error")
|
|
17
|
+
* }
|
|
12
18
|
* ```
|
|
19
|
+
*
|
|
20
|
+
* Slot children are rendered by the *host* app (they're ordinary host
|
|
21
|
+
* nodes with full access to host state and actions); the handler only
|
|
22
|
+
* toggles their visibility as the connection moves through
|
|
23
|
+
* loading → connected / error. Children without a recognized slot render
|
|
24
|
+
* unconditionally. When a slot isn't provided, a built-in fallback is
|
|
25
|
+
* used ("Connecting..." text / red error message).
|
|
26
|
+
*
|
|
27
|
+
* The embedded app is rendered by a full `DOMRenderer` driven by its own
|
|
28
|
+
* `RemoteEngine` — the exact pairing the generic client uses at top level —
|
|
29
|
+
* so embedded apps get identical styling, event dispatch, router patch
|
|
30
|
+
* handling, and a11y behavior to a standalone page. (Route-change focus is
|
|
31
|
+
* disabled: the *host* app owns focus management; an embedded frame stealing
|
|
32
|
+
* focus on its internal navigations would fight it.) It renders into a
|
|
33
|
+
* dedicated content wrapper so slot toggling and error handling never
|
|
34
|
+
* touch host-owned slot children, and vice versa.
|
|
13
35
|
*/
|
|
14
36
|
|
|
15
37
|
import type { ComponentHandler } from "./index.js";
|
|
16
38
|
import { RemoteEngine } from "@hypen-space/core/remote/client";
|
|
17
39
|
import type { Patch } from "@hypen-space/core/types";
|
|
40
|
+
import { getElementDisposables } from "@hypen-space/core/disposable";
|
|
18
41
|
import { frameworkLoggers } from "@hypen-space/core/logger";
|
|
42
|
+
import type { DOMRenderer } from "../renderer.js";
|
|
19
43
|
|
|
20
44
|
const log = frameworkLoggers.remote;
|
|
21
45
|
|
|
46
|
+
export const LOADING_SLOT = "loading";
|
|
47
|
+
export const ERROR_SLOT = "error";
|
|
48
|
+
|
|
49
|
+
type EmbedStatus = "loading" | "connected" | "error";
|
|
50
|
+
|
|
51
|
+
interface HypenAppInstance {
|
|
52
|
+
engine: RemoteEngine | null;
|
|
53
|
+
renderer: DOMRenderer | null;
|
|
54
|
+
url: string | null;
|
|
55
|
+
dispose(): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
22
58
|
// Store active HypenApp instances for cleanup
|
|
23
|
-
const activeInstances = new WeakMap<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
59
|
+
const activeInstances = new WeakMap<HTMLElement, HypenAppInstance>();
|
|
60
|
+
|
|
61
|
+
/** Direct children of the container tagged with `.slot(name)`. */
|
|
62
|
+
function slotChildren(element: HTMLElement, name: string): HTMLElement[] {
|
|
63
|
+
const out: HTMLElement[] = [];
|
|
64
|
+
for (const child of Array.from(element.children)) {
|
|
65
|
+
if ((child as HTMLElement).dataset?.hypenSlot === name) {
|
|
66
|
+
out.push(child as HTMLElement);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Hide/show an element while preserving its inline display value —
|
|
74
|
+
* applicators set `display: flex` etc. inline, so a plain `display = ""`
|
|
75
|
+
* on re-show would lose the element's layout.
|
|
76
|
+
*/
|
|
77
|
+
function setVisible(el: HTMLElement, visible: boolean): void {
|
|
78
|
+
const hidden = el.dataset.hypenSlotHidden === "true";
|
|
79
|
+
if (visible && hidden) {
|
|
80
|
+
el.style.display = el.dataset.hypenPrevDisplay ?? "";
|
|
81
|
+
delete el.dataset.hypenPrevDisplay;
|
|
82
|
+
delete el.dataset.hypenSlotHidden;
|
|
83
|
+
} else if (!visible && !hidden) {
|
|
84
|
+
el.dataset.hypenPrevDisplay = el.style.display;
|
|
85
|
+
el.dataset.hypenSlotHidden = "true";
|
|
86
|
+
el.style.display = "none";
|
|
28
87
|
}
|
|
29
|
-
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Connection-state → visibility controller for one HypenApp container.
|
|
92
|
+
*
|
|
93
|
+
* Slot children arrive via Insert patches *after* the container's Create
|
|
94
|
+
* (and can arrive/leave any time under ForEach/When), so visibility is
|
|
95
|
+
* re-applied from a MutationObserver rather than computed once.
|
|
96
|
+
*/
|
|
97
|
+
class SlotVisibilityController {
|
|
98
|
+
private status: EmbedStatus = "loading";
|
|
99
|
+
private errorMessage = "";
|
|
100
|
+
private defaultLoading: HTMLElement | null = null;
|
|
101
|
+
private defaultError: HTMLElement | null = null;
|
|
102
|
+
private observer: MutationObserver | null = null;
|
|
103
|
+
|
|
104
|
+
constructor(
|
|
105
|
+
private element: HTMLElement,
|
|
106
|
+
private contentHost: HTMLElement,
|
|
107
|
+
) {
|
|
108
|
+
// Slot children arrive via Insert patches *after* this controller is
|
|
109
|
+
// constructed, so re-apply visibility whenever the container's child
|
|
110
|
+
// list changes (falls back to status-change-only updates where
|
|
111
|
+
// MutationObserver is unavailable, e.g. non-DOM test envs).
|
|
112
|
+
if (typeof MutationObserver !== "undefined") {
|
|
113
|
+
this.observer = new MutationObserver(() => this.update());
|
|
114
|
+
this.observer.observe(element, { childList: true });
|
|
115
|
+
}
|
|
116
|
+
this.update();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
setStatus(status: EmbedStatus, errorMessage?: string): void {
|
|
120
|
+
this.status = status;
|
|
121
|
+
if (errorMessage !== undefined) this.errorMessage = errorMessage;
|
|
122
|
+
this.element.dataset.hypenAppStatus = status;
|
|
123
|
+
this.update();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private update(): void {
|
|
127
|
+
const showLoading = this.status === "loading";
|
|
128
|
+
const showError = this.status === "error";
|
|
129
|
+
|
|
130
|
+
const loadingSlots = slotChildren(this.element, LOADING_SLOT);
|
|
131
|
+
const errorSlots = slotChildren(this.element, ERROR_SLOT);
|
|
132
|
+
|
|
133
|
+
for (const el of loadingSlots) setVisible(el, showLoading);
|
|
134
|
+
for (const el of errorSlots) setVisible(el, showError);
|
|
135
|
+
|
|
136
|
+
// Built-in fallbacks only cover states the host didn't style.
|
|
137
|
+
this.toggleDefaultLoading(showLoading && loadingSlots.length === 0);
|
|
138
|
+
this.toggleDefaultError(showError && errorSlots.length === 0);
|
|
139
|
+
|
|
140
|
+
// A failed connection may leave a partial embedded tree behind;
|
|
141
|
+
// don't show it alongside the error UI.
|
|
142
|
+
setVisible(this.contentHost, !showError);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private toggleDefaultLoading(show: boolean): void {
|
|
146
|
+
if (show && !this.defaultLoading) {
|
|
147
|
+
const el = document.createElement("div");
|
|
148
|
+
el.className = "hypen-app-loading";
|
|
149
|
+
el.textContent = "Connecting...";
|
|
150
|
+
this.defaultLoading = el;
|
|
151
|
+
this.element.appendChild(el);
|
|
152
|
+
} else if (!show && this.defaultLoading) {
|
|
153
|
+
this.defaultLoading.remove();
|
|
154
|
+
this.defaultLoading = null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private toggleDefaultError(show: boolean): void {
|
|
159
|
+
if (show && !this.defaultError) {
|
|
160
|
+
const el = document.createElement("div");
|
|
161
|
+
el.className = "hypen-app-error";
|
|
162
|
+
el.style.color = "red";
|
|
163
|
+
this.defaultError = el;
|
|
164
|
+
this.element.appendChild(el);
|
|
165
|
+
}
|
|
166
|
+
if (this.defaultError) {
|
|
167
|
+
if (show) {
|
|
168
|
+
this.defaultError.textContent = this.errorMessage || "HypenApp: Connection failed";
|
|
169
|
+
} else {
|
|
170
|
+
this.defaultError.remove();
|
|
171
|
+
this.defaultError = null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
dispose(): void {
|
|
177
|
+
this.observer?.disconnect();
|
|
178
|
+
this.defaultLoading?.remove();
|
|
179
|
+
this.defaultError?.remove();
|
|
180
|
+
this.defaultLoading = null;
|
|
181
|
+
this.defaultError = null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
30
184
|
|
|
31
185
|
export const hypenAppHandler: ComponentHandler = {
|
|
32
|
-
create(
|
|
33
|
-
const el =
|
|
186
|
+
create(): HTMLElement {
|
|
187
|
+
const el = document.createElement("div");
|
|
34
188
|
el.dataset.hypenType = "hypenapp";
|
|
35
189
|
el.style.display = "contents"; // Don't affect layout
|
|
36
190
|
return el;
|
|
@@ -40,57 +194,101 @@ export const hypenAppHandler: ComponentHandler = {
|
|
|
40
194
|
// Get URL from props (can be positional "0" or named "url")
|
|
41
195
|
const url = props["0"] || props.url;
|
|
42
196
|
|
|
197
|
+
const existing = activeInstances.get(element);
|
|
198
|
+
if (existing) {
|
|
199
|
+
if (existing.url === url) return; // Already connected to this app
|
|
200
|
+
// URL changed (or became invalid) — tear down and reconnect below.
|
|
201
|
+
existing.dispose();
|
|
202
|
+
activeInstances.delete(element);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// The nested renderer owns this wrapper; host-owned slot children are
|
|
206
|
+
// siblings of it, so neither side's cleanup can clobber the other.
|
|
207
|
+
const contentHost = document.createElement("div");
|
|
208
|
+
contentHost.className = "hypen-app-content";
|
|
209
|
+
contentHost.style.display = "contents";
|
|
210
|
+
element.appendChild(contentHost);
|
|
211
|
+
|
|
212
|
+
const visibility = new SlotVisibilityController(element, contentHost);
|
|
213
|
+
|
|
43
214
|
if (!url || typeof url !== "string") {
|
|
44
215
|
log.error("HypenApp: URL is required");
|
|
45
|
-
|
|
216
|
+
visibility.setStatus("error", "HypenApp: URL required");
|
|
217
|
+
activeInstances.set(element, {
|
|
218
|
+
engine: null,
|
|
219
|
+
renderer: null,
|
|
220
|
+
url: null,
|
|
221
|
+
dispose() {
|
|
222
|
+
visibility.dispose();
|
|
223
|
+
contentHost.remove();
|
|
224
|
+
},
|
|
225
|
+
});
|
|
46
226
|
return;
|
|
47
227
|
}
|
|
48
228
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
229
|
+
// Lazy-required to break the static cycle:
|
|
230
|
+
// renderer.ts → components/index.ts → hypenapp.ts → renderer.ts
|
|
231
|
+
const { DOMRenderer: Renderer } =
|
|
232
|
+
require("../renderer.js") as typeof import("../renderer.js");
|
|
55
233
|
|
|
56
|
-
// Create the remote engine
|
|
57
234
|
const engine = new RemoteEngine(url, {
|
|
58
235
|
autoReconnect: props.autoReconnect ?? true,
|
|
59
236
|
reconnectInterval: props.reconnectInterval ?? 3000,
|
|
60
237
|
maxReconnectAttempts: props.maxReconnectAttempts ?? 10,
|
|
61
238
|
});
|
|
62
239
|
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
240
|
+
const renderer = new Renderer(contentHost, engine, undefined, { routeFocus: "off" });
|
|
241
|
+
const instance: HypenAppInstance = {
|
|
242
|
+
engine,
|
|
243
|
+
renderer,
|
|
244
|
+
url,
|
|
245
|
+
dispose() {
|
|
246
|
+
engine.disconnect();
|
|
247
|
+
visibility.dispose();
|
|
248
|
+
contentHost.remove();
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
activeInstances.set(element, instance);
|
|
252
|
+
|
|
253
|
+
// Same replacement-root handling as the generic client: a fresh root
|
|
254
|
+
// insert after we've already rendered one means "replace the tree".
|
|
255
|
+
let hasRenderedRoot = false;
|
|
256
|
+
engine.onPatches((patches: Patch[]) => {
|
|
257
|
+
visibility.setStatus("connected");
|
|
258
|
+
const createdIds = new Set(
|
|
259
|
+
patches.filter((patch) => patch.type === "create").map((patch) => patch.id),
|
|
260
|
+
);
|
|
261
|
+
const hasReplacementRoot = patches.some(
|
|
262
|
+
(patch) =>
|
|
263
|
+
patch.type === "insert" &&
|
|
264
|
+
patch.parentId === "root" &&
|
|
265
|
+
createdIds.has(patch.id),
|
|
266
|
+
);
|
|
267
|
+
if (hasReplacementRoot && hasRenderedRoot) {
|
|
268
|
+
renderer.clear();
|
|
269
|
+
}
|
|
270
|
+
renderer.applyPatches(patches);
|
|
271
|
+
hasRenderedRoot = hasRenderedRoot || hasReplacementRoot;
|
|
75
272
|
});
|
|
76
273
|
|
|
77
|
-
// Show loading state
|
|
78
|
-
element.innerHTML = '<div class="hypen-app-loading">Connecting...</div>';
|
|
79
|
-
|
|
80
|
-
// Connect
|
|
81
274
|
engine
|
|
82
275
|
.connect()
|
|
83
|
-
.then(() => {
|
|
84
|
-
|
|
85
|
-
|
|
276
|
+
.then((result: any) => {
|
|
277
|
+
if (result && result.ok === false) {
|
|
278
|
+
visibility.setStatus(
|
|
279
|
+
"error",
|
|
280
|
+
`HypenApp: Connection failed - ${result.error?.message ?? result.error}`,
|
|
281
|
+
);
|
|
282
|
+
log.error("HypenApp connection failed:", result.error);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
86
285
|
log.debug(`HypenApp connected to ${url}`);
|
|
87
286
|
})
|
|
88
|
-
.catch((error) => {
|
|
89
|
-
|
|
287
|
+
.catch((error: Error) => {
|
|
288
|
+
visibility.setStatus("error", `HypenApp: Connection failed - ${error.message}`);
|
|
90
289
|
log.error("HypenApp connection failed:", error);
|
|
91
290
|
});
|
|
92
291
|
|
|
93
|
-
// Handle disconnection
|
|
94
292
|
engine.onDisconnect(() => {
|
|
95
293
|
log.debug("HypenApp disconnected");
|
|
96
294
|
});
|
|
@@ -99,251 +297,25 @@ export const hypenAppHandler: ComponentHandler = {
|
|
|
99
297
|
log.error("HypenApp error:", error);
|
|
100
298
|
});
|
|
101
299
|
|
|
102
|
-
// Cleanup
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
log.debug("HypenApp cleaned up");
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
300
|
+
// Cleanup rides the renderer's element-disposal channel: the host
|
|
301
|
+
// DOMRenderer runs this on a real `remove` patch (and on `clear()`),
|
|
302
|
+
// but NOT on a Router `detach` — so a cached route keeps its live
|
|
303
|
+
// connection and re-attaches warm, while LRU eviction closes it.
|
|
304
|
+
getElementDisposables(element).addCallback(() => {
|
|
305
|
+
instance.dispose();
|
|
306
|
+
activeInstances.delete(element);
|
|
307
|
+
log.debug("HypenApp cleaned up");
|
|
115
308
|
});
|
|
116
|
-
|
|
117
|
-
// Observe parent for removal
|
|
118
|
-
if (element.parentNode) {
|
|
119
|
-
observer.observe(element.parentNode, { childList: true, subtree: true });
|
|
120
|
-
}
|
|
121
309
|
},
|
|
122
310
|
};
|
|
123
311
|
|
|
124
|
-
/**
|
|
125
|
-
* Minimal patch application for HypenApp container
|
|
126
|
-
*/
|
|
127
|
-
function applyPatches(
|
|
128
|
-
container: HTMLElement,
|
|
129
|
-
nodes: Map<string, HTMLElement>,
|
|
130
|
-
patches: Patch[],
|
|
131
|
-
engine: RemoteEngine,
|
|
132
|
-
onRoot: (id: string) => void
|
|
133
|
-
): void {
|
|
134
|
-
for (const patch of patches) {
|
|
135
|
-
switch (patch.type) {
|
|
136
|
-
case "create": {
|
|
137
|
-
const el = createElement(patch.elementType!, patch.props || {});
|
|
138
|
-
el.dataset.hypenId = patch.id!;
|
|
139
|
-
(el as any).__hypenEngine = engine;
|
|
140
|
-
nodes.set(patch.id!, el);
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
case "setProp": {
|
|
145
|
-
const el = nodes.get(patch.id!);
|
|
146
|
-
if (el) {
|
|
147
|
-
applyProp(el, patch.name!, patch.value);
|
|
148
|
-
}
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
case "setText": {
|
|
153
|
-
const el = nodes.get(patch.id!);
|
|
154
|
-
if (el) {
|
|
155
|
-
el.textContent = patch.text!;
|
|
156
|
-
}
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
case "insert": {
|
|
161
|
-
const parentId = patch.parentId!;
|
|
162
|
-
const parent = parentId === "root" ? container : nodes.get(parentId);
|
|
163
|
-
const child = nodes.get(patch.id!);
|
|
164
|
-
const beforeId = patch.beforeId;
|
|
165
|
-
|
|
166
|
-
if (parent && child) {
|
|
167
|
-
if (parentId === "root") {
|
|
168
|
-
onRoot(patch.id!);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (beforeId) {
|
|
172
|
-
const before = nodes.get(beforeId);
|
|
173
|
-
if (before && before.parentNode === parent) {
|
|
174
|
-
parent.insertBefore(child, before);
|
|
175
|
-
} else if (!parent.contains(child)) {
|
|
176
|
-
parent.appendChild(child);
|
|
177
|
-
}
|
|
178
|
-
} else if (!parent.contains(child)) {
|
|
179
|
-
parent.appendChild(child);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
break;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
case "move": {
|
|
186
|
-
const parentId = patch.parentId!;
|
|
187
|
-
const parent = parentId === "root" ? container : nodes.get(parentId);
|
|
188
|
-
const child = nodes.get(patch.id!);
|
|
189
|
-
const beforeId = patch.beforeId;
|
|
190
|
-
|
|
191
|
-
if (parent && child) {
|
|
192
|
-
if (beforeId) {
|
|
193
|
-
const before = nodes.get(beforeId);
|
|
194
|
-
if (before && before.parentNode === parent) {
|
|
195
|
-
parent.insertBefore(child, before);
|
|
196
|
-
}
|
|
197
|
-
} else {
|
|
198
|
-
parent.appendChild(child);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
break;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
case "remove": {
|
|
205
|
-
const el = nodes.get(patch.id!);
|
|
206
|
-
if (el && el.parentNode) {
|
|
207
|
-
el.parentNode.removeChild(el);
|
|
208
|
-
}
|
|
209
|
-
nodes.delete(patch.id!);
|
|
210
|
-
break;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Create element by type
|
|
218
|
-
*/
|
|
219
|
-
function createElement(type: string, props: Record<string, any>): HTMLElement {
|
|
220
|
-
const normalizedType = type.toLowerCase();
|
|
221
|
-
|
|
222
|
-
// Map Hypen types to HTML elements
|
|
223
|
-
const tagMap: Record<string, string> = {
|
|
224
|
-
column: "div",
|
|
225
|
-
row: "div",
|
|
226
|
-
text: "span",
|
|
227
|
-
button: "button",
|
|
228
|
-
input: "input",
|
|
229
|
-
image: "img",
|
|
230
|
-
container: "div",
|
|
231
|
-
box: "div",
|
|
232
|
-
center: "div",
|
|
233
|
-
list: "div",
|
|
234
|
-
spacer: "div",
|
|
235
|
-
stack: "div",
|
|
236
|
-
divider: "hr",
|
|
237
|
-
grid: "div",
|
|
238
|
-
card: "div",
|
|
239
|
-
heading: "h2",
|
|
240
|
-
link: "a",
|
|
241
|
-
textarea: "textarea",
|
|
242
|
-
checkbox: "input",
|
|
243
|
-
select: "select",
|
|
244
|
-
slider: "input",
|
|
245
|
-
switch: "input",
|
|
246
|
-
spinner: "div",
|
|
247
|
-
badge: "span",
|
|
248
|
-
avatar: "img",
|
|
249
|
-
progressbar: "div",
|
|
250
|
-
video: "video",
|
|
251
|
-
audio: "audio",
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
const tag = tagMap[normalizedType] || "div";
|
|
255
|
-
const el = document.createElement(tag);
|
|
256
|
-
el.dataset.hypenType = normalizedType;
|
|
257
|
-
|
|
258
|
-
// Apply basic styles
|
|
259
|
-
if (normalizedType === "column") {
|
|
260
|
-
el.style.display = "flex";
|
|
261
|
-
el.style.flexDirection = "column";
|
|
262
|
-
} else if (normalizedType === "row") {
|
|
263
|
-
el.style.display = "flex";
|
|
264
|
-
el.style.flexDirection = "row";
|
|
265
|
-
} else if (normalizedType === "center") {
|
|
266
|
-
el.style.display = "flex";
|
|
267
|
-
el.style.alignItems = "center";
|
|
268
|
-
el.style.justifyContent = "center";
|
|
269
|
-
} else if (normalizedType === "text") {
|
|
270
|
-
// Text content from props
|
|
271
|
-
if (props["0"]) {
|
|
272
|
-
el.textContent = String(props["0"]);
|
|
273
|
-
}
|
|
274
|
-
} else if (normalizedType === "button") {
|
|
275
|
-
el.style.cursor = "pointer";
|
|
276
|
-
} else if (normalizedType === "checkbox" || normalizedType === "switch") {
|
|
277
|
-
(el as HTMLInputElement).type = "checkbox";
|
|
278
|
-
} else if (normalizedType === "slider") {
|
|
279
|
-
(el as HTMLInputElement).type = "range";
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return el;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Apply a prop to an element
|
|
287
|
-
*/
|
|
288
|
-
function applyProp(el: HTMLElement, name: string, value: any): void {
|
|
289
|
-
// Text content
|
|
290
|
-
if (name === "0" || name === "text") {
|
|
291
|
-
el.textContent = String(value);
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Style props
|
|
296
|
-
const styleProps: Record<string, string> = {
|
|
297
|
-
padding: "padding",
|
|
298
|
-
margin: "margin",
|
|
299
|
-
backgroundColor: "backgroundColor",
|
|
300
|
-
background: "background",
|
|
301
|
-
color: "color",
|
|
302
|
-
fontSize: "fontSize",
|
|
303
|
-
fontWeight: "fontWeight",
|
|
304
|
-
width: "width",
|
|
305
|
-
height: "height",
|
|
306
|
-
minWidth: "minWidth",
|
|
307
|
-
minHeight: "minHeight",
|
|
308
|
-
maxWidth: "maxWidth",
|
|
309
|
-
maxHeight: "maxHeight",
|
|
310
|
-
borderRadius: "borderRadius",
|
|
311
|
-
border: "border",
|
|
312
|
-
gap: "gap",
|
|
313
|
-
flex: "flex",
|
|
314
|
-
opacity: "opacity",
|
|
315
|
-
overflow: "overflow",
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
if (styleProps[name]) {
|
|
319
|
-
const cssValue = typeof value === "number" ? `${value}px` : String(value);
|
|
320
|
-
(el.style as any)[styleProps[name]] = cssValue;
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Event handlers
|
|
325
|
-
if (name === "onClick" || name === "onclick") {
|
|
326
|
-
el.onclick = () => {
|
|
327
|
-
const engine = (el as any).__hypenEngine as RemoteEngine;
|
|
328
|
-
if (engine && typeof value === "string" && value.startsWith("@actions.")) {
|
|
329
|
-
const action = value.replace("@actions.", "");
|
|
330
|
-
engine.dispatchAction(action);
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Other attributes
|
|
337
|
-
el.setAttribute(name, String(value));
|
|
338
|
-
}
|
|
339
|
-
|
|
340
312
|
/**
|
|
341
313
|
* Disconnect a HypenApp instance
|
|
342
314
|
*/
|
|
343
315
|
export function disconnectHypenApp(element: HTMLElement): void {
|
|
344
316
|
const instance = activeInstances.get(element);
|
|
345
317
|
if (instance) {
|
|
346
|
-
instance.engine
|
|
318
|
+
instance.engine?.disconnect();
|
|
347
319
|
activeInstances.delete(element);
|
|
348
320
|
}
|
|
349
321
|
}
|
|
@@ -11,8 +11,8 @@ import type { ComponentHandler } from "./index.js";
|
|
|
11
11
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
12
12
|
|
|
13
13
|
export const iconHandler: ComponentHandler = {
|
|
14
|
-
create(
|
|
15
|
-
const el =
|
|
14
|
+
create(): HTMLElement {
|
|
15
|
+
const el = document.createElement("span");
|
|
16
16
|
el.style.display = "inline-flex";
|
|
17
17
|
el.style.alignItems = "center";
|
|
18
18
|
el.style.justifyContent = "center";
|
|
@@ -22,7 +22,6 @@ export const iconHandler: ComponentHandler = {
|
|
|
22
22
|
},
|
|
23
23
|
|
|
24
24
|
applyProps(el: HTMLElement, props: Record<string, any>): void {
|
|
25
|
-
const doc = el.ownerDocument as Document;
|
|
26
25
|
// Server-resolved icon data (from engine's ResourceRegistry)
|
|
27
26
|
const paths: Array<{
|
|
28
27
|
d: string;
|
|
@@ -40,7 +39,7 @@ export const iconHandler: ComponentHandler = {
|
|
|
40
39
|
|
|
41
40
|
if (paths && Array.isArray(paths)) {
|
|
42
41
|
// Render from pre-resolved SVG path data
|
|
43
|
-
const svg =
|
|
42
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
44
43
|
svg.setAttribute("xmlns", SVG_NS);
|
|
45
44
|
svg.setAttribute("width", String(size));
|
|
46
45
|
svg.setAttribute("height", String(size));
|
|
@@ -49,7 +48,7 @@ export const iconHandler: ComponentHandler = {
|
|
|
49
48
|
svg.style.display = "block";
|
|
50
49
|
|
|
51
50
|
for (const pathData of paths) {
|
|
52
|
-
const path =
|
|
51
|
+
const path = document.createElementNS(SVG_NS, "path");
|
|
53
52
|
path.setAttribute("d", pathData.d);
|
|
54
53
|
path.setAttribute("fill", pathData.fill || "none");
|
|
55
54
|
path.setAttribute(
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import type { ComponentHandler } from "./index.js";
|
|
6
6
|
|
|
7
7
|
export const imageHandler: ComponentHandler = {
|
|
8
|
-
create(
|
|
9
|
-
const el =
|
|
8
|
+
create(): HTMLElement {
|
|
9
|
+
const el = document.createElement("img");
|
|
10
10
|
el.dataset.hypenType = "image";
|
|
11
11
|
return el as any as HTMLElement;
|
|
12
12
|
},
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export interface ComponentHandler {
|
|
8
|
-
create(
|
|
8
|
+
create(): HTMLElement;
|
|
9
9
|
applyProps?(element: HTMLElement, props: Record<string, any>): void;
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -34,12 +34,12 @@ export class ComponentRegistry {
|
|
|
34
34
|
/**
|
|
35
35
|
* Create element for a component type
|
|
36
36
|
*/
|
|
37
|
-
createElement(type: string, props: Record<string, any> = {}
|
|
37
|
+
createElement(type: string, props: Record<string, any> = {}): HTMLElement | null {
|
|
38
38
|
if (!type) return null;
|
|
39
39
|
const handler = this.get(type);
|
|
40
40
|
if (!handler) return null;
|
|
41
41
|
|
|
42
|
-
const element = handler.create(
|
|
42
|
+
const element = handler.create();
|
|
43
43
|
if (handler.applyProps) {
|
|
44
44
|
handler.applyProps(element, props);
|
|
45
45
|
}
|
|
@@ -84,8 +84,11 @@ export class ComponentRegistry {
|
|
|
84
84
|
const { hypenAppHandler } = require("./hypenapp.js");
|
|
85
85
|
const { appHandler } = require("./app.js");
|
|
86
86
|
const { iconHandler } = require("./icon.js");
|
|
87
|
+
const { visuallyHiddenHandler } = require("./visuallyhidden.js");
|
|
88
|
+
const { tabsHandler, tabHandler, tabPanelHandler, optionHandler } = require("./tabs.js");
|
|
87
89
|
|
|
88
90
|
this.register("app", appHandler);
|
|
91
|
+
this.register("visuallyhidden", visuallyHiddenHandler);
|
|
89
92
|
this.register("icon", iconHandler);
|
|
90
93
|
this.register("column", columnHandler);
|
|
91
94
|
this.register("row", rowHandler);
|
|
@@ -116,6 +119,10 @@ export class ComponentRegistry {
|
|
|
116
119
|
this.register("video", videoHandler);
|
|
117
120
|
this.register("audio", audioHandler);
|
|
118
121
|
this.register("paragraph", paragraphHandler);
|
|
122
|
+
this.register("tabs", tabsHandler);
|
|
123
|
+
this.register("tab", tabHandler);
|
|
124
|
+
this.register("tabpanel", tabPanelHandler);
|
|
125
|
+
this.register("option", optionHandler);
|
|
119
126
|
this.register("router", routerHandler);
|
|
120
127
|
this.register("route", routeHandler);
|
|
121
128
|
this.register("hypenapp", hypenAppHandler);
|