@hypen-space/web 0.4.956 → 0.5.2

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 (166) hide show
  1. package/README.md +20 -13
  2. package/dist/canvas/events.js +46 -7
  3. package/dist/canvas/events.js.map +1 -1
  4. package/dist/canvas/index.d.ts +0 -1
  5. package/dist/canvas/index.js +0 -1
  6. package/dist/canvas/index.js.map +1 -1
  7. package/dist/canvas/layout.js +57 -44
  8. package/dist/canvas/layout.js.map +1 -1
  9. package/dist/canvas/paint.js +159 -60
  10. package/dist/canvas/paint.js.map +1 -1
  11. package/dist/canvas/renderer.d.ts +8 -3
  12. package/dist/canvas/renderer.js +32 -43
  13. package/dist/canvas/renderer.js.map +1 -1
  14. package/dist/canvas/types.d.ts +3 -0
  15. package/dist/canvas/utils.d.ts +0 -8
  16. package/dist/canvas/utils.js +3 -34
  17. package/dist/canvas/utils.js.map +1 -1
  18. package/dist/canvas/variants.d.ts +49 -0
  19. package/dist/canvas/variants.js +137 -0
  20. package/dist/canvas/variants.js.map +1 -0
  21. package/dist/dom/applicators/events.js +1 -0
  22. package/dist/dom/applicators/events.js.map +1 -1
  23. package/dist/dom/applicators/font.d.ts +11 -6
  24. package/dist/dom/applicators/font.js +16 -27
  25. package/dist/dom/applicators/font.js.map +1 -1
  26. package/dist/dom/applicators/index.js +47 -45
  27. package/dist/dom/applicators/index.js.map +1 -1
  28. package/dist/dom/canvas/index.js +2 -2
  29. package/dist/dom/canvas/index.js.map +1 -1
  30. package/dist/dom/components/app.js +2 -2
  31. package/dist/dom/components/app.js.map +1 -1
  32. package/dist/dom/components/audio.js +2 -2
  33. package/dist/dom/components/audio.js.map +1 -1
  34. package/dist/dom/components/avatar.js +3 -4
  35. package/dist/dom/components/avatar.js.map +1 -1
  36. package/dist/dom/components/badge.js +2 -2
  37. package/dist/dom/components/badge.js.map +1 -1
  38. package/dist/dom/components/button.js +2 -2
  39. package/dist/dom/components/button.js.map +1 -1
  40. package/dist/dom/components/card.js +2 -2
  41. package/dist/dom/components/card.js.map +1 -1
  42. package/dist/dom/components/center.js +2 -2
  43. package/dist/dom/components/center.js.map +1 -1
  44. package/dist/dom/components/checkbox.js +4 -5
  45. package/dist/dom/components/checkbox.js.map +1 -1
  46. package/dist/dom/components/column.js +2 -2
  47. package/dist/dom/components/column.js.map +1 -1
  48. package/dist/dom/components/container.js +2 -2
  49. package/dist/dom/components/container.js.map +1 -1
  50. package/dist/dom/components/divider.js +2 -2
  51. package/dist/dom/components/divider.js.map +1 -1
  52. package/dist/dom/components/grid.js +10 -10
  53. package/dist/dom/components/grid.js.map +1 -1
  54. package/dist/dom/components/heading.js +3 -4
  55. package/dist/dom/components/heading.js.map +1 -1
  56. package/dist/dom/components/hypenapp.js +2 -2
  57. package/dist/dom/components/hypenapp.js.map +1 -1
  58. package/dist/dom/components/icon.js +4 -5
  59. package/dist/dom/components/icon.js.map +1 -1
  60. package/dist/dom/components/image.js +2 -2
  61. package/dist/dom/components/image.js.map +1 -1
  62. package/dist/dom/components/index.d.ts +2 -2
  63. package/dist/dom/components/index.js +2 -2
  64. package/dist/dom/components/index.js.map +1 -1
  65. package/dist/dom/components/input.js +2 -2
  66. package/dist/dom/components/input.js.map +1 -1
  67. package/dist/dom/components/link.js +2 -2
  68. package/dist/dom/components/link.js.map +1 -1
  69. package/dist/dom/components/list.js +2 -2
  70. package/dist/dom/components/list.js.map +1 -1
  71. package/dist/dom/components/paragraph.js +2 -2
  72. package/dist/dom/components/paragraph.js.map +1 -1
  73. package/dist/dom/components/progressbar.js +3 -3
  74. package/dist/dom/components/progressbar.js.map +1 -1
  75. package/dist/dom/components/route.js +2 -2
  76. package/dist/dom/components/route.js.map +1 -1
  77. package/dist/dom/components/router.js +2 -2
  78. package/dist/dom/components/router.js.map +1 -1
  79. package/dist/dom/components/row.js +10 -10
  80. package/dist/dom/components/row.js.map +1 -1
  81. package/dist/dom/components/select.js +3 -4
  82. package/dist/dom/components/select.js.map +1 -1
  83. package/dist/dom/components/slider.js +2 -2
  84. package/dist/dom/components/slider.js.map +1 -1
  85. package/dist/dom/components/spacer.js +2 -2
  86. package/dist/dom/components/spacer.js.map +1 -1
  87. package/dist/dom/components/spinner.js +4 -4
  88. package/dist/dom/components/spinner.js.map +1 -1
  89. package/dist/dom/components/stack.js +10 -10
  90. package/dist/dom/components/stack.js.map +1 -1
  91. package/dist/dom/components/switch.js +5 -6
  92. package/dist/dom/components/switch.js.map +1 -1
  93. package/dist/dom/components/text.js +2 -2
  94. package/dist/dom/components/text.js.map +1 -1
  95. package/dist/dom/components/textarea.js +2 -2
  96. package/dist/dom/components/textarea.js.map +1 -1
  97. package/dist/dom/components/video.js +2 -2
  98. package/dist/dom/components/video.js.map +1 -1
  99. package/dist/dom/debug.js +6 -8
  100. package/dist/dom/debug.js.map +1 -1
  101. package/dist/dom/index.d.ts +0 -1
  102. package/dist/dom/index.js +0 -1
  103. package/dist/dom/index.js.map +1 -1
  104. package/dist/dom/renderer.js +2 -3
  105. package/dist/dom/renderer.js.map +1 -1
  106. package/dist/variants.d.ts +90 -0
  107. package/dist/variants.js +206 -0
  108. package/dist/variants.js.map +1 -0
  109. package/package.json +2 -2
  110. package/src/canvas/QUICKSTART.md +12 -7
  111. package/src/canvas/README.md +8 -5
  112. package/src/canvas/events.ts +49 -7
  113. package/src/canvas/index.ts +0 -8
  114. package/src/canvas/layout.ts +57 -49
  115. package/src/canvas/paint.ts +163 -58
  116. package/src/canvas/renderer.ts +38 -46
  117. package/src/canvas/types.ts +14 -0
  118. package/src/canvas/utils.ts +3 -37
  119. package/src/canvas/variants.ts +153 -0
  120. package/src/dom/README.md +8 -7
  121. package/src/dom/applicators/events.ts +1 -0
  122. package/src/dom/applicators/font.ts +16 -29
  123. package/src/dom/applicators/index.ts +56 -46
  124. package/src/dom/canvas/index.ts +2 -2
  125. package/src/dom/components/app.ts +2 -2
  126. package/src/dom/components/audio.ts +2 -2
  127. package/src/dom/components/avatar.ts +3 -4
  128. package/src/dom/components/badge.ts +2 -2
  129. package/src/dom/components/button.ts +2 -2
  130. package/src/dom/components/card.ts +2 -2
  131. package/src/dom/components/center.ts +2 -2
  132. package/src/dom/components/checkbox.ts +4 -5
  133. package/src/dom/components/column.ts +2 -2
  134. package/src/dom/components/container.ts +2 -2
  135. package/src/dom/components/divider.ts +2 -2
  136. package/src/dom/components/grid.ts +10 -10
  137. package/src/dom/components/heading.ts +3 -4
  138. package/src/dom/components/hypenapp.ts +2 -2
  139. package/src/dom/components/icon.ts +4 -5
  140. package/src/dom/components/image.ts +2 -2
  141. package/src/dom/components/index.ts +3 -3
  142. package/src/dom/components/input.ts +2 -2
  143. package/src/dom/components/link.ts +2 -2
  144. package/src/dom/components/list.ts +2 -2
  145. package/src/dom/components/paragraph.ts +2 -2
  146. package/src/dom/components/progressbar.ts +3 -3
  147. package/src/dom/components/route.ts +2 -2
  148. package/src/dom/components/router.ts +2 -2
  149. package/src/dom/components/row.ts +10 -10
  150. package/src/dom/components/select.ts +3 -4
  151. package/src/dom/components/slider.ts +2 -2
  152. package/src/dom/components/spacer.ts +2 -2
  153. package/src/dom/components/spinner.ts +4 -4
  154. package/src/dom/components/stack.ts +10 -10
  155. package/src/dom/components/switch.ts +5 -6
  156. package/src/dom/components/text.ts +2 -2
  157. package/src/dom/components/textarea.ts +2 -2
  158. package/src/dom/components/video.ts +2 -2
  159. package/src/dom/debug.ts +6 -8
  160. package/src/dom/index.ts +0 -8
  161. package/src/dom/renderer.ts +2 -3
  162. package/src/variants.ts +249 -0
  163. package/dist/client.d.ts +0 -36
  164. package/dist/client.js +0 -47
  165. package/dist/client.js.map +0 -1
  166. package/src/client.ts +0 -88
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Shared variant (responsive breakpoint + interaction state) support.
3
+ *
4
+ * The engine emits applicator props using the canonical key format:
5
+ *
6
+ * <camelBase><variant?><argSuffix>
7
+ *
8
+ * - camelBase: camelCase applicator name, e.g. "padding", "backgroundColor"
9
+ * - variant?: "@<bp>" and/or ":<state>", in that order if combined.
10
+ * bp in { sm, md, lg, xl, 2xl }
11
+ * state in { hover, focus, active, disabled,
12
+ * focus-visible, focus-within }
13
+ * combined example: "backgroundColor@md:hover" (BOTH md AND hover)
14
+ * - argSuffix: ".<index>" (almost always ".0") or ".<name>" for named args.
15
+ *
16
+ * Examples emitted by the engine:
17
+ * "padding.0", "padding@md.0", "backgroundColor:hover.0",
18
+ * "backgroundColor@md:hover.0".
19
+ *
20
+ * IMPORTANT: the variant marker sits BETWEEN the base name and the ".0" arg
21
+ * suffix. A lookup that forgets the ".0" (e.g. building just "padding@md")
22
+ * MISSES the real key. Always parse the variant out of the full key.
23
+ *
24
+ * Both the DOM applicator path and the Canvas renderer import this module so
25
+ * breakpoint pixels, state names, and precedence stay byte-compatible across
26
+ * renderers. The DOM renderer leans on the CSS engine for actual resolution
27
+ * (media queries + pseudo-classes), so it only needs PARSE_* here; Canvas does
28
+ * its own paint/layout and therefore also uses the runtime resolver.
29
+ */
30
+ /**
31
+ * Tailwind breakpoint min-widths (px). Order matters: ascending min-width.
32
+ * Mirrors the DOM `BREAKPOINTS` table and the iOS reference.
33
+ */
34
+ export const BREAKPOINTS = {
35
+ sm: 640,
36
+ md: 768,
37
+ lg: 1024,
38
+ xl: 1280,
39
+ "2xl": 1536,
40
+ };
41
+ /** Breakpoint names in ascending min-width order (sm < md < lg < xl < 2xl). */
42
+ export const BREAKPOINT_ORDER = ["sm", "md", "lg", "xl", "2xl"];
43
+ /** Interaction-state names recognised in variant keys (same set as the DOM). */
44
+ export const VALID_STATES = [
45
+ "hover",
46
+ "focus",
47
+ "active",
48
+ "disabled",
49
+ "focus-visible",
50
+ "focus-within",
51
+ ];
52
+ const VALID_STATE_SET = new Set(VALID_STATES);
53
+ /**
54
+ * Parse a prop key into `{ base, breakpoint, state, arg }`.
55
+ *
56
+ * Splits the arg suffix first (the LAST `.<token>`), then peels the variant
57
+ * markers (`@bp` and/or `:state`) off the remainder. An unknown breakpoint or
58
+ * state is left as part of the base name (caller decides what to do); this
59
+ * matches the DOM behaviour where an invalid `@invalid` / `:invalid` simply
60
+ * never matches a real variant.
61
+ *
62
+ * Examples:
63
+ * "padding.0" -> { base:"padding", breakpoint:null, state:null, arg:"0" }
64
+ * "padding@md.0" -> { base:"padding", breakpoint:"md", state:null, arg:"0" }
65
+ * "backgroundColor:hover.0" -> { base:"backgroundColor", breakpoint:null, state:"hover", arg:"0" }
66
+ * "backgroundColor@md:hover.0" -> { base:"backgroundColor", breakpoint:"md", state:"hover", arg:"0" }
67
+ * "backgroundColor@md:hover" -> { base:"backgroundColor", breakpoint:"md", state:"hover", arg:null }
68
+ */
69
+ export function parseVariantKey(key) {
70
+ let work = key;
71
+ let arg = null;
72
+ // Peel the arg suffix: the segment after the LAST dot, but only if that dot
73
+ // comes AFTER any variant markers (so we never split inside "focus-within"
74
+ // — that has no dot — and never treat "padding" alone as having an arg).
75
+ const lastDot = work.lastIndexOf(".");
76
+ if (lastDot > 0) {
77
+ arg = work.slice(lastDot + 1);
78
+ work = work.slice(0, lastDot);
79
+ }
80
+ // Peel the state marker (":state") off the end of the remainder.
81
+ let state = null;
82
+ const colon = work.indexOf(":");
83
+ if (colon !== -1) {
84
+ const candidate = work.slice(colon + 1);
85
+ if (VALID_STATE_SET.has(candidate)) {
86
+ state = candidate;
87
+ work = work.slice(0, colon);
88
+ }
89
+ }
90
+ // Peel the breakpoint marker ("@bp").
91
+ let breakpoint = null;
92
+ const at = work.indexOf("@");
93
+ if (at !== -1) {
94
+ const candidate = work.slice(at + 1);
95
+ if (candidate in BREAKPOINTS) {
96
+ breakpoint = candidate;
97
+ work = work.slice(0, at);
98
+ }
99
+ }
100
+ return { base: work, breakpoint, state, arg };
101
+ }
102
+ /**
103
+ * Precedence rank for a parsed variant (higher wins). Combines the breakpoint
104
+ * tier and the state tier into a single sortable number.
105
+ *
106
+ * Precedence (low -> high), later overrides earlier:
107
+ * base
108
+ * < breakpoints ascending (sm < md < lg < xl < 2xl)
109
+ * < disabled < hover < focus < active
110
+ *
111
+ * State always outranks any breakpoint (a `:hover` override beats a `@2xl`
112
+ * sizing override on the same prop), matching the iOS reference where state
113
+ * overrides are merged AFTER the responsive base. Within the same state tier,
114
+ * a higher breakpoint still breaks the tie so `@md:hover` beats `:hover`.
115
+ */
116
+ const STATE_RANK = {
117
+ disabled: 1,
118
+ hover: 2,
119
+ // focus-visible / focus-within are focus-flavoured states: they share the
120
+ // `focus` precedence slot (NOT base/0) so they outrank hover and lose to
121
+ // active, matching the engine's `state_rank` in portable/variant.rs.
122
+ focus: 3,
123
+ "focus-visible": 3,
124
+ "focus-within": 3,
125
+ active: 4,
126
+ };
127
+ // State tier must be weighted by a value strictly greater than the largest
128
+ // possible breakpoint tier so the bp tiebreak can never leak across state
129
+ // bands (mirrors the engine's STATE_STEP > max-breakpoint invariant). Derived
130
+ // from BREAKPOINT_ORDER.length so adding breakpoints can't silently break it.
131
+ const STATE_WEIGHT = BREAKPOINT_ORDER.length + 1;
132
+ function variantRank(breakpoint, state) {
133
+ const bpTier = breakpoint ? BREAKPOINT_ORDER.indexOf(breakpoint) + 1 : 0; // 0..len
134
+ const stateTier = state ? STATE_RANK[state] ?? 0 : 0; // 0..4
135
+ // State tier dominates; breakpoint tier breaks ties within a state tier.
136
+ return stateTier * STATE_WEIGHT + bpTier;
137
+ }
138
+ /**
139
+ * Is a parsed variant currently applicable, given the viewport width and the
140
+ * active interaction states? A combined key requires BOTH conditions.
141
+ */
142
+ function variantApplies(parsed, width, states) {
143
+ if (parsed.breakpoint) {
144
+ const minWidth = BREAKPOINTS[parsed.breakpoint];
145
+ if (minWidth === undefined || width < minWidth)
146
+ return false;
147
+ }
148
+ if (parsed.state) {
149
+ if (!states[parsed.state])
150
+ return false;
151
+ }
152
+ return true;
153
+ }
154
+ /**
155
+ * Resolve variant-bearing props against the current viewport width and active
156
+ * interaction states, producing a flat map of `base -> winning value`.
157
+ *
158
+ * Input keys are the already-normalised flat applicator keys (variant markers
159
+ * present, arg suffix already stripped by the applicator-grouping pass), e.g.
160
+ * `{ padding: 8, "padding@md": 16, "backgroundColor:hover": "red" }`.
161
+ *
162
+ * Only bases that actually have at least one applicable variant appear in the
163
+ * result with an overridden value; a base with only its plain entry resolves to
164
+ * that plain value. The caller merges the result over the node's base props.
165
+ */
166
+ export function resolveVariantProps(props, width, states) {
167
+ // base -> { value, rank }
168
+ const winners = new Map();
169
+ for (const key of Object.keys(props)) {
170
+ const parsed = parseVariantKey(key);
171
+ // Only consider entries that carry a variant marker. Plain keys (no bp/state)
172
+ // are the base and are handled implicitly via rank 0 below so a base with a
173
+ // single applicable variant still falls back to base when the variant is off.
174
+ if (!parsed.breakpoint && !parsed.state) {
175
+ // Seed the base value at rank 0 so variant lookups have a fallback and so
176
+ // a base that ALSO has variants resolves correctly when none apply.
177
+ const existing = winners.get(parsed.base);
178
+ if (!existing || existing.rank === 0) {
179
+ winners.set(parsed.base, { value: props[key], rank: 0 });
180
+ }
181
+ continue;
182
+ }
183
+ if (!variantApplies(parsed, width, states))
184
+ continue;
185
+ const rank = variantRank(parsed.breakpoint, parsed.state);
186
+ const existing = winners.get(parsed.base);
187
+ if (!existing || rank >= existing.rank) {
188
+ winners.set(parsed.base, { value: props[key], rank });
189
+ }
190
+ }
191
+ const out = {};
192
+ for (const [base, win] of winners) {
193
+ out[base] = win.value;
194
+ }
195
+ return out;
196
+ }
197
+ /** True if any key in `props` carries a variant marker (`@bp` or `:state`). */
198
+ export function hasVariantProps(props) {
199
+ for (const key of Object.keys(props)) {
200
+ const parsed = parseVariantKey(key);
201
+ if (parsed.breakpoint || parsed.state)
202
+ return true;
203
+ }
204
+ return false;
205
+ }
206
+ //# sourceMappingURL=variants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variants.js","sourceRoot":"","sources":["../src/variants.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypen-space/web",
3
- "version": "0.4.956",
3
+ "version": "0.5.2",
4
4
  "description": "Hypen web renderers - DOM and Canvas rendering for browsers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@chenglou/pretext": "^0.0.3",
46
- "@hypen-space/core": "^0.4.956",
46
+ "@hypen-space/core": "^0.5.2",
47
47
  "taffy-layout": "^2.0.3"
48
48
  },
49
49
  "devDependencies": {
@@ -30,7 +30,7 @@ bun add @hypen-space/core @hypen-space/web @hypen-space/web-engine
30
30
  ```typescript
31
31
  import { app, HypenModuleInstance } from "@hypen-space/core";
32
32
  import { Engine } from "@hypen-space/web-engine";
33
- import { createHypenClient } from "@hypen-space/web/canvas";
33
+ import { CanvasRenderer } from "@hypen-space/web/canvas";
34
34
 
35
35
  // Define state
36
36
  type AppState = {
@@ -54,12 +54,17 @@ async function main() {
54
54
  const engine = new Engine();
55
55
  await engine.init();
56
56
 
57
- // Create renderer + patch subscription in one call
58
- createHypenClient(canvas, engine, {
57
+ // Create renderer
58
+ const renderer = new CanvasRenderer(canvas, engine, {
59
59
  devicePixelRatio: window.devicePixelRatio,
60
60
  backgroundColor: "#ffffff",
61
61
  });
62
62
 
63
+ // Connect renderer
64
+ engine.setRenderCallback((patches) => {
65
+ renderer.applyPatches(patches);
66
+ });
67
+
63
68
  // Create module
64
69
  const instance = new HypenModuleInstance(engine, appModule);
65
70
 
@@ -244,7 +249,7 @@ Container {
244
249
  ## Configuration Options
245
250
 
246
251
  ```typescript
247
- const { renderer } = createHypenClient(canvas, engine, {
252
+ const renderer = new CanvasRenderer(canvas, engine, {
248
253
  // Display
249
254
  devicePixelRatio: window.devicePixelRatio, // HiDPI support
250
255
  backgroundColor: "#ffffff", // Canvas background
@@ -349,7 +354,7 @@ let timeoutId: number;
349
354
  ### 3. Measure Performance
350
355
 
351
356
  ```typescript
352
- const { renderer } = createHypenClient(canvas, engine, {
357
+ const renderer = new CanvasRenderer(canvas, engine, {
353
358
  logPerformance: true, // Logs FPS and frame times
354
359
  });
355
360
  ```
@@ -361,7 +366,7 @@ const { renderer } = createHypenClient(canvas, engine, {
361
366
  Make sure to set `devicePixelRatio`:
362
367
 
363
368
  ```typescript
364
- const { renderer } = createHypenClient(canvas, engine, {
369
+ const renderer = new CanvasRenderer(canvas, engine, {
365
370
  devicePixelRatio: window.devicePixelRatio,
366
371
  });
367
372
  ```
@@ -371,7 +376,7 @@ const { renderer } = createHypenClient(canvas, engine, {
371
376
  Ensure `enableInputOverlay` is true:
372
377
 
373
378
  ```typescript
374
- const { renderer } = createHypenClient(canvas, engine, {
379
+ const renderer = new CanvasRenderer(canvas, engine, {
375
380
  enableInputOverlay: true,
376
381
  });
377
382
  ```
@@ -13,12 +13,10 @@ The Canvas Renderer is a complete alternative to the DOM renderer that draws all
13
13
 
14
14
  ## Quick Start
15
15
 
16
- `createHypenClient` is the recommended one-call wiring (constructs the renderer and subscribes to engine patches). `CanvasRenderer` is still exported directly for advanced setups.
17
-
18
16
  ```typescript
19
17
  import { app } from "@hypen-space/core";
20
18
  import { Engine } from "@hypen-space/web-engine";
21
- import { createHypenClient } from "@hypen-space/web/canvas";
19
+ import { CanvasRenderer } from "@hypen-space/web/canvas";
22
20
 
23
21
  // Setup canvas
24
22
  const canvas = document.getElementById("app") as HTMLCanvasElement;
@@ -27,8 +25,13 @@ const canvas = document.getElementById("app") as HTMLCanvasElement;
27
25
  const engine = new Engine();
28
26
  await engine.init();
29
27
 
30
- // Create canvas renderer + patch subscription in one call
31
- createHypenClient(canvas, engine);
28
+ // Create canvas renderer
29
+ const renderer = new CanvasRenderer(canvas, engine);
30
+
31
+ // Set render callback
32
+ engine.setRenderCallback((patches) => {
33
+ renderer.applyPatches(patches);
34
+ });
32
35
 
33
36
  // Render UI
34
37
  await engine.renderSource(`
@@ -14,6 +14,21 @@ interface IEngine {
14
14
  dispatchAction(name: string, payload?: any): void;
15
15
  }
16
16
 
17
+ // Maps a DOM event type to the applicator prop names the engine may have set
18
+ // on a node, in priority order. Multi-word events (`mouseenter`) need their
19
+ // proper camelCase form (`onMouseEnter`) since the engine emits applicator
20
+ // names verbatim. `mouseenter` also accepts `onHover` as an alias.
21
+ const CANVAS_EVENT_PROP_NAMES: Record<string, string[]> = {
22
+ mouseenter: ["onMouseEnter", "onHover", "onmouseenter", "mouseenter"],
23
+ mouseleave: ["onMouseLeave", "onmouseleave", "mouseleave"],
24
+ mousedown: ["onMouseDown", "onmousedown", "mousedown"],
25
+ mouseup: ["onMouseUp", "onmouseup", "mouseup"],
26
+ dblclick: ["onDblClick", "onDoubleClick", "ondblclick", "dblclick"],
27
+ contextmenu: ["onContextMenu", "oncontextmenu", "contextmenu"],
28
+ keydown: ["onKeyDown", "onkeydown", "keydown"],
29
+ keyup: ["onKeyUp", "onkeyup", "keyup"],
30
+ };
31
+
17
32
  /**
18
33
  * Canvas Event Manager
19
34
  */
@@ -210,6 +225,13 @@ export class CanvasEventManager {
210
225
 
211
226
  this.mouseDownNode = node;
212
227
 
228
+ // Track pressed (`:active`) state so paint-time `:active` variants resolve.
229
+ // Repaint so the active style appears immediately on press.
230
+ if (node) {
231
+ node.pressed = true;
232
+ this.requestRedraw();
233
+ }
234
+
213
235
  if (node && node.clickable) {
214
236
  this.dispatchNodeEvent(node, "mousedown", {
215
237
  button: e.button,
@@ -234,6 +256,13 @@ export class CanvasEventManager {
234
256
  const hit = this.hitTest(point);
235
257
  const node = this.findClickableAncestor(hit) ?? hit;
236
258
 
259
+ // Clear the pressed (`:active`) flag from the node that was pressed —
260
+ // release ends `:active` even if the pointer drifted off the node first.
261
+ if (this.mouseDownNode && this.mouseDownNode.pressed) {
262
+ this.mouseDownNode.pressed = false;
263
+ this.requestRedraw();
264
+ }
265
+
237
266
  if (node && node.clickable) {
238
267
  this.dispatchNodeEvent(node, "mouseup", {
239
268
  button: e.button,
@@ -383,13 +412,25 @@ export class CanvasEventManager {
383
412
  * Dispatch event to engine
384
413
  */
385
414
  private dispatchNodeEvent(node: VirtualNode, eventType: string, data: any): void {
386
- // Engine emits event applicators in camelCase (`onClick`, `onChange`). The
387
- // older flat form `onclick` is still accepted. After prop normalisation the
388
- // value is either a string (action name) or an object carrying an action
389
- // name at `"0"` plus an auxiliary payload (e.g. `{ "0": "@router.push",
390
- // to: "/notifications" }` for `@router.push(to: "/notifications")`).
391
- const camel = `on${eventType.charAt(0).toUpperCase()}${eventType.slice(1)}`;
392
- let spec: unknown = node.props[camel] ?? node.props[`on${eventType}`] ?? node.props[eventType];
415
+ // Engine emits event applicators in camelCase (`onClick`, `onMouseEnter`).
416
+ // Multi-word DOM events like `mouseenter` must map to `onMouseEnter`, not
417
+ // the naive `onMouseenter` that `on${capitalize(eventType)}` would produce.
418
+ // The older flat form `onclick`/`onmouseenter` is still accepted. After
419
+ // prop normalisation the value is either a string (action name) or an
420
+ // object carrying an action name at `"0"` plus an auxiliary payload.
421
+ const propNames = CANVAS_EVENT_PROP_NAMES[eventType] ?? [
422
+ `on${eventType.charAt(0).toUpperCase()}${eventType.slice(1)}`,
423
+ `on${eventType}`,
424
+ eventType,
425
+ ];
426
+
427
+ let spec: unknown;
428
+ for (const name of propNames) {
429
+ if (node.props[name] != null) {
430
+ spec = node.props[name];
431
+ break;
432
+ }
433
+ }
393
434
 
394
435
  // Actionable components fall back to the bare `action` prop on click.
395
436
  if (spec == null && eventType === "click") {
@@ -429,6 +470,7 @@ export class CanvasEventManager {
429
470
  this.canvas.removeEventListener("contextmenu", this.boundOnContextMenu);
430
471
  this.canvas.removeEventListener("keydown", this.boundOnKeyDown);
431
472
  this.canvas.removeEventListener("keyup", this.boundOnKeyUp);
473
+ if (this.mouseDownNode) this.mouseDownNode.pressed = false;
432
474
  this.rootNode = null;
433
475
  this.hoveredNode = null;
434
476
  this.focusedNode = null;
@@ -5,14 +5,6 @@
5
5
  */
6
6
 
7
7
  export { CanvasRenderer } from "./renderer.js";
8
- export {
9
- createCanvasClient as createHypenClient,
10
- createCanvasClient,
11
- type CanvasClient as HypenClient,
12
- type CanvasClient,
13
- type CanvasClientOptions as HypenClientOptions,
14
- type CanvasClientOptions,
15
- } from "../client.js";
16
8
  export { registerPainter } from "./paint.js";
17
9
  export { CanvasEventManager } from "./events.js";
18
10
  export { InputOverlay } from "./input.js";
@@ -7,14 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { VirtualNode, Layout, BoxSpacing } from "./types.js";
10
- import {
11
- parseSpacing,
12
- parseSize,
13
- cssLengthToPx,
14
- cssLengthToDimension,
15
- setCurrentViewport,
16
- getCurrentViewport,
17
- } from "./utils.js";
10
+ import { parseSpacing, parseSize, cssLengthToPx, cssLengthToDimension } from "./utils.js";
18
11
  import { measureText } from "./text.js";
19
12
  import { getImageNaturalAspect } from "./paint.js";
20
13
 
@@ -54,6 +47,19 @@ export async function initTaffyLayout(): Promise<boolean> {
54
47
  taffyInitPromise = (async () => {
55
48
  try {
56
49
  taffy = await import("taffy-layout");
50
+ // In a server runtime (Bun/Node — including test runs where a JSDOM
51
+ // `window` may be globally registered by another test file), load the
52
+ // WASM from node_modules via fs. This path is deterministic; the
53
+ // fetch-based branch below depends on a reachable origin/CDN and must
54
+ // never be selected just because a test polyfilled `window`.
55
+ const isServerRuntime =
56
+ typeof process !== "undefined" &&
57
+ !!(process.versions?.bun || process.versions?.node);
58
+ if (isServerRuntime) {
59
+ await taffy.loadTaffy();
60
+ taffyReady = true;
61
+ return;
62
+ }
57
63
  // Bundlers (esp. Bun's browser bundler) sometimes inline a file:// URL
58
64
  // for import.meta.url inside taffy_wasm.js, which the browser blocks
59
65
  // ("Not allowed to load local resource"). The package's own `loadTaffy`
@@ -61,26 +67,40 @@ export async function initTaffyLayout(): Promise<boolean> {
61
67
  // raw `__wbg_init` (default export of the `taffy-layout/wasm` subpath)
62
68
  // directly with an explicit Response.
63
69
  let usedExplicit = false;
64
- if (typeof window !== "undefined" && typeof fetch === "function") {
65
- const candidates = [
66
- new URL("/taffy_wasm_bg.wasm", window.location.origin),
67
- new URL("https://cdn.jsdelivr.net/npm/taffy-layout@2.0.3/pkg/taffy_wasm_bg.wasm"),
68
- new URL("https://unpkg.com/taffy-layout@2.0.3/pkg/taffy_wasm_bg.wasm"),
69
- ];
70
- const rawWasm = await import(
71
- /* @vite-ignore */ "taffy-layout/wasm" as string
72
- );
73
- const wbgInit = rawWasm.default as (input?: any) => Promise<unknown>;
74
- for (const url of candidates) {
75
- try {
76
- const res = await fetch(url);
77
- if (!res.ok) continue;
78
- await wbgInit({ module_or_path: res });
79
- usedExplicit = true;
80
- break;
81
- } catch {
82
- // Try next candidate
70
+ // Only take the fetch path in a real browser. Test environments (and
71
+ // anything else that installs a fake `window` global) must fall through
72
+ // to `loadTaffy()`, which resolves the WASM from the package on disk —
73
+ // a fake window without `location.origin` used to throw here and
74
+ // silently disable Taffy for the rest of the process.
75
+ const isRealBrowser =
76
+ typeof window !== "undefined" &&
77
+ typeof fetch === "function" &&
78
+ typeof window.location?.origin === "string" &&
79
+ window.document?.defaultView === window;
80
+ if (isRealBrowser) {
81
+ try {
82
+ const candidates = [
83
+ new URL("/taffy_wasm_bg.wasm", window.location.origin),
84
+ new URL("https://cdn.jsdelivr.net/npm/taffy-layout@2.0.3/pkg/taffy_wasm_bg.wasm"),
85
+ new URL("https://unpkg.com/taffy-layout@2.0.3/pkg/taffy_wasm_bg.wasm"),
86
+ ];
87
+ const rawWasm = await import(
88
+ /* @vite-ignore */ "taffy-layout/wasm" as string
89
+ );
90
+ const wbgInit = rawWasm.default as (input?: any) => Promise<unknown>;
91
+ for (const url of candidates) {
92
+ try {
93
+ const res = await fetch(url);
94
+ if (!res.ok) continue;
95
+ await wbgInit({ module_or_path: res });
96
+ usedExplicit = true;
97
+ break;
98
+ } catch {
99
+ // Try next candidate
100
+ }
83
101
  }
102
+ } catch {
103
+ // Fall through to loadTaffy()
84
104
  }
85
105
  }
86
106
  if (!usedExplicit) {
@@ -1088,30 +1108,18 @@ export function computeLayout(
1088
1108
  x: number = 0,
1089
1109
  y: number = 0,
1090
1110
  ): void {
1091
- // Publish the active viewport so `cssLengthToPx` can resolve `vw` / `vh`
1092
- // against the actual canvas dimensions during this layout pass. Without
1093
- // this, `vh` falls back to `window.innerHeight` (wrong inside an iframe
1094
- // or scaled preview pane) or, worse, to a unitless pixel value that
1095
- // collapses entire layouts. Layout is synchronous, so a try/finally
1096
- // restoring the previous slot is safe under nested or re-entrant calls.
1097
- const prevViewport = getCurrentViewport();
1098
- setCurrentViewport({ width: availableWidth, height: availableHeight });
1099
- try {
1100
- if (taffyReady && taffy) {
1111
+ if (taffyReady && taffy) {
1112
+ computeLayoutTaffy(ctx, node, availableWidth, availableHeight, x, y);
1113
+ // After the first pass, Taffy's grid auto-row sizing for aspect-ratio
1114
+ // image leaves can collapse rows (the leaf's max-content height is 0
1115
+ // before column widths are known). Detect that pattern and re-run
1116
+ // with explicit `gridAutoRows` derived from the now-known column
1117
+ // width see `annotateCollapsedAspectGrids`.
1118
+ if (annotateCollapsedAspectGrids(node)) {
1101
1119
  computeLayoutTaffy(ctx, node, availableWidth, availableHeight, x, y);
1102
- // After the first pass, Taffy's grid auto-row sizing for aspect-ratio
1103
- // image leaves can collapse rows (the leaf's max-content height is 0
1104
- // before column widths are known). Detect that pattern and re-run
1105
- // with explicit `gridAutoRows` derived from the now-known column
1106
- // width — see `annotateCollapsedAspectGrids`.
1107
- if (annotateCollapsedAspectGrids(node)) {
1108
- computeLayoutTaffy(ctx, node, availableWidth, availableHeight, x, y);
1109
- }
1110
- } else {
1111
- computeLayoutFallback(ctx, node, availableWidth, availableHeight, x, y);
1112
1120
  }
1113
- } finally {
1114
- setCurrentViewport(prevViewport);
1121
+ } else {
1122
+ computeLayoutFallback(ctx, node, availableWidth, availableHeight, x, y);
1115
1123
  }
1116
1124
  }
1117
1125