@hypen-space/web 0.4.950 → 0.4.952

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 (143) hide show
  1. package/README.md +13 -20
  2. package/dist/canvas/index.d.ts +1 -0
  3. package/dist/canvas/index.js +1 -0
  4. package/dist/canvas/index.js.map +1 -1
  5. package/dist/canvas/paint.js +11 -3
  6. package/dist/canvas/paint.js.map +1 -1
  7. package/dist/canvas/renderer.d.ts +3 -0
  8. package/dist/canvas/renderer.js +41 -3
  9. package/dist/canvas/renderer.js.map +1 -1
  10. package/dist/client.d.ts +36 -0
  11. package/dist/client.js +47 -0
  12. package/dist/client.js.map +1 -0
  13. package/dist/dom/applicators/font.d.ts +6 -11
  14. package/dist/dom/applicators/font.js +27 -16
  15. package/dist/dom/applicators/font.js.map +1 -1
  16. package/dist/dom/applicators/index.js +34 -17
  17. package/dist/dom/applicators/index.js.map +1 -1
  18. package/dist/dom/canvas/index.js +2 -2
  19. package/dist/dom/canvas/index.js.map +1 -1
  20. package/dist/dom/components/app.js +2 -2
  21. package/dist/dom/components/app.js.map +1 -1
  22. package/dist/dom/components/audio.js +2 -2
  23. package/dist/dom/components/audio.js.map +1 -1
  24. package/dist/dom/components/avatar.js +4 -3
  25. package/dist/dom/components/avatar.js.map +1 -1
  26. package/dist/dom/components/badge.js +2 -2
  27. package/dist/dom/components/badge.js.map +1 -1
  28. package/dist/dom/components/button.js +2 -2
  29. package/dist/dom/components/button.js.map +1 -1
  30. package/dist/dom/components/card.js +2 -2
  31. package/dist/dom/components/card.js.map +1 -1
  32. package/dist/dom/components/center.js +2 -2
  33. package/dist/dom/components/center.js.map +1 -1
  34. package/dist/dom/components/checkbox.js +5 -4
  35. package/dist/dom/components/checkbox.js.map +1 -1
  36. package/dist/dom/components/column.js +2 -2
  37. package/dist/dom/components/column.js.map +1 -1
  38. package/dist/dom/components/container.js +2 -2
  39. package/dist/dom/components/container.js.map +1 -1
  40. package/dist/dom/components/divider.js +2 -2
  41. package/dist/dom/components/divider.js.map +1 -1
  42. package/dist/dom/components/grid.js +10 -10
  43. package/dist/dom/components/grid.js.map +1 -1
  44. package/dist/dom/components/heading.js +4 -3
  45. package/dist/dom/components/heading.js.map +1 -1
  46. package/dist/dom/components/hypenapp.js +2 -2
  47. package/dist/dom/components/hypenapp.js.map +1 -1
  48. package/dist/dom/components/icon.js +5 -4
  49. package/dist/dom/components/icon.js.map +1 -1
  50. package/dist/dom/components/image.js +2 -2
  51. package/dist/dom/components/image.js.map +1 -1
  52. package/dist/dom/components/index.d.ts +2 -2
  53. package/dist/dom/components/index.js +2 -2
  54. package/dist/dom/components/index.js.map +1 -1
  55. package/dist/dom/components/input.js +2 -2
  56. package/dist/dom/components/input.js.map +1 -1
  57. package/dist/dom/components/link.js +2 -2
  58. package/dist/dom/components/link.js.map +1 -1
  59. package/dist/dom/components/list.js +2 -2
  60. package/dist/dom/components/list.js.map +1 -1
  61. package/dist/dom/components/paragraph.js +2 -2
  62. package/dist/dom/components/paragraph.js.map +1 -1
  63. package/dist/dom/components/progressbar.js +3 -3
  64. package/dist/dom/components/progressbar.js.map +1 -1
  65. package/dist/dom/components/route.js +2 -2
  66. package/dist/dom/components/route.js.map +1 -1
  67. package/dist/dom/components/router.js +2 -2
  68. package/dist/dom/components/router.js.map +1 -1
  69. package/dist/dom/components/row.js +10 -10
  70. package/dist/dom/components/row.js.map +1 -1
  71. package/dist/dom/components/select.js +4 -3
  72. package/dist/dom/components/select.js.map +1 -1
  73. package/dist/dom/components/slider.js +2 -2
  74. package/dist/dom/components/slider.js.map +1 -1
  75. package/dist/dom/components/spacer.js +2 -2
  76. package/dist/dom/components/spacer.js.map +1 -1
  77. package/dist/dom/components/spinner.js +4 -4
  78. package/dist/dom/components/spinner.js.map +1 -1
  79. package/dist/dom/components/stack.js +10 -10
  80. package/dist/dom/components/stack.js.map +1 -1
  81. package/dist/dom/components/switch.js +6 -5
  82. package/dist/dom/components/switch.js.map +1 -1
  83. package/dist/dom/components/text.js +2 -2
  84. package/dist/dom/components/text.js.map +1 -1
  85. package/dist/dom/components/textarea.js +2 -2
  86. package/dist/dom/components/textarea.js.map +1 -1
  87. package/dist/dom/components/video.js +2 -2
  88. package/dist/dom/components/video.js.map +1 -1
  89. package/dist/dom/debug.js +8 -6
  90. package/dist/dom/debug.js.map +1 -1
  91. package/dist/dom/index.d.ts +1 -0
  92. package/dist/dom/index.js +1 -0
  93. package/dist/dom/index.js.map +1 -1
  94. package/dist/dom/renderer.js +3 -2
  95. package/dist/dom/renderer.js.map +1 -1
  96. package/package.json +2 -2
  97. package/src/canvas/QUICKSTART.md +7 -12
  98. package/src/canvas/README.md +5 -8
  99. package/src/canvas/index.ts +8 -0
  100. package/src/canvas/paint.ts +11 -3
  101. package/src/canvas/renderer.ts +44 -4
  102. package/src/client.ts +88 -0
  103. package/src/dom/README.md +7 -8
  104. package/src/dom/applicators/font.ts +29 -16
  105. package/src/dom/applicators/index.ts +35 -17
  106. package/src/dom/canvas/index.ts +2 -2
  107. package/src/dom/components/app.ts +2 -2
  108. package/src/dom/components/audio.ts +2 -2
  109. package/src/dom/components/avatar.ts +4 -3
  110. package/src/dom/components/badge.ts +2 -2
  111. package/src/dom/components/button.ts +2 -2
  112. package/src/dom/components/card.ts +2 -2
  113. package/src/dom/components/center.ts +2 -2
  114. package/src/dom/components/checkbox.ts +5 -4
  115. package/src/dom/components/column.ts +2 -2
  116. package/src/dom/components/container.ts +2 -2
  117. package/src/dom/components/divider.ts +2 -2
  118. package/src/dom/components/grid.ts +10 -10
  119. package/src/dom/components/heading.ts +4 -3
  120. package/src/dom/components/hypenapp.ts +2 -2
  121. package/src/dom/components/icon.ts +5 -4
  122. package/src/dom/components/image.ts +2 -2
  123. package/src/dom/components/index.ts +3 -3
  124. package/src/dom/components/input.ts +2 -2
  125. package/src/dom/components/link.ts +2 -2
  126. package/src/dom/components/list.ts +2 -2
  127. package/src/dom/components/paragraph.ts +2 -2
  128. package/src/dom/components/progressbar.ts +3 -3
  129. package/src/dom/components/route.ts +2 -2
  130. package/src/dom/components/router.ts +2 -2
  131. package/src/dom/components/row.ts +10 -10
  132. package/src/dom/components/select.ts +4 -3
  133. package/src/dom/components/slider.ts +2 -2
  134. package/src/dom/components/spacer.ts +2 -2
  135. package/src/dom/components/spinner.ts +4 -4
  136. package/src/dom/components/stack.ts +10 -10
  137. package/src/dom/components/switch.ts +6 -5
  138. package/src/dom/components/text.ts +2 -2
  139. package/src/dom/components/textarea.ts +2 -2
  140. package/src/dom/components/video.ts +2 -2
  141. package/src/dom/debug.ts +8 -6
  142. package/src/dom/index.ts +8 -0
  143. package/src/dom/renderer.ts +3 -2
package/src/client.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * createHypenClient — single-call wiring for renderer + engine.
3
+ *
4
+ * Replaces the boilerplate:
5
+ * const renderer = new DOMRenderer(container, engine);
6
+ * engine.onPatches((patches) => renderer.applyPatches(patches));
7
+ *
8
+ * Works with either a local Engine (`setRenderCallback`) or RemoteEngine
9
+ * (`onPatches`) — they expose different APIs but the same intent.
10
+ */
11
+
12
+ import type { Patch } from "@hypen-space/core/types";
13
+ import { DOMRenderer } from "./dom/renderer.js";
14
+ import type { DebugConfig } from "./dom/debug.js";
15
+ import { CanvasRenderer } from "./canvas/renderer.js";
16
+ import type { CanvasRendererOptions } from "./canvas/types.js";
17
+
18
+ interface PatchSourceLike {
19
+ dispatchAction(name: string, payload?: unknown): void;
20
+ onPatches?(callback: (patches: Patch[]) => void): unknown;
21
+ setRenderCallback?(callback: (patches: Patch[]) => void): void;
22
+ }
23
+
24
+ function attachPatchListener(
25
+ engine: PatchSourceLike,
26
+ callback: (patches: Patch[]) => void,
27
+ ): void {
28
+ if (typeof engine.onPatches === "function") {
29
+ engine.onPatches(callback);
30
+ return;
31
+ }
32
+ if (typeof engine.setRenderCallback === "function") {
33
+ engine.setRenderCallback(callback);
34
+ return;
35
+ }
36
+ throw new Error(
37
+ "createHypenClient: engine has neither onPatches() nor setRenderCallback() — cannot subscribe to patches.",
38
+ );
39
+ }
40
+
41
+ export interface DomClientOptions {
42
+ debug?: Partial<DebugConfig>;
43
+ }
44
+
45
+ export interface DomClient {
46
+ renderer: DOMRenderer;
47
+ destroy(): void;
48
+ }
49
+
50
+ export function createDomClient(
51
+ container: HTMLElement,
52
+ engine: PatchSourceLike,
53
+ options?: DomClientOptions,
54
+ ): DomClient {
55
+ const renderer = new DOMRenderer(container, engine as any, options?.debug);
56
+ attachPatchListener(engine, (patches) => renderer.applyPatches(patches));
57
+ return {
58
+ renderer,
59
+ destroy() {
60
+ // DOMRenderer has no destroy yet; clearing the container is the
61
+ // closest approximation. Patch subscriptions live on the engine —
62
+ // the caller controls engine lifetime, so we don't tear them down.
63
+ container.innerHTML = "";
64
+ },
65
+ };
66
+ }
67
+
68
+ export interface CanvasClientOptions extends Partial<CanvasRendererOptions> {}
69
+
70
+ export interface CanvasClient {
71
+ renderer: CanvasRenderer;
72
+ destroy(): void;
73
+ }
74
+
75
+ export function createCanvasClient(
76
+ canvas: HTMLCanvasElement,
77
+ engine: PatchSourceLike,
78
+ options?: CanvasClientOptions,
79
+ ): CanvasClient {
80
+ const renderer = new CanvasRenderer(canvas, engine as any, options);
81
+ attachPatchListener(engine, (patches) => renderer.applyPatches(patches));
82
+ return {
83
+ renderer,
84
+ destroy() {
85
+ renderer.destroy();
86
+ },
87
+ };
88
+ }
package/src/dom/README.md CHANGED
@@ -37,18 +37,17 @@ src/dom/
37
37
 
38
38
  ### Browser Import
39
39
 
40
+ The recommended one-call wiring is `createHypenClient`. The bare `DOMRenderer` constructor is still exported for advanced cases (custom subscription, multi-listener fan-out, etc.).
41
+
40
42
  ```typescript
41
43
  import { app, HypenModuleInstance } from "@hypen-space/core";
42
44
  import { Engine } from "@hypen-space/web-engine";
43
- import { DOMRenderer } from "@hypen-space/web";
45
+ import { createHypenClient } from "@hypen-space/web/dom";
44
46
 
45
47
  const engine = new Engine();
46
48
  await engine.init();
47
49
 
48
- const renderer = new DOMRenderer(document.body, engine);
49
- engine.setRenderCallback((patches) => {
50
- renderer.applyPatches(patches);
51
- });
50
+ createHypenClient(document.body, engine);
52
51
  ```
53
52
 
54
53
  ### Server Import
@@ -79,7 +78,7 @@ All components are registered by default:
79
78
  ### Custom Components
80
79
 
81
80
  ```typescript
82
- const renderer = new DOMRenderer(document.body, engine);
81
+ const { renderer } = createHypenClient(document.body, engine);
83
82
  const registry = renderer.getComponentRegistry();
84
83
 
85
84
  registry.register("mycomponent", {
@@ -141,7 +140,7 @@ Style applicators are registered by default:
141
140
  ### Custom Applicators
142
141
 
143
142
  ```typescript
144
- const renderer = new DOMRenderer(document.body, engine);
143
+ const { renderer } = createHypenClient(document.body, engine);
145
144
  const applicators = renderer.getApplicatorRegistry();
146
145
 
147
146
  applicators.register("shadow", (el, value) => {
@@ -183,7 +182,7 @@ Canvas components have special applicators for drawing:
183
182
 
184
183
  ```typescript
185
184
  // Register canvas applicators
186
- const renderer = new DOMRenderer(document.body, engine);
185
+ const { renderer } = createHypenClient(document.body, engine);
187
186
  const applicators = renderer.getApplicatorRegistry();
188
187
 
189
188
  // Canvas-specific applicators
@@ -5,8 +5,17 @@
5
5
  import type { ApplicatorHandler } from "./types.js";
6
6
  import { toCssLength } from "./size.js";
7
7
 
8
- // Track loaded Google Fonts to avoid duplicate link tags
9
- const loadedGoogleFonts = new Set<string>();
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
+ }
10
19
 
11
20
  // System font keywords that shouldn't be loaded from Google Fonts
12
21
  const systemFontKeywords = new Set([
@@ -27,40 +36,42 @@ function isSystemFont(fontName: string): boolean {
27
36
  }
28
37
 
29
38
  /**
30
- * Load a Google Font by injecting a link tag.
39
+ * Load a Google Font by injecting a link tag into the given document.
31
40
  * @param fontName - The Google Font name (e.g., "Roboto", "Open Sans")
41
+ * @param doc - The Document to inject into.
32
42
  */
33
- function loadGoogleFont(fontName: string): void {
43
+ function loadGoogleFont(fontName: string, doc: Document): void {
34
44
  const normalized = fontName.trim();
45
+ const loaded = getLoadedSet(doc);
35
46
 
36
47
  // Skip if already loaded or is a system font
37
- if (loadedGoogleFonts.has(normalized) || isSystemFont(normalized)) {
48
+ if (loaded.has(normalized) || isSystemFont(normalized)) {
38
49
  return;
39
50
  }
40
51
 
41
52
  // Mark as loading to avoid duplicates
42
- loadedGoogleFonts.add(normalized);
53
+ loaded.add(normalized);
43
54
 
44
55
  // Create link element for Google Fonts
45
- const link = document.createElement("link");
56
+ const link = doc.createElement("link");
46
57
  link.rel = "stylesheet";
47
58
  link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(normalized)}:wght@100;200;300;400;500;600;700;800;900&display=swap`;
48
59
 
49
60
  // Add to document head
50
- document.head.appendChild(link);
61
+ doc.head.appendChild(link);
51
62
  }
52
63
 
53
64
  /**
54
65
  * Parse a fontFamily value and load any Google Fonts.
55
66
  * Returns a CSS-safe font-family string.
56
67
  */
57
- function processFontFamily(value: string): string {
68
+ function processFontFamily(value: string, doc: Document): string {
58
69
  // Split by comma to handle font stacks
59
70
  const fonts = value.split(",").map(f => f.trim().replace(/["']/g, ""));
60
71
 
61
72
  for (const font of fonts) {
62
73
  if (!isSystemFont(font)) {
63
- loadGoogleFont(font);
74
+ loadGoogleFont(font, doc);
64
75
  }
65
76
  }
66
77
 
@@ -86,7 +97,7 @@ export const fontHandlers: Record<string, ApplicatorHandler> = {
86
97
  fontFamily: (el, value) => {
87
98
  const fontValue = String(value);
88
99
  // Process font family and load Google Fonts as needed
89
- el.style.fontFamily = processFontFamily(fontValue);
100
+ el.style.fontFamily = processFontFamily(fontValue, el.ownerDocument as Document);
90
101
  },
91
102
 
92
103
  textAlign: (el, value) => {
@@ -127,18 +138,20 @@ export const fontHandlers: Record<string, ApplicatorHandler> = {
127
138
  export const GoogleFonts = {
128
139
  /**
129
140
  * Preload a Google Font before it's used.
141
+ * Defaults to the global `document` if no doc is provided.
130
142
  */
131
- preload: loadGoogleFont,
143
+ preload: (fontName: string, doc: Document = document) => loadGoogleFont(fontName, doc),
132
144
 
133
145
  /**
134
- * Check if a font has been loaded.
146
+ * Check if a font has been loaded in the given document (defaults to global).
135
147
  */
136
- isLoaded: (fontName: string) => loadedGoogleFonts.has(fontName.trim()),
148
+ isLoaded: (fontName: string, doc: Document = document) =>
149
+ getLoadedSet(doc).has(fontName.trim()),
137
150
 
138
151
  /**
139
- * Get list of loaded fonts.
152
+ * Get list of loaded fonts for the given document (defaults to global).
140
153
  */
141
- getLoadedFonts: () => Array.from(loadedGoogleFonts),
154
+ getLoadedFonts: (doc: Document = document) => Array.from(getLoadedSet(doc)),
142
155
 
143
156
  /**
144
157
  * Popular Google Fonts for reference.
@@ -53,26 +53,40 @@ const BREAKPOINTS: Record<string, string> = {
53
53
  };
54
54
 
55
55
  /**
56
- * Singleton stylesheet for variant CSS rules
56
+ * Per-document stylesheet for variant CSS rules
57
57
  */
58
- let variantStyleSheet: CSSStyleSheet | null = null;
58
+ const variantStyleSheets = new Map<Document, CSSStyleSheet>();
59
59
 
60
60
  /**
61
- * Track inserted rules to avoid duplicates
61
+ * Track inserted rules to avoid duplicates (per-document)
62
62
  */
63
- const insertedRules = new Set<string>();
63
+ const insertedRules = new Map<Document, Set<string>>();
64
64
 
65
65
  /**
66
- * Get or create the variant stylesheet
66
+ * Get or create the variant stylesheet for a document
67
67
  */
68
- function getVariantStyleSheet(): CSSStyleSheet {
69
- if (!variantStyleSheet) {
70
- const style = document.createElement('style');
68
+ function getVariantStyleSheet(doc: Document): CSSStyleSheet {
69
+ let sheet = variantStyleSheets.get(doc);
70
+ if (!sheet) {
71
+ const style = doc.createElement('style');
71
72
  style.id = 'hypen-variants';
72
- document.head.appendChild(style);
73
- variantStyleSheet = style.sheet as CSSStyleSheet;
73
+ doc.head.appendChild(style);
74
+ sheet = style.sheet as CSSStyleSheet;
75
+ variantStyleSheets.set(doc, sheet);
74
76
  }
75
- return variantStyleSheet;
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;
76
90
  }
77
91
 
78
92
  /**
@@ -334,19 +348,21 @@ export class ApplicatorRegistry {
334
348
  const minWidth = BREAKPOINTS[breakpoint];
335
349
 
336
350
  if (minWidth) {
351
+ const doc = element.ownerDocument as Document;
352
+ const dedup = getInsertedRules(doc);
337
353
  const cssName = this.toKebabCase(prop);
338
354
  const cssValue = this.formatCssValue(cssName, value);
339
355
  const className = `hypen-${cssName.replace(/[^a-zA-Z0-9-]/g, '')}-${breakpoint}-${hashValue(value)}`;
340
356
  const ruleKey = `${className}:${cssValue}`;
341
357
 
342
358
  // Avoid duplicate rule insertion
343
- if (!insertedRules.has(ruleKey)) {
344
- const sheet = getVariantStyleSheet();
359
+ if (!dedup.has(ruleKey)) {
360
+ const sheet = getVariantStyleSheet(doc);
345
361
  sheet.insertRule(
346
362
  `@media (min-width: ${minWidth}) { .${className} { ${cssName}: ${cssValue}; } }`,
347
363
  sheet.cssRules.length
348
364
  );
349
- insertedRules.add(ruleKey);
365
+ dedup.add(ruleKey);
350
366
  }
351
367
 
352
368
  element.classList.add(className);
@@ -362,19 +378,21 @@ export class ApplicatorRegistry {
362
378
  // Only handle known CSS pseudo-states
363
379
  const validStates = ['hover', 'focus', 'active', 'disabled', 'focus-visible', 'focus-within'];
364
380
  if (validStates.includes(state)) {
381
+ const doc = element.ownerDocument as Document;
382
+ const dedup = getInsertedRules(doc);
365
383
  const cssName = this.toKebabCase(prop);
366
384
  const cssValue = this.formatCssValue(cssName, value);
367
385
  const className = `hypen-${cssName.replace(/[^a-zA-Z0-9-]/g, '')}-${state}-${hashValue(value)}`;
368
386
  const ruleKey = `${className}:${cssValue}`;
369
387
 
370
388
  // Avoid duplicate rule insertion
371
- if (!insertedRules.has(ruleKey)) {
372
- const sheet = getVariantStyleSheet();
389
+ if (!dedup.has(ruleKey)) {
390
+ const sheet = getVariantStyleSheet(doc);
373
391
  sheet.insertRule(
374
392
  `.${className}:${state} { ${cssName}: ${cssValue}; }`,
375
393
  sheet.cssRules.length
376
394
  );
377
- insertedRules.add(ruleKey);
395
+ dedup.add(ruleKey);
378
396
  }
379
397
 
380
398
  element.classList.add(className);
@@ -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(): HTMLElement {
12
- const el = document.createElement("canvas");
11
+ create(doc: Document): HTMLElement {
12
+ const el = doc.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(): HTMLElement {
12
- const el = document.createElement("div");
11
+ create(doc: Document): HTMLElement {
12
+ const el = doc.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(): HTMLElement {
9
- const el = document.createElement("audio");
8
+ create(doc: Document): HTMLElement {
9
+ const el = doc.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(): HTMLElement {
9
- const el = document.createElement("div");
8
+ create(doc: Document): HTMLElement {
9
+ const el = doc.createElement("div");
10
10
  el.dataset.hypenType = "avatar";
11
11
  el.style.display = "inline-flex";
12
12
  el.style.alignItems = "center";
@@ -23,10 +23,11 @@ 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;
26
27
  // Image source - support named arg with .0 suffix, positional arg, or plain name
27
28
  const src = props["src.0"] || props["0"] || props.src || props.source;
28
29
  if (src !== undefined) {
29
- const img = document.createElement("img");
30
+ const img = doc.createElement("img");
30
31
  img.src = String(src);
31
32
  img.style.width = "100%";
32
33
  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(): HTMLElement {
9
- const el = document.createElement("span");
8
+ create(doc: Document): HTMLElement {
9
+ const el = doc.createElement("span");
10
10
  el.dataset.hypenType = "badge";
11
11
  el.style.display = "inline-block";
12
12
  el.style.padding = "4px 8px";
@@ -9,8 +9,8 @@
9
9
  import type { ComponentHandler } from "./index.js";
10
10
 
11
11
  export const buttonHandler: ComponentHandler = {
12
- create(): HTMLElement {
13
- const el = document.createElement("button");
12
+ create(doc: Document): HTMLElement {
13
+ const el = doc.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(): HTMLElement {
9
- const el = document.createElement("div");
8
+ create(doc: Document): HTMLElement {
9
+ const el = doc.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(): HTMLElement {
13
- const el = document.createElement("div");
12
+ create(doc: Document): HTMLElement {
13
+ const el = doc.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(): HTMLElement {
9
- const wrapper = document.createElement("label");
8
+ create(doc: Document): HTMLElement {
9
+ const wrapper = doc.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 = document.createElement("input");
16
+ const input = doc.createElement("input");
17
17
  input.type = "checkbox";
18
18
  input.dataset.hypenCheckbox = "true";
19
19
 
@@ -23,6 +23,7 @@ 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;
26
27
  const input = el.querySelector('input[type="checkbox"]') as HTMLInputElement;
27
28
  if (!input) return;
28
29
 
@@ -46,7 +47,7 @@ export const checkboxHandler: ComponentHandler = {
46
47
  textNodes.forEach(node => node.remove());
47
48
 
48
49
  // Add new label text
49
- el.appendChild(document.createTextNode(String(label)));
50
+ el.appendChild(doc.createTextNode(String(label)));
50
51
  }
51
52
  },
52
53
  };
@@ -17,8 +17,8 @@
17
17
  import type { ComponentHandler } from "./index.js";
18
18
 
19
19
  export const columnHandler: ComponentHandler = {
20
- create(): HTMLElement {
21
- const el = document.createElement("div");
20
+ create(doc: Document): HTMLElement {
21
+ const el = doc.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(): HTMLElement {
9
- const el = document.createElement("div");
8
+ create(doc: Document): HTMLElement {
9
+ const el = doc.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(): HTMLElement {
9
- const el = document.createElement("hr");
8
+ create(doc: Document): HTMLElement {
9
+ const el = doc.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
10
- let gridStylesInjected = false;
11
- function ensureGridStyles(): void {
12
- if (gridStylesInjected) return;
13
- gridStylesInjected = true;
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);
14
14
 
15
- const style = document.createElement("style");
15
+ const style = doc.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(): void {
21
21
  align-self: stretch;
22
22
  }
23
23
  `;
24
- document.head.appendChild(style);
24
+ doc.head.appendChild(style);
25
25
  }
26
26
 
27
27
  export const gridHandler: ComponentHandler = {
28
- create(): HTMLElement {
29
- ensureGridStyles();
28
+ create(doc: Document): HTMLElement {
29
+ ensureGridStyles(doc);
30
30
 
31
- const el = document.createElement("div");
31
+ const el = doc.createElement("div");
32
32
  el.style.display = "grid";
33
33
  el.dataset.hypenType = "grid";
34
34
  return el;
@@ -5,17 +5,18 @@
5
5
  import type { ComponentHandler } from "./index.js";
6
6
 
7
7
  export const headingHandler: ComponentHandler = {
8
- create(): HTMLElement {
9
- const el = document.createElement("h2");
8
+ create(doc: Document): HTMLElement {
9
+ const el = doc.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;
15
16
  // Level property (1-6)
16
17
  if (props.level !== undefined) {
17
18
  const level = Math.max(1, Math.min(6, Number(props.level)));
18
- const newEl = document.createElement(`h${level}`);
19
+ const newEl = doc.createElement(`h${level}`);
19
20
  newEl.dataset.hypenType = "heading";
20
21
 
21
22
  // Copy content and attributes
@@ -29,8 +29,8 @@ const activeInstances = new WeakMap<
29
29
  >();
30
30
 
31
31
  export const hypenAppHandler: ComponentHandler = {
32
- create(): HTMLElement {
33
- const el = document.createElement("div");
32
+ create(doc: Document): HTMLElement {
33
+ const el = doc.createElement("div");
34
34
  el.dataset.hypenType = "hypenapp";
35
35
  el.style.display = "contents"; // Don't affect layout
36
36
  return el;
@@ -11,8 +11,8 @@ import type { ComponentHandler } from "./index.js";
11
11
  const SVG_NS = "http://www.w3.org/2000/svg";
12
12
 
13
13
  export const iconHandler: ComponentHandler = {
14
- create(): HTMLElement {
15
- const el = document.createElement("span");
14
+ create(doc: Document): HTMLElement {
15
+ const el = doc.createElement("span");
16
16
  el.style.display = "inline-flex";
17
17
  el.style.alignItems = "center";
18
18
  el.style.justifyContent = "center";
@@ -22,6 +22,7 @@ export const iconHandler: ComponentHandler = {
22
22
  },
23
23
 
24
24
  applyProps(el: HTMLElement, props: Record<string, any>): void {
25
+ const doc = el.ownerDocument as Document;
25
26
  // Server-resolved icon data (from engine's ResourceRegistry)
26
27
  const paths: Array<{
27
28
  d: string;
@@ -39,7 +40,7 @@ export const iconHandler: ComponentHandler = {
39
40
 
40
41
  if (paths && Array.isArray(paths)) {
41
42
  // Render from pre-resolved SVG path data
42
- const svg = document.createElementNS(SVG_NS, "svg");
43
+ const svg = doc.createElementNS(SVG_NS, "svg");
43
44
  svg.setAttribute("xmlns", SVG_NS);
44
45
  svg.setAttribute("width", String(size));
45
46
  svg.setAttribute("height", String(size));
@@ -48,7 +49,7 @@ export const iconHandler: ComponentHandler = {
48
49
  svg.style.display = "block";
49
50
 
50
51
  for (const pathData of paths) {
51
- const path = document.createElementNS(SVG_NS, "path");
52
+ const path = doc.createElementNS(SVG_NS, "path");
52
53
  path.setAttribute("d", pathData.d);
53
54
  path.setAttribute("fill", pathData.fill || "none");
54
55
  path.setAttribute(
@@ -5,8 +5,8 @@
5
5
  import type { ComponentHandler } from "./index.js";
6
6
 
7
7
  export const imageHandler: ComponentHandler = {
8
- create(): HTMLElement {
9
- const el = document.createElement("img");
8
+ create(doc: Document): HTMLElement {
9
+ const el = doc.createElement("img");
10
10
  el.dataset.hypenType = "image";
11
11
  return el as any as HTMLElement;
12
12
  },