@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
@@ -21,6 +21,8 @@ import {
21
21
  unregisterEvent,
22
22
  getKeyTarget,
23
23
  setKeyTarget,
24
+ getMeta,
25
+ setMeta,
24
26
  } from "../element-data.js";
25
27
 
26
28
  const log = frameworkLoggers.events;
@@ -197,6 +199,7 @@ function createEventHandler(
197
199
  eventType: string,
198
200
  options: EventHandlerOptions = {}
199
201
  ): ApplicatorHandler {
202
+ const actionMetaKey = `event:${eventType}`;
200
203
  return (element: HTMLElement, value: unknown) => {
201
204
  const { actionName, payload: customPayload } = extractActionDetails(value);
202
205
 
@@ -205,14 +208,18 @@ function createEventHandler(
205
208
  return;
206
209
  }
207
210
 
211
+ // Store the current action on the element; the persistent listener reads
212
+ // it per event, so re-applying with a different action (e.g. a keyed-
213
+ // reused node) replaces the dispatch target instead of stacking listeners.
214
+ setMeta(element, actionMetaKey, { actionName, customPayload });
215
+
208
216
  const disposables = getElementDisposables(element);
209
217
 
210
218
  // Track that we've registered this event type
211
219
  // The disposable stack handles cleanup automatically
212
- const eventKey = `${eventType}:${actionName}`;
220
+ const eventKey = eventType;
213
221
  if (getRegisteredEvents(element).has(eventKey)) {
214
- // Already registered - skip to avoid duplicates
215
- // This can happen during re-renders
222
+ // Listener already installed the meta update above retargets it
216
223
  return;
217
224
  }
218
225
  registerEvent(element, eventKey);
@@ -222,6 +229,12 @@ function createEventHandler(
222
229
 
223
230
  // Create the event listener
224
231
  const listener = (event: Event) => {
232
+ const current = getMeta<{ actionName: string; customPayload: Record<string, unknown> }>(
233
+ element,
234
+ actionMetaKey,
235
+ );
236
+ if (!current) return;
237
+
225
238
  // Handle throttling
226
239
  if (options.throttleMs && throttleTimer) {
227
240
  return;
@@ -240,8 +253,8 @@ function createEventHandler(
240
253
 
241
254
  // Build payload
242
255
  const payload =
243
- Object.keys(customPayload).length > 0
244
- ? { ...customPayload }
256
+ Object.keys(current.customPayload).length > 0
257
+ ? { ...current.customPayload }
245
258
  : options.extractPayload
246
259
  ? options.extractPayload(event, element)
247
260
  : extractEventData(event, element);
@@ -250,9 +263,9 @@ function createEventHandler(
250
263
  const engine = getEngine(element);
251
264
  if (engine) {
252
265
  try {
253
- engine.dispatchAction(actionName, payload);
266
+ engine.dispatchAction(current.actionName, payload);
254
267
  } catch (err) {
255
- log.error(`Error dispatching action "${actionName}":`, err);
268
+ log.error(`Error dispatching action "${current.actionName}":`, err);
256
269
  }
257
270
  }
258
271
  };
@@ -274,6 +287,24 @@ function createEventHandler(
274
287
  };
275
288
  }
276
289
 
290
+ /**
291
+ * Dispatch the action encoded in an applicator value (e.g. `"@actions.save"`)
292
+ * to the element's engine. Shared by click wiring and keyboard activation so
293
+ * both routes dispatch identically. No-op if the value carries no action or
294
+ * the element has no engine.
295
+ */
296
+ export function triggerElementAction(element: HTMLElement, value: unknown): void {
297
+ const { actionName, payload } = extractActionDetails(value);
298
+ if (!actionName) return;
299
+ const engine = getEngine(element);
300
+ if (!engine) return;
301
+ try {
302
+ engine.dispatchAction(actionName, payload);
303
+ } catch (err) {
304
+ log.error(`Error dispatching action "${actionName}":`, err);
305
+ }
306
+ }
307
+
277
308
  /**
278
309
  * Create a keyboard event handler that filters by key
279
310
  */
@@ -496,6 +527,7 @@ export const eventHandlers: Record<string, ApplicatorHandler> = {
496
527
  // Mouse hover events
497
528
  onMouseEnter: createEventHandler("mouseenter", { extractPayload: mousePayload }),
498
529
  onMouseLeave: createEventHandler("mouseleave", { extractPayload: mousePayload }),
530
+ onHover: createEventHandler("mouseenter", { extractPayload: mousePayload }), // Alias for onMouseEnter
499
531
 
500
532
  // Two-way binding for .bind(@state.x)
501
533
  bind: ((element: HTMLElement, value: unknown) => {
@@ -5,17 +5,8 @@
5
5
  import type { ApplicatorHandler } from "./types.js";
6
6
  import { toCssLength } from "./size.js";
7
7
 
8
- // Track loaded Google Fonts per-document to avoid duplicate link tags
9
- const loadedGoogleFonts = new WeakMap<Document, Set<string>>();
10
-
11
- function getLoadedSet(doc: Document): Set<string> {
12
- let set = loadedGoogleFonts.get(doc);
13
- if (!set) {
14
- set = new Set<string>();
15
- loadedGoogleFonts.set(doc, set);
16
- }
17
- return set;
18
- }
8
+ // Track loaded Google Fonts to avoid duplicate link tags
9
+ const loadedGoogleFonts = new Set<string>();
19
10
 
20
11
  // System font keywords that shouldn't be loaded from Google Fonts
21
12
  const systemFontKeywords = new Set([
@@ -36,42 +27,40 @@ function isSystemFont(fontName: string): boolean {
36
27
  }
37
28
 
38
29
  /**
39
- * Load a Google Font by injecting a link tag into the given document.
30
+ * Load a Google Font by injecting a link tag.
40
31
  * @param fontName - The Google Font name (e.g., "Roboto", "Open Sans")
41
- * @param doc - The Document to inject into.
42
32
  */
43
- function loadGoogleFont(fontName: string, doc: Document): void {
33
+ function loadGoogleFont(fontName: string): void {
44
34
  const normalized = fontName.trim();
45
- const loaded = getLoadedSet(doc);
46
35
 
47
36
  // Skip if already loaded or is a system font
48
- if (loaded.has(normalized) || isSystemFont(normalized)) {
37
+ if (loadedGoogleFonts.has(normalized) || isSystemFont(normalized)) {
49
38
  return;
50
39
  }
51
40
 
52
41
  // Mark as loading to avoid duplicates
53
- loaded.add(normalized);
42
+ loadedGoogleFonts.add(normalized);
54
43
 
55
44
  // Create link element for Google Fonts
56
- const link = doc.createElement("link");
45
+ const link = document.createElement("link");
57
46
  link.rel = "stylesheet";
58
47
  link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(normalized)}:wght@100;200;300;400;500;600;700;800;900&display=swap`;
59
48
 
60
49
  // Add to document head
61
- doc.head.appendChild(link);
50
+ document.head.appendChild(link);
62
51
  }
63
52
 
64
53
  /**
65
54
  * Parse a fontFamily value and load any Google Fonts.
66
55
  * Returns a CSS-safe font-family string.
67
56
  */
68
- function processFontFamily(value: string, doc: Document): string {
57
+ function processFontFamily(value: string): string {
69
58
  // Split by comma to handle font stacks
70
59
  const fonts = value.split(",").map(f => f.trim().replace(/["']/g, ""));
71
60
 
72
61
  for (const font of fonts) {
73
62
  if (!isSystemFont(font)) {
74
- loadGoogleFont(font, doc);
63
+ loadGoogleFont(font);
75
64
  }
76
65
  }
77
66
 
@@ -97,7 +86,7 @@ export const fontHandlers: Record<string, ApplicatorHandler> = {
97
86
  fontFamily: (el, value) => {
98
87
  const fontValue = String(value);
99
88
  // Process font family and load Google Fonts as needed
100
- el.style.fontFamily = processFontFamily(fontValue, el.ownerDocument as Document);
89
+ el.style.fontFamily = processFontFamily(fontValue);
101
90
  },
102
91
 
103
92
  textAlign: (el, value) => {
@@ -138,20 +127,18 @@ export const fontHandlers: Record<string, ApplicatorHandler> = {
138
127
  export const GoogleFonts = {
139
128
  /**
140
129
  * Preload a Google Font before it's used.
141
- * Defaults to the global `document` if no doc is provided.
142
130
  */
143
- preload: (fontName: string, doc: Document = document) => loadGoogleFont(fontName, doc),
131
+ preload: loadGoogleFont,
144
132
 
145
133
  /**
146
- * Check if a font has been loaded in the given document (defaults to global).
134
+ * Check if a font has been loaded.
147
135
  */
148
- isLoaded: (fontName: string, doc: Document = document) =>
149
- getLoadedSet(doc).has(fontName.trim()),
136
+ isLoaded: (fontName: string) => loadedGoogleFonts.has(fontName.trim()),
150
137
 
151
138
  /**
152
- * Get list of loaded fonts for the given document (defaults to global).
139
+ * Get list of loaded fonts.
153
140
  */
154
- getLoadedFonts: (doc: Document = document) => Array.from(getLoadedSet(doc)),
141
+ getLoadedFonts: () => Array.from(loadedGoogleFonts),
155
142
 
156
143
  /**
157
144
  * Popular Google Fonts for reference.
@@ -40,53 +40,39 @@ import { advancedLayoutHandlers } from "./advanced-layout.js";
40
40
  import { backgroundHandlers } from "./background.js";
41
41
  import { displayHandlers } from "./display.js";
42
42
  import { transitionHandlers } from "./transition.js";
43
+ import { BREAKPOINTS as VARIANT_BREAKPOINTS, VALID_STATES } from "../../variants.js";
44
+ import { ariaHandlers } from "./aria.js";
43
45
 
44
46
  /**
45
- * Tailwind breakpoint values for responsive variants
47
+ * Tailwind breakpoint values for responsive variants.
48
+ * Derived from the shared `variants.ts` table (single source of truth across
49
+ * DOM + Canvas renderers) and expressed as CSS `px` strings for media queries.
46
50
  */
47
- const BREAKPOINTS: Record<string, string> = {
48
- sm: '640px',
49
- md: '768px',
50
- lg: '1024px',
51
- xl: '1280px',
52
- '2xl': '1536px',
53
- };
51
+ const BREAKPOINTS: Record<string, string> = Object.fromEntries(
52
+ Object.entries(VARIANT_BREAKPOINTS).map(([name, px]) => [name, `${px}px`]),
53
+ );
54
54
 
55
55
  /**
56
- * Per-document stylesheet for variant CSS rules
56
+ * Singleton stylesheet for variant CSS rules
57
57
  */
58
- const variantStyleSheets = new Map<Document, CSSStyleSheet>();
58
+ let variantStyleSheet: CSSStyleSheet | null = null;
59
59
 
60
60
  /**
61
- * Track inserted rules to avoid duplicates (per-document)
61
+ * Track inserted rules to avoid duplicates
62
62
  */
63
- const insertedRules = new Map<Document, Set<string>>();
63
+ const insertedRules = new Set<string>();
64
64
 
65
65
  /**
66
- * Get or create the variant stylesheet for a document
66
+ * Get or create the variant stylesheet
67
67
  */
68
- function getVariantStyleSheet(doc: Document): CSSStyleSheet {
69
- let sheet = variantStyleSheets.get(doc);
70
- if (!sheet) {
71
- const style = doc.createElement('style');
68
+ function getVariantStyleSheet(): CSSStyleSheet {
69
+ if (!variantStyleSheet) {
70
+ const style = document.createElement('style');
72
71
  style.id = 'hypen-variants';
73
- doc.head.appendChild(style);
74
- sheet = style.sheet as CSSStyleSheet;
75
- variantStyleSheets.set(doc, sheet);
72
+ document.head.appendChild(style);
73
+ variantStyleSheet = style.sheet as CSSStyleSheet;
76
74
  }
77
- return sheet;
78
- }
79
-
80
- /**
81
- * Get or create the inserted-rules dedup set for a document
82
- */
83
- function getInsertedRules(doc: Document): Set<string> {
84
- let set = insertedRules.get(doc);
85
- if (!set) {
86
- set = new Set<string>();
87
- insertedRules.set(doc, set);
88
- }
89
- return set;
75
+ return variantStyleSheet;
90
76
  }
91
77
 
92
78
  /**
@@ -341,6 +327,36 @@ export class ApplicatorRegistry {
341
327
  const atIndex = name.indexOf('@');
342
328
  const colonIndex = name.indexOf(':');
343
329
 
330
+ // Combined responsive + state variant: backgroundColor@md:hover.
331
+ // Canonical key order is `<prop>@<bp>:<state>`, so the breakpoint sits
332
+ // between '@' and ':'. Emitted as a pseudo-class rule nested in a media
333
+ // query so it applies only when BOTH the width matches AND the state holds.
334
+ if (atIndex !== -1 && colonIndex !== -1 && colonIndex > atIndex) {
335
+ const prop = name.slice(0, atIndex);
336
+ const breakpoint = name.slice(atIndex + 1, colonIndex);
337
+ const state = name.slice(colonIndex + 1);
338
+ const minWidth = BREAKPOINTS[breakpoint];
339
+
340
+ if (minWidth && VALID_STATES.includes(state)) {
341
+ const cssName = this.toKebabCase(prop);
342
+ const cssValue = this.formatCssValue(cssName, value);
343
+ const className = `hypen-${cssName.replace(/[^a-zA-Z0-9-]/g, '')}-${breakpoint}-${state}-${hashValue(value)}`;
344
+ const ruleKey = `${className}:${cssValue}`;
345
+
346
+ if (!insertedRules.has(ruleKey)) {
347
+ const sheet = getVariantStyleSheet();
348
+ sheet.insertRule(
349
+ `@media (min-width: ${minWidth}) { .${className}:${state} { ${cssName}: ${cssValue}; } }`,
350
+ sheet.cssRules.length
351
+ );
352
+ insertedRules.add(ruleKey);
353
+ }
354
+
355
+ element.classList.add(className);
356
+ }
357
+ return;
358
+ }
359
+
344
360
  // Responsive variant: padding@md, width@lg, etc.
345
361
  if (atIndex !== -1) {
346
362
  const prop = name.slice(0, atIndex);
@@ -348,21 +364,19 @@ export class ApplicatorRegistry {
348
364
  const minWidth = BREAKPOINTS[breakpoint];
349
365
 
350
366
  if (minWidth) {
351
- const doc = element.ownerDocument as Document;
352
- const dedup = getInsertedRules(doc);
353
367
  const cssName = this.toKebabCase(prop);
354
368
  const cssValue = this.formatCssValue(cssName, value);
355
369
  const className = `hypen-${cssName.replace(/[^a-zA-Z0-9-]/g, '')}-${breakpoint}-${hashValue(value)}`;
356
370
  const ruleKey = `${className}:${cssValue}`;
357
371
 
358
372
  // Avoid duplicate rule insertion
359
- if (!dedup.has(ruleKey)) {
360
- const sheet = getVariantStyleSheet(doc);
373
+ if (!insertedRules.has(ruleKey)) {
374
+ const sheet = getVariantStyleSheet();
361
375
  sheet.insertRule(
362
376
  `@media (min-width: ${minWidth}) { .${className} { ${cssName}: ${cssValue}; } }`,
363
377
  sheet.cssRules.length
364
378
  );
365
- dedup.add(ruleKey);
379
+ insertedRules.add(ruleKey);
366
380
  }
367
381
 
368
382
  element.classList.add(className);
@@ -375,24 +389,21 @@ export class ApplicatorRegistry {
375
389
  const prop = name.slice(0, colonIndex);
376
390
  const state = name.slice(colonIndex + 1);
377
391
 
378
- // Only handle known CSS pseudo-states
379
- const validStates = ['hover', 'focus', 'active', 'disabled', 'focus-visible', 'focus-within'];
380
- if (validStates.includes(state)) {
381
- const doc = element.ownerDocument as Document;
382
- const dedup = getInsertedRules(doc);
392
+ // Only handle known CSS pseudo-states (shared with the Canvas resolver)
393
+ if (VALID_STATES.includes(state)) {
383
394
  const cssName = this.toKebabCase(prop);
384
395
  const cssValue = this.formatCssValue(cssName, value);
385
396
  const className = `hypen-${cssName.replace(/[^a-zA-Z0-9-]/g, '')}-${state}-${hashValue(value)}`;
386
397
  const ruleKey = `${className}:${cssValue}`;
387
398
 
388
399
  // Avoid duplicate rule insertion
389
- if (!dedup.has(ruleKey)) {
390
- const sheet = getVariantStyleSheet(doc);
400
+ if (!insertedRules.has(ruleKey)) {
401
+ const sheet = getVariantStyleSheet();
391
402
  sheet.insertRule(
392
403
  `.${className}:${state} { ${cssName}: ${cssValue}; }`,
393
404
  sheet.cssRules.length
394
405
  );
395
- dedup.add(ruleKey);
406
+ insertedRules.add(ruleKey);
396
407
  }
397
408
 
398
409
  element.classList.add(className);
@@ -510,5 +521,9 @@ export class ApplicatorRegistry {
510
521
  for (const [name, handler] of Object.entries(transitionHandlers)) {
511
522
  this.register(name, handler as ApplicatorHandler);
512
523
  }
524
+
525
+ for (const [name, handler] of Object.entries(ariaHandlers)) {
526
+ this.register(name, handler as ApplicatorHandler);
527
+ }
513
528
  }
514
529
  }
@@ -8,8 +8,8 @@ import type { ComponentHandler } from "../components/index.js";
8
8
  import type { ApplicatorHandler } from "../applicators/index.js";
9
9
 
10
10
  export const canvasHandler: ComponentHandler = {
11
- create(doc: Document): HTMLElement {
12
- const el = doc.createElement("canvas");
11
+ create(): HTMLElement {
12
+ const el = document.createElement("canvas");
13
13
  el.dataset.hypenType = "canvas";
14
14
  return el as any as HTMLElement;
15
15
  },
@@ -8,8 +8,8 @@
8
8
  import type { ComponentHandler } from "./index.js";
9
9
 
10
10
  export const appHandler: ComponentHandler = {
11
- create(doc: Document): HTMLElement {
12
- const el = doc.createElement("div");
11
+ create(): HTMLElement {
12
+ const el = document.createElement("div");
13
13
  el.style.display = "flex";
14
14
  el.style.flexDirection = "column";
15
15
  el.style.minHeight = "100vh";
@@ -5,8 +5,8 @@
5
5
  import type { ComponentHandler } from "./index.js";
6
6
 
7
7
  export const audioHandler: ComponentHandler = {
8
- create(doc: Document): HTMLElement {
9
- const el = doc.createElement("audio");
8
+ create(): HTMLElement {
9
+ const el = document.createElement("audio");
10
10
  el.dataset.hypenType = "audio";
11
11
  return el as any as HTMLElement;
12
12
  },
@@ -5,8 +5,8 @@
5
5
  import type { ComponentHandler } from "./index.js";
6
6
 
7
7
  export const avatarHandler: ComponentHandler = {
8
- create(doc: Document): HTMLElement {
9
- const el = doc.createElement("div");
8
+ create(): HTMLElement {
9
+ const el = document.createElement("div");
10
10
  el.dataset.hypenType = "avatar";
11
11
  el.style.display = "inline-flex";
12
12
  el.style.alignItems = "center";
@@ -23,11 +23,10 @@ export const avatarHandler: ComponentHandler = {
23
23
  },
24
24
 
25
25
  applyProps(el: HTMLElement, props: Record<string, any>): void {
26
- const doc = el.ownerDocument as Document;
27
26
  // Image source - support named arg with .0 suffix, positional arg, or plain name
28
27
  const src = props["src.0"] || props["0"] || props.src || props.source;
29
28
  if (src !== undefined) {
30
- const img = doc.createElement("img");
29
+ const img = document.createElement("img");
31
30
  img.src = String(src);
32
31
  img.style.width = "100%";
33
32
  img.style.height = "100%";
@@ -5,8 +5,8 @@
5
5
  import type { ComponentHandler } from "./index.js";
6
6
 
7
7
  export const badgeHandler: ComponentHandler = {
8
- create(doc: Document): HTMLElement {
9
- const el = doc.createElement("span");
8
+ create(): HTMLElement {
9
+ const el = document.createElement("span");
10
10
  el.dataset.hypenType = "badge";
11
11
  el.style.display = "inline-block";
12
12
  el.style.padding = "4px 8px";
@@ -35,8 +35,7 @@ export const badgeHandler: ComponentHandler = {
35
35
  }
36
36
 
37
37
  // Text content
38
- // `??` (not `||`) so the literal `0`, `false`, `""` still render.
39
- const text = props["0"] ?? props.text;
38
+ const text = props["0"] || props.text;
40
39
  if (text !== undefined) {
41
40
  el.textContent = String(text);
42
41
  }
@@ -9,8 +9,8 @@
9
9
  import type { ComponentHandler } from "./index.js";
10
10
 
11
11
  export const buttonHandler: ComponentHandler = {
12
- create(doc: Document): HTMLElement {
13
- const el = doc.createElement("button");
12
+ create(): HTMLElement {
13
+ const el = document.createElement("button");
14
14
  // Reset default button styles for cross-platform consistency
15
15
  el.style.border = "none";
16
16
  el.style.background = "none";
@@ -5,8 +5,8 @@
5
5
  import type { ComponentHandler } from "./index.js";
6
6
 
7
7
  export const cardHandler: ComponentHandler = {
8
- create(doc: Document): HTMLElement {
9
- const el = doc.createElement("div");
8
+ create(): HTMLElement {
9
+ const el = document.createElement("div");
10
10
  el.dataset.hypenType = "card";
11
11
  el.style.backgroundColor = "#ffffff";
12
12
  el.style.borderRadius = "8px";
@@ -9,8 +9,8 @@
9
9
  import type { ComponentHandler } from "./index.js";
10
10
 
11
11
  export const centerHandler: ComponentHandler = {
12
- create(doc: Document): HTMLElement {
13
- const el = doc.createElement("div");
12
+ create(): HTMLElement {
13
+ const el = document.createElement("div");
14
14
  el.style.display = "flex";
15
15
  el.style.alignItems = "center";
16
16
  el.style.justifyContent = "center";
@@ -5,15 +5,15 @@
5
5
  import type { ComponentHandler } from "./index.js";
6
6
 
7
7
  export const checkboxHandler: ComponentHandler = {
8
- create(doc: Document): HTMLElement {
9
- const wrapper = doc.createElement("label");
8
+ create(): HTMLElement {
9
+ const wrapper = document.createElement("label");
10
10
  wrapper.dataset.hypenType = "checkbox";
11
11
  wrapper.style.display = "inline-flex";
12
12
  wrapper.style.alignItems = "center";
13
13
  wrapper.style.gap = "8px";
14
14
  wrapper.style.cursor = "pointer";
15
15
 
16
- const input = doc.createElement("input");
16
+ const input = document.createElement("input");
17
17
  input.type = "checkbox";
18
18
  input.dataset.hypenCheckbox = "true";
19
19
 
@@ -23,7 +23,6 @@ export const checkboxHandler: ComponentHandler = {
23
23
  },
24
24
 
25
25
  applyProps(el: HTMLElement, props: Record<string, any>): void {
26
- const doc = el.ownerDocument as Document;
27
26
  const input = el.querySelector('input[type="checkbox"]') as HTMLInputElement;
28
27
  if (!input) return;
29
28
 
@@ -47,7 +46,7 @@ export const checkboxHandler: ComponentHandler = {
47
46
  textNodes.forEach(node => node.remove());
48
47
 
49
48
  // Add new label text
50
- el.appendChild(doc.createTextNode(String(label)));
49
+ el.appendChild(document.createTextNode(String(label)));
51
50
  }
52
51
  },
53
52
  };
@@ -17,8 +17,8 @@
17
17
  import type { ComponentHandler } from "./index.js";
18
18
 
19
19
  export const columnHandler: ComponentHandler = {
20
- create(doc: Document): HTMLElement {
21
- const el = doc.createElement("div");
20
+ create(): HTMLElement {
21
+ const el = document.createElement("div");
22
22
  el.style.display = "flex";
23
23
  el.style.flexDirection = "column";
24
24
  el.style.alignItems = "stretch";
@@ -5,8 +5,8 @@
5
5
  import type { ComponentHandler } from "./index.js";
6
6
 
7
7
  export const containerHandler: ComponentHandler = {
8
- create(doc: Document): HTMLElement {
9
- const el = doc.createElement("div");
8
+ create(): HTMLElement {
9
+ const el = document.createElement("div");
10
10
  // Simple block container - wraps to content by default
11
11
  // Use .fillMaxWidth(true) to stretch
12
12
  el.dataset.hypenType = "container";
@@ -5,8 +5,8 @@
5
5
  import type { ComponentHandler } from "./index.js";
6
6
 
7
7
  export const dividerHandler: ComponentHandler = {
8
- create(doc: Document): HTMLElement {
9
- const el = doc.createElement("hr");
8
+ create(): HTMLElement {
9
+ const el = document.createElement("hr");
10
10
  el.dataset.hypenType = "divider";
11
11
  el.style.border = "none";
12
12
  el.style.borderTop = "1px solid #e0e0e0";
@@ -6,13 +6,13 @@
6
6
 
7
7
  import type { ComponentHandler } from "./index.js";
8
8
 
9
- // Inject global styles for grid children (per-document)
10
- const injectedDocs = new WeakSet<Document>();
11
- function ensureGridStyles(doc: Document): void {
12
- if (injectedDocs.has(doc)) return;
13
- injectedDocs.add(doc);
9
+ // Inject global styles for grid children
10
+ let gridStylesInjected = false;
11
+ function ensureGridStyles(): void {
12
+ if (gridStylesInjected) return;
13
+ gridStylesInjected = true;
14
14
 
15
- const style = doc.createElement("style");
15
+ const style = document.createElement("style");
16
16
  style.id = "hypen-grid-styles";
17
17
  style.textContent = `
18
18
  /* Grid children stretch to fill cells by default (matches Android behavior) */
@@ -21,14 +21,14 @@ function ensureGridStyles(doc: Document): void {
21
21
  align-self: stretch;
22
22
  }
23
23
  `;
24
- doc.head.appendChild(style);
24
+ document.head.appendChild(style);
25
25
  }
26
26
 
27
27
  export const gridHandler: ComponentHandler = {
28
- create(doc: Document): HTMLElement {
29
- ensureGridStyles(doc);
28
+ create(): HTMLElement {
29
+ ensureGridStyles();
30
30
 
31
- const el = doc.createElement("div");
31
+ const el = document.createElement("div");
32
32
  el.style.display = "grid";
33
33
  el.dataset.hypenType = "grid";
34
34
  return el;
@@ -5,18 +5,17 @@
5
5
  import type { ComponentHandler } from "./index.js";
6
6
 
7
7
  export const headingHandler: ComponentHandler = {
8
- create(doc: Document): HTMLElement {
9
- const el = doc.createElement("h2");
8
+ create(): HTMLElement {
9
+ const el = document.createElement("h2");
10
10
  el.dataset.hypenType = "heading";
11
11
  return el;
12
12
  },
13
13
 
14
14
  applyProps(el: HTMLElement, props: Record<string, any>): void {
15
- const doc = el.ownerDocument as Document;
16
15
  // Level property (1-6)
17
16
  if (props.level !== undefined) {
18
17
  const level = Math.max(1, Math.min(6, Number(props.level)));
19
- const newEl = doc.createElement(`h${level}`);
18
+ const newEl = document.createElement(`h${level}`);
20
19
  newEl.dataset.hypenType = "heading";
21
20
 
22
21
  // Copy content and attributes
@@ -32,8 +31,7 @@ export const headingHandler: ComponentHandler = {
32
31
  }
33
32
 
34
33
  // Text content
35
- // `??` (not `||`) so the literal `0`, `false`, `""` still render.
36
- const text = props["0"] ?? props.text;
34
+ const text = props["0"] || props.text;
37
35
  if (text !== undefined) {
38
36
  el.textContent = String(text);
39
37
  }