@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
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Focus Manager
3
+ *
4
+ * Real DOM focus on accessibility-mirror elements (canvas fallback content)
5
+ * is the single source of truth for canvas focus. Tab/Shift+Tab traverse
6
+ * the mirror natively; `focusin`/`focusout` translate DOM focus into
7
+ * `node.focused` + repaint; the pointer path (canvas hit-test) funnels into
8
+ * the same place by focusing the node's mirror element.
9
+ *
10
+ * Loop-free by construction: DOM focus events only ever *read* into node
11
+ * state — nothing in the sync path calls `.focus()` back.
12
+ *
13
+ * The mirror also carries keyboard/AT interaction back to the engine:
14
+ * Enter/Space on a mirror `<button>` produces a native `click` (dispatched
15
+ * to the node's action), and keydown/keyup on the focused element dispatch
16
+ * the node's `onKeyDown`/`onKeyUp` applicators — which the old
17
+ * canvas-attached listeners never could (a canvas without tabindex never
18
+ * receives key events).
19
+ */
20
+ import { dispatchNodeEvent } from "./dispatch.js";
21
+ export class FocusManager {
22
+ mirror;
23
+ engine;
24
+ hooks;
25
+ focusedNode = null;
26
+ boundFocusIn = (e) => this.onFocusIn(e);
27
+ boundFocusOut = (e) => this.onFocusOut(e);
28
+ boundClick = (e) => this.onMirrorClick(e);
29
+ boundKeyDown = (e) => this.onMirrorKey("keydown", e);
30
+ boundKeyUp = (e) => this.onMirrorKey("keyup", e);
31
+ constructor(mirror, engine, hooks) {
32
+ this.mirror = mirror;
33
+ this.engine = engine;
34
+ this.hooks = hooks;
35
+ const root = mirror.getRoot();
36
+ if (root && typeof root.addEventListener === "function") {
37
+ root.addEventListener("focusin", this.boundFocusIn);
38
+ root.addEventListener("focusout", this.boundFocusOut);
39
+ root.addEventListener("click", this.boundClick);
40
+ root.addEventListener("keydown", this.boundKeyDown);
41
+ root.addEventListener("keyup", this.boundKeyUp);
42
+ }
43
+ }
44
+ // -------------------------------------------------------------------------
45
+ // DOM → node state (the only direction focus state flows)
46
+ // -------------------------------------------------------------------------
47
+ /** Climb from an event target to the mirror element's virtual node. */
48
+ resolveNode(target) {
49
+ let el = target;
50
+ const root = this.mirror.getRoot();
51
+ while (el && el !== root) {
52
+ const id = el.getAttribute?.("data-hypen-id");
53
+ if (id) {
54
+ return this.hooks.getNode(id) ?? null;
55
+ }
56
+ el = el.parentNode ?? null;
57
+ }
58
+ return null;
59
+ }
60
+ onFocusIn(e) {
61
+ this.syncFocus(this.resolveNode(e.target));
62
+ }
63
+ onFocusOut(e) {
64
+ // If focus moves to another mirror element, its focusin handles the
65
+ // transition; a handoff to the editor's IME proxy keeps the node
66
+ // focused; only a departure from both clears focus.
67
+ const next = e.relatedTarget;
68
+ if (next && this.hooks.isAuxFocusTarget?.(next))
69
+ return;
70
+ const root = this.mirror.getRoot();
71
+ if (!next || !root || !(root.contains?.(next) ?? false)) {
72
+ this.syncFocus(null);
73
+ }
74
+ }
75
+ /** Update node.focused flags from the new DOM focus target. Idempotent. */
76
+ syncFocus(node) {
77
+ if (node === this.focusedNode)
78
+ return;
79
+ const prev = this.focusedNode;
80
+ if (prev) {
81
+ prev.focused = false;
82
+ dispatchNodeEvent(this.engine, prev, "blur", {});
83
+ }
84
+ this.focusedNode = node;
85
+ if (node) {
86
+ node.focused = true;
87
+ dispatchNodeEvent(this.engine, node, "focus", {});
88
+ }
89
+ this.hooks.onFocusChange(node, prev);
90
+ }
91
+ // -------------------------------------------------------------------------
92
+ // Pointer path — canvas hit-test funnels into DOM focus
93
+ // -------------------------------------------------------------------------
94
+ /**
95
+ * Focus a node from the canvas pointer path. Focuses the mirror element
96
+ * (so `document.activeElement` agrees) and syncs node state directly —
97
+ * in real browsers the element's focusin already did that synchronously
98
+ * and the second sync is a no-op; in environments without focus events
99
+ * the direct sync keeps behavior identical.
100
+ */
101
+ requestFocus(node) {
102
+ if (!node) {
103
+ this.clearFocus();
104
+ return;
105
+ }
106
+ const el = this.mirror.getElement(node.id);
107
+ el?.focus?.({ preventScroll: true });
108
+ this.syncFocus(node);
109
+ }
110
+ /** Pointer landed on nothing focusable: blur any mirror focus and clear. */
111
+ clearFocus() {
112
+ if (typeof document !== "undefined") {
113
+ const active = document.activeElement;
114
+ const root = this.mirror.getRoot();
115
+ if (active &&
116
+ ((root && (root.contains?.(active) ?? false)) ||
117
+ this.hooks.isAuxFocusTarget?.(active))) {
118
+ active.blur?.();
119
+ }
120
+ }
121
+ this.syncFocus(null);
122
+ }
123
+ /**
124
+ * Clear focus if the focused node is `node` or one of its descendants —
125
+ * called before a subtree is removed/detached (the mirror element may
126
+ * leave the DOM without a browser blur when focus sits on the IME proxy).
127
+ */
128
+ clearIfWithin(node) {
129
+ let current = this.focusedNode;
130
+ while (current) {
131
+ if (current === node) {
132
+ this.syncFocus(null);
133
+ return;
134
+ }
135
+ current = current.parent;
136
+ }
137
+ }
138
+ getFocusedNode() {
139
+ return this.focusedNode;
140
+ }
141
+ // -------------------------------------------------------------------------
142
+ // Mirror interaction → engine actions
143
+ // -------------------------------------------------------------------------
144
+ /**
145
+ * Native click on a mirror element — keyboard activation (Enter/Space on
146
+ * a real `<button>`) or an AT-initiated action. Pointer clicks never land
147
+ * here (fallback content has no rendered boxes), so this can't double-fire
148
+ * with the canvas hit-test click path.
149
+ */
150
+ onMirrorClick(e) {
151
+ const node = this.resolveNode(e.target);
152
+ if (!node)
153
+ return;
154
+ if (!node.clickable)
155
+ return;
156
+ dispatchNodeEvent(this.engine, node, "click", {});
157
+ }
158
+ onMirrorKey(type, e) {
159
+ const node = this.resolveNode(e.target);
160
+ if (!node)
161
+ return;
162
+ dispatchNodeEvent(this.engine, node, type, {
163
+ key: e.key,
164
+ code: e.code,
165
+ ctrlKey: e.ctrlKey,
166
+ shiftKey: e.shiftKey,
167
+ altKey: e.altKey,
168
+ });
169
+ }
170
+ destroy() {
171
+ const root = this.mirror.getRoot();
172
+ if (root && typeof root.removeEventListener === "function") {
173
+ root.removeEventListener("focusin", this.boundFocusIn);
174
+ root.removeEventListener("focusout", this.boundFocusOut);
175
+ root.removeEventListener("click", this.boundClick);
176
+ root.removeEventListener("keydown", this.boundKeyDown);
177
+ root.removeEventListener("keyup", this.boundKeyUp);
178
+ }
179
+ this.focusedNode = null;
180
+ }
181
+ }
182
+ //# sourceMappingURL=focus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus.js","sourceRoot":"","sources":["../../src/canvas/focus.ts"],"names":[],"mappings":"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"}
@@ -4,13 +4,14 @@
4
4
  * Browser-only module for rendering Hypen UI to Canvas
5
5
  */
6
6
  export { CanvasRenderer } from "./renderer.js";
7
- export { createCanvasClient as createHypenClient, createCanvasClient, type CanvasClient as HypenClient, type CanvasClient, type CanvasClientOptions as HypenClientOptions, type CanvasClientOptions, } from "../client.js";
8
7
  export { registerPainter } from "./paint.js";
9
8
  export { CanvasEventManager } from "./events.js";
10
- export { InputOverlay } from "./input.js";
11
9
  export { AccessibilityLayer } from "./accessibility.js";
10
+ export { FocusManager } from "./focus.js";
11
+ export { TextEditController } from "./editing.js";
12
12
  export { initTaffyLayout } from "./layout.js";
13
13
  export { ScrollManager } from "./scroll.js";
14
14
  export { DirtyRectTracker } from "./dirty.js";
15
15
  export { SelectionManager } from "./selection.js";
16
+ export { pointToOffset, offsetToCaretRect, rangeToRects, type TextGeometry, } from "./text-geometry.js";
16
17
  export type { VirtualNode, Layout, Rectangle, Point, FontStyle, TextStyle, TextMetrics, ScrollState, CanvasRendererOptions, PainterFunction, LayoutFunction, } from "./types.js";
@@ -4,13 +4,14 @@
4
4
  * Browser-only module for rendering Hypen UI to Canvas
5
5
  */
6
6
  export { CanvasRenderer } from "./renderer.js";
7
- export { createCanvasClient as createHypenClient, createCanvasClient, } from "../client.js";
8
7
  export { registerPainter } from "./paint.js";
9
8
  export { CanvasEventManager } from "./events.js";
10
- export { InputOverlay } from "./input.js";
11
9
  export { AccessibilityLayer } from "./accessibility.js";
10
+ export { FocusManager } from "./focus.js";
11
+ export { TextEditController } from "./editing.js";
12
12
  export { initTaffyLayout } from "./layout.js";
13
13
  export { ScrollManager } from "./scroll.js";
14
14
  export { DirtyRectTracker } from "./dirty.js";
15
15
  export { SelectionManager } from "./selection.js";
16
+ export { pointToOffset, offsetToCaretRect, rangeToRects, } from "./text-geometry.js";
16
17
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/canvas/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EACL,kBAAkB,IAAI,iBAAiB,EACvC,kBAAkB,GAKnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/canvas/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,YAAY,GAEb,MAAM,oBAAoB,CAAC"}
@@ -5,7 +5,7 @@
5
5
  * layout. Falls back to a basic flexbox implementation in environments
6
6
  * without WASM support (e.g. test runners).
7
7
  */
8
- import { parseSpacing, parseSize, cssLengthToPx, cssLengthToDimension, resolveLineHeight, setCurrentViewport, getCurrentViewport, } from "./utils.js";
8
+ import { parseSpacing, parseSize, cssLengthToPx, cssLengthToDimension } from "./utils.js";
9
9
  import { measureText } from "./text.js";
10
10
  import { getImageNaturalAspect } from "./paint.js";
11
11
  /**
@@ -43,6 +43,18 @@ export async function initTaffyLayout() {
43
43
  taffyInitPromise = (async () => {
44
44
  try {
45
45
  taffy = await import("taffy-layout");
46
+ // In a server runtime (Bun/Node — including test runs where a JSDOM
47
+ // `window` may be globally registered by another test file), load the
48
+ // WASM from node_modules via fs. This path is deterministic; the
49
+ // fetch-based branch below depends on a reachable origin/CDN and must
50
+ // never be selected just because a test polyfilled `window`.
51
+ const isServerRuntime = typeof process !== "undefined" &&
52
+ !!(process.versions?.bun || process.versions?.node);
53
+ if (isServerRuntime) {
54
+ await taffy.loadTaffy();
55
+ taffyReady = true;
56
+ return;
57
+ }
46
58
  // Bundlers (esp. Bun's browser bundler) sometimes inline a file:// URL
47
59
  // for import.meta.url inside taffy_wasm.js, which the browser blocks
48
60
  // ("Not allowed to load local resource"). The package's own `loadTaffy`
@@ -50,28 +62,42 @@ export async function initTaffyLayout() {
50
62
  // raw `__wbg_init` (default export of the `taffy-layout/wasm` subpath)
51
63
  // directly with an explicit Response.
52
64
  let usedExplicit = false;
53
- if (typeof window !== "undefined" && typeof fetch === "function") {
54
- const candidates = [
55
- new URL("/taffy_wasm_bg.wasm", window.location.origin),
56
- new URL("https://cdn.jsdelivr.net/npm/taffy-layout@2.0.3/pkg/taffy_wasm_bg.wasm"),
57
- new URL("https://unpkg.com/taffy-layout@2.0.3/pkg/taffy_wasm_bg.wasm"),
58
- ];
59
- const rawWasm = await import(
60
- /* @vite-ignore */ "taffy-layout/wasm");
61
- const wbgInit = rawWasm.default;
62
- for (const url of candidates) {
63
- try {
64
- const res = await fetch(url);
65
- if (!res.ok)
66
- continue;
67
- await wbgInit({ module_or_path: res });
68
- usedExplicit = true;
69
- break;
70
- }
71
- catch {
72
- // Try next candidate
65
+ // Only take the fetch path in a real browser. Test environments (and
66
+ // anything else that installs a fake `window` global) must fall through
67
+ // to `loadTaffy()`, which resolves the WASM from the package on disk —
68
+ // a fake window without `location.origin` used to throw here and
69
+ // silently disable Taffy for the rest of the process.
70
+ const isRealBrowser = typeof window !== "undefined" &&
71
+ typeof fetch === "function" &&
72
+ typeof window.location?.origin === "string" &&
73
+ window.document?.defaultView === window;
74
+ if (isRealBrowser) {
75
+ try {
76
+ const candidates = [
77
+ new URL("/taffy_wasm_bg.wasm", window.location.origin),
78
+ new URL("https://cdn.jsdelivr.net/npm/taffy-layout@2.0.3/pkg/taffy_wasm_bg.wasm"),
79
+ new URL("https://unpkg.com/taffy-layout@2.0.3/pkg/taffy_wasm_bg.wasm"),
80
+ ];
81
+ const rawWasm = await import(
82
+ /* @vite-ignore */ "taffy-layout/wasm");
83
+ const wbgInit = rawWasm.default;
84
+ for (const url of candidates) {
85
+ try {
86
+ const res = await fetch(url);
87
+ if (!res.ok)
88
+ continue;
89
+ await wbgInit({ module_or_path: res });
90
+ usedExplicit = true;
91
+ break;
92
+ }
93
+ catch {
94
+ // Try next candidate
95
+ }
73
96
  }
74
97
  }
98
+ catch {
99
+ // Fall through to loadTaffy()
100
+ }
75
101
  }
76
102
  if (!usedExplicit) {
77
103
  await taffy.loadTaffy();
@@ -166,21 +192,9 @@ function buildTaffyStyle(node, parent = null) {
166
192
  // `List` is the DSL's vertical-stack iterator (see dom/components/list.ts
167
193
  // — `flex-direction: column` is its default). Match that here so feeds
168
194
  // like Notifications stack their rows vertically instead of flowing
169
- // sideways. The legacy `direction` prop can flip it to row, and so can
170
- // a Tailwind `flex-row` class which lands on `props.flexDirection`
171
- // (without this, top-tab strips built as `List.flex-row` stacked
172
- // vertically because the List default won the tie-break).
173
- const isListColumn = type === "list" &&
174
- props.direction !== "horizontal" &&
175
- props.flexDirection !== "row";
176
- // Button defaults to `flex-direction: column` to match the DOM renderer
177
- // (see dom/components/button.ts). With Taffy's default `align-items:
178
- // stretch`, treating Button as a row caused inner `Column { Icon, Text }`
179
- // children to collapse onto each other (icon overlapping label). Only
180
- // apply the column default when no explicit `flexDirection` is set so a
181
- // Tailwind `flex-row` on a button still wins.
182
- const isButtonColumn = type === "button" && props.flexDirection === undefined;
183
- const isColumn = !isStack && (type === "column" || isListColumn || isButtonColumn || props.flexDirection === "column");
195
+ // sideways. The `direction` prop can still flip it to row.
196
+ const isListColumn = type === "list" && props.direction !== "horizontal";
197
+ const isColumn = !isStack && (type === "column" || isListColumn || props.flexDirection === "column");
184
198
  const isGrid = !isStack && (type === "grid" || props.display === "grid");
185
199
  if (isStack) {
186
200
  style.display = T.Display.Grid;
@@ -375,7 +389,7 @@ function buildTaffyStyle(node, parent = null) {
375
389
  // Taffy defaults to `box-sizing: border-box`, so we add the
376
390
  // padding/border ourselves into `size.height`.
377
391
  const fontSize = cssLengthToPx(props.fontSize) ?? 16;
378
- const lineHeight = resolveLineHeight(props.lineHeight, fontSize) ?? fontSize * 1.5;
392
+ const lineHeight = cssLengthToPx(props.lineHeight) ?? fontSize * 1.5;
379
393
  const minRows = type === "textarea" ? Math.max(1, Number(props.rows) || 3) : 1;
380
394
  const padTop = cssLengthToPx(props.paddingTop ?? props.padding) ?? 0;
381
395
  const padBottom = cssLengthToPx(props.paddingBottom ?? props.padding) ?? 0;
@@ -397,32 +411,7 @@ function buildTaffyStyle(node, parent = null) {
397
411
  (parent.type.toLowerCase() === "grid" || parent.props.display === "grid");
398
412
  const widthIn = parentIsGridLayout && props.width === "100%" ? undefined : props.width;
399
413
  const heightIn = parentIsGridLayout && props.height === "100%" ? undefined : props.height;
400
- // Mirror the DOM rule `[data-hypen-type="row"]:has(> [data-hypen-flex])
401
- // { width: 100% }` (and the symmetric column case). When a flex
402
- // container has no explicit main-axis size and any child opts into
403
- // weighted growth (`flex` / `flexGrow > 0`), Taffy would otherwise
404
- // size the container to its min-content — so a Row of `flex-1`
405
- // segmented-control buttons stayed pill-sized instead of stretching
406
- // across the parent. Only do this when at least one child explicitly
407
- // asks for flex, matching the DOM `:has(> [data-hypen-flex])` guard.
408
- const isRowContainer = !isStack && !isGrid && !isColumn;
409
- const isColumnContainer = !isStack && !isGrid && isColumn;
410
- const childAsksFlex = node.children.some((c) => {
411
- const fg = parseFloat(c.props.flexGrow);
412
- if (Number.isFinite(fg) && fg > 0)
413
- return true;
414
- const f = parseFloat(c.props.flex);
415
- return Number.isFinite(f) && f > 0;
416
- });
417
- let resolvedWidth = toDimension(widthIn);
418
- let resolvedHeight = toDimension(heightIn);
419
- if (childAsksFlex) {
420
- if (isRowContainer && widthIn === undefined)
421
- resolvedWidth = "100%";
422
- if (isColumnContainer && heightIn === undefined)
423
- resolvedHeight = "100%";
424
- }
425
- style.size = { width: resolvedWidth, height: resolvedHeight };
414
+ style.size = { width: toDimension(widthIn), height: toDimension(heightIn) };
426
415
  }
427
416
  // --- Min / Max constraints -------------------------------------------------
428
417
  // Default `min-width: 0` for children of a Grid container. CSS makes
@@ -763,17 +752,12 @@ function buildTree(tree, node, parent = null) {
763
752
  // container with size 0×0, and downstream siblings were placed on top of
764
753
  // each other ("1,431 likes" overlapping the caption row was the visible
765
754
  // symptom).
766
- // `props[0] != null` not `props[0]` truthy. The number `0`, the empty
767
- // string, and `false` are valid text content; falsy guards here let
768
- // those cases skip the measurement leaf and inherit a 0-height container,
769
- // which made siblings stack on top of each other (a bound `0` overlapping
770
- // the next line was the visible symptom in calorie-counter's hero).
771
- if (node.type.toLowerCase() === "text" && props[0] != null && node.children.length === 0) {
772
- const text = String(props[0]);
755
+ if (node.type.toLowerCase() === "text" && props[0] && node.children.length === 0) {
756
+ const text = String(props[0] || "");
773
757
  const fontSize = cssLengthToPx(props.fontSize) ?? 16;
774
758
  const fontWeight = props.fontWeight || "normal";
775
759
  const fontFamily = props.fontFamily || "system-ui, sans-serif";
776
- const lineHeight = resolveLineHeight(props.lineHeight, fontSize) ?? fontSize * 1.2;
760
+ const lineHeight = cssLengthToPx(props.lineHeight) ?? fontSize * 1.2;
777
761
  const p = parseSpacing(props.padding || 0);
778
762
  if (props.paddingTop !== undefined)
779
763
  p.top = cssLengthToPx(props.paddingTop) ?? 0;
@@ -1041,32 +1025,19 @@ function computeLayoutTaffy(ctx, node, availableWidth, availableHeight, x, y) {
1041
1025
  * layout. Falls back to a basic JS flexbox implementation otherwise.
1042
1026
  */
1043
1027
  export function computeLayout(ctx, node, availableWidth, availableHeight, x = 0, y = 0) {
1044
- // Publish the active viewport so `cssLengthToPx` can resolve `vw` / `vh`
1045
- // against the actual canvas dimensions during this layout pass. Without
1046
- // this, `vh` falls back to `window.innerHeight` (wrong inside an iframe
1047
- // or scaled preview pane) or, worse, to a unitless pixel value that
1048
- // collapses entire layouts. Layout is synchronous, so a try/finally
1049
- // restoring the previous slot is safe under nested or re-entrant calls.
1050
- const prevViewport = getCurrentViewport();
1051
- setCurrentViewport({ width: availableWidth, height: availableHeight });
1052
- try {
1053
- if (taffyReady && taffy) {
1028
+ if (taffyReady && taffy) {
1029
+ computeLayoutTaffy(ctx, node, availableWidth, availableHeight, x, y);
1030
+ // After the first pass, Taffy's grid auto-row sizing for aspect-ratio
1031
+ // image leaves can collapse rows (the leaf's max-content height is 0
1032
+ // before column widths are known). Detect that pattern and re-run
1033
+ // with explicit `gridAutoRows` derived from the now-known column
1034
+ // width see `annotateCollapsedAspectGrids`.
1035
+ if (annotateCollapsedAspectGrids(node)) {
1054
1036
  computeLayoutTaffy(ctx, node, availableWidth, availableHeight, x, y);
1055
- // After the first pass, Taffy's grid auto-row sizing for aspect-ratio
1056
- // image leaves can collapse rows (the leaf's max-content height is 0
1057
- // before column widths are known). Detect that pattern and re-run
1058
- // with explicit `gridAutoRows` derived from the now-known column
1059
- // width — see `annotateCollapsedAspectGrids`.
1060
- if (annotateCollapsedAspectGrids(node)) {
1061
- computeLayoutTaffy(ctx, node, availableWidth, availableHeight, x, y);
1062
- }
1063
- }
1064
- else {
1065
- computeLayoutFallback(ctx, node, availableWidth, availableHeight, x, y);
1066
1037
  }
1067
1038
  }
1068
- finally {
1069
- setCurrentViewport(prevViewport);
1039
+ else {
1040
+ computeLayoutFallback(ctx, node, availableWidth, availableHeight, x, y);
1070
1041
  }
1071
1042
  }
1072
1043
  // ---------------------------------------------------------------------------
@@ -1182,13 +1153,12 @@ function computeLayoutFallback(ctx, node, availableWidth, availableHeight, x = 0
1182
1153
  if (height === null)
1183
1154
  height = size;
1184
1155
  }
1185
- // Same `!= null` rule as above — `0` / `""` / `false` are valid text.
1186
- if (node.type.toLowerCase() === "text" && node.props[0] != null) {
1187
- const text = String(node.props[0]);
1156
+ if (node.type.toLowerCase() === "text" && node.props[0]) {
1157
+ const text = String(node.props[0] || "");
1188
1158
  const fontSize = cssLengthToPx(props.fontSize) ?? 16;
1189
1159
  const fontWeight = props.fontWeight || "normal";
1190
1160
  const fontFamily = props.fontFamily || "system-ui, sans-serif";
1191
- const lineHeight = resolveLineHeight(props.lineHeight, fontSize) ?? fontSize * 1.2;
1161
+ const lineHeight = cssLengthToPx(props.lineHeight) ?? fontSize * 1.2;
1192
1162
  const maxWidth = width || availableAfterMargin.width - padding.left - padding.right;
1193
1163
  const maxLinesRaw = props.maxLines;
1194
1164
  const maxLines = typeof maxLinesRaw === "number"