@hyperfixi/behaviors 3.2.0 → 4.0.0

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 (232) hide show
  1. package/CHANGELOG.md +203 -1
  2. package/README.md +29 -9
  3. package/dist/all.browser.global.js +47 -102
  4. package/dist/all.browser.global.js.map +1 -1
  5. package/dist/autodismiss.browser.global.js +5 -11
  6. package/dist/autodismiss.browser.global.js.map +1 -1
  7. package/dist/behaviors/autodismiss.cjs +19 -22
  8. package/dist/behaviors/autodismiss.cjs.map +1 -1
  9. package/dist/behaviors/autodismiss.d.cts +4 -4
  10. package/dist/behaviors/autodismiss.d.ts +4 -4
  11. package/dist/behaviors/autodismiss.js +3 -3
  12. package/dist/behaviors/clickoutside.cjs +19 -18
  13. package/dist/behaviors/clickoutside.cjs.map +1 -1
  14. package/dist/behaviors/clickoutside.d.cts +4 -4
  15. package/dist/behaviors/clickoutside.d.ts +4 -4
  16. package/dist/behaviors/clickoutside.js +3 -3
  17. package/dist/behaviors/clipboard.cjs +18 -19
  18. package/dist/behaviors/clipboard.cjs.map +1 -1
  19. package/dist/behaviors/clipboard.d.cts +4 -4
  20. package/dist/behaviors/clipboard.d.ts +4 -4
  21. package/dist/behaviors/clipboard.js +3 -3
  22. package/dist/behaviors/draggable.cjs +20 -25
  23. package/dist/behaviors/draggable.cjs.map +1 -1
  24. package/dist/behaviors/draggable.d.cts +4 -4
  25. package/dist/behaviors/draggable.d.ts +4 -4
  26. package/dist/behaviors/draggable.js +3 -3
  27. package/dist/behaviors/focustrap.cjs +18 -19
  28. package/dist/behaviors/focustrap.cjs.map +1 -1
  29. package/dist/behaviors/focustrap.d.cts +4 -4
  30. package/dist/behaviors/focustrap.d.ts +4 -4
  31. package/dist/behaviors/focustrap.js +3 -3
  32. package/dist/behaviors/removable.cjs +18 -21
  33. package/dist/behaviors/removable.cjs.map +1 -1
  34. package/dist/behaviors/removable.d.cts +4 -3
  35. package/dist/behaviors/removable.d.ts +4 -3
  36. package/dist/behaviors/removable.js +3 -3
  37. package/dist/behaviors/resizable.cjs +21 -21
  38. package/dist/behaviors/resizable.cjs.map +1 -1
  39. package/dist/behaviors/resizable.d.cts +4 -4
  40. package/dist/behaviors/resizable.d.ts +4 -4
  41. package/dist/behaviors/resizable.js +3 -3
  42. package/dist/behaviors/scrollreveal.cjs +19 -22
  43. package/dist/behaviors/scrollreveal.cjs.map +1 -1
  44. package/dist/behaviors/scrollreveal.d.cts +4 -4
  45. package/dist/behaviors/scrollreveal.d.ts +4 -4
  46. package/dist/behaviors/scrollreveal.js +3 -3
  47. package/dist/behaviors/sortable.cjs +19 -20
  48. package/dist/behaviors/sortable.cjs.map +1 -1
  49. package/dist/behaviors/sortable.d.cts +4 -4
  50. package/dist/behaviors/sortable.d.ts +4 -4
  51. package/dist/behaviors/sortable.js +3 -3
  52. package/dist/behaviors/tabs.cjs +20 -25
  53. package/dist/behaviors/tabs.cjs.map +1 -1
  54. package/dist/behaviors/tabs.d.cts +4 -4
  55. package/dist/behaviors/tabs.d.ts +4 -4
  56. package/dist/behaviors/tabs.js +3 -3
  57. package/dist/behaviors/toggleable.cjs +25 -26
  58. package/dist/behaviors/toggleable.cjs.map +1 -1
  59. package/dist/behaviors/toggleable.d.cts +4 -4
  60. package/dist/behaviors/toggleable.d.ts +4 -4
  61. package/dist/behaviors/toggleable.js +3 -3
  62. package/dist/chunk-2GIS4DCY.js +30 -0
  63. package/dist/chunk-2GIS4DCY.js.map +1 -0
  64. package/dist/chunk-3BCBZR6R.js +30 -0
  65. package/dist/chunk-3BCBZR6R.js.map +1 -0
  66. package/dist/chunk-4YUIDZAO.js +30 -0
  67. package/dist/chunk-4YUIDZAO.js.map +1 -0
  68. package/dist/{chunk-XYZ7FOXJ.js → chunk-AQCOK5J5.js} +10 -14
  69. package/dist/chunk-AQCOK5J5.js.map +1 -0
  70. package/dist/chunk-ATGHQQC7.js +30 -0
  71. package/dist/chunk-ATGHQQC7.js.map +1 -0
  72. package/dist/{chunk-HQDYH45I.js → chunk-CWLAXLWR.js} +5 -13
  73. package/dist/chunk-CWLAXLWR.js.map +1 -0
  74. package/dist/chunk-DMWN52SV.js +30 -0
  75. package/dist/chunk-DMWN52SV.js.map +1 -0
  76. package/dist/{chunk-PDMOOWFW.js → chunk-EIVPVTUI.js} +4 -8
  77. package/dist/{chunk-PDMOOWFW.js.map → chunk-EIVPVTUI.js.map} +1 -1
  78. package/dist/chunk-HHTEMPIT.js +30 -0
  79. package/dist/chunk-HHTEMPIT.js.map +1 -0
  80. package/dist/{chunk-26IX7QU7.js → chunk-HJESSMKB.js} +4 -6
  81. package/dist/{chunk-26IX7QU7.js.map → chunk-HJESSMKB.js.map} +1 -1
  82. package/dist/{chunk-5LFOFVZ3.js → chunk-IFAH652L.js} +6 -9
  83. package/dist/chunk-IFAH652L.js.map +1 -0
  84. package/dist/chunk-IUGJG4RA.js +30 -0
  85. package/dist/chunk-IUGJG4RA.js.map +1 -0
  86. package/dist/{chunk-6EG2MNC7.js → chunk-JNLVXPHR.js} +13 -13
  87. package/dist/chunk-KEZEGPEX.js +30 -0
  88. package/dist/chunk-KEZEGPEX.js.map +1 -0
  89. package/dist/{chunk-66U2RSYJ.js → chunk-KP4GNXYS.js} +4 -10
  90. package/dist/chunk-KP4GNXYS.js.map +1 -0
  91. package/dist/chunk-KXQMCN4O.js +30 -0
  92. package/dist/chunk-KXQMCN4O.js.map +1 -0
  93. package/dist/{chunk-YXS27XM6.js → chunk-KYBOZ7C4.js} +3 -7
  94. package/dist/chunk-KYBOZ7C4.js.map +1 -0
  95. package/dist/chunk-LBWM6TTU.js +30 -0
  96. package/dist/chunk-LBWM6TTU.js.map +1 -0
  97. package/dist/{chunk-3DVDS7CT.js → chunk-LCDONZBX.js} +5 -5
  98. package/dist/chunk-LCDONZBX.js.map +1 -0
  99. package/dist/{chunk-OJ3YWO3W.js → chunk-NE76IBV2.js} +3 -7
  100. package/dist/chunk-NE76IBV2.js.map +1 -0
  101. package/dist/{chunk-HMU342XE.js → chunk-PVO6TZCZ.js} +3 -9
  102. package/dist/{chunk-HMU342XE.js.map → chunk-PVO6TZCZ.js.map} +1 -1
  103. package/dist/{chunk-URZGNPTQ.js → chunk-RILYRQ72.js} +4 -10
  104. package/dist/chunk-RILYRQ72.js.map +1 -0
  105. package/dist/{chunk-FBQ3X3UY.js → chunk-U4QMFVAM.js} +5 -13
  106. package/dist/chunk-U4QMFVAM.js.map +1 -0
  107. package/dist/chunk-VWDTOSE3.js +26 -0
  108. package/dist/chunk-VWDTOSE3.js.map +1 -0
  109. package/dist/chunk-YRQBU2OW.js +30 -0
  110. package/dist/chunk-YRQBU2OW.js.map +1 -0
  111. package/dist/clickoutside.browser.global.js +5 -7
  112. package/dist/clickoutside.browser.global.js.map +1 -1
  113. package/dist/clipboard.browser.global.js +4 -8
  114. package/dist/clipboard.browser.global.js.map +1 -1
  115. package/dist/core.browser.global.js +13 -25
  116. package/dist/core.browser.global.js.map +1 -1
  117. package/dist/draggable.browser.global.js +5 -13
  118. package/dist/draggable.browser.global.js.map +1 -1
  119. package/dist/focustrap.browser.global.js +4 -8
  120. package/dist/focustrap.browser.global.js.map +1 -1
  121. package/dist/index.cjs +98 -273
  122. package/dist/index.cjs.map +1 -1
  123. package/dist/index.d.cts +10 -10
  124. package/dist/index.d.ts +10 -10
  125. package/dist/index.js +42 -55
  126. package/dist/index.js.map +1 -1
  127. package/dist/loaders.cjs +77 -239
  128. package/dist/loaders.cjs.map +1 -1
  129. package/dist/loaders.js +13 -13
  130. package/dist/registry.cjs +4 -4
  131. package/dist/registry.cjs.map +1 -1
  132. package/dist/registry.d.cts +5 -6
  133. package/dist/registry.d.ts +5 -6
  134. package/dist/registry.js +1 -1
  135. package/dist/removable.browser.global.js +3 -9
  136. package/dist/removable.browser.global.js.map +1 -1
  137. package/dist/resizable.browser.global.js +6 -9
  138. package/dist/resizable.browser.global.js.map +1 -1
  139. package/dist/resolver.browser.global.js +47 -102
  140. package/dist/resolver.browser.global.js.map +1 -1
  141. package/dist/schemas/index.cjs +40 -95
  142. package/dist/schemas/index.cjs.map +1 -1
  143. package/dist/schemas/index.d.cts +2 -2
  144. package/dist/schemas/index.d.ts +2 -2
  145. package/dist/schemas/index.js +11 -11
  146. package/dist/schemas/index.js.map +1 -1
  147. package/dist/scrollreveal.browser.global.js +5 -11
  148. package/dist/scrollreveal.browser.global.js.map +1 -1
  149. package/dist/sortable.browser.global.js +5 -9
  150. package/dist/sortable.browser.global.js.map +1 -1
  151. package/dist/tabs.browser.global.js +6 -14
  152. package/dist/tabs.browser.global.js.map +1 -1
  153. package/dist/toggleable.browser.global.js +9 -13
  154. package/dist/toggleable.browser.global.js.map +1 -1
  155. package/dist/{types-DCCtVQS5.d.cts → types-CbA4L8XJ.d.cts} +16 -23
  156. package/dist/{types-DCCtVQS5.d.ts → types-CbA4L8XJ.d.ts} +16 -23
  157. package/package.json +5 -5
  158. package/src/behavior-resolver.ts +24 -56
  159. package/src/behaviors/autodismiss.test.ts +12 -20
  160. package/src/behaviors/autodismiss.ts +8 -23
  161. package/src/behaviors/clickoutside.test.ts +13 -21
  162. package/src/behaviors/clickoutside.ts +8 -23
  163. package/src/behaviors/clipboard.test.ts +12 -34
  164. package/src/behaviors/clipboard.ts +8 -23
  165. package/src/behaviors/curated-runtime.test.ts +22 -24
  166. package/src/behaviors/draggable.ts +10 -24
  167. package/src/behaviors/experimental-runtime.test.ts +19 -18
  168. package/src/behaviors/focustrap.test.ts +12 -20
  169. package/src/behaviors/focustrap.ts +8 -23
  170. package/src/behaviors/integration.test.ts +19 -54
  171. package/src/behaviors/optional-runtime.test.ts +17 -22
  172. package/src/behaviors/removable.ts +6 -20
  173. package/src/behaviors/resizable.ts +9 -24
  174. package/src/behaviors/scrollreveal.test.ts +13 -21
  175. package/src/behaviors/scrollreveal.ts +8 -23
  176. package/src/behaviors/sortable.ts +7 -22
  177. package/src/behaviors/tabs.test.ts +12 -34
  178. package/src/behaviors/tabs.ts +8 -23
  179. package/src/behaviors/toggleable.ts +10 -26
  180. package/src/browser-resolver-bundle.ts +20 -30
  181. package/src/core-bundle.ts +11 -11
  182. package/src/index.test.ts +2 -2
  183. package/src/index.ts +30 -47
  184. package/src/registry.test.ts +9 -34
  185. package/src/registry.ts +7 -11
  186. package/src/schemas/autodismiss.schema.ts +3 -9
  187. package/src/schemas/clickoutside.schema.ts +3 -5
  188. package/src/schemas/clipboard.schema.ts +2 -6
  189. package/src/schemas/draggable.schema.ts +4 -12
  190. package/src/schemas/focustrap.schema.ts +2 -6
  191. package/src/schemas/index.ts +2 -1
  192. package/src/schemas/removable.schema.ts +2 -8
  193. package/src/schemas/resizable.schema.ts +5 -8
  194. package/src/schemas/scrollreveal.schema.ts +3 -9
  195. package/src/schemas/sortable.schema.ts +3 -7
  196. package/src/schemas/tabs.schema.ts +4 -12
  197. package/src/schemas/toggleable.schema.ts +10 -13
  198. package/src/schemas/types.ts +37 -30
  199. package/dist/chunk-2HUF6RMI.js +0 -40
  200. package/dist/chunk-2HUF6RMI.js.map +0 -1
  201. package/dist/chunk-3DVDS7CT.js.map +0 -1
  202. package/dist/chunk-5LFOFVZ3.js.map +0 -1
  203. package/dist/chunk-66U2RSYJ.js.map +0 -1
  204. package/dist/chunk-6TJXDCAQ.js +0 -11
  205. package/dist/chunk-6TJXDCAQ.js.map +0 -1
  206. package/dist/chunk-7NGOA3T5.js +0 -40
  207. package/dist/chunk-7NGOA3T5.js.map +0 -1
  208. package/dist/chunk-7TPDLB7W.js +0 -40
  209. package/dist/chunk-7TPDLB7W.js.map +0 -1
  210. package/dist/chunk-CR3J7VV6.js +0 -40
  211. package/dist/chunk-CR3J7VV6.js.map +0 -1
  212. package/dist/chunk-EDBG2F57.js +0 -40
  213. package/dist/chunk-EDBG2F57.js.map +0 -1
  214. package/dist/chunk-FBQ3X3UY.js.map +0 -1
  215. package/dist/chunk-HQDYH45I.js.map +0 -1
  216. package/dist/chunk-HX663BJ4.js +0 -40
  217. package/dist/chunk-HX663BJ4.js.map +0 -1
  218. package/dist/chunk-KROW43CG.js +0 -40
  219. package/dist/chunk-KROW43CG.js.map +0 -1
  220. package/dist/chunk-L52AGCMP.js +0 -40
  221. package/dist/chunk-L52AGCMP.js.map +0 -1
  222. package/dist/chunk-OJ3YWO3W.js.map +0 -1
  223. package/dist/chunk-ORBJ7MRW.js +0 -40
  224. package/dist/chunk-ORBJ7MRW.js.map +0 -1
  225. package/dist/chunk-SLSCZV4A.js +0 -40
  226. package/dist/chunk-SLSCZV4A.js.map +0 -1
  227. package/dist/chunk-URZGNPTQ.js.map +0 -1
  228. package/dist/chunk-WKBRZGGL.js +0 -40
  229. package/dist/chunk-WKBRZGGL.js.map +0 -1
  230. package/dist/chunk-XYZ7FOXJ.js.map +0 -1
  231. package/dist/chunk-YXS27XM6.js.map +0 -1
  232. /package/dist/{chunk-6EG2MNC7.js.map → chunk-JNLVXPHR.js.map} +0 -0
@@ -2,50 +2,28 @@ import { describe, it, expect, vi } from 'vitest';
2
2
 
3
3
  describe('Clipboard behavior', () => {
4
4
  describe('registerClipboard', () => {
5
- it('should compile its hyperscript source and execute', async () => {
5
+ it('defines the behavior on the host from its hyperscript source', async () => {
6
6
  const { registerClipboard, clipboardSource } = await import('./clipboard');
7
- const mock = {
8
- compileSync: vi.fn().mockReturnValue({ ok: true, ast: { type: 'behavior' } }),
9
- execute: vi.fn().mockResolvedValue(undefined),
10
- createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
11
- };
7
+ const host = { evaluate: vi.fn() };
12
8
 
13
- await registerClipboard(mock);
9
+ await registerClipboard(host);
14
10
 
15
- expect(mock.compileSync).toHaveBeenCalledWith(clipboardSource, { traditional: true });
16
- expect(mock.execute).toHaveBeenCalledWith(
17
- { type: 'behavior' },
18
- expect.objectContaining({ locals: expect.any(Map), globals: expect.any(Map) })
19
- );
11
+ expect(host.evaluate).toHaveBeenCalledWith(clipboardSource);
20
12
  });
21
13
 
22
- it('should throw on compile failure', async () => {
14
+ it('should throw when the host rejects the source', async () => {
23
15
  const { registerClipboard } = await import('./clipboard');
24
- const mock = {
25
- compileSync: vi.fn().mockReturnValue({ ok: false, errors: [{ message: 'boom' }] }),
26
- execute: vi.fn(),
27
- createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
16
+ const host = {
17
+ evaluate: vi.fn(() => {
18
+ throw new Error('boom');
19
+ }),
28
20
  };
29
- await expect(registerClipboard(mock)).rejects.toThrowError(/Failed to compile Clipboard/);
30
- });
31
-
32
- it('should throw when no runtime available', async () => {
33
- const { registerClipboard } = await import('./clipboard');
34
- await expect(registerClipboard(undefined)).rejects.toThrowError(/LokaScript not found/);
21
+ await expect(registerClipboard(host)).rejects.toThrowError(/Failed to define Clipboard/);
35
22
  });
36
23
 
37
- it('should fall back to manual context when createContext is missing', async () => {
24
+ it('should throw when no host is available', async () => {
38
25
  const { registerClipboard } = await import('./clipboard');
39
- const mock = {
40
- compileSync: vi.fn().mockReturnValue({ ok: true, ast: { type: 'behavior' } }),
41
- execute: vi.fn().mockResolvedValue(undefined),
42
- };
43
-
44
- await registerClipboard(mock as any);
45
- expect(mock.execute).toHaveBeenCalledWith(
46
- expect.anything(),
47
- expect.objectContaining({ locals: expect.any(Map), globals: expect.any(Map) })
48
- );
26
+ await expect(registerClipboard(undefined)).rejects.toThrowError(/No hyperscript host/);
49
27
  });
50
28
  });
51
29
 
@@ -5,39 +5,24 @@
5
5
  * Uses navigator.clipboard.writeText() with execCommand fallback (in its
6
6
  * hyperscript `source`'s js() body).
7
7
  *
8
- * Compiled from its hyperscript `source` (the single source of truth shared with
9
- * the CDN resolver bundle and patterns-reference).
8
+ * Defined from its hyperscript `source` (the single source of truth shared with
9
+ * the browser bundle and patterns-reference).
10
10
  */
11
11
 
12
12
  import { clipboardSchema } from '../schemas/clipboard.schema';
13
- import type { LokaScriptInstance } from '../schemas/types';
14
- import { resolveRuntime } from '../schemas/types';
13
+ import { defineBehavior, resolveRuntime } from '../schemas/types';
14
+ import type { HyperscriptHost } from '../schemas/types';
15
15
 
16
16
  // Re-export schema-derived values
17
17
  export const clipboardSource = clipboardSchema.source;
18
18
  export const clipboardMetadata = clipboardSchema;
19
19
 
20
20
  /**
21
- * Register the Clipboard behavior with LokaScript by compiling its
22
- * hyperscript source and executing the resulting behavior definition.
21
+ * Define the Clipboard behavior on a hyperscript host (`window.hyperfixi` when none is given)
22
+ * from its source, as a `<script type="text/hyperscript">` would.
23
23
  */
24
- export async function registerClipboard(hyperfixi?: LokaScriptInstance): Promise<void> {
25
- const hf = hyperfixi || resolveRuntime();
26
-
27
- if (!hf) {
28
- throw new Error(
29
- 'LokaScript not found. Make sure @hyperfixi/core is loaded before registering behaviors.'
30
- );
31
- }
32
-
33
- const result = hf.compileSync(clipboardSchema.source, { traditional: true });
34
-
35
- if (!result.ok) {
36
- throw new Error(`Failed to compile Clipboard behavior: ${JSON.stringify(result.errors)}`);
37
- }
38
-
39
- const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
40
- await hf.execute(result.ast, ctx);
24
+ export async function registerClipboard(host?: HyperscriptHost): Promise<void> {
25
+ defineBehavior(clipboardSchema, host);
41
26
  }
42
27
 
43
28
  // Auto-register when loaded as a script tag
@@ -1,45 +1,43 @@
1
1
  // @vitest-environment happy-dom
2
2
  /**
3
- * Curated behaviors — REAL runtime behavior tests (Phase 2).
3
+ * Curated behaviors — REAL runtime behavior tests.
4
4
  *
5
- * These exercise each curated behavior's actual hyperscript `source` through the
6
- * real @hyperfixi/core runtime: register → install on an element → drive the DOM →
7
- * assert both the effect AND the documented lifecycle events. This is the
8
- * "parses ≠ works" guard (BEHAVIORS_CONSOLIDATION_PLAN.md §2/§5) — the mock-based
9
- * unit tests only prove `register*()` calls compileSync; these prove the js()
10
- * bodies actually do the right thing at runtime.
5
+ * These exercise each curated behavior's actual hyperscript `source` on the real
6
+ * @hyperfixi/engine: define → install on an element → drive the DOM → assert both the
7
+ * effect AND the documented lifecycle events. This is the "parses ≠ works" guard — the
8
+ * mock-based unit tests only prove `register*()` hands the source to the host; these prove
9
+ * the sources do the right thing when they run.
11
10
  *
12
- * These tests already paid for themselves: they surfaced two source bugs the
13
- * imperative path was masking — ClickOutside read `event.target` without passing
14
- * `event` into its js() block, and Clipboard used top-level `await` (invalid in a
15
- * js() block). Both are fixed in the schema sources.
11
+ * The engine runs the same source the pages do, and upstream _hyperscript runs it the same
12
+ * way (measured in a browser, 2026-10-03, with at least one argument given: upstream's
13
+ * `install X` without parentheses throws; `install X()` works there).
16
14
  */
17
15
  import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest';
18
- import { hyperscript } from '@hyperfixi/core';
16
+ import { api, everything, processNode, register } from '@hyperfixi/engine';
19
17
  import { registerToggleable } from './toggleable';
20
18
  import { registerRemovable } from './removable';
21
19
  import { registerClickOutside } from './clickoutside';
22
20
  import { registerClipboard } from './clipboard';
23
21
  import { registerAutoDismiss } from './autodismiss';
24
22
 
25
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
- const hf = hyperscript as any;
27
-
28
23
  const tick = (ms = 30) => new Promise(r => setTimeout(r, ms));
29
24
 
25
+ /** Install from an attribute, as a page does: the engine reads `_` and runs the install. */
30
26
  async function install(code: string, el: HTMLElement): Promise<void> {
31
- const r = hyperscript.compileSync(code, { traditional: true });
32
- if (!r.ok) throw new Error(`install compile failed: ${JSON.stringify(r.errors)}`);
33
- await hyperscript.execute(r.ast, hyperscript.createContext(el));
27
+ el.setAttribute('_', code);
28
+ processNode(el);
29
+ await tick();
34
30
  }
35
31
 
36
32
  beforeAll(async () => {
37
- // Behavior registry is a runtime singleton; register the curated set once.
38
- await registerToggleable(hf);
39
- await registerRemovable(hf);
40
- await registerClickOutside(hf);
41
- await registerClipboard(hf);
42
- await registerAutoDismiss(hf);
33
+ // The engine is assembled from modules; this bundle is every one. Behaviors are globals
34
+ // (`window.Toggleable`), so define the set once per file.
35
+ register(...everything);
36
+ await registerToggleable(api);
37
+ await registerRemovable(api);
38
+ await registerClickOutside(api);
39
+ await registerClipboard(api);
40
+ await registerAutoDismiss(api);
43
41
  });
44
42
 
45
43
  afterEach(() => {
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Draggable Behavior
3
3
  *
4
- * Makes elements draggable with pointer events. Compiled from its hyperscript
5
- * `source` (the single source of truth shared with the CDN resolver bundle and
4
+ * Makes elements draggable with pointer events. Defined from its hyperscript
5
+ * `source` (the single source of truth shared with the browser bundle and
6
6
  * patterns-reference) — no imperative installer. The source uses
7
7
  * `repeat until event pointerup` + `wait for pointermove or pointerup` to run the
8
- * drag loop, `measure` for the start offset, and `add { left: ${…}px }` to move.
8
+ * drag loop, `my offsetLeft` / `my offsetTop` for the start offset, and
9
+ * `add { left: ${…}px }` to move.
9
10
  *
10
11
  * @example
11
12
  * ```html
@@ -21,34 +22,19 @@
21
22
  */
22
23
 
23
24
  import { draggableSchema } from '../schemas/draggable.schema';
24
- import type { LokaScriptInstance } from '../schemas/types';
25
- import { resolveRuntime } from '../schemas/types';
25
+ import { defineBehavior, resolveRuntime } from '../schemas/types';
26
+ import type { HyperscriptHost } from '../schemas/types';
26
27
 
27
28
  // Re-export schema-derived values for backwards compatibility
28
29
  export const draggableSource = draggableSchema.source;
29
30
  export const draggableMetadata = draggableSchema;
30
31
 
31
32
  /**
32
- * Register the Draggable behavior with LokaScript by compiling its hyperscript
33
- * source and executing the resulting behavior definition.
33
+ * Define the Draggable behavior on a hyperscript host (`window.hyperfixi` when none is given)
34
+ * from its source, as a `<script type="text/hyperscript">` would.
34
35
  */
35
- export async function registerDraggable(hyperfixi?: LokaScriptInstance): Promise<void> {
36
- const hf = hyperfixi || resolveRuntime();
37
-
38
- if (!hf) {
39
- throw new Error(
40
- 'LokaScript not found. Make sure @hyperfixi/core is loaded before registering behaviors.'
41
- );
42
- }
43
-
44
- const result = hf.compileSync(draggableSchema.source, { traditional: true });
45
-
46
- if (!result.ok) {
47
- throw new Error(`Failed to compile Draggable behavior: ${JSON.stringify(result.errors)}`);
48
- }
49
-
50
- const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
51
- await hf.execute(result.ast, ctx);
36
+ export async function registerDraggable(host?: HyperscriptHost): Promise<void> {
37
+ defineBehavior(draggableSchema, host);
52
38
  }
53
39
 
54
40
  // Auto-register when loaded as a script tag
@@ -2,39 +2,40 @@
2
2
  /**
3
3
  * Experimental behaviors — REAL runtime tests.
4
4
  *
5
- * Draggable / Sortable / Resizable used to run imperative JS installers. They now
6
- * compile from their hyperscript `source` like every other behavior (the
7
- * "no imperative JS" rule — no behavior may carry an imperative installer field).
8
- * These tests are the "parses ≠ works" guard: register → install → drive pointer
9
- * events → assert the DOM effect AND the documented lifecycle events.
5
+ * Draggable / Sortable / Resizable are defined from their hyperscript `source` like every
6
+ * other behavior (the "no imperative JS" rule). These tests are the "parses ≠ works" guard:
7
+ * define → install → drive pointer events → assert the DOM effect AND the lifecycle events.
10
8
  *
11
- * happy-dom has no PointerEvent and zero layout, so we drive `MouseEvent`s (which
12
- * carry clientX/clientY) and assert deltas, which are layout-independent.
9
+ * happy-dom has no PointerEvent and zero layout (`my offsetLeft` and `my offsetWidth` read
10
+ * 0), so we drive `MouseEvent`s (which carry clientX/clientY) and assert deltas, which are
11
+ * layout-independent.
13
12
  */
14
13
  import { describe, it, expect, beforeAll, afterEach } from 'vitest';
15
- import { hyperscript } from '@hyperfixi/core';
14
+ import { api, everything, processNode, register } from '@hyperfixi/engine';
16
15
  import { registerDraggable } from './draggable';
17
16
  import { registerSortable } from './sortable';
18
17
  import { registerResizable } from './resizable';
19
18
 
20
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
- const hf = hyperscript as any;
22
19
  const tick = (ms = 30) => new Promise(r => setTimeout(r, ms));
23
20
 
24
21
  function pointer(type: string, x: number, y: number): MouseEvent {
25
22
  return new MouseEvent(type, { clientX: x, clientY: y, bubbles: true, cancelable: true });
26
23
  }
27
24
 
25
+ /** Install from an attribute, as a page does: the engine reads `_` and runs the install. */
28
26
  async function install(code: string, el: HTMLElement): Promise<void> {
29
- const r = hyperscript.compileSync(code, { traditional: true });
30
- if (!r.ok) throw new Error(`install compile failed: ${JSON.stringify(r.errors)}`);
31
- await hyperscript.execute(r.ast, hyperscript.createContext(el));
27
+ el.setAttribute('_', code);
28
+ processNode(el);
29
+ await tick();
32
30
  }
33
31
 
34
32
  beforeAll(async () => {
35
- await registerDraggable(hf);
36
- await registerSortable(hf);
37
- await registerResizable(hf);
33
+ // The engine is assembled from modules; this bundle is every one. Behaviors are globals
34
+ // (`window.Toggleable`), so define the set once per file.
35
+ register(...everything);
36
+ await registerDraggable(api);
37
+ await registerSortable(api);
38
+ await registerResizable(api);
38
39
  });
39
40
 
40
41
  afterEach(() => {
@@ -63,7 +64,7 @@ describe('Draggable — runtime (source-compiled)', () => {
63
64
  document.dispatchEvent(pointer('pointerup', 60, 80));
64
65
  await tick(40);
65
66
 
66
- // measure x/y = 0 in happy-dom → xoff = 10, so left = 60 - 10 = 50, top = 80 - 10 = 70
67
+ // offsetLeft/offsetTop = 0 in happy-dom → xoff = 10, so left = 60 - 10 = 50, top = 80 - 10 = 70
67
68
  expect(el.style.left).toBe('50px');
68
69
  expect(el.style.top).toBe('70px');
69
70
  expect(ev).toContain('start');
@@ -126,7 +127,7 @@ describe('Resizable — runtime (source-compiled)', () => {
126
127
  document.dispatchEvent(pointer('pointerup', 60, 40));
127
128
  await tick(40);
128
129
 
129
- // measure width/height = 0 in happy-dom → newWidth = 0 + 60 - 10 = 50, newHeight = 0 + 40 - 10 = 30
130
+ // offsetWidth/offsetHeight = 0 in happy-dom → newWidth = 0 + 60 - 10 = 50, newHeight = 0 + 40 - 10 = 30
130
131
  expect(box.style.width).toBe('50px');
131
132
  expect(box.style.height).toBe('30px');
132
133
  expect(ev).toContain('start');
@@ -2,36 +2,28 @@ import { describe, it, expect, vi } from 'vitest';
2
2
 
3
3
  describe('FocusTrap behavior', () => {
4
4
  describe('registerFocusTrap', () => {
5
- it('should compile its hyperscript source and execute', async () => {
5
+ it('defines the behavior on the host from its hyperscript source', async () => {
6
6
  const { registerFocusTrap, focusTrapSource } = await import('./focustrap');
7
- const mock = {
8
- compileSync: vi.fn().mockReturnValue({ ok: true, ast: { type: 'behavior' } }),
9
- execute: vi.fn().mockResolvedValue(undefined),
10
- createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
11
- };
7
+ const host = { evaluate: vi.fn() };
12
8
 
13
- await registerFocusTrap(mock);
9
+ await registerFocusTrap(host);
14
10
 
15
- expect(mock.compileSync).toHaveBeenCalledWith(focusTrapSource, { traditional: true });
16
- expect(mock.execute).toHaveBeenCalledWith(
17
- { type: 'behavior' },
18
- expect.objectContaining({ locals: expect.any(Map), globals: expect.any(Map) })
19
- );
11
+ expect(host.evaluate).toHaveBeenCalledWith(focusTrapSource);
20
12
  });
21
13
 
22
- it('should throw on compile failure', async () => {
14
+ it('should throw when the host rejects the source', async () => {
23
15
  const { registerFocusTrap } = await import('./focustrap');
24
- const mock = {
25
- compileSync: vi.fn().mockReturnValue({ ok: false, errors: [{ message: 'boom' }] }),
26
- execute: vi.fn(),
27
- createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
16
+ const host = {
17
+ evaluate: vi.fn(() => {
18
+ throw new Error('boom');
19
+ }),
28
20
  };
29
- await expect(registerFocusTrap(mock)).rejects.toThrowError(/Failed to compile FocusTrap/);
21
+ await expect(registerFocusTrap(host)).rejects.toThrowError(/Failed to define FocusTrap/);
30
22
  });
31
23
 
32
- it('should throw when no runtime available', async () => {
24
+ it('should throw when no host is available', async () => {
33
25
  const { registerFocusTrap } = await import('./focustrap');
34
- await expect(registerFocusTrap(undefined)).rejects.toThrowError(/LokaScript not found/);
26
+ await expect(registerFocusTrap(undefined)).rejects.toThrowError(/No hyperscript host/);
35
27
  });
36
28
  });
37
29
 
@@ -5,41 +5,26 @@
5
5
  * and restores focus on deactivation. The Tab-trapping / focus-model logic
6
6
  * lives in the schema's hyperscript `source` (an `init`-block `js()` body).
7
7
  *
8
- * Compiled from its hyperscript `source` — the single source of truth shared with
9
- * the CDN resolver bundle and patterns-reference. This collapses FocusTrap onto
8
+ * Defined from its hyperscript `source` — the single source of truth shared with
9
+ * the browser bundle and patterns-reference. This collapses FocusTrap onto
10
10
  * the one runtime path (no more imperative-installer fork between CDN and npm);
11
11
  * see docs-internal/BEHAVIORS_CONSOLIDATION_PLAN.md §3d.
12
12
  */
13
13
 
14
14
  import { focusTrapSchema } from '../schemas/focustrap.schema';
15
- import type { LokaScriptInstance } from '../schemas/types';
16
- import { resolveRuntime } from '../schemas/types';
15
+ import { defineBehavior, resolveRuntime } from '../schemas/types';
16
+ import type { HyperscriptHost } from '../schemas/types';
17
17
 
18
18
  // Re-export schema-derived values
19
19
  export const focusTrapSource = focusTrapSchema.source;
20
20
  export const focusTrapMetadata = focusTrapSchema;
21
21
 
22
22
  /**
23
- * Register the FocusTrap behavior with LokaScript by compiling its
24
- * hyperscript source and executing the resulting behavior definition.
23
+ * Define the FocusTrap behavior on a hyperscript host (`window.hyperfixi` when none is given)
24
+ * from its source, as a `<script type="text/hyperscript">` would.
25
25
  */
26
- export async function registerFocusTrap(hyperfixi?: LokaScriptInstance): Promise<void> {
27
- const hf = hyperfixi || resolveRuntime();
28
-
29
- if (!hf) {
30
- throw new Error(
31
- 'LokaScript not found. Make sure @hyperfixi/core is loaded before registering behaviors.'
32
- );
33
- }
34
-
35
- const result = hf.compileSync(focusTrapSchema.source, { traditional: true });
36
-
37
- if (!result.ok) {
38
- throw new Error(`Failed to compile FocusTrap behavior: ${JSON.stringify(result.errors)}`);
39
- }
40
-
41
- const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
42
- await hf.execute(result.ast, ctx);
26
+ export async function registerFocusTrap(host?: HyperscriptHost): Promise<void> {
27
+ defineBehavior(focusTrapSchema, host);
43
28
  }
44
29
 
45
30
  // Auto-register when loaded as a script tag
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
- import type { LokaScriptInstance } from '../schemas/types';
2
+ import type { HyperscriptHost } from '../schemas/types';
3
3
  import { registerDraggable, draggableSource } from './draggable';
4
4
  import { registerToggleable, toggleableSource } from './toggleable';
5
5
  import { registerRemovable, removableSource } from './removable';
@@ -12,21 +12,15 @@ import { registerFocusTrap, focusTrapSource } from './focustrap';
12
12
  import { registerScrollReveal, scrollRevealSource } from './scrollreveal';
13
13
  import { registerTabs, tabsSource } from './tabs';
14
14
 
15
- function createMockInstance(overrides: Partial<LokaScriptInstance> = {}): LokaScriptInstance {
16
- return {
17
- compileSync: vi.fn().mockReturnValue({ ok: true, ast: { type: 'behavior' } }),
18
- execute: vi.fn().mockResolvedValue(undefined),
19
- createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
20
- ...overrides,
21
- };
15
+ function createMockHost(overrides: Partial<HyperscriptHost> = {}): HyperscriptHost {
16
+ return { evaluate: vi.fn(), ...overrides };
22
17
  }
23
18
 
24
- // Source-compiled behaviors (use compileSync + execute).
25
- // EVERY behavior compiles its hyperscript `source` — one runtime path, identical
26
- // browser/npm, no imperative JS installer (the "no imperative JS" rule). The optional
27
- // three (FocusTrap/ScrollReveal/Tabs) carry their web-API logic in an `init`-block
19
+ // EVERY behavior is defined from its hyperscript `source` with the host's `evaluate` — one
20
+ // path, identical browser/npm, no imperative JS installer (the "no imperative JS" rule). The
21
+ // optional three (FocusTrap/ScrollReveal/Tabs) carry their web-API logic in an `init`-block
28
22
  // `js()` body, and the experimental three (Draggable/Sortable/Resizable) run their
29
- // pointer-drag loops via `repeat until event` — all through the single compile path.
23
+ // pointer-drag loops via `repeat until event` — all through the single path.
30
24
  const compiledBehaviors = [
31
25
  { name: 'Removable', register: registerRemovable, source: removableSource },
32
26
  { name: 'Toggleable', register: registerToggleable, source: toggleableSource },
@@ -42,57 +36,28 @@ const compiledBehaviors = [
42
36
  ] as const;
43
37
 
44
38
  describe('behavior registration integration', () => {
45
- describe('source-compiled behaviors', () => {
39
+ describe('source-defined behaviors', () => {
46
40
  for (const { name, register, source } of compiledBehaviors) {
47
41
  describe(name, () => {
48
- it('should compile with traditional parser', async () => {
49
- const mock = createMockInstance();
50
- await register(mock);
42
+ it('should define the source on the host', async () => {
43
+ const host = createMockHost();
44
+ await register(host);
51
45
 
52
- expect(mock.compileSync).toHaveBeenCalledWith(source, { traditional: true });
46
+ expect(host.evaluate).toHaveBeenCalledWith(source);
53
47
  });
54
48
 
55
- it('should create context and execute', async () => {
56
- const mock = createMockInstance();
57
- await register(mock);
58
-
59
- expect(mock.createContext).toHaveBeenCalled();
60
- expect(mock.execute).toHaveBeenCalledWith(
61
- { type: 'behavior' },
62
- { locals: expect.any(Map), globals: expect.any(Map) }
63
- );
64
- });
65
-
66
- it('should throw on compile failure', async () => {
67
- const mock = createMockInstance({
68
- compileSync: vi.fn().mockReturnValue({
69
- ok: false,
70
- errors: [{ message: 'syntax error', line: 1 }],
49
+ it('should throw when the host rejects the source', async () => {
50
+ const host = createMockHost({
51
+ evaluate: vi.fn(() => {
52
+ throw new Error('syntax error');
71
53
  }),
72
54
  });
73
55
 
74
- await expect(register(mock)).rejects.toThrowError(
75
- new RegExp(`Failed to compile ${name}`)
76
- );
56
+ await expect(register(host)).rejects.toThrowError(new RegExp(`Failed to define ${name}`));
77
57
  });
78
58
 
79
- it('should throw when lokascript is not available', async () => {
80
- await expect(register(undefined)).rejects.toThrowError(/LokaScript not found/);
81
- });
82
-
83
- it('should fall back to manual context when createContext is missing', async () => {
84
- const mock = createMockInstance();
85
- delete (mock as Partial<LokaScriptInstance>).createContext;
86
-
87
- await register(mock);
88
-
89
- expect(mock.execute).toHaveBeenCalledWith(
90
- { type: 'behavior' },
91
- expect.objectContaining({
92
- locals: expect.any(Map),
93
- globals: expect.any(Map),
94
- })
95
- );
59
+ it('should throw when no host is available', async () => {
60
+ await expect(register(undefined)).rejects.toThrowError(/No hyperscript host/);
96
61
  });
97
62
  });
98
63
  }
@@ -2,40 +2,35 @@
2
2
  /**
3
3
  * Optional behaviors — REAL runtime behavior tests.
4
4
  *
5
- * The optional three (FocusTrap / ScrollReveal / Tabs) were converted from
6
- * imperative installers onto the single source-compiled runtime path
7
- * (BEHAVIORS_CONSOLIDATION_PLAN.md §3d follow-up). Like curated-runtime.test.ts,
8
- * these exercise each behavior's actual hyperscript `source` through the real
9
- * @hyperfixi/core runtime — register → install on an element → drive the DOM →
10
- * assert both the effect AND the documented lifecycle events.
11
- *
12
- * This is the "parses ≠ works" guard (§2/§5): the mock-based unit tests only prove
13
- * `register*()` calls compileSync; these prove the `init`-block `js()` bodies (the
14
- * focus model, the IntersectionObserver, the ARIA/keyboard wiring) actually run
15
- * correctly once compiled — exactly the class of bug the imperative path masked.
5
+ * Like curated-runtime.test.ts, these exercise each behavior's actual hyperscript `source`
6
+ * on the real @hyperfixi/engine — define → install on an element → drive the DOM → assert
7
+ * both the effect AND the documented lifecycle events. The optional three carry their
8
+ * web-API logic (a focus model, an IntersectionObserver, ARIA/keyboard wiring) in an
9
+ * `init`-block `js()` body; these prove those bodies run correctly once the source is
10
+ * defined — exactly the class of bug a parse check cannot see.
16
11
  */
17
12
  import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest';
18
- import { hyperscript } from '@hyperfixi/core';
13
+ import { api, everything, processNode, register } from '@hyperfixi/engine';
19
14
  import { registerFocusTrap } from './focustrap';
20
15
  import { registerScrollReveal } from './scrollreveal';
21
16
  import { registerTabs } from './tabs';
22
17
 
23
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
- const hf = hyperscript as any;
25
-
26
18
  const tick = (ms = 30) => new Promise(r => setTimeout(r, ms));
27
19
 
20
+ /** Install from an attribute, as a page does: the engine reads `_` and runs the install. */
28
21
  async function install(code: string, el: HTMLElement): Promise<void> {
29
- const r = hyperscript.compileSync(code, { traditional: true });
30
- if (!r.ok) throw new Error(`install compile failed: ${JSON.stringify(r.errors)}`);
31
- await hyperscript.execute(r.ast, hyperscript.createContext(el));
22
+ el.setAttribute('_', code);
23
+ processNode(el);
24
+ await tick();
32
25
  }
33
26
 
34
27
  beforeAll(async () => {
35
- // Behavior registry is a runtime singleton; register the optional set once.
36
- await registerFocusTrap(hf);
37
- await registerScrollReveal(hf);
38
- await registerTabs(hf);
28
+ // The engine is assembled from modules; this bundle is every one. Behaviors are globals
29
+ // (`window.Toggleable`), so define the set once per file.
30
+ register(...everything);
31
+ await registerFocusTrap(api);
32
+ await registerScrollReveal(api);
33
+ await registerTabs(api);
39
34
  });
40
35
 
41
36
  afterEach(() => {
@@ -13,33 +13,19 @@
13
13
  */
14
14
 
15
15
  import { removableSchema } from '../schemas/removable.schema';
16
- import type { LokaScriptInstance } from '../schemas/types';
17
- import { resolveRuntime } from '../schemas/types';
16
+ import { defineBehavior, resolveRuntime } from '../schemas/types';
17
+ import type { HyperscriptHost } from '../schemas/types';
18
18
 
19
19
  // Re-export schema-derived values for backwards compatibility
20
20
  export const removableSource = removableSchema.source;
21
21
  export const removableMetadata = removableSchema;
22
22
 
23
23
  /**
24
- * Register the Removable behavior with LokaScript.
24
+ * Define the Removable behavior on a hyperscript host (`window.hyperfixi` when none is given)
25
+ * from its source, as a `<script type="text/hyperscript">` would.
25
26
  */
26
- export async function registerRemovable(hyperfixi?: LokaScriptInstance): Promise<void> {
27
- const hf = hyperfixi || resolveRuntime();
28
-
29
- if (!hf) {
30
- throw new Error(
31
- 'LokaScript not found. Make sure @hyperfixi/core is loaded before registering behaviors.'
32
- );
33
- }
34
-
35
- const result = hf.compileSync(removableSchema.source, { traditional: true });
36
-
37
- if (!result.ok) {
38
- throw new Error(`Failed to compile Removable behavior: ${JSON.stringify(result.errors)}`);
39
- }
40
-
41
- const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
42
- await hf.execute(result.ast, ctx);
27
+ export async function registerRemovable(host?: HyperscriptHost): Promise<void> {
28
+ defineBehavior(removableSchema, host);
43
29
  }
44
30
 
45
31
  // Auto-register when loaded as a script tag