@hyperfixi/behaviors 2.0.0 → 2.1.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 (189) hide show
  1. package/dist/all.browser.global.js +35 -6
  2. package/dist/all.browser.global.js.map +1 -1
  3. package/dist/autodismiss.browser.global.js +3 -0
  4. package/dist/autodismiss.browser.global.js.map +1 -0
  5. package/dist/behaviors/autodismiss.cjs +177 -0
  6. package/dist/behaviors/autodismiss.cjs.map +1 -0
  7. package/dist/behaviors/autodismiss.d.cts +15 -0
  8. package/dist/behaviors/autodismiss.d.ts +15 -0
  9. package/dist/behaviors/autodismiss.js +15 -0
  10. package/dist/behaviors/autodismiss.js.map +1 -0
  11. package/dist/behaviors/clickoutside.cjs +123 -0
  12. package/dist/behaviors/clickoutside.cjs.map +1 -0
  13. package/dist/behaviors/clickoutside.d.cts +15 -0
  14. package/dist/behaviors/clickoutside.d.ts +15 -0
  15. package/dist/behaviors/clickoutside.js +15 -0
  16. package/dist/behaviors/clickoutside.js.map +1 -0
  17. package/dist/behaviors/clipboard.cjs +193 -0
  18. package/dist/behaviors/clipboard.cjs.map +1 -0
  19. package/dist/behaviors/clipboard.d.cts +15 -0
  20. package/dist/behaviors/clipboard.d.ts +15 -0
  21. package/dist/behaviors/clipboard.js +15 -0
  22. package/dist/behaviors/clipboard.js.map +1 -0
  23. package/dist/behaviors/draggable.cjs +49 -7
  24. package/dist/behaviors/draggable.cjs.map +1 -1
  25. package/dist/behaviors/draggable.d.cts +2 -1
  26. package/dist/behaviors/draggable.d.ts +2 -1
  27. package/dist/behaviors/draggable.js +2 -1
  28. package/dist/behaviors/focustrap.cjs +191 -0
  29. package/dist/behaviors/focustrap.cjs.map +1 -0
  30. package/dist/behaviors/focustrap.d.cts +15 -0
  31. package/dist/behaviors/focustrap.d.ts +15 -0
  32. package/dist/behaviors/focustrap.js +15 -0
  33. package/dist/behaviors/focustrap.js.map +1 -0
  34. package/dist/behaviors/removable.cjs +9 -2
  35. package/dist/behaviors/removable.cjs.map +1 -1
  36. package/dist/behaviors/removable.d.cts +1 -1
  37. package/dist/behaviors/removable.d.ts +1 -1
  38. package/dist/behaviors/removable.js +2 -1
  39. package/dist/behaviors/resizable.cjs +54 -7
  40. package/dist/behaviors/resizable.cjs.map +1 -1
  41. package/dist/behaviors/resizable.d.cts +1 -1
  42. package/dist/behaviors/resizable.d.ts +1 -1
  43. package/dist/behaviors/resizable.js +2 -1
  44. package/dist/behaviors/scrollreveal.cjs +149 -0
  45. package/dist/behaviors/scrollreveal.cjs.map +1 -0
  46. package/dist/behaviors/scrollreveal.d.cts +15 -0
  47. package/dist/behaviors/scrollreveal.d.ts +15 -0
  48. package/dist/behaviors/scrollreveal.js +15 -0
  49. package/dist/behaviors/scrollreveal.js.map +1 -0
  50. package/dist/behaviors/sortable.cjs +49 -7
  51. package/dist/behaviors/sortable.cjs.map +1 -1
  52. package/dist/behaviors/sortable.d.cts +1 -1
  53. package/dist/behaviors/sortable.d.ts +1 -1
  54. package/dist/behaviors/sortable.js +2 -1
  55. package/dist/behaviors/tabs.cjs +247 -0
  56. package/dist/behaviors/tabs.cjs.map +1 -0
  57. package/dist/behaviors/tabs.d.cts +15 -0
  58. package/dist/behaviors/tabs.d.ts +15 -0
  59. package/dist/behaviors/tabs.js +15 -0
  60. package/dist/behaviors/tabs.js.map +1 -0
  61. package/dist/behaviors/toggleable.cjs +36 -7
  62. package/dist/behaviors/toggleable.cjs.map +1 -1
  63. package/dist/behaviors/toggleable.d.cts +2 -1
  64. package/dist/behaviors/toggleable.d.ts +2 -1
  65. package/dist/behaviors/toggleable.js +2 -1
  66. package/dist/chunk-244WSZOL.js +67 -0
  67. package/dist/chunk-244WSZOL.js.map +1 -0
  68. package/dist/chunk-2NSKQMNM.js +99 -0
  69. package/dist/chunk-2NSKQMNM.js.map +1 -0
  70. package/dist/chunk-64DIQGQI.js +37 -0
  71. package/dist/chunk-64DIQGQI.js.map +1 -0
  72. package/dist/chunk-6TJXDCAQ.js +11 -0
  73. package/dist/chunk-6TJXDCAQ.js.map +1 -0
  74. package/dist/chunk-7JDV2THA.js +62 -0
  75. package/dist/chunk-7JDV2THA.js.map +1 -0
  76. package/dist/chunk-AFGA54SA.js +116 -0
  77. package/dist/chunk-AFGA54SA.js.map +1 -0
  78. package/dist/chunk-DYHBKDKB.js +62 -0
  79. package/dist/chunk-DYHBKDKB.js.map +1 -0
  80. package/dist/chunk-GPTHAUWN.js +58 -0
  81. package/dist/chunk-GPTHAUWN.js.map +1 -0
  82. package/dist/chunk-GVJPA2EK.js +68 -0
  83. package/dist/chunk-GVJPA2EK.js.map +1 -0
  84. package/dist/chunk-H2MCHR5A.js +89 -0
  85. package/dist/chunk-H2MCHR5A.js.map +1 -0
  86. package/dist/{chunk-2NQHIHD5.js → chunk-H5Y5ARYG.js} +6 -3
  87. package/dist/chunk-H5Y5ARYG.js.map +1 -0
  88. package/dist/chunk-JKBWIJFI.js +155 -0
  89. package/dist/chunk-JKBWIJFI.js.map +1 -0
  90. package/dist/chunk-QKDYW4VM.js +70 -0
  91. package/dist/chunk-QKDYW4VM.js.map +1 -0
  92. package/dist/chunk-RB7OB6OD.js +51 -0
  93. package/dist/chunk-RB7OB6OD.js.map +1 -0
  94. package/dist/chunk-RPERQTFU.js +75 -0
  95. package/dist/chunk-RPERQTFU.js.map +1 -0
  96. package/dist/chunk-S4QDV2MH.js +75 -0
  97. package/dist/chunk-S4QDV2MH.js.map +1 -0
  98. package/dist/chunk-WTUVPVSD.js +80 -0
  99. package/dist/chunk-WTUVPVSD.js.map +1 -0
  100. package/dist/chunk-YHQRFUWV.js +62 -0
  101. package/dist/chunk-YHQRFUWV.js.map +1 -0
  102. package/dist/chunk-ZQQZTMCJ.js +64 -0
  103. package/dist/chunk-ZQQZTMCJ.js.map +1 -0
  104. package/dist/clickoutside.browser.global.js +12 -0
  105. package/dist/clickoutside.browser.global.js.map +1 -0
  106. package/dist/clipboard.browser.global.js +3 -0
  107. package/dist/clipboard.browser.global.js.map +1 -0
  108. package/dist/core.browser.global.js +3 -3
  109. package/dist/core.browser.global.js.map +1 -1
  110. package/dist/draggable.browser.global.js +2 -2
  111. package/dist/draggable.browser.global.js.map +1 -1
  112. package/dist/focustrap.browser.global.js +9 -0
  113. package/dist/focustrap.browser.global.js.map +1 -0
  114. package/dist/index.cjs +1270 -50
  115. package/dist/index.cjs.map +1 -1
  116. package/dist/index.d.cts +13 -7
  117. package/dist/index.d.ts +13 -7
  118. package/dist/index.js +155 -48
  119. package/dist/index.js.map +1 -1
  120. package/dist/loaders.cjs +1151 -30
  121. package/dist/loaders.cjs.map +1 -1
  122. package/dist/loaders.js +9 -3
  123. package/dist/registry.d.cts +1 -1
  124. package/dist/registry.d.ts +1 -1
  125. package/dist/removable.browser.global.js +2 -2
  126. package/dist/removable.browser.global.js.map +1 -1
  127. package/dist/resizable.browser.global.js +2 -2
  128. package/dist/resizable.browser.global.js.map +1 -1
  129. package/dist/schemas/index.cjs +343 -1
  130. package/dist/schemas/index.cjs.map +1 -1
  131. package/dist/schemas/index.d.cts +45 -3
  132. package/dist/schemas/index.d.ts +45 -3
  133. package/dist/schemas/index.js +34 -4
  134. package/dist/schemas/index.js.map +1 -1
  135. package/dist/scrollreveal.browser.global.js +11 -0
  136. package/dist/scrollreveal.browser.global.js.map +1 -0
  137. package/dist/sortable.browser.global.js +2 -2
  138. package/dist/sortable.browser.global.js.map +1 -1
  139. package/dist/tabs.browser.global.js +3 -0
  140. package/dist/tabs.browser.global.js.map +1 -0
  141. package/dist/toggleable.browser.global.js +2 -2
  142. package/dist/toggleable.browser.global.js.map +1 -1
  143. package/dist/{types-CCc_2wAb.d.cts → types-DCCtVQS5.d.cts} +1 -1
  144. package/dist/{types-CCc_2wAb.d.ts → types-DCCtVQS5.d.ts} +1 -1
  145. package/package.json +45 -2
  146. package/src/behaviors/autodismiss.test.ts +61 -0
  147. package/src/behaviors/autodismiss.ts +107 -0
  148. package/src/behaviors/clickoutside.test.ts +52 -0
  149. package/src/behaviors/clickoutside.ts +77 -0
  150. package/src/behaviors/clipboard.test.ts +67 -0
  151. package/src/behaviors/clipboard.ts +130 -0
  152. package/src/behaviors/draggable.ts +62 -15
  153. package/src/behaviors/focustrap.test.ts +59 -0
  154. package/src/behaviors/focustrap.ts +158 -0
  155. package/src/behaviors/index.ts +37 -0
  156. package/src/behaviors/integration.test.ts +120 -45
  157. package/src/behaviors/removable.ts +4 -5
  158. package/src/behaviors/resizable.ts +63 -14
  159. package/src/behaviors/scrollreveal.test.ts +59 -0
  160. package/src/behaviors/scrollreveal.ts +78 -0
  161. package/src/behaviors/sortable.ts +61 -14
  162. package/src/behaviors/tabs.test.ts +81 -0
  163. package/src/behaviors/tabs.ts +196 -0
  164. package/src/behaviors/toggleable.ts +45 -13
  165. package/src/generated/metadata.ts +38 -7
  166. package/src/generated/types.ts +5 -5
  167. package/src/index.test.ts +143 -5
  168. package/src/index.ts +65 -13
  169. package/src/loaders.ts +18 -0
  170. package/src/registry.test.ts +26 -5
  171. package/src/schemas/autodismiss.schema.ts +66 -0
  172. package/src/schemas/clickoutside.schema.ts +39 -0
  173. package/src/schemas/clipboard.schema.ts +72 -0
  174. package/src/schemas/focustrap.schema.ts +53 -0
  175. package/src/schemas/index.ts +18 -0
  176. package/src/schemas/scrollreveal.schema.ts +60 -0
  177. package/src/schemas/tabs.schema.ts +66 -0
  178. package/src/schemas/types.ts +13 -1
  179. package/dist/chunk-2NQHIHD5.js.map +0 -1
  180. package/dist/chunk-3Q652JQB.js +0 -37
  181. package/dist/chunk-3Q652JQB.js.map +0 -1
  182. package/dist/chunk-HMRPQWQU.js +0 -37
  183. package/dist/chunk-HMRPQWQU.js.map +0 -1
  184. package/dist/chunk-MEP6B7XW.js +0 -37
  185. package/dist/chunk-MEP6B7XW.js.map +0 -1
  186. package/dist/chunk-URBOGGW6.js +0 -32
  187. package/dist/chunk-URBOGGW6.js.map +0 -1
  188. package/dist/chunk-WU37DFAL.js +0 -37
  189. package/dist/chunk-WU37DFAL.js.map +0 -1
@@ -5,6 +5,12 @@ import { registerToggleable, toggleableSource } from './toggleable';
5
5
  import { registerRemovable, removableSource } from './removable';
6
6
  import { registerSortable, sortableSource } from './sortable';
7
7
  import { registerResizable, resizableSource } from './resizable';
8
+ import { registerClipboard, clipboardSource } from './clipboard';
9
+ import { registerAutoDismiss, autoDismissSource } from './autodismiss';
10
+ import { registerClickOutside, clickOutsideSource } from './clickoutside';
11
+ import { registerFocusTrap, focusTrapSource } from './focustrap';
12
+ import { registerScrollReveal, scrollRevealSource } from './scrollreveal';
13
+ import { registerTabs, tabsSource } from './tabs';
8
14
 
9
15
  function createMockInstance(overrides: Partial<LokaScriptInstance> = {}): LokaScriptInstance {
10
16
  return {
@@ -15,66 +21,135 @@ function createMockInstance(overrides: Partial<LokaScriptInstance> = {}): LokaSc
15
21
  };
16
22
  }
17
23
 
18
- const behaviors = [
24
+ // Source-compiled behaviors (use compileSync + execute)
25
+ const compiledBehaviors = [
26
+ { name: 'Removable', register: registerRemovable, source: removableSource },
27
+ ] as const;
28
+
29
+ // Imperative behaviors (use synthetic node with imperativeInstaller)
30
+ const imperativeBehaviors = [
19
31
  { name: 'Draggable', register: registerDraggable, source: draggableSource },
20
32
  { name: 'Toggleable', register: registerToggleable, source: toggleableSource },
21
- { name: 'Removable', register: registerRemovable, source: removableSource },
22
33
  { name: 'Sortable', register: registerSortable, source: sortableSource },
23
34
  { name: 'Resizable', register: registerResizable, source: resizableSource },
35
+ { name: 'Clipboard', register: registerClipboard, source: clipboardSource },
36
+ { name: 'AutoDismiss', register: registerAutoDismiss, source: autoDismissSource },
37
+ { name: 'ClickOutside', register: registerClickOutside, source: clickOutsideSource },
38
+ { name: 'FocusTrap', register: registerFocusTrap, source: focusTrapSource },
39
+ { name: 'ScrollReveal', register: registerScrollReveal, source: scrollRevealSource },
40
+ { name: 'Tabs', register: registerTabs, source: tabsSource },
24
41
  ] as const;
25
42
 
26
43
  describe('behavior registration integration', () => {
27
- for (const { name, register, source } of behaviors) {
28
- describe(name, () => {
29
- it('should compile with traditional parser', async () => {
30
- const mock = createMockInstance();
31
- await register(mock);
44
+ describe('source-compiled behaviors', () => {
45
+ for (const { name, register, source } of compiledBehaviors) {
46
+ describe(name, () => {
47
+ it('should compile with traditional parser', async () => {
48
+ const mock = createMockInstance();
49
+ await register(mock);
32
50
 
33
- expect(mock.compileSync).toHaveBeenCalledWith(source, { traditional: true });
34
- });
51
+ expect(mock.compileSync).toHaveBeenCalledWith(source, { traditional: true });
52
+ });
35
53
 
36
- it('should create context and execute', async () => {
37
- const mock = createMockInstance();
38
- await register(mock);
54
+ it('should create context and execute', async () => {
55
+ const mock = createMockInstance();
56
+ await register(mock);
39
57
 
40
- expect(mock.createContext).toHaveBeenCalled();
41
- expect(mock.execute).toHaveBeenCalledWith(
42
- { type: 'behavior' },
43
- { locals: expect.any(Map), globals: expect.any(Map) }
44
- );
45
- });
58
+ expect(mock.createContext).toHaveBeenCalled();
59
+ expect(mock.execute).toHaveBeenCalledWith(
60
+ { type: 'behavior' },
61
+ { locals: expect.any(Map), globals: expect.any(Map) }
62
+ );
63
+ });
64
+
65
+ it('should throw on compile failure', async () => {
66
+ const mock = createMockInstance({
67
+ compileSync: vi.fn().mockReturnValue({
68
+ ok: false,
69
+ errors: [{ message: 'syntax error', line: 1 }],
70
+ }),
71
+ });
46
72
 
47
- it('should throw on compile failure', async () => {
48
- const mock = createMockInstance({
49
- compileSync: vi.fn().mockReturnValue({
50
- ok: false,
51
- errors: [{ message: 'syntax error', line: 1 }],
52
- }),
73
+ await expect(register(mock)).rejects.toThrowError(
74
+ new RegExp(`Failed to compile ${name}`)
75
+ );
53
76
  });
54
77
 
55
- await expect(register(mock)).rejects.toThrowError(new RegExp(`Failed to compile ${name}`));
56
- });
78
+ it('should throw when lokascript is not available', async () => {
79
+ await expect(register(undefined)).rejects.toThrowError(/LokaScript not found/);
80
+ });
81
+
82
+ it('should fall back to manual context when createContext is missing', async () => {
83
+ const mock = createMockInstance();
84
+ delete (mock as Partial<LokaScriptInstance>).createContext;
85
+
86
+ await register(mock);
57
87
 
58
- it('should throw when lokascript is not available', async () => {
59
- await expect(register(undefined)).rejects.toThrowError(/LokaScript not found/);
88
+ expect(mock.execute).toHaveBeenCalledWith(
89
+ { type: 'behavior' },
90
+ expect.objectContaining({
91
+ locals: expect.any(Map),
92
+ globals: expect.any(Map),
93
+ })
94
+ );
95
+ });
60
96
  });
97
+ }
98
+ });
99
+
100
+ describe('imperative behaviors', () => {
101
+ for (const { name, register } of imperativeBehaviors) {
102
+ describe(name, () => {
103
+ it('should register via synthetic node with imperativeInstaller', async () => {
104
+ const mock = createMockInstance();
105
+ await register(mock);
106
+
107
+ // Imperative behaviors do NOT call compileSync
108
+ expect(mock.compileSync).not.toHaveBeenCalled();
109
+
110
+ // They call execute with a synthetic node containing imperativeInstaller
111
+ expect(mock.execute).toHaveBeenCalledWith(
112
+ expect.objectContaining({
113
+ type: 'behavior',
114
+ name,
115
+ imperativeInstaller: expect.any(Function),
116
+ }),
117
+ expect.objectContaining({
118
+ locals: expect.any(Map),
119
+ globals: expect.any(Map),
120
+ })
121
+ );
122
+ });
123
+
124
+ it('should create context', async () => {
125
+ const mock = createMockInstance();
126
+ await register(mock);
127
+ expect(mock.createContext).toHaveBeenCalled();
128
+ });
129
+
130
+ it('should throw when lokascript is not available', async () => {
131
+ await expect(register(undefined)).rejects.toThrowError(/LokaScript not found/);
132
+ });
61
133
 
62
- it('should fall back to manual context when createContext is missing', async () => {
63
- const mock = createMockInstance();
64
- // Remove createContext
65
- delete (mock as Partial<LokaScriptInstance>).createContext;
66
-
67
- await register(mock);
68
-
69
- // Should still call execute with a fallback context
70
- expect(mock.execute).toHaveBeenCalledWith(
71
- { type: 'behavior' },
72
- expect.objectContaining({
73
- locals: expect.any(Map),
74
- globals: expect.any(Map),
75
- })
76
- );
134
+ it('should fall back to manual context when createContext is missing', async () => {
135
+ const mock = createMockInstance();
136
+ delete (mock as Partial<LokaScriptInstance>).createContext;
137
+
138
+ await register(mock);
139
+
140
+ expect(mock.execute).toHaveBeenCalledWith(
141
+ expect.objectContaining({
142
+ type: 'behavior',
143
+ name,
144
+ imperativeInstaller: expect.any(Function),
145
+ }),
146
+ expect.objectContaining({
147
+ locals: expect.any(Map),
148
+ globals: expect.any(Map),
149
+ })
150
+ );
151
+ });
77
152
  });
78
- });
79
- }
153
+ }
154
+ });
80
155
  });
@@ -13,7 +13,8 @@
13
13
  */
14
14
 
15
15
  import { removableSchema } from '../schemas/removable.schema';
16
- import type { LokaScriptInstance, LokaScriptWindow } from '../schemas/types';
16
+ import type { LokaScriptInstance } from '../schemas/types';
17
+ import { resolveRuntime } from '../schemas/types';
17
18
 
18
19
  // Re-export schema-derived values for backwards compatibility
19
20
  export const removableSource = removableSchema.source;
@@ -23,9 +24,7 @@ export const removableMetadata = removableSchema;
23
24
  * Register the Removable behavior with LokaScript.
24
25
  */
25
26
  export async function registerRemovable(hyperfixi?: LokaScriptInstance): Promise<void> {
26
- const hf =
27
- hyperfixi ||
28
- (typeof window !== 'undefined' ? (window as unknown as LokaScriptWindow).lokascript : null);
27
+ const hf = hyperfixi || resolveRuntime();
29
28
 
30
29
  if (!hf) {
31
30
  throw new Error(
@@ -44,7 +43,7 @@ export async function registerRemovable(hyperfixi?: LokaScriptInstance): Promise
44
43
  }
45
44
 
46
45
  // Auto-register when loaded as a script tag
47
- if (typeof window !== 'undefined' && (window as unknown as LokaScriptWindow).lokascript) {
46
+ if (resolveRuntime()) {
48
47
  registerRemovable().catch(console.error);
49
48
  }
50
49
 
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Resizable Behavior
2
+ * Resizable Behavior — Imperative Implementation
3
3
  *
4
- * A behavior that makes elements resizable by dragging edges or corners.
4
+ * Makes elements resizable by dragging edges or corners.
5
5
  * Supports minimum/maximum dimensions and resize handles.
6
6
  *
7
7
  * @example
@@ -13,19 +13,67 @@
13
13
  */
14
14
 
15
15
  import { resizableSchema } from '../schemas/resizable.schema';
16
- import type { LokaScriptInstance, LokaScriptWindow } from '../schemas/types';
16
+ import type { LokaScriptInstance } from '../schemas/types';
17
+ import { resolveRuntime } from '../schemas/types';
17
18
 
18
19
  // Re-export schema-derived values for backwards compatibility
19
20
  export const resizableSource = resizableSchema.source;
20
21
  export const resizableMetadata = resizableSchema;
21
22
 
23
+ /**
24
+ * Imperative installer for Resizable behavior.
25
+ */
26
+ function installResizable(element: HTMLElement, params: Record<string, any>): void {
27
+ // Resolve handle element
28
+ let handleEl: HTMLElement = element;
29
+ if (params.handle && params.handle !== 'me') {
30
+ if (typeof params.handle === 'string') {
31
+ const found = element.querySelector(params.handle);
32
+ if (found instanceof HTMLElement) handleEl = found;
33
+ } else if (params.handle instanceof HTMLElement) {
34
+ handleEl = params.handle;
35
+ }
36
+ }
37
+
38
+ const minW = params.minWidth ?? 50;
39
+ const minH = params.minHeight ?? 50;
40
+ const maxW = params.maxWidth ?? 9999;
41
+ const maxH = params.maxHeight ?? 9999;
42
+
43
+ handleEl.addEventListener('pointerdown', (e: PointerEvent) => {
44
+ e.preventDefault();
45
+ element.dispatchEvent(new CustomEvent('resizable:start', { bubbles: true }));
46
+
47
+ const startWidth = element.offsetWidth;
48
+ const startHeight = element.offsetHeight;
49
+ const startX = e.clientX;
50
+ const startY = e.clientY;
51
+
52
+ function onMove(ev: PointerEvent) {
53
+ const newWidth = Math.max(minW, Math.min(maxW, startWidth + (ev.clientX - startX)));
54
+ const newHeight = Math.max(minH, Math.min(maxH, startHeight + (ev.clientY - startY)));
55
+
56
+ element.style.width = `${newWidth}px`;
57
+ element.style.height = `${newHeight}px`;
58
+ element.dispatchEvent(new CustomEvent('resizable:resize', { bubbles: true }));
59
+ }
60
+
61
+ function onUp() {
62
+ document.removeEventListener('pointermove', onMove);
63
+ document.removeEventListener('pointerup', onUp);
64
+ element.dispatchEvent(new CustomEvent('resizable:end', { bubbles: true }));
65
+ }
66
+
67
+ document.addEventListener('pointermove', onMove);
68
+ document.addEventListener('pointerup', onUp);
69
+ });
70
+ }
71
+
22
72
  /**
23
73
  * Register the Resizable behavior with LokaScript.
24
74
  */
25
75
  export async function registerResizable(hyperfixi?: LokaScriptInstance): Promise<void> {
26
- const hf =
27
- hyperfixi ||
28
- (typeof window !== 'undefined' ? (window as unknown as LokaScriptWindow).lokascript : null);
76
+ const hf = hyperfixi || resolveRuntime();
29
77
 
30
78
  if (!hf) {
31
79
  throw new Error(
@@ -33,18 +81,19 @@ export async function registerResizable(hyperfixi?: LokaScriptInstance): Promise
33
81
  );
34
82
  }
35
83
 
36
- const result = hf.compileSync(resizableSchema.source, { traditional: true });
37
-
38
- if (!result.ok) {
39
- throw new Error(`Failed to compile Resizable behavior: ${JSON.stringify(result.errors)}`);
40
- }
41
-
84
+ const syntheticNode = {
85
+ type: 'behavior',
86
+ name: 'Resizable',
87
+ parameters: ['handle', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight'],
88
+ eventHandlers: [],
89
+ imperativeInstaller: installResizable,
90
+ };
42
91
  const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
43
- await hf.execute(result.ast, ctx);
92
+ await hf.execute(syntheticNode, ctx);
44
93
  }
45
94
 
46
95
  // Auto-register when loaded as a script tag
47
- if (typeof window !== 'undefined' && (window as unknown as LokaScriptWindow).lokascript) {
96
+ if (resolveRuntime()) {
48
97
  registerResizable().catch(console.error);
49
98
  }
50
99
 
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+
3
+ describe('ScrollReveal behavior', () => {
4
+ describe('registerScrollReveal', () => {
5
+ it('should register with synthetic node via execute', async () => {
6
+ const { registerScrollReveal } = await import('./scrollreveal');
7
+ const mock = {
8
+ compileSync: vi.fn(),
9
+ execute: vi.fn().mockResolvedValue(undefined),
10
+ createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
11
+ };
12
+
13
+ await registerScrollReveal(mock);
14
+
15
+ expect(mock.compileSync).not.toHaveBeenCalled();
16
+ expect(mock.execute).toHaveBeenCalledWith(
17
+ expect.objectContaining({
18
+ type: 'behavior',
19
+ name: 'ScrollReveal',
20
+ imperativeInstaller: expect.any(Function),
21
+ }),
22
+ expect.objectContaining({ locals: expect.any(Map), globals: expect.any(Map) })
23
+ );
24
+ });
25
+
26
+ it('should throw when no runtime available', async () => {
27
+ const { registerScrollReveal } = await import('./scrollreveal');
28
+ await expect(registerScrollReveal(undefined)).rejects.toThrowError(/LokaScript not found/);
29
+ });
30
+ });
31
+
32
+ describe('schema', () => {
33
+ it('should export valid metadata', async () => {
34
+ const { scrollRevealMetadata } = await import('./scrollreveal');
35
+ expect(scrollRevealMetadata.name).toBe('ScrollReveal');
36
+ expect(scrollRevealMetadata.category).toBe('layout');
37
+ expect(scrollRevealMetadata.tier).toBe('common');
38
+ });
39
+
40
+ it('should have source containing behavior declaration', async () => {
41
+ const { scrollRevealSource } = await import('./scrollreveal');
42
+ expect(scrollRevealSource).toContain('behavior ScrollReveal');
43
+ expect(scrollRevealSource).toContain('IntersectionObserver');
44
+ });
45
+
46
+ it('should document enter/exit events', async () => {
47
+ const { scrollRevealMetadata } = await import('./scrollreveal');
48
+ const eventNames = scrollRevealMetadata.events.map(e => e.name);
49
+ expect(eventNames).toContain('scrollreveal:enter');
50
+ expect(eventNames).toContain('scrollreveal:exit');
51
+ });
52
+
53
+ it('should document IntersectionObserver requirement', async () => {
54
+ const { scrollRevealMetadata } = await import('./scrollreveal');
55
+ const reqs = scrollRevealMetadata.requirements!.join(' ');
56
+ expect(reqs).toContain('IntersectionObserver');
57
+ });
58
+ });
59
+ });
@@ -0,0 +1,78 @@
1
+ /**
2
+ * ScrollReveal Behavior — Imperative Implementation
3
+ *
4
+ * Adds a class or fires events when the element enters or exits the viewport.
5
+ * Uses the IntersectionObserver web standard API.
6
+ */
7
+
8
+ import { scrollRevealSchema } from '../schemas/scrollreveal.schema';
9
+ import type { LokaScriptInstance } from '../schemas/types';
10
+ import { resolveRuntime } from '../schemas/types';
11
+
12
+ // Re-export schema-derived values
13
+ export const scrollRevealSource = scrollRevealSchema.source;
14
+ export const scrollRevealMetadata = scrollRevealSchema;
15
+
16
+ /**
17
+ * Imperative installer for ScrollReveal behavior.
18
+ */
19
+ function installScrollReveal(element: HTMLElement, params: Record<string, any>): void {
20
+ const cls = typeof params.cls === 'string' ? params.cls : 'revealed';
21
+ const threshold = typeof params.threshold === 'number' ? params.threshold : 0.1;
22
+ const once = params.once !== false;
23
+ const rootMargin = typeof params.rootMargin === 'string' ? params.rootMargin : '0px';
24
+
25
+ const observer = new IntersectionObserver(
26
+ entries => {
27
+ for (const entry of entries) {
28
+ if (entry.isIntersecting) {
29
+ element.classList.add(cls);
30
+ element.dispatchEvent(new CustomEvent('scrollreveal:enter', { bubbles: true }));
31
+ if (once) {
32
+ observer.disconnect();
33
+ }
34
+ } else if (!once) {
35
+ element.classList.remove(cls);
36
+ element.dispatchEvent(new CustomEvent('scrollreveal:exit', { bubbles: true }));
37
+ }
38
+ }
39
+ },
40
+ { threshold, rootMargin }
41
+ );
42
+
43
+ observer.observe(element);
44
+ }
45
+
46
+ /**
47
+ * Register the ScrollReveal behavior with LokaScript.
48
+ */
49
+ export async function registerScrollReveal(hyperfixi?: LokaScriptInstance): Promise<void> {
50
+ const hf = hyperfixi || resolveRuntime();
51
+
52
+ if (!hf) {
53
+ throw new Error(
54
+ 'LokaScript not found. Make sure @hyperfixi/core is loaded before registering behaviors.'
55
+ );
56
+ }
57
+
58
+ const syntheticNode = {
59
+ type: 'behavior',
60
+ name: 'ScrollReveal',
61
+ parameters: ['cls', 'threshold', 'once', 'rootMargin'],
62
+ eventHandlers: [],
63
+ imperativeInstaller: installScrollReveal,
64
+ };
65
+ const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
66
+ await hf.execute(syntheticNode, ctx);
67
+ }
68
+
69
+ // Auto-register when loaded as a script tag
70
+ if (resolveRuntime()) {
71
+ registerScrollReveal().catch(console.error);
72
+ }
73
+
74
+ export default {
75
+ source: scrollRevealSchema.source,
76
+ metadata: scrollRevealSchema,
77
+ register: registerScrollReveal,
78
+ };
@@ -1,9 +1,13 @@
1
1
  /**
2
- * Sortable Behavior
2
+ * Sortable Behavior — Imperative Implementation
3
3
  *
4
- * A behavior for drag-and-drop reordering of list items.
4
+ * Drag-and-drop reordering of list items.
5
5
  * Apply to a container element; children become sortable.
6
6
  *
7
+ * Note: This behavior fires lifecycle events but does NOT automatically
8
+ * reorder DOM elements. Users must handle actual reordering in their
9
+ * `sortable:move` event handlers.
10
+ *
7
11
  * @example
8
12
  * ```html
9
13
  * <ul _="install Sortable">
@@ -14,19 +18,61 @@
14
18
  */
15
19
 
16
20
  import { sortableSchema } from '../schemas/sortable.schema';
17
- import type { LokaScriptInstance, LokaScriptWindow } from '../schemas/types';
21
+ import type { LokaScriptInstance } from '../schemas/types';
22
+ import { resolveRuntime } from '../schemas/types';
18
23
 
19
24
  // Re-export schema-derived values for backwards compatibility
20
25
  export const sortableSource = sortableSchema.source;
21
26
  export const sortableMetadata = sortableSchema;
22
27
 
28
+ /**
29
+ * Imperative installer for Sortable behavior.
30
+ */
31
+ function installSortable(element: HTMLElement, params: Record<string, any>): void {
32
+ const dragClass = params.dragClass || 'sorting';
33
+ const handleSelector = params.handle || null;
34
+
35
+ element.addEventListener('pointerdown', (e: PointerEvent) => {
36
+ const target = e.target as HTMLElement;
37
+ const item = target.closest('li');
38
+ if (!item || !element.contains(item)) return;
39
+
40
+ // If a handle selector is specified, only start drag from the handle
41
+ if (handleSelector) {
42
+ const handleEl = target.closest(handleSelector);
43
+ if (!handleEl) return;
44
+ }
45
+
46
+ e.preventDefault();
47
+ item.classList.add(dragClass);
48
+ element.dispatchEvent(new CustomEvent('sortable:start', { bubbles: true, detail: { item } }));
49
+
50
+ function onMove(ev: PointerEvent) {
51
+ element.dispatchEvent(
52
+ new CustomEvent('sortable:move', {
53
+ bubbles: true,
54
+ detail: { clientY: ev.clientY, item },
55
+ })
56
+ );
57
+ }
58
+
59
+ function onUp() {
60
+ document.removeEventListener('pointermove', onMove);
61
+ document.removeEventListener('pointerup', onUp);
62
+ item!.classList.remove(dragClass);
63
+ element.dispatchEvent(new CustomEvent('sortable:end', { bubbles: true, detail: { item } }));
64
+ }
65
+
66
+ document.addEventListener('pointermove', onMove);
67
+ document.addEventListener('pointerup', onUp);
68
+ });
69
+ }
70
+
23
71
  /**
24
72
  * Register the Sortable behavior with LokaScript.
25
73
  */
26
74
  export async function registerSortable(hyperfixi?: LokaScriptInstance): Promise<void> {
27
- const hf =
28
- hyperfixi ||
29
- (typeof window !== 'undefined' ? (window as unknown as LokaScriptWindow).lokascript : null);
75
+ const hf = hyperfixi || resolveRuntime();
30
76
 
31
77
  if (!hf) {
32
78
  throw new Error(
@@ -34,18 +80,19 @@ export async function registerSortable(hyperfixi?: LokaScriptInstance): Promise<
34
80
  );
35
81
  }
36
82
 
37
- const result = hf.compileSync(sortableSchema.source, { traditional: true });
38
-
39
- if (!result.ok) {
40
- throw new Error(`Failed to compile Sortable behavior: ${JSON.stringify(result.errors)}`);
41
- }
42
-
83
+ const syntheticNode = {
84
+ type: 'behavior',
85
+ name: 'Sortable',
86
+ parameters: ['handle', 'dragClass'],
87
+ eventHandlers: [],
88
+ imperativeInstaller: installSortable,
89
+ };
43
90
  const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
44
- await hf.execute(result.ast, ctx);
91
+ await hf.execute(syntheticNode, ctx);
45
92
  }
46
93
 
47
94
  // Auto-register when loaded as a script tag
48
- if (typeof window !== 'undefined' && (window as unknown as LokaScriptWindow).lokascript) {
95
+ if (resolveRuntime()) {
49
96
  registerSortable().catch(console.error);
50
97
  }
51
98
 
@@ -0,0 +1,81 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+
3
+ describe('Tabs behavior', () => {
4
+ describe('registerTabs', () => {
5
+ it('should register with synthetic node via execute', async () => {
6
+ const { registerTabs } = await import('./tabs');
7
+ const mock = {
8
+ compileSync: vi.fn(),
9
+ execute: vi.fn().mockResolvedValue(undefined),
10
+ createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
11
+ };
12
+
13
+ await registerTabs(mock);
14
+
15
+ expect(mock.compileSync).not.toHaveBeenCalled();
16
+ expect(mock.execute).toHaveBeenCalledWith(
17
+ expect.objectContaining({
18
+ type: 'behavior',
19
+ name: 'Tabs',
20
+ imperativeInstaller: expect.any(Function),
21
+ }),
22
+ expect.objectContaining({ locals: expect.any(Map), globals: expect.any(Map) })
23
+ );
24
+ });
25
+
26
+ it('should throw when no runtime available', async () => {
27
+ const { registerTabs } = await import('./tabs');
28
+ await expect(registerTabs(undefined)).rejects.toThrowError(/LokaScript not found/);
29
+ });
30
+
31
+ it('should fall back to manual context when createContext is missing', async () => {
32
+ const { registerTabs } = await import('./tabs');
33
+ const mock = {
34
+ compileSync: vi.fn(),
35
+ execute: vi.fn().mockResolvedValue(undefined),
36
+ };
37
+
38
+ await registerTabs(mock as any);
39
+ expect(mock.execute).toHaveBeenCalledWith(
40
+ expect.anything(),
41
+ expect.objectContaining({ locals: expect.any(Map), globals: expect.any(Map) })
42
+ );
43
+ });
44
+ });
45
+
46
+ describe('schema', () => {
47
+ it('should export valid metadata', async () => {
48
+ const { tabsMetadata } = await import('./tabs');
49
+ expect(tabsMetadata.name).toBe('Tabs');
50
+ expect(tabsMetadata.category).toBe('ui');
51
+ expect(tabsMetadata.tier).toBe('core');
52
+ });
53
+
54
+ it('should have source containing behavior declaration', async () => {
55
+ const { tabsSource } = await import('./tabs');
56
+ expect(tabsSource).toContain('behavior Tabs');
57
+ expect(tabsSource).toContain('orientation');
58
+ });
59
+
60
+ it('should document tabs:change and tabs:changed events', async () => {
61
+ const { tabsMetadata } = await import('./tabs');
62
+ const eventNames = tabsMetadata.events.map(e => e.name);
63
+ expect(eventNames).toContain('tabs:change');
64
+ expect(eventNames).toContain('tabs:changed');
65
+ });
66
+
67
+ it('should document ARIA requirements', async () => {
68
+ const { tabsMetadata } = await import('./tabs');
69
+ expect(tabsMetadata.requirements).toBeDefined();
70
+ const reqs = tabsMetadata.requirements!.join(' ');
71
+ expect(reqs).toContain('aria-selected');
72
+ expect(reqs).toContain('tabindex');
73
+ });
74
+
75
+ it('should have orientation enum parameter', async () => {
76
+ const { tabsMetadata } = await import('./tabs');
77
+ const orientationParam = tabsMetadata.parameters.find(p => p.name === 'orientation');
78
+ expect(orientationParam?.enum).toEqual(['horizontal', 'vertical']);
79
+ });
80
+ });
81
+ });