@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.
Files changed (229) hide show
  1. package/README.md +20 -13
  2. package/dist/canvas/accessibility.d.ts +118 -11
  3. package/dist/canvas/accessibility.js +470 -108
  4. package/dist/canvas/accessibility.js.map +1 -1
  5. package/dist/canvas/dispatch.d.ts +24 -0
  6. package/dist/canvas/dispatch.js +62 -0
  7. package/dist/canvas/dispatch.js.map +1 -0
  8. package/dist/canvas/editing.d.ts +178 -0
  9. package/dist/canvas/editing.js +590 -0
  10. package/dist/canvas/editing.js.map +1 -0
  11. package/dist/canvas/events.d.ts +28 -23
  12. package/dist/canvas/events.js +90 -103
  13. package/dist/canvas/events.js.map +1 -1
  14. package/dist/canvas/focus.d.ts +78 -0
  15. package/dist/canvas/focus.js +182 -0
  16. package/dist/canvas/focus.js.map +1 -0
  17. package/dist/canvas/index.d.ts +3 -2
  18. package/dist/canvas/index.js +3 -2
  19. package/dist/canvas/index.js.map +1 -1
  20. package/dist/canvas/layout.js +68 -98
  21. package/dist/canvas/layout.js.map +1 -1
  22. package/dist/canvas/paint.d.ts +16 -3
  23. package/dist/canvas/paint.js +449 -110
  24. package/dist/canvas/paint.js.map +1 -1
  25. package/dist/canvas/renderer.d.ts +28 -4
  26. package/dist/canvas/renderer.js +201 -81
  27. package/dist/canvas/renderer.js.map +1 -1
  28. package/dist/canvas/selection.js +31 -177
  29. package/dist/canvas/selection.js.map +1 -1
  30. package/dist/canvas/text-geometry.d.ts +72 -0
  31. package/dist/canvas/text-geometry.js +244 -0
  32. package/dist/canvas/text-geometry.js.map +1 -0
  33. package/dist/canvas/text.js +44 -18
  34. package/dist/canvas/text.js.map +1 -1
  35. package/dist/canvas/types.d.ts +10 -1
  36. package/dist/canvas/utils.d.ts +0 -23
  37. package/dist/canvas/utils.js +3 -67
  38. package/dist/canvas/utils.js.map +1 -1
  39. package/dist/canvas/variants.d.ts +52 -0
  40. package/dist/canvas/variants.js +156 -0
  41. package/dist/canvas/variants.js.map +1 -0
  42. package/dist/dom/a11y-styles.d.ts +20 -0
  43. package/dist/dom/a11y-styles.js +61 -0
  44. package/dist/dom/a11y-styles.js.map +1 -0
  45. package/dist/dom/applicators/aria.d.ts +19 -0
  46. package/dist/dom/applicators/aria.js +36 -0
  47. package/dist/dom/applicators/aria.js.map +1 -0
  48. package/dist/dom/applicators/events.d.ts +7 -0
  49. package/dist/dom/applicators/events.js +36 -8
  50. package/dist/dom/applicators/events.js.map +1 -1
  51. package/dist/dom/applicators/font.d.ts +11 -6
  52. package/dist/dom/applicators/font.js +16 -27
  53. package/dist/dom/applicators/font.js.map +1 -1
  54. package/dist/dom/applicators/index.js +51 -45
  55. package/dist/dom/applicators/index.js.map +1 -1
  56. package/dist/dom/canvas/index.js +2 -2
  57. package/dist/dom/canvas/index.js.map +1 -1
  58. package/dist/dom/components/app.js +2 -2
  59. package/dist/dom/components/app.js.map +1 -1
  60. package/dist/dom/components/audio.js +2 -2
  61. package/dist/dom/components/audio.js.map +1 -1
  62. package/dist/dom/components/avatar.js +3 -4
  63. package/dist/dom/components/avatar.js.map +1 -1
  64. package/dist/dom/components/badge.js +3 -4
  65. package/dist/dom/components/badge.js.map +1 -1
  66. package/dist/dom/components/button.js +2 -2
  67. package/dist/dom/components/button.js.map +1 -1
  68. package/dist/dom/components/card.js +2 -2
  69. package/dist/dom/components/card.js.map +1 -1
  70. package/dist/dom/components/center.js +2 -2
  71. package/dist/dom/components/center.js.map +1 -1
  72. package/dist/dom/components/checkbox.js +4 -5
  73. package/dist/dom/components/checkbox.js.map +1 -1
  74. package/dist/dom/components/column.js +2 -2
  75. package/dist/dom/components/column.js.map +1 -1
  76. package/dist/dom/components/container.js +2 -2
  77. package/dist/dom/components/container.js.map +1 -1
  78. package/dist/dom/components/divider.js +2 -2
  79. package/dist/dom/components/divider.js.map +1 -1
  80. package/dist/dom/components/grid.js +10 -10
  81. package/dist/dom/components/grid.js.map +1 -1
  82. package/dist/dom/components/heading.js +4 -6
  83. package/dist/dom/components/heading.js.map +1 -1
  84. package/dist/dom/components/hypenapp.d.ts +25 -1
  85. package/dist/dom/components/hypenapp.js +215 -247
  86. package/dist/dom/components/hypenapp.js.map +1 -1
  87. package/dist/dom/components/icon.js +4 -5
  88. package/dist/dom/components/icon.js.map +1 -1
  89. package/dist/dom/components/image.js +2 -2
  90. package/dist/dom/components/image.js.map +1 -1
  91. package/dist/dom/components/index.d.ts +2 -2
  92. package/dist/dom/components/index.js +9 -2
  93. package/dist/dom/components/index.js.map +1 -1
  94. package/dist/dom/components/input.js +2 -2
  95. package/dist/dom/components/input.js.map +1 -1
  96. package/dist/dom/components/link.js +2 -2
  97. package/dist/dom/components/link.js.map +1 -1
  98. package/dist/dom/components/list.js +2 -2
  99. package/dist/dom/components/list.js.map +1 -1
  100. package/dist/dom/components/paragraph.js +3 -4
  101. package/dist/dom/components/paragraph.js.map +1 -1
  102. package/dist/dom/components/progressbar.js +3 -3
  103. package/dist/dom/components/progressbar.js.map +1 -1
  104. package/dist/dom/components/route.js +2 -2
  105. package/dist/dom/components/route.js.map +1 -1
  106. package/dist/dom/components/router.js +2 -2
  107. package/dist/dom/components/router.js.map +1 -1
  108. package/dist/dom/components/row.js +10 -10
  109. package/dist/dom/components/row.js.map +1 -1
  110. package/dist/dom/components/select.js +3 -4
  111. package/dist/dom/components/select.js.map +1 -1
  112. package/dist/dom/components/slider.js +2 -2
  113. package/dist/dom/components/slider.js.map +1 -1
  114. package/dist/dom/components/spacer.js +2 -2
  115. package/dist/dom/components/spacer.js.map +1 -1
  116. package/dist/dom/components/spinner.js +27 -13
  117. package/dist/dom/components/spinner.js.map +1 -1
  118. package/dist/dom/components/stack.js +10 -10
  119. package/dist/dom/components/stack.js.map +1 -1
  120. package/dist/dom/components/switch.js +5 -6
  121. package/dist/dom/components/switch.js.map +1 -1
  122. package/dist/dom/components/tabs.d.ts +27 -0
  123. package/dist/dom/components/tabs.js +69 -0
  124. package/dist/dom/components/tabs.js.map +1 -0
  125. package/dist/dom/components/text.js +4 -7
  126. package/dist/dom/components/text.js.map +1 -1
  127. package/dist/dom/components/textarea.js +2 -2
  128. package/dist/dom/components/textarea.js.map +1 -1
  129. package/dist/dom/components/video.js +2 -2
  130. package/dist/dom/components/video.js.map +1 -1
  131. package/dist/dom/components/visuallyhidden.d.ts +9 -0
  132. package/dist/dom/components/visuallyhidden.js +26 -0
  133. package/dist/dom/components/visuallyhidden.js.map +1 -0
  134. package/dist/dom/debug.js +6 -8
  135. package/dist/dom/debug.js.map +1 -1
  136. package/dist/dom/index.d.ts +0 -1
  137. package/dist/dom/index.js +0 -1
  138. package/dist/dom/index.js.map +1 -1
  139. package/dist/dom/operability.d.ts +100 -0
  140. package/dist/dom/operability.js +298 -0
  141. package/dist/dom/operability.js.map +1 -0
  142. package/dist/dom/renderer.d.ts +98 -5
  143. package/dist/dom/renderer.js +400 -66
  144. package/dist/dom/renderer.js.map +1 -1
  145. package/dist/dom/route-focus.d.ts +42 -0
  146. package/dist/dom/route-focus.js +88 -0
  147. package/dist/dom/route-focus.js.map +1 -0
  148. package/dist/dom/semantics.d.ts +35 -0
  149. package/dist/dom/semantics.js +184 -0
  150. package/dist/dom/semantics.js.map +1 -0
  151. package/dist/variants.d.ts +90 -0
  152. package/dist/variants.js +206 -0
  153. package/dist/variants.js.map +1 -0
  154. package/package.json +2 -2
  155. package/src/canvas/QUICKSTART.md +16 -11
  156. package/src/canvas/README.md +63 -34
  157. package/src/canvas/accessibility.ts +507 -115
  158. package/src/canvas/dispatch.ts +78 -0
  159. package/src/canvas/editing.ts +697 -0
  160. package/src/canvas/events.ts +104 -115
  161. package/src/canvas/focus.ts +216 -0
  162. package/src/canvas/index.ts +8 -9
  163. package/src/canvas/layout.ts +68 -103
  164. package/src/canvas/paint.ts +509 -109
  165. package/src/canvas/renderer.ts +239 -99
  166. package/src/canvas/selection.ts +52 -210
  167. package/src/canvas/text-geometry.ts +311 -0
  168. package/src/canvas/text.ts +45 -18
  169. package/src/canvas/types.ts +28 -1
  170. package/src/canvas/utils.ts +3 -69
  171. package/src/canvas/variants.ts +172 -0
  172. package/src/dom/README.md +8 -7
  173. package/src/dom/a11y-styles.ts +65 -0
  174. package/src/dom/applicators/aria.ts +41 -0
  175. package/src/dom/applicators/events.ts +39 -7
  176. package/src/dom/applicators/font.ts +16 -29
  177. package/src/dom/applicators/index.ts +61 -46
  178. package/src/dom/canvas/index.ts +2 -2
  179. package/src/dom/components/app.ts +2 -2
  180. package/src/dom/components/audio.ts +2 -2
  181. package/src/dom/components/avatar.ts +3 -4
  182. package/src/dom/components/badge.ts +3 -4
  183. package/src/dom/components/button.ts +2 -2
  184. package/src/dom/components/card.ts +2 -2
  185. package/src/dom/components/center.ts +2 -2
  186. package/src/dom/components/checkbox.ts +4 -5
  187. package/src/dom/components/column.ts +2 -2
  188. package/src/dom/components/container.ts +2 -2
  189. package/src/dom/components/divider.ts +2 -2
  190. package/src/dom/components/grid.ts +10 -10
  191. package/src/dom/components/heading.ts +4 -6
  192. package/src/dom/components/hypenapp.ts +246 -274
  193. package/src/dom/components/icon.ts +4 -5
  194. package/src/dom/components/image.ts +2 -2
  195. package/src/dom/components/index.ts +10 -3
  196. package/src/dom/components/input.ts +2 -2
  197. package/src/dom/components/link.ts +2 -2
  198. package/src/dom/components/list.ts +2 -2
  199. package/src/dom/components/paragraph.ts +3 -4
  200. package/src/dom/components/progressbar.ts +3 -3
  201. package/src/dom/components/route.ts +2 -2
  202. package/src/dom/components/router.ts +2 -2
  203. package/src/dom/components/row.ts +10 -10
  204. package/src/dom/components/select.ts +3 -4
  205. package/src/dom/components/slider.ts +2 -2
  206. package/src/dom/components/spacer.ts +2 -2
  207. package/src/dom/components/spinner.ts +34 -16
  208. package/src/dom/components/stack.ts +10 -10
  209. package/src/dom/components/switch.ts +5 -6
  210. package/src/dom/components/tabs.ts +76 -0
  211. package/src/dom/components/text.ts +4 -7
  212. package/src/dom/components/textarea.ts +2 -2
  213. package/src/dom/components/video.ts +2 -2
  214. package/src/dom/components/visuallyhidden.ts +30 -0
  215. package/src/dom/debug.ts +6 -8
  216. package/src/dom/index.ts +0 -8
  217. package/src/dom/operability.ts +312 -0
  218. package/src/dom/renderer.ts +457 -66
  219. package/src/dom/route-focus.ts +98 -0
  220. package/src/dom/semantics.ts +199 -0
  221. package/src/variants.ts +249 -0
  222. package/dist/canvas/input.d.ts +0 -76
  223. package/dist/canvas/input.js +0 -207
  224. package/dist/canvas/input.js.map +0 -1
  225. package/dist/client.d.ts +0 -36
  226. package/dist/client.js +0 -47
  227. package/dist/client.js.map +0 -1
  228. package/src/canvas/input.ts +0 -251
  229. package/src/client.ts +0 -88
@@ -1,207 +0,0 @@
1
- /**
2
- * Input Overlay System
3
- *
4
- * Handle text input using DOM overlay elements
5
- */
6
- import { getAbsoluteBounds, cssLengthToPx } from "./utils.js";
7
- /**
8
- * Input Overlay Manager
9
- */
10
- export class InputOverlay {
11
- container;
12
- overlay = null;
13
- focusedNode = null;
14
- onChangeCallback = null;
15
- bindPath = null;
16
- engine = null;
17
- constructor(container) {
18
- this.container = container || {};
19
- }
20
- /**
21
- * Show input overlay for a node.
22
- *
23
- * Type comparison is case-insensitive — the engine emits `"Input"` /
24
- * `"Textarea"`, but the overlay branch was looking for lowercase
25
- * `"textarea"` and always created an `<input>` (so multi-line typing
26
- * couldn't enter newlines and the visible caret was the wrong height).
27
- */
28
- showInput(node, canvasBounds, onChange, engine) {
29
- // Skip if document is not available (non-browser environment)
30
- if (typeof document === "undefined")
31
- return;
32
- this.hideInput();
33
- // Store bind path and engine for two-way binding
34
- this.bindPath = node.props.bind || null;
35
- this.engine = engine || null;
36
- const bounds = getAbsoluteBounds(node);
37
- if (!bounds)
38
- return;
39
- const isMultiline = node.type.toLowerCase() === "textarea";
40
- this.overlay = isMultiline
41
- ? document.createElement("textarea")
42
- : document.createElement("input");
43
- // Style overlay
44
- this.styleOverlay(node, bounds, canvasBounds);
45
- // Set initial value
46
- const value = node.props.value || "";
47
- this.overlay.value = value;
48
- // Setup event handlers
49
- this.onChangeCallback = onChange;
50
- this.overlay.addEventListener("input", this.onInput.bind(this));
51
- this.overlay.addEventListener("blur", this.onBlur.bind(this));
52
- this.overlay.addEventListener("keydown", this.onKeyDown.bind(this));
53
- // Mount on document.body so absolute positioning is relative to the
54
- // viewport (`canvasBounds.left/top` are page coords too) — no
55
- // dependency on the canvas's wrapper having `position: relative`.
56
- document.body.appendChild(this.overlay);
57
- this.overlay.focus();
58
- this.focusedNode = node;
59
- }
60
- /**
61
- * Hide input overlay
62
- */
63
- hideInput() {
64
- if (this.overlay) {
65
- this.overlay.remove();
66
- this.overlay = null;
67
- }
68
- this.focusedNode = null;
69
- this.onChangeCallback = null;
70
- this.bindPath = null;
71
- this.engine = null;
72
- }
73
- /**
74
- * Update overlay position
75
- */
76
- updatePosition(node, canvasBounds) {
77
- if (!this.overlay || node !== this.focusedNode)
78
- return;
79
- const bounds = getAbsoluteBounds(node);
80
- if (!bounds)
81
- return;
82
- this.positionOverlay(bounds, canvasBounds);
83
- }
84
- /**
85
- * Style overlay to match canvas node
86
- */
87
- styleOverlay(node, bounds, canvasBounds) {
88
- if (!this.overlay)
89
- return;
90
- const props = node.props;
91
- // Position
92
- this.positionOverlay(bounds, canvasBounds);
93
- // Font and text styling — go through `cssLengthToPx` so values that
94
- // arrive in `rem` ("0.875rem" for `text-sm`) round to real pixels
95
- // instead of `parseFloat`'s bare 0.875 (which made the caret disappear).
96
- const fontSize = cssLengthToPx(props.fontSize) ?? 16;
97
- const fontWeight = props.fontWeight || "normal";
98
- const fontFamily = props.fontFamily || "system-ui, sans-serif";
99
- const color = props.color || "#000000";
100
- const padTop = cssLengthToPx(props.paddingTop ?? props.padding) ?? 0;
101
- const padRight = cssLengthToPx(props.paddingRight ?? props.padding) ?? 0;
102
- const padBottom = cssLengthToPx(props.paddingBottom ?? props.padding) ?? 0;
103
- const padLeft = cssLengthToPx(props.paddingLeft ?? props.padding) ?? 0;
104
- const borderRadius = cssLengthToPx(props.borderRadius) ?? 4;
105
- Object.assign(this.overlay.style, {
106
- fontSize: `${fontSize}px`,
107
- fontWeight: String(fontWeight),
108
- fontFamily: fontFamily,
109
- color: color,
110
- border: "none",
111
- outline: "none",
112
- backgroundColor: props.backgroundColor || "#ffffff",
113
- padding: `${padTop}px ${padRight}px ${padBottom}px ${padLeft}px`,
114
- borderRadius: `${borderRadius}px`,
115
- boxSizing: "border-box",
116
- resize: "none",
117
- // Keep the overlay above the canvas; the Bun-served stage has no
118
- // stacking context of its own so a plain z-index works.
119
- zIndex: "1000",
120
- });
121
- // Placeholder
122
- if (props.placeholder) {
123
- this.overlay.placeholder = props.placeholder;
124
- }
125
- // Input type
126
- if (this.overlay instanceof HTMLInputElement && props.type) {
127
- this.overlay.type = props.type;
128
- }
129
- }
130
- /**
131
- * Position overlay over canvas node.
132
- *
133
- * `bounds` is in canvas-local CSS pixels (the layout coordinate space
134
- * the renderer paints into after `ctx.scale(dpr, dpr)`). The overlay is
135
- * mounted on `document.body`, so we add `canvasBounds.left/top`
136
- * (page coords from `getBoundingClientRect`) to land at the right
137
- * absolute position regardless of where the canvas sits on the page.
138
- * Adding `window.scrollX/Y` keeps the overlay pinned to the canvas
139
- * even when the page scrolls between focus and reposition.
140
- */
141
- positionOverlay(bounds, canvasBounds) {
142
- if (!this.overlay)
143
- return;
144
- const sx = typeof window !== "undefined" ? window.scrollX : 0;
145
- const sy = typeof window !== "undefined" ? window.scrollY : 0;
146
- Object.assign(this.overlay.style, {
147
- position: "absolute",
148
- left: `${canvasBounds.left + sx + bounds.x}px`,
149
- top: `${canvasBounds.top + sy + bounds.y}px`,
150
- width: `${bounds.width}px`,
151
- height: `${bounds.height}px`,
152
- });
153
- }
154
- /**
155
- * Handle input event
156
- */
157
- onInput(e) {
158
- if (!this.overlay || !this.onChangeCallback)
159
- return;
160
- const value = this.overlay.value;
161
- this.onChangeCallback(value);
162
- // Dispatch __hypen_bind for two-way binding
163
- if (this.bindPath && this.engine) {
164
- this.engine.dispatchAction("__hypen_bind", {
165
- path: this.bindPath,
166
- value,
167
- });
168
- }
169
- }
170
- /**
171
- * Handle blur event
172
- */
173
- onBlur() {
174
- // Hide overlay when focus is lost
175
- this.hideInput();
176
- }
177
- /**
178
- * Handle keyboard events
179
- */
180
- onKeyDown(e) {
181
- if (!this.overlay)
182
- return;
183
- // Enter key submits (unless multiline)
184
- if (e.key === "Enter" && this.overlay instanceof HTMLInputElement) {
185
- e.preventDefault();
186
- this.overlay.blur();
187
- }
188
- // Escape cancels
189
- if (e.key === "Escape") {
190
- e.preventDefault();
191
- this.overlay.blur();
192
- }
193
- }
194
- /**
195
- * Check if input is currently shown
196
- */
197
- isShown() {
198
- return this.overlay !== null;
199
- }
200
- /**
201
- * Get current focused node
202
- */
203
- getFocusedNode() {
204
- return this.focusedNode;
205
- }
206
- }
207
- //# sourceMappingURL=input.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"input.js","sourceRoot":"","sources":["../../src/canvas/input.ts"],"names":[],"mappings":"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"}
package/dist/client.d.ts DELETED
@@ -1,36 +0,0 @@
1
- /**
2
- * createHypenClient — single-call wiring for renderer + engine.
3
- *
4
- * Replaces the boilerplate:
5
- * const renderer = new DOMRenderer(container, engine);
6
- * engine.onPatches((patches) => renderer.applyPatches(patches));
7
- *
8
- * Works with either a local Engine (`setRenderCallback`) or RemoteEngine
9
- * (`onPatches`) — they expose different APIs but the same intent.
10
- */
11
- import type { Patch } from "@hypen-space/core/types";
12
- import { DOMRenderer } from "./dom/renderer.js";
13
- import type { DebugConfig } from "./dom/debug.js";
14
- import { CanvasRenderer } from "./canvas/renderer.js";
15
- import type { CanvasRendererOptions } from "./canvas/types.js";
16
- interface PatchSourceLike {
17
- dispatchAction(name: string, payload?: unknown): void;
18
- onPatches?(callback: (patches: Patch[]) => void): unknown;
19
- setRenderCallback?(callback: (patches: Patch[]) => void): void;
20
- }
21
- export interface DomClientOptions {
22
- debug?: Partial<DebugConfig>;
23
- }
24
- export interface DomClient {
25
- renderer: DOMRenderer;
26
- destroy(): void;
27
- }
28
- export declare function createDomClient(container: HTMLElement, engine: PatchSourceLike, options?: DomClientOptions): DomClient;
29
- export interface CanvasClientOptions extends Partial<CanvasRendererOptions> {
30
- }
31
- export interface CanvasClient {
32
- renderer: CanvasRenderer;
33
- destroy(): void;
34
- }
35
- export declare function createCanvasClient(canvas: HTMLCanvasElement, engine: PatchSourceLike, options?: CanvasClientOptions): CanvasClient;
36
- export {};
package/dist/client.js DELETED
@@ -1,47 +0,0 @@
1
- /**
2
- * createHypenClient — single-call wiring for renderer + engine.
3
- *
4
- * Replaces the boilerplate:
5
- * const renderer = new DOMRenderer(container, engine);
6
- * engine.onPatches((patches) => renderer.applyPatches(patches));
7
- *
8
- * Works with either a local Engine (`setRenderCallback`) or RemoteEngine
9
- * (`onPatches`) — they expose different APIs but the same intent.
10
- */
11
- import { DOMRenderer } from "./dom/renderer.js";
12
- import { CanvasRenderer } from "./canvas/renderer.js";
13
- function attachPatchListener(engine, callback) {
14
- if (typeof engine.onPatches === "function") {
15
- engine.onPatches(callback);
16
- return;
17
- }
18
- if (typeof engine.setRenderCallback === "function") {
19
- engine.setRenderCallback(callback);
20
- return;
21
- }
22
- throw new Error("createHypenClient: engine has neither onPatches() nor setRenderCallback() — cannot subscribe to patches.");
23
- }
24
- export function createDomClient(container, engine, options) {
25
- const renderer = new DOMRenderer(container, engine, options?.debug);
26
- attachPatchListener(engine, (patches) => renderer.applyPatches(patches));
27
- return {
28
- renderer,
29
- destroy() {
30
- // DOMRenderer has no destroy yet; clearing the container is the
31
- // closest approximation. Patch subscriptions live on the engine —
32
- // the caller controls engine lifetime, so we don't tear them down.
33
- container.innerHTML = "";
34
- },
35
- };
36
- }
37
- export function createCanvasClient(canvas, engine, options) {
38
- const renderer = new CanvasRenderer(canvas, engine, options);
39
- attachPatchListener(engine, (patches) => renderer.applyPatches(patches));
40
- return {
41
- renderer,
42
- destroy() {
43
- renderer.destroy();
44
- },
45
- };
46
- }
47
- //# sourceMappingURL=client.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAStD,SAAS,mBAAmB,CAC1B,MAAuB,EACvB,QAAoC;IAEpC,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,UAAU,EAAE,CAAC;QAC3C,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC3B,OAAO;IACT,CAAC;IACD,IAAI,OAAO,MAAM,CAAC,iBAAiB,KAAK,UAAU,EAAE,CAAC;QACnD,MAAM,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QACnC,OAAO;IACT,CAAC;IACD,MAAM,IAAI,KAAK,CACb,0GAA0G,CAC3G,CAAC;AACJ,CAAC;AAWD,MAAM,UAAU,eAAe,CAC7B,SAAsB,EACtB,MAAuB,EACvB,OAA0B;IAE1B,MAAM,QAAQ,GAAG,IAAI,WAAW,CAAC,SAAS,EAAE,MAAa,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAC3E,mBAAmB,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;IACzE,OAAO;QACL,QAAQ;QACR,OAAO;YACL,gEAAgE;YAChE,kEAAkE;YAClE,mEAAmE;YACnE,SAAS,CAAC,SAAS,GAAG,EAAE,CAAC;QAC3B,CAAC;KACF,CAAC;AACJ,CAAC;AASD,MAAM,UAAU,kBAAkB,CAChC,MAAyB,EACzB,MAAuB,EACvB,OAA6B;IAE7B,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAC,MAAM,EAAE,MAAa,EAAE,OAAO,CAAC,CAAC;IACpE,mBAAmB,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;IACzE,OAAO;QACL,QAAQ;QACR,OAAO;YACL,QAAQ,CAAC,OAAO,EAAE,CAAC;QACrB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -1,251 +0,0 @@
1
- /**
2
- * Input Overlay System
3
- *
4
- * Handle text input using DOM overlay elements
5
- */
6
-
7
- import type { VirtualNode, Rectangle } from "./types.js";
8
- import { getAbsoluteBounds, cssLengthToPx } from "./utils.js";
9
-
10
- /** Minimal engine interface for dispatching bind actions */
11
- interface BindEngine {
12
- dispatchAction(name: string, payload?: unknown): void;
13
- }
14
-
15
- /**
16
- * Input Overlay Manager
17
- */
18
- export class InputOverlay {
19
- private container: HTMLElement;
20
- private overlay: HTMLInputElement | HTMLTextAreaElement | null = null;
21
- private focusedNode: VirtualNode | null = null;
22
- private onChangeCallback: ((value: string) => void) | null = null;
23
- private bindPath: string | null = null;
24
- private engine: BindEngine | null = null;
25
-
26
- constructor(container: HTMLElement | null) {
27
- this.container = container || ({} as HTMLElement);
28
- }
29
-
30
- /**
31
- * Show input overlay for a node.
32
- *
33
- * Type comparison is case-insensitive — the engine emits `"Input"` /
34
- * `"Textarea"`, but the overlay branch was looking for lowercase
35
- * `"textarea"` and always created an `<input>` (so multi-line typing
36
- * couldn't enter newlines and the visible caret was the wrong height).
37
- */
38
- showInput(
39
- node: VirtualNode,
40
- canvasBounds: DOMRect,
41
- onChange: (value: string) => void,
42
- engine?: BindEngine | null
43
- ): void {
44
- // Skip if document is not available (non-browser environment)
45
- if (typeof document === "undefined") return;
46
-
47
- this.hideInput();
48
-
49
- // Store bind path and engine for two-way binding
50
- this.bindPath = (node.props.bind as string) || null;
51
- this.engine = engine || null;
52
-
53
- const bounds = getAbsoluteBounds(node);
54
- if (!bounds) return;
55
-
56
- const isMultiline = node.type.toLowerCase() === "textarea";
57
- this.overlay = isMultiline
58
- ? document.createElement("textarea")
59
- : document.createElement("input");
60
-
61
- // Style overlay
62
- this.styleOverlay(node, bounds, canvasBounds);
63
-
64
- // Set initial value
65
- const value = node.props.value || "";
66
- this.overlay.value = value;
67
-
68
- // Setup event handlers
69
- this.onChangeCallback = onChange;
70
- this.overlay.addEventListener("input", this.onInput.bind(this));
71
- this.overlay.addEventListener("blur", this.onBlur.bind(this));
72
- this.overlay.addEventListener("keydown", this.onKeyDown.bind(this) as EventListener);
73
-
74
- // Mount on document.body so absolute positioning is relative to the
75
- // viewport (`canvasBounds.left/top` are page coords too) — no
76
- // dependency on the canvas's wrapper having `position: relative`.
77
- document.body.appendChild(this.overlay);
78
- this.overlay.focus();
79
-
80
- this.focusedNode = node;
81
- }
82
-
83
- /**
84
- * Hide input overlay
85
- */
86
- hideInput(): void {
87
- if (this.overlay) {
88
- this.overlay.remove();
89
- this.overlay = null;
90
- }
91
- this.focusedNode = null;
92
- this.onChangeCallback = null;
93
- this.bindPath = null;
94
- this.engine = null;
95
- }
96
-
97
- /**
98
- * Update overlay position
99
- */
100
- updatePosition(node: VirtualNode, canvasBounds: DOMRect): void {
101
- if (!this.overlay || node !== this.focusedNode) return;
102
-
103
- const bounds = getAbsoluteBounds(node);
104
- if (!bounds) return;
105
-
106
- this.positionOverlay(bounds, canvasBounds);
107
- }
108
-
109
- /**
110
- * Style overlay to match canvas node
111
- */
112
- private styleOverlay(
113
- node: VirtualNode,
114
- bounds: Rectangle,
115
- canvasBounds: DOMRect
116
- ): void {
117
- if (!this.overlay) return;
118
-
119
- const props = node.props;
120
-
121
- // Position
122
- this.positionOverlay(bounds, canvasBounds);
123
-
124
- // Font and text styling — go through `cssLengthToPx` so values that
125
- // arrive in `rem` ("0.875rem" for `text-sm`) round to real pixels
126
- // instead of `parseFloat`'s bare 0.875 (which made the caret disappear).
127
- const fontSize = cssLengthToPx(props.fontSize) ?? 16;
128
- const fontWeight = props.fontWeight || "normal";
129
- const fontFamily = props.fontFamily || "system-ui, sans-serif";
130
- const color = props.color || "#000000";
131
-
132
- const padTop = cssLengthToPx(props.paddingTop ?? props.padding) ?? 0;
133
- const padRight = cssLengthToPx(props.paddingRight ?? props.padding) ?? 0;
134
- const padBottom = cssLengthToPx(props.paddingBottom ?? props.padding) ?? 0;
135
- const padLeft = cssLengthToPx(props.paddingLeft ?? props.padding) ?? 0;
136
- const borderRadius = cssLengthToPx(props.borderRadius) ?? 4;
137
-
138
- Object.assign(this.overlay.style, {
139
- fontSize: `${fontSize}px`,
140
- fontWeight: String(fontWeight),
141
- fontFamily: fontFamily,
142
- color: color,
143
- border: "none",
144
- outline: "none",
145
- backgroundColor: props.backgroundColor || "#ffffff",
146
- padding: `${padTop}px ${padRight}px ${padBottom}px ${padLeft}px`,
147
- borderRadius: `${borderRadius}px`,
148
- boxSizing: "border-box",
149
- resize: "none",
150
- // Keep the overlay above the canvas; the Bun-served stage has no
151
- // stacking context of its own so a plain z-index works.
152
- zIndex: "1000",
153
- });
154
-
155
- // Placeholder
156
- if (props.placeholder) {
157
- this.overlay.placeholder = props.placeholder;
158
- }
159
-
160
- // Input type
161
- if (this.overlay instanceof HTMLInputElement && props.type) {
162
- this.overlay.type = props.type;
163
- }
164
- }
165
-
166
- /**
167
- * Position overlay over canvas node.
168
- *
169
- * `bounds` is in canvas-local CSS pixels (the layout coordinate space
170
- * the renderer paints into after `ctx.scale(dpr, dpr)`). The overlay is
171
- * mounted on `document.body`, so we add `canvasBounds.left/top`
172
- * (page coords from `getBoundingClientRect`) to land at the right
173
- * absolute position regardless of where the canvas sits on the page.
174
- * Adding `window.scrollX/Y` keeps the overlay pinned to the canvas
175
- * even when the page scrolls between focus and reposition.
176
- */
177
- private positionOverlay(bounds: Rectangle, canvasBounds: DOMRect): void {
178
- if (!this.overlay) return;
179
-
180
- const sx = typeof window !== "undefined" ? window.scrollX : 0;
181
- const sy = typeof window !== "undefined" ? window.scrollY : 0;
182
-
183
- Object.assign(this.overlay.style, {
184
- position: "absolute",
185
- left: `${canvasBounds.left + sx + bounds.x}px`,
186
- top: `${canvasBounds.top + sy + bounds.y}px`,
187
- width: `${bounds.width}px`,
188
- height: `${bounds.height}px`,
189
- });
190
- }
191
-
192
- /**
193
- * Handle input event
194
- */
195
- private onInput(e: Event): void {
196
- if (!this.overlay || !this.onChangeCallback) return;
197
-
198
- const value = this.overlay.value;
199
- this.onChangeCallback(value);
200
-
201
- // Dispatch __hypen_bind for two-way binding
202
- if (this.bindPath && this.engine) {
203
- this.engine.dispatchAction("__hypen_bind", {
204
- path: this.bindPath,
205
- value,
206
- });
207
- }
208
- }
209
-
210
- /**
211
- * Handle blur event
212
- */
213
- private onBlur(): void {
214
- // Hide overlay when focus is lost
215
- this.hideInput();
216
- }
217
-
218
- /**
219
- * Handle keyboard events
220
- */
221
- private onKeyDown(e: KeyboardEvent): void {
222
- if (!this.overlay) return;
223
-
224
- // Enter key submits (unless multiline)
225
- if (e.key === "Enter" && this.overlay instanceof HTMLInputElement) {
226
- e.preventDefault();
227
- this.overlay.blur();
228
- }
229
-
230
- // Escape cancels
231
- if (e.key === "Escape") {
232
- e.preventDefault();
233
- this.overlay.blur();
234
- }
235
- }
236
-
237
- /**
238
- * Check if input is currently shown
239
- */
240
- isShown(): boolean {
241
- return this.overlay !== null;
242
- }
243
-
244
- /**
245
- * Get current focused node
246
- */
247
- getFocusedNode(): VirtualNode | null {
248
- return this.focusedNode;
249
- }
250
- }
251
-
package/src/client.ts DELETED
@@ -1,88 +0,0 @@
1
- /**
2
- * createHypenClient — single-call wiring for renderer + engine.
3
- *
4
- * Replaces the boilerplate:
5
- * const renderer = new DOMRenderer(container, engine);
6
- * engine.onPatches((patches) => renderer.applyPatches(patches));
7
- *
8
- * Works with either a local Engine (`setRenderCallback`) or RemoteEngine
9
- * (`onPatches`) — they expose different APIs but the same intent.
10
- */
11
-
12
- import type { Patch } from "@hypen-space/core/types";
13
- import { DOMRenderer } from "./dom/renderer.js";
14
- import type { DebugConfig } from "./dom/debug.js";
15
- import { CanvasRenderer } from "./canvas/renderer.js";
16
- import type { CanvasRendererOptions } from "./canvas/types.js";
17
-
18
- interface PatchSourceLike {
19
- dispatchAction(name: string, payload?: unknown): void;
20
- onPatches?(callback: (patches: Patch[]) => void): unknown;
21
- setRenderCallback?(callback: (patches: Patch[]) => void): void;
22
- }
23
-
24
- function attachPatchListener(
25
- engine: PatchSourceLike,
26
- callback: (patches: Patch[]) => void,
27
- ): void {
28
- if (typeof engine.onPatches === "function") {
29
- engine.onPatches(callback);
30
- return;
31
- }
32
- if (typeof engine.setRenderCallback === "function") {
33
- engine.setRenderCallback(callback);
34
- return;
35
- }
36
- throw new Error(
37
- "createHypenClient: engine has neither onPatches() nor setRenderCallback() — cannot subscribe to patches.",
38
- );
39
- }
40
-
41
- export interface DomClientOptions {
42
- debug?: Partial<DebugConfig>;
43
- }
44
-
45
- export interface DomClient {
46
- renderer: DOMRenderer;
47
- destroy(): void;
48
- }
49
-
50
- export function createDomClient(
51
- container: HTMLElement,
52
- engine: PatchSourceLike,
53
- options?: DomClientOptions,
54
- ): DomClient {
55
- const renderer = new DOMRenderer(container, engine as any, options?.debug);
56
- attachPatchListener(engine, (patches) => renderer.applyPatches(patches));
57
- return {
58
- renderer,
59
- destroy() {
60
- // DOMRenderer has no destroy yet; clearing the container is the
61
- // closest approximation. Patch subscriptions live on the engine —
62
- // the caller controls engine lifetime, so we don't tear them down.
63
- container.innerHTML = "";
64
- },
65
- };
66
- }
67
-
68
- export interface CanvasClientOptions extends Partial<CanvasRendererOptions> {}
69
-
70
- export interface CanvasClient {
71
- renderer: CanvasRenderer;
72
- destroy(): void;
73
- }
74
-
75
- export function createCanvasClient(
76
- canvas: HTMLCanvasElement,
77
- engine: PatchSourceLike,
78
- options?: CanvasClientOptions,
79
- ): CanvasClient {
80
- const renderer = new CanvasRenderer(canvas, engine as any, options);
81
- attachPatchListener(engine, (patches) => renderer.applyPatches(patches));
82
- return {
83
- renderer,
84
- destroy() {
85
- renderer.destroy();
86
- },
87
- };
88
- }