@hyperfixi/behaviors 2.5.1 → 2.7.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 (182) hide show
  1. package/README.md +100 -0
  2. package/dist/all.browser.global.js +43 -40
  3. package/dist/all.browser.global.js.map +1 -1
  4. package/dist/autodismiss.browser.global.js +2 -2
  5. package/dist/autodismiss.browser.global.js.map +1 -1
  6. package/dist/behaviors/autodismiss.cjs +5 -54
  7. package/dist/behaviors/autodismiss.cjs.map +1 -1
  8. package/dist/behaviors/autodismiss.d.cts +3 -2
  9. package/dist/behaviors/autodismiss.d.ts +3 -2
  10. package/dist/behaviors/autodismiss.js +1 -1
  11. package/dist/behaviors/clickoutside.cjs +6 -28
  12. package/dist/behaviors/clickoutside.cjs.map +1 -1
  13. package/dist/behaviors/clickoutside.d.cts +3 -2
  14. package/dist/behaviors/clickoutside.d.ts +3 -2
  15. package/dist/behaviors/clickoutside.js +2 -2
  16. package/dist/behaviors/clipboard.cjs +30 -86
  17. package/dist/behaviors/clipboard.cjs.map +1 -1
  18. package/dist/behaviors/clipboard.d.cts +3 -2
  19. package/dist/behaviors/clipboard.d.ts +3 -2
  20. package/dist/behaviors/clipboard.js +2 -2
  21. package/dist/behaviors/draggable.cjs +6 -41
  22. package/dist/behaviors/draggable.cjs.map +1 -1
  23. package/dist/behaviors/draggable.d.cts +3 -3
  24. package/dist/behaviors/draggable.d.ts +3 -3
  25. package/dist/behaviors/draggable.js +2 -2
  26. package/dist/behaviors/focustrap.cjs +5 -81
  27. package/dist/behaviors/focustrap.cjs.map +1 -1
  28. package/dist/behaviors/focustrap.d.cts +3 -2
  29. package/dist/behaviors/focustrap.d.ts +3 -2
  30. package/dist/behaviors/focustrap.js +1 -1
  31. package/dist/behaviors/removable.d.cts +1 -1
  32. package/dist/behaviors/removable.d.ts +1 -1
  33. package/dist/behaviors/resizable.cjs +9 -49
  34. package/dist/behaviors/resizable.cjs.map +1 -1
  35. package/dist/behaviors/resizable.d.cts +3 -2
  36. package/dist/behaviors/resizable.d.ts +3 -2
  37. package/dist/behaviors/resizable.js +2 -2
  38. package/dist/behaviors/scrollreveal.cjs +5 -32
  39. package/dist/behaviors/scrollreveal.cjs.map +1 -1
  40. package/dist/behaviors/scrollreveal.d.cts +3 -2
  41. package/dist/behaviors/scrollreveal.d.ts +3 -2
  42. package/dist/behaviors/scrollreveal.js +1 -1
  43. package/dist/behaviors/sortable.cjs +8 -43
  44. package/dist/behaviors/sortable.cjs.map +1 -1
  45. package/dist/behaviors/sortable.d.cts +3 -2
  46. package/dist/behaviors/sortable.d.ts +3 -2
  47. package/dist/behaviors/sortable.js +2 -2
  48. package/dist/behaviors/tabs.cjs +5 -120
  49. package/dist/behaviors/tabs.cjs.map +1 -1
  50. package/dist/behaviors/tabs.d.cts +3 -2
  51. package/dist/behaviors/tabs.d.ts +3 -2
  52. package/dist/behaviors/tabs.js +1 -1
  53. package/dist/behaviors/toggleable.cjs +5 -27
  54. package/dist/behaviors/toggleable.cjs.map +1 -1
  55. package/dist/behaviors/toggleable.d.cts +3 -3
  56. package/dist/behaviors/toggleable.d.ts +3 -3
  57. package/dist/behaviors/toggleable.js +1 -1
  58. package/dist/{chunk-SLOXO6IY.js → chunk-26IX7QU7.js} +2 -2
  59. package/dist/{chunk-SLOXO6IY.js.map → chunk-26IX7QU7.js.map} +1 -1
  60. package/dist/chunk-2HUF6RMI.js +40 -0
  61. package/dist/chunk-2HUF6RMI.js.map +1 -0
  62. package/dist/{chunk-ZBSWTV6I.js → chunk-5LFOFVZ3.js} +5 -5
  63. package/dist/chunk-5LFOFVZ3.js.map +1 -0
  64. package/dist/{chunk-BBIIOGMF.js → chunk-6EG2MNC7.js} +6 -6
  65. package/dist/chunk-7NGOA3T5.js +40 -0
  66. package/dist/chunk-7NGOA3T5.js.map +1 -0
  67. package/dist/{chunk-ZZ2WSAX3.js → chunk-7TPDLB7W.js} +7 -29
  68. package/dist/chunk-7TPDLB7W.js.map +1 -0
  69. package/dist/chunk-CR3J7VV6.js +40 -0
  70. package/dist/chunk-CR3J7VV6.js.map +1 -0
  71. package/dist/chunk-EDBG2F57.js +40 -0
  72. package/dist/chunk-EDBG2F57.js.map +1 -0
  73. package/dist/{chunk-G6WR2AYR.js → chunk-FBQ3X3UY.js} +2 -2
  74. package/dist/chunk-FBQ3X3UY.js.map +1 -0
  75. package/dist/chunk-HX663BJ4.js +40 -0
  76. package/dist/chunk-HX663BJ4.js.map +1 -0
  77. package/dist/chunk-KROW43CG.js +40 -0
  78. package/dist/chunk-KROW43CG.js.map +1 -0
  79. package/dist/chunk-ORBJ7MRW.js +40 -0
  80. package/dist/chunk-ORBJ7MRW.js.map +1 -0
  81. package/dist/{chunk-V2M4W3LO.js → chunk-PDMOOWFW.js} +4 -4
  82. package/dist/chunk-PDMOOWFW.js.map +1 -0
  83. package/dist/chunk-SLSCZV4A.js +40 -0
  84. package/dist/chunk-SLSCZV4A.js.map +1 -0
  85. package/dist/chunk-WKBRZGGL.js +40 -0
  86. package/dist/chunk-WKBRZGGL.js.map +1 -0
  87. package/dist/{chunk-WOTZO7K4.js → chunk-YXS27XM6.js} +26 -23
  88. package/dist/chunk-YXS27XM6.js.map +1 -0
  89. package/dist/clickoutside.browser.global.js +3 -3
  90. package/dist/clickoutside.browser.global.js.map +1 -1
  91. package/dist/clipboard.browser.global.js +27 -24
  92. package/dist/clipboard.browser.global.js.map +1 -1
  93. package/dist/core.browser.global.js +3 -3
  94. package/dist/core.browser.global.js.map +1 -1
  95. package/dist/draggable.browser.global.js +2 -2
  96. package/dist/draggable.browser.global.js.map +1 -1
  97. package/dist/focustrap.browser.global.js +2 -2
  98. package/dist/focustrap.browser.global.js.map +1 -1
  99. package/dist/index.cjs +120 -563
  100. package/dist/index.cjs.map +1 -1
  101. package/dist/index.d.cts +41 -3
  102. package/dist/index.d.ts +41 -3
  103. package/dist/index.js +44 -16
  104. package/dist/index.js.map +1 -1
  105. package/dist/loaders.cjs +86 -563
  106. package/dist/loaders.cjs.map +1 -1
  107. package/dist/loaders.js +6 -6
  108. package/dist/registry.d.cts +1 -1
  109. package/dist/registry.d.ts +1 -1
  110. package/dist/resizable.browser.global.js +5 -5
  111. package/dist/resizable.browser.global.js.map +1 -1
  112. package/dist/resolver.browser.global.js +34 -31
  113. package/dist/resolver.browser.global.js.map +1 -1
  114. package/dist/schemas/index.cjs +34 -31
  115. package/dist/schemas/index.cjs.map +1 -1
  116. package/dist/schemas/index.d.cts +2 -2
  117. package/dist/schemas/index.d.ts +2 -2
  118. package/dist/schemas/index.js +5 -5
  119. package/dist/scrollreveal.browser.global.js +2 -2
  120. package/dist/scrollreveal.browser.global.js.map +1 -1
  121. package/dist/sortable.browser.global.js +4 -4
  122. package/dist/sortable.browser.global.js.map +1 -1
  123. package/dist/tabs.browser.global.js +2 -2
  124. package/dist/tabs.browser.global.js.map +1 -1
  125. package/dist/toggleable.browser.global.js +2 -2
  126. package/dist/toggleable.browser.global.js.map +1 -1
  127. package/dist/{types-BY-jB5FV.d.ts → types-DCCtVQS5.d.cts} +1 -1
  128. package/dist/{types-BY-jB5FV.d.cts → types-DCCtVQS5.d.ts} +1 -1
  129. package/package.json +4 -4
  130. package/src/behaviors/autodismiss.test.ts +15 -9
  131. package/src/behaviors/autodismiss.ts +15 -70
  132. package/src/behaviors/clickoutside.test.ts +17 -9
  133. package/src/behaviors/clickoutside.ts +13 -39
  134. package/src/behaviors/clipboard.test.ts +16 -10
  135. package/src/behaviors/clipboard.ts +15 -93
  136. package/src/behaviors/curated-runtime.test.ts +158 -0
  137. package/src/behaviors/draggable.ts +15 -59
  138. package/src/behaviors/experimental-runtime.test.ts +136 -0
  139. package/src/behaviors/focustrap.test.ts +15 -9
  140. package/src/behaviors/focustrap.ts +17 -121
  141. package/src/behaviors/integration.test.ts +9 -64
  142. package/src/behaviors/optional-runtime.test.ts +251 -0
  143. package/src/behaviors/resizable.ts +14 -61
  144. package/src/behaviors/scrollreveal.test.ts +17 -9
  145. package/src/behaviors/scrollreveal.ts +17 -41
  146. package/src/behaviors/sortable.ts +15 -58
  147. package/src/behaviors/tabs.test.ts +16 -10
  148. package/src/behaviors/tabs.ts +18 -160
  149. package/src/behaviors/toggleable.ts +14 -41
  150. package/src/curation.test.ts +45 -0
  151. package/src/curation.ts +58 -0
  152. package/src/index.ts +14 -0
  153. package/src/registry.test.ts +6 -11
  154. package/src/schemas/clickoutside.schema.ts +1 -1
  155. package/src/schemas/clipboard.schema.ts +25 -22
  156. package/src/schemas/draggable.schema.ts +2 -1
  157. package/src/schemas/resizable.schema.ts +5 -4
  158. package/src/schemas/sortable.schema.ts +4 -3
  159. package/dist/chunk-7O7UOSBH.js +0 -99
  160. package/dist/chunk-7O7UOSBH.js.map +0 -1
  161. package/dist/chunk-EKKJVXDZ.js +0 -67
  162. package/dist/chunk-EKKJVXDZ.js.map +0 -1
  163. package/dist/chunk-F3CASJ7Q.js +0 -116
  164. package/dist/chunk-F3CASJ7Q.js.map +0 -1
  165. package/dist/chunk-G6WR2AYR.js.map +0 -1
  166. package/dist/chunk-GALKOMJ5.js +0 -62
  167. package/dist/chunk-GALKOMJ5.js.map +0 -1
  168. package/dist/chunk-IMDFEGIT.js +0 -75
  169. package/dist/chunk-IMDFEGIT.js.map +0 -1
  170. package/dist/chunk-IT6TYVMW.js +0 -80
  171. package/dist/chunk-IT6TYVMW.js.map +0 -1
  172. package/dist/chunk-LQJOQTBZ.js +0 -89
  173. package/dist/chunk-LQJOQTBZ.js.map +0 -1
  174. package/dist/chunk-S4QDV2MH.js +0 -75
  175. package/dist/chunk-S4QDV2MH.js.map +0 -1
  176. package/dist/chunk-V2M4W3LO.js.map +0 -1
  177. package/dist/chunk-WOTZO7K4.js.map +0 -1
  178. package/dist/chunk-WPOQIMCC.js +0 -155
  179. package/dist/chunk-WPOQIMCC.js.map +0 -1
  180. package/dist/chunk-ZBSWTV6I.js.map +0 -1
  181. package/dist/chunk-ZZ2WSAX3.js.map +0 -1
  182. /package/dist/{chunk-BBIIOGMF.js.map → chunk-6EG2MNC7.js.map} +0 -0
@@ -1,8 +1,15 @@
1
1
  /**
2
- * Tabs Behavior — Imperative Implementation
2
+ * Tabs Behavior
3
3
  *
4
4
  * WAI-ARIA compliant tabs with roving tabindex keyboard navigation.
5
- * Auto-wires role, aria-selected, aria-controls, and aria-labelledby.
5
+ * Auto-wires role, aria-selected, aria-controls, and aria-labelledby. The ARIA
6
+ * wiring / keyboard-navigation logic lives in the schema's hyperscript `source`
7
+ * (an `init`-block `js()` body).
8
+ *
9
+ * Compiled from its hyperscript `source` — the single source of truth shared with
10
+ * the CDN resolver bundle and patterns-reference. This collapses Tabs onto the one
11
+ * runtime path (no more imperative-installer fork between CDN and npm);
12
+ * see docs-internal/BEHAVIORS_CONSOLIDATION_PLAN.md §3d.
6
13
  */
7
14
 
8
15
  import { tabsSchema } from '../schemas/tabs.schema';
@@ -12,157 +19,9 @@ import { resolveRuntime } from '../schemas/types';
12
19
  export const tabsSource = tabsSchema.source;
13
20
  export const tabsMetadata = tabsSchema;
14
21
 
15
- let idCounter = 0;
16
-
17
- function ensureId(el: HTMLElement, prefix: string): string {
18
- if (!el.id) {
19
- el.id = `${prefix}-${++idCounter}`;
20
- }
21
- return el.id;
22
- }
23
-
24
- /**
25
- * Imperative installer for Tabs behavior.
26
- */
27
- function installTabs(element: HTMLElement, params: Record<string, any>): void {
28
- const orientation = params.orientation === 'vertical' ? 'vertical' : 'horizontal';
29
- const wrap = params.wrap !== false;
30
- const activeClass = (params.activeClass as string) || 'active';
31
- const initialIndex = typeof params.activeTab === 'number' ? params.activeTab : 0;
32
-
33
- // Discover tabs and panels
34
- const tabs = Array.from(element.querySelectorAll<HTMLElement>('[role="tab"]'));
35
- const panels = Array.from(element.querySelectorAll<HTMLElement>('[role="tabpanel"]'));
36
-
37
- if (tabs.length === 0 || panels.length === 0) return;
38
-
39
- // Find or mark the tablist
40
- const tablist = element.querySelector<HTMLElement>('[role="tablist"]') || tabs[0].parentElement;
41
- if (tablist && !tablist.getAttribute('role')) {
42
- tablist.setAttribute('role', 'tablist');
43
- }
44
- if (tablist) {
45
- tablist.setAttribute('aria-orientation', orientation);
46
- }
47
-
48
- // Wire IDs, aria-controls, aria-labelledby
49
- const count = Math.min(tabs.length, panels.length);
50
- for (let i = 0; i < count; i++) {
51
- const tabId = ensureId(tabs[i], 'tab');
52
- const panelId = ensureId(panels[i], 'tabpanel');
53
- tabs[i].setAttribute('aria-controls', panelId);
54
- panels[i].setAttribute('aria-labelledby', tabId);
55
- }
56
-
57
- let currentIndex = -1;
58
-
59
- function selectTab(index: number, previousIndex: number): boolean {
60
- if (index === previousIndex || index < 0 || index >= count) return false;
61
-
62
- const detail = {
63
- tab: tabs[index],
64
- panel: panels[index],
65
- index,
66
- previousIndex,
67
- };
68
-
69
- // Cancelable before-event
70
- const changeEvent = new CustomEvent('tabs:change', {
71
- bubbles: true,
72
- cancelable: true,
73
- detail,
74
- });
75
- element.dispatchEvent(changeEvent);
76
- if (changeEvent.defaultPrevented) return false;
77
-
78
- // Deactivate previous
79
- if (previousIndex >= 0 && previousIndex < count) {
80
- tabs[previousIndex].setAttribute('aria-selected', 'false');
81
- tabs[previousIndex].setAttribute('tabindex', '-1');
82
- tabs[previousIndex].classList.remove(activeClass);
83
- panels[previousIndex].setAttribute('aria-hidden', 'true');
84
- panels[previousIndex].classList.remove(activeClass);
85
- }
86
-
87
- // Activate new
88
- tabs[index].setAttribute('aria-selected', 'true');
89
- tabs[index].setAttribute('tabindex', '0');
90
- tabs[index].classList.add(activeClass);
91
- panels[index].setAttribute('aria-hidden', 'false');
92
- panels[index].classList.add(activeClass);
93
-
94
- currentIndex = index;
95
-
96
- element.dispatchEvent(new CustomEvent('tabs:changed', { bubbles: true, detail }));
97
- return true;
98
- }
99
-
100
- // Initialize all tabs as inactive, then activate initial
101
- for (let i = 0; i < count; i++) {
102
- tabs[i].setAttribute('aria-selected', 'false');
103
- tabs[i].setAttribute('tabindex', '-1');
104
- tabs[i].classList.remove(activeClass);
105
- panels[i].setAttribute('aria-hidden', 'true');
106
- panels[i].classList.remove(activeClass);
107
- }
108
-
109
- const clamped = Math.max(0, Math.min(initialIndex, count - 1));
110
- // Directly activate without firing change event on init
111
- tabs[clamped].setAttribute('aria-selected', 'true');
112
- tabs[clamped].setAttribute('tabindex', '0');
113
- tabs[clamped].classList.add(activeClass);
114
- panels[clamped].setAttribute('aria-hidden', 'false');
115
- panels[clamped].classList.add(activeClass);
116
- currentIndex = clamped;
117
-
118
- // Click handler
119
- for (let i = 0; i < count; i++) {
120
- tabs[i].addEventListener('click', () => {
121
- selectTab(i, currentIndex);
122
- tabs[i].focus();
123
- });
124
- }
125
-
126
- // Keyboard handler on tablist
127
- const keyTarget = tablist || element;
128
- keyTarget.addEventListener('keydown', (e: KeyboardEvent) => {
129
- const isHorizontal = orientation === 'horizontal';
130
- const nextKey = isHorizontal ? 'ArrowRight' : 'ArrowDown';
131
- const prevKey = isHorizontal ? 'ArrowLeft' : 'ArrowUp';
132
-
133
- let nextIndex: number | null = null;
134
-
135
- if (e.key === nextKey) {
136
- nextIndex = currentIndex + 1;
137
- if (nextIndex >= count) nextIndex = wrap ? 0 : null;
138
- } else if (e.key === prevKey) {
139
- nextIndex = currentIndex - 1;
140
- if (nextIndex < 0) nextIndex = wrap ? count - 1 : null;
141
- } else if (e.key === 'Home') {
142
- nextIndex = 0;
143
- } else if (e.key === 'End') {
144
- nextIndex = count - 1;
145
- }
146
-
147
- if (nextIndex !== null) {
148
- e.preventDefault();
149
- if (selectTab(nextIndex, currentIndex)) {
150
- tabs[nextIndex].focus();
151
- }
152
- }
153
- });
154
-
155
- // Programmatic tab selection
156
- element.addEventListener('tabs:select', ((e: CustomEvent) => {
157
- const index = e.detail?.index;
158
- if (typeof index === 'number') {
159
- selectTab(index, currentIndex);
160
- }
161
- }) as EventListener);
162
- }
163
-
164
22
  /**
165
- * Register the Tabs behavior with LokaScript.
23
+ * Register the Tabs behavior with LokaScript by compiling its
24
+ * hyperscript source and executing the resulting behavior definition.
166
25
  */
167
26
  export async function registerTabs(hyperfixi?: LokaScriptInstance): Promise<void> {
168
27
  const hf = hyperfixi || resolveRuntime();
@@ -173,15 +32,14 @@ export async function registerTabs(hyperfixi?: LokaScriptInstance): Promise<void
173
32
  );
174
33
  }
175
34
 
176
- const syntheticNode = {
177
- type: 'behavior',
178
- name: 'Tabs',
179
- parameters: ['orientation', 'activeTab', 'wrap', 'activeClass'],
180
- eventHandlers: [],
181
- imperativeInstaller: installTabs,
182
- };
35
+ const result = hf.compileSync(tabsSchema.source, { traditional: true });
36
+
37
+ if (!result.ok) {
38
+ throw new Error(`Failed to compile Tabs behavior: ${JSON.stringify(result.errors)}`);
39
+ }
40
+
183
41
  const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
184
- await hf.execute(syntheticNode, ctx);
42
+ await hf.execute(result.ast, ctx);
185
43
  }
186
44
 
187
45
  // Auto-register when loaded as a script tag
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Toggleable Behavior — Imperative Implementation
2
+ * Toggleable Behavior
3
3
  *
4
4
  * A behavior that toggles a CSS class on click.
5
5
  * Useful for accordions, dropdowns, and toggle buttons.
6
6
  *
7
- * Uses direct DOM APIs instead of compiling hyperscript source,
8
- * because `toggle .{cls}` template interpolation doesn't resolve
9
- * behavior parameters at runtime.
7
+ * Compiled from its hyperscript `source` (the single source of truth shared with
8
+ * the CDN resolver bundle and patterns-reference). The `.{cls}` dynamic class
9
+ * selector resolves the behavior's `cls` parameter at runtime — see
10
+ * `packages/core/src/commands/behaviors/__tests__/template-interpolation.test.ts`.
10
11
  *
11
12
  * @example
12
13
  * ```html
@@ -25,35 +26,8 @@ export const toggleableSource = toggleableSchema.source;
25
26
  export const toggleableMetadata = toggleableSchema;
26
27
 
27
28
  /**
28
- * Imperative installer for Toggleable behavior.
29
- * Called directly by the runtime when `install Toggleable` is executed.
30
- */
31
- function installToggleable(element: HTMLElement, params: Record<string, any>): void {
32
- const cls = params.cls || 'active';
33
-
34
- // Resolve target element
35
- let targetEl: HTMLElement = element;
36
- if (params.target && params.target !== 'me') {
37
- if (typeof params.target === 'string') {
38
- const found = document.querySelector(params.target);
39
- if (found instanceof HTMLElement) targetEl = found;
40
- } else if (params.target instanceof HTMLElement) {
41
- targetEl = params.target;
42
- }
43
- }
44
-
45
- element.addEventListener('click', () => {
46
- const wasOn = targetEl.classList.contains(cls);
47
- targetEl.classList.toggle(cls);
48
- targetEl.dispatchEvent(
49
- new CustomEvent(wasOn ? 'toggleable:off' : 'toggleable:on', { bubbles: true })
50
- );
51
- });
52
- }
53
-
54
- /**
55
- * Register the Toggleable behavior with LokaScript.
56
- * Uses imperative installer via synthetic behavior node.
29
+ * Register the Toggleable behavior with LokaScript by compiling its
30
+ * hyperscript source and executing the resulting behavior definition.
57
31
  */
58
32
  export async function registerToggleable(hyperfixi?: LokaScriptInstance): Promise<void> {
59
33
  const hf = hyperfixi || resolveRuntime();
@@ -64,15 +38,14 @@ export async function registerToggleable(hyperfixi?: LokaScriptInstance): Promis
64
38
  );
65
39
  }
66
40
 
67
- const syntheticNode = {
68
- type: 'behavior',
69
- name: 'Toggleable',
70
- parameters: ['cls', 'target'],
71
- eventHandlers: [],
72
- imperativeInstaller: installToggleable,
73
- };
41
+ const result = hf.compileSync(toggleableSchema.source, { traditional: true });
42
+
43
+ if (!result.ok) {
44
+ throw new Error(`Failed to compile Toggleable behavior: ${JSON.stringify(result.errors)}`);
45
+ }
46
+
74
47
  const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
75
- await hf.execute(syntheticNode, ctx);
48
+ await hf.execute(result.ast, ctx);
76
49
  }
77
50
 
78
51
  // Auto-register when loaded as a script tag
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ CURATED_BEHAVIORS,
4
+ OPTIONAL_BEHAVIORS,
5
+ EXPERIMENTAL_BEHAVIORS,
6
+ CURATION_STATUS,
7
+ curationStatusOf,
8
+ isCurated,
9
+ } from './curation';
10
+ import { ALL_BEHAVIOR_NAMES } from './generated/metadata';
11
+
12
+ describe('curation', () => {
13
+ it('partitions all behaviors into exactly one status', () => {
14
+ const all = [...CURATED_BEHAVIORS, ...OPTIONAL_BEHAVIORS, ...EXPERIMENTAL_BEHAVIORS];
15
+ // No overlaps.
16
+ expect(new Set(all).size).toBe(all.length);
17
+ // Covers exactly the known behavior set.
18
+ expect(new Set(all)).toEqual(new Set(ALL_BEHAVIOR_NAMES));
19
+ });
20
+
21
+ it('marks the curated 5', () => {
22
+ expect([...CURATED_BEHAVIORS].sort()).toEqual(
23
+ ['AutoDismiss', 'ClickOutside', 'Clipboard', 'Removable', 'Toggleable'].sort()
24
+ );
25
+ });
26
+
27
+ it('marks the experimental 3 (beyond the boundary)', () => {
28
+ expect([...EXPERIMENTAL_BEHAVIORS].sort()).toEqual(['Draggable', 'Resizable', 'Sortable']);
29
+ });
30
+
31
+ it('curationStatusOf / isCurated agree with the table', () => {
32
+ expect(curationStatusOf('Toggleable')).toBe('curated');
33
+ expect(curationStatusOf('Draggable')).toBe('experimental');
34
+ expect(curationStatusOf('Tabs')).toBe('optional');
35
+ expect(curationStatusOf('Nope')).toBeUndefined();
36
+ expect(isCurated('Clipboard')).toBe(true);
37
+ expect(isCurated('Sortable')).toBe(false);
38
+ });
39
+
40
+ it('every behavior has a status', () => {
41
+ for (const name of ALL_BEHAVIOR_NAMES) {
42
+ expect(CURATION_STATUS[name]).toBeDefined();
43
+ }
44
+ });
45
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Curation status — the product decision about which behaviors are first-class.
3
+ *
4
+ * This is ORTHOGONAL to {@link BehaviorTier} (core/common/optional), which is about
5
+ * lazy-loading priority. Curation is about support level and the boundary rule:
6
+ *
7
+ * A behavior is a named, parameterized, *reusable inline script* — `on event →
8
+ * DOM action`. Not a component. When it needs an observer, a focus model, or an
9
+ * async pointer loop, it has left hyperfixi's lane.
10
+ *
11
+ * See docs-internal/BEHAVIORS_CONSOLIDATION_PLAN.md for the full rationale.
12
+ */
13
+
14
+ export type CurationStatus = 'curated' | 'optional' | 'experimental';
15
+
16
+ /**
17
+ * Curated set — reliable, runtime-tested, multilingual. The supported story.
18
+ * Real `on event → DOM action` behaviors (Tier A) plus two honestly-labeled
19
+ * JS-backed conveniences (Clipboard, AutoDismiss).
20
+ */
21
+ export const CURATED_BEHAVIORS = [
22
+ 'Toggleable',
23
+ 'Removable',
24
+ 'ClickOutside',
25
+ 'Clipboard',
26
+ 'AutoDismiss',
27
+ ] as const;
28
+
29
+ /**
30
+ * Optional — useful, kept, documented as primitives / nice-to-haves.
31
+ * FocusTrap + ClickOutside are the primitives a Modal composes from; ScrollReveal
32
+ * is a nice-to-have; Tabs is high-value but heavy.
33
+ */
34
+ export const OPTIONAL_BEHAVIORS = ['FocusTrap', 'ScrollReveal', 'Tabs'] as const;
35
+
36
+ /**
37
+ * Experimental — stateful async components (drag/sort/resize). These sit *beyond*
38
+ * the inline-scripting boundary; kept working but explicitly outside the curated,
39
+ * supported, marketed story.
40
+ */
41
+ export const EXPERIMENTAL_BEHAVIORS = ['Draggable', 'Sortable', 'Resizable'] as const;
42
+
43
+ /** Curation status for every behavior, keyed by name. */
44
+ export const CURATION_STATUS: Record<string, CurationStatus> = {
45
+ ...Object.fromEntries(CURATED_BEHAVIORS.map(n => [n, 'curated' as const])),
46
+ ...Object.fromEntries(OPTIONAL_BEHAVIORS.map(n => [n, 'optional' as const])),
47
+ ...Object.fromEntries(EXPERIMENTAL_BEHAVIORS.map(n => [n, 'experimental' as const])),
48
+ };
49
+
50
+ /** Curation status for a behavior name, or undefined if unknown. */
51
+ export function curationStatusOf(name: string): CurationStatus | undefined {
52
+ return CURATION_STATUS[name];
53
+ }
54
+
55
+ /** True for the curated, supported set. */
56
+ export function isCurated(name: string): boolean {
57
+ return CURATION_STATUS[name] === 'curated';
58
+ }
package/src/index.ts CHANGED
@@ -185,6 +185,20 @@ export {
185
185
  BEHAVIOR_TIERS,
186
186
  } from './generated/metadata';
187
187
 
188
+ // =============================================================================
189
+ // Curation status (curated / optional / experimental) — see curation.ts
190
+ // =============================================================================
191
+
192
+ export type { CurationStatus } from './curation';
193
+ export {
194
+ CURATED_BEHAVIORS,
195
+ OPTIONAL_BEHAVIORS,
196
+ EXPERIMENTAL_BEHAVIORS,
197
+ CURATION_STATUS,
198
+ curationStatusOf,
199
+ isCurated,
200
+ } from './curation';
201
+
188
202
  // =============================================================================
189
203
  // Convenience functions
190
204
  // =============================================================================
@@ -175,7 +175,7 @@ describe('registry', () => {
175
175
  });
176
176
 
177
177
  describe('registerWithRuntime', () => {
178
- it('should load and call module.register (imperative behavior)', async () => {
178
+ it('should load and call module.register (Draggable — now source-compiled)', async () => {
179
179
  const mockInstance = {
180
180
  compileSync: vi.fn().mockReturnValue({ ok: true, ast: {} }),
181
181
  execute: vi.fn().mockResolvedValue(undefined),
@@ -185,15 +185,10 @@ describe('registry', () => {
185
185
  await loadBehavior('Draggable');
186
186
  await registerWithRuntime('Draggable', mockInstance);
187
187
 
188
- // Imperative behaviors use execute with synthetic node, not compileSync
189
- expect(mockInstance.execute).toHaveBeenCalledWith(
190
- expect.objectContaining({
191
- type: 'behavior',
192
- name: 'Draggable',
193
- imperativeInstaller: expect.any(Function),
194
- }),
195
- expect.objectContaining({ locals: expect.any(Map), globals: expect.any(Map) })
196
- );
188
+ // Draggable used to install imperatively; it now compiles its hyperscript
189
+ // `source` like every other behavior (no imperative installer).
190
+ expect(mockInstance.compileSync).toHaveBeenCalled();
191
+ expect(mockInstance.execute).toHaveBeenCalled();
197
192
  });
198
193
 
199
194
  it('should load and call module.register (compiled behavior)', async () => {
@@ -221,7 +216,7 @@ describe('registry', () => {
221
216
 
222
217
  await loadAll();
223
218
  await registerAllWithRuntime(mockInstance);
224
- // execute called for each behavior (imperative + compiled)
219
+ // execute called for each behavior (all source-compiled now)
225
220
  expect(mockInstance.execute.mock.calls.length).toBeGreaterThanOrEqual(10);
226
221
  });
227
222
  });
@@ -41,7 +41,7 @@ behavior ClickOutside(active)
41
41
  end
42
42
  end
43
43
  on pointerdown from document
44
- js(me, active)
44
+ js(me, active, event)
45
45
  if (!me.isConnected) return;
46
46
  if (active && !me.contains(event.target)) {
47
47
  me.dispatchEvent(new CustomEvent('clickoutside', { bubbles: true }));
@@ -65,29 +65,32 @@ behavior Clipboard(text, source, feedback, feedbackDuration)
65
65
  set copyText to source's textContent
66
66
  end
67
67
  js(me, copyText, feedback, feedbackDuration)
68
- async function copyToClipboard(t) {
69
- if (navigator.clipboard && navigator.clipboard.writeText) {
70
- await navigator.clipboard.writeText(t);
71
- return;
68
+ // Wrapped in an async IIFE: hyperscript's js() block has no top-level await.
69
+ (async function() {
70
+ async function copyToClipboard(t) {
71
+ if (navigator.clipboard && navigator.clipboard.writeText) {
72
+ await navigator.clipboard.writeText(t);
73
+ return;
74
+ }
75
+ var ta = document.createElement('textarea');
76
+ ta.value = t;
77
+ ta.style.position = 'fixed';
78
+ ta.style.opacity = '0';
79
+ document.body.appendChild(ta);
80
+ ta.select();
81
+ try { document.execCommand('copy'); } finally { document.body.removeChild(ta); }
72
82
  }
73
- var ta = document.createElement('textarea');
74
- ta.value = t;
75
- ta.style.position = 'fixed';
76
- ta.style.opacity = '0';
77
- document.body.appendChild(ta);
78
- ta.select();
79
- try { document.execCommand('copy'); } finally { document.body.removeChild(ta); }
80
- }
81
- try {
82
- await copyToClipboard(copyText);
83
- var feedbackEl = feedback ? (typeof feedback === 'string' ? document.querySelector(feedback) : feedback) : me;
84
- if (!feedbackEl) feedbackEl = me;
85
- feedbackEl.classList.add('copied');
86
- me.dispatchEvent(new CustomEvent('clipboard:copied', { bubbles: true, detail: { text: copyText } }));
87
- setTimeout(function() { feedbackEl.classList.remove('copied'); }, feedbackDuration);
88
- } catch(err) {
89
- me.dispatchEvent(new CustomEvent('clipboard:error', { bubbles: true, detail: { error: err } }));
90
- }
83
+ try {
84
+ await copyToClipboard(copyText);
85
+ var feedbackEl = feedback ? (typeof feedback === 'string' ? document.querySelector(feedback) : feedback) : me;
86
+ if (!feedbackEl) feedbackEl = me;
87
+ feedbackEl.classList.add('copied');
88
+ me.dispatchEvent(new CustomEvent('clipboard:copied', { bubbles: true, detail: { text: copyText } }));
89
+ setTimeout(function() { feedbackEl.classList.remove('copied'); }, feedbackDuration);
90
+ } catch(err) {
91
+ me.dispatchEvent(new CustomEvent('clipboard:error', { bubbles: true, detail: { error: err } }));
92
+ }
93
+ })();
91
94
  end
92
95
  end
93
96
  end`.trim(),
@@ -11,7 +11,8 @@ export const draggableSchema: BehaviorSchema = {
11
11
  category: 'ui',
12
12
  tier: 'core',
13
13
  version: '1.0.0',
14
- description: 'Makes elements draggable with pointer events',
14
+ description:
15
+ 'Makes elements draggable with pointer events (EXPERIMENTAL — stateful async component, beyond the inline-scripting boundary; not part of the curated set)',
15
16
  parameters: [
16
17
  {
17
18
  name: 'dragHandle',
@@ -11,7 +11,8 @@ export const resizableSchema: BehaviorSchema = {
11
11
  category: 'ui',
12
12
  tier: 'optional',
13
13
  version: '1.0.0',
14
- description: 'Makes elements resizable by dragging',
14
+ description:
15
+ 'Makes elements resizable by dragging (EXPERIMENTAL — stateful async component, beyond the inline-scripting boundary; not part of the curated set)',
15
16
  parameters: [
16
17
  {
17
18
  name: 'minWidth',
@@ -59,15 +60,15 @@ behavior Resizable(minWidth, minHeight, maxWidth, maxHeight)
59
60
  set startX to clientX
60
61
  set startY to clientY
61
62
  repeat until event pointerup from document
62
- wait for pointermove(clientX, clientY) from document
63
+ wait for pointermove(clientX, clientY) or pointerup(clientX, clientY) from document
63
64
  set newWidth to startWidth + clientX - startX
64
65
  set newHeight to startHeight + clientY - startY
65
66
  if newWidth < minWidth then set newWidth to minWidth end
66
67
  if newWidth > maxWidth then set newWidth to maxWidth end
67
68
  if newHeight < minHeight then set newHeight to minHeight end
68
69
  if newHeight > maxHeight then set newHeight to maxHeight end
69
- set my style.width to newWidth + "px"
70
- set my style.height to newHeight + "px"
70
+ set my *width to newWidth + "px"
71
+ set my *height to newHeight + "px"
71
72
  trigger resizable:resize
72
73
  end
73
74
  trigger resizable:end
@@ -15,7 +15,8 @@ export const sortableSchema: BehaviorSchema = {
15
15
  category: 'ui',
16
16
  tier: 'common',
17
17
  version: '1.0.0',
18
- description: 'Makes child elements reorderable via drag-and-drop',
18
+ description:
19
+ 'Makes child elements reorderable via drag-and-drop (EXPERIMENTAL — stateful async component, beyond the inline-scripting boundary; not part of the curated set)',
19
20
  parameters: [
20
21
  {
21
22
  name: 'dragClass',
@@ -42,7 +43,7 @@ behavior Sortable(dragClass)
42
43
  end
43
44
  end
44
45
  on pointerdown(clientY) from me
45
- set item to the closest <li/> to the target
46
+ set item to the target.closest("li")
46
47
  if item is null
47
48
  exit
48
49
  end
@@ -50,7 +51,7 @@ behavior Sortable(dragClass)
50
51
  add .{dragClass} to item
51
52
  trigger sortable:start on me
52
53
  repeat until event pointerup from document
53
- wait for pointermove(clientY) from document
54
+ wait for pointermove(clientY) or pointerup(clientY) from document
54
55
  trigger sortable:move on me
55
56
  end
56
57
  remove .{dragClass} from item
@@ -1,99 +0,0 @@
1
- import {
2
- clipboardSchema
3
- } from "./chunk-WOTZO7K4.js";
4
- import {
5
- resolveRuntime
6
- } from "./chunk-6TJXDCAQ.js";
7
-
8
- // src/behaviors/clipboard.ts
9
- var clipboardSource = clipboardSchema.source;
10
- var clipboardMetadata = clipboardSchema;
11
- async function copyToClipboard(text) {
12
- if (navigator.clipboard?.writeText) {
13
- await navigator.clipboard.writeText(text);
14
- return;
15
- }
16
- const textarea = document.createElement("textarea");
17
- textarea.value = text;
18
- textarea.style.position = "fixed";
19
- textarea.style.opacity = "0";
20
- document.body.appendChild(textarea);
21
- textarea.select();
22
- try {
23
- document.execCommand("copy");
24
- } finally {
25
- document.body.removeChild(textarea);
26
- }
27
- }
28
- function resolveElement(element, param) {
29
- if (!param || param === "me") return element;
30
- if (param instanceof HTMLElement) return param;
31
- if (typeof param === "string") {
32
- return element.querySelector(param) || document.querySelector(param) || null;
33
- }
34
- return null;
35
- }
36
- function installClipboard(element, params) {
37
- const feedbackDuration = typeof params.feedbackDuration === "number" ? params.feedbackDuration : 2e3;
38
- element.addEventListener("click", async () => {
39
- let copyText;
40
- if (params.text != null) {
41
- copyText = String(params.text);
42
- } else {
43
- const sourceEl = resolveElement(element, params.source) || element;
44
- copyText = sourceEl.value ?? sourceEl.textContent ?? "";
45
- }
46
- try {
47
- await copyToClipboard(copyText);
48
- const feedbackEl = resolveElement(element, params.feedback) || element;
49
- feedbackEl.classList.add("copied");
50
- setTimeout(() => feedbackEl.classList.remove("copied"), feedbackDuration);
51
- element.dispatchEvent(
52
- new CustomEvent("clipboard:copied", {
53
- bubbles: true,
54
- detail: { text: copyText }
55
- })
56
- );
57
- } catch (error) {
58
- element.dispatchEvent(
59
- new CustomEvent("clipboard:error", {
60
- bubbles: true,
61
- detail: { error }
62
- })
63
- );
64
- }
65
- });
66
- }
67
- async function registerClipboard(hyperfixi) {
68
- const hf = hyperfixi || resolveRuntime();
69
- if (!hf) {
70
- throw new Error(
71
- "LokaScript not found. Make sure @hyperfixi/core is loaded before registering behaviors."
72
- );
73
- }
74
- const syntheticNode = {
75
- type: "behavior",
76
- name: "Clipboard",
77
- parameters: ["text", "source", "feedback", "feedbackDuration"],
78
- eventHandlers: [],
79
- imperativeInstaller: installClipboard
80
- };
81
- const ctx = hf.createContext ? hf.createContext() : { locals: /* @__PURE__ */ new Map(), globals: /* @__PURE__ */ new Map() };
82
- await hf.execute(syntheticNode, ctx);
83
- }
84
- if (resolveRuntime()) {
85
- registerClipboard().catch(console.error);
86
- }
87
- var clipboard_default = {
88
- source: clipboardSchema.source,
89
- metadata: clipboardSchema,
90
- register: registerClipboard
91
- };
92
-
93
- export {
94
- clipboardSource,
95
- clipboardMetadata,
96
- registerClipboard,
97
- clipboard_default
98
- };
99
- //# sourceMappingURL=chunk-7O7UOSBH.js.map