@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 +1 @@
1
- {"version":3,"sources":["../src/behaviors/toggleable.ts","../src/schemas/toggleable.schema.ts","../src/schemas/types.ts"],"sourcesContent":["/**\n * Toggleable Behavior — Imperative Implementation\n *\n * A behavior that toggles a CSS class on click.\n * Useful for accordions, dropdowns, and toggle buttons.\n *\n * Uses direct DOM APIs instead of compiling hyperscript source,\n * because `toggle .{cls}` template interpolation doesn't resolve\n * behavior parameters at runtime.\n *\n * @example\n * ```html\n * <button _=\"install Toggleable\">Toggle</button>\n *\n * <button _=\"install Toggleable(cls: expanded, target: #menu)\">Menu</button>\n * ```\n */\n\nimport { toggleableSchema } from '../schemas/toggleable.schema';\nimport type { LokaScriptInstance } from '../schemas/types';\nimport { resolveRuntime } from '../schemas/types';\n\n// Re-export schema-derived values for backwards compatibility\nexport const toggleableSource = toggleableSchema.source;\nexport const toggleableMetadata = toggleableSchema;\n\n/**\n * Imperative installer for Toggleable behavior.\n * Called directly by the runtime when `install Toggleable` is executed.\n */\nfunction installToggleable(element: HTMLElement, params: Record<string, any>): void {\n const cls = params.cls || 'active';\n\n // Resolve target element\n let targetEl: HTMLElement = element;\n if (params.target && params.target !== 'me') {\n if (typeof params.target === 'string') {\n const found = document.querySelector(params.target);\n if (found instanceof HTMLElement) targetEl = found;\n } else if (params.target instanceof HTMLElement) {\n targetEl = params.target;\n }\n }\n\n element.addEventListener('click', () => {\n const wasOn = targetEl.classList.contains(cls);\n targetEl.classList.toggle(cls);\n targetEl.dispatchEvent(\n new CustomEvent(wasOn ? 'toggleable:off' : 'toggleable:on', { bubbles: true })\n );\n });\n}\n\n/**\n * Register the Toggleable behavior with LokaScript.\n * Uses imperative installer via synthetic behavior node.\n */\nexport async function registerToggleable(hyperfixi?: LokaScriptInstance): Promise<void> {\n const hf = hyperfixi || resolveRuntime();\n\n if (!hf) {\n throw new Error(\n 'LokaScript not found. Make sure @hyperfixi/core is loaded before registering behaviors.'\n );\n }\n\n const syntheticNode = {\n type: 'behavior',\n name: 'Toggleable',\n parameters: ['cls', 'target'],\n eventHandlers: [],\n imperativeInstaller: installToggleable,\n };\n const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };\n await hf.execute(syntheticNode, ctx);\n}\n\n// Auto-register when loaded as a script tag\nif (resolveRuntime()) {\n registerToggleable().catch(console.error);\n}\n\nexport default {\n source: toggleableSchema.source,\n metadata: toggleableSchema,\n register: registerToggleable,\n};\n","import type { BehaviorSchema } from './types';\n\n/**\n * Toggleable Behavior Schema\n *\n * Toggles a CSS class on click.\n * Useful for accordions, dropdowns, and toggle buttons.\n */\nexport const toggleableSchema: BehaviorSchema = {\n name: 'Toggleable',\n category: 'form',\n tier: 'core',\n version: '1.0.0',\n description: 'Toggles a CSS class on click',\n parameters: [\n {\n name: 'cls',\n type: 'string',\n optional: true,\n default: 'active',\n description: 'CSS class to toggle',\n },\n {\n name: 'target',\n type: 'selector',\n optional: true,\n default: 'me',\n description: 'Element to toggle the class on',\n },\n ],\n events: [\n { name: 'toggleable:on', description: 'Fired when class is added' },\n { name: 'toggleable:off', description: 'Fired when class is removed' },\n ],\n source: `\nbehavior Toggleable(cls, target)\n init\n if cls is undefined\n set cls to \"active\"\n end\n if target is undefined\n set target to me\n end\n end\n on click\n toggle .{cls} on target\n js(target, cls)\n var eventName = target.classList.contains(cls) ? 'toggleable:on' : 'toggleable:off';\n target.dispatchEvent(new CustomEvent(eventName, { bubbles: true }));\n end\n end\nend`.trim(),\n};\n","/**\n * Behavior Schema Types\n *\n * Defines the structure for behavior metadata used for:\n * - Code generation (TypeScript types, documentation)\n * - Runtime categorization and lazy loading\n * - Tooling and IDE integration\n */\n\n/**\n * Behavior categories for organization.\n * - ui: Visual interaction behaviors (drag, resize, sort)\n * - data: Data manipulation behaviors (remove, transform)\n * - animation: Animation and transition behaviors\n * - form: Form-related behaviors (validation, toggle states)\n * - layout: Layout behaviors (sticky, scrollspy)\n */\nexport type BehaviorCategory = 'ui' | 'data' | 'animation' | 'form' | 'layout';\n\n/**\n * Loading tiers for lazy loading optimization.\n * - core: Always loaded (essential behaviors)\n * - common: High usage (loaded for typical apps)\n * - optional: Low usage (loaded on demand)\n */\nexport type BehaviorTier = 'core' | 'common' | 'optional';\n\n/**\n * Schema for a behavior parameter.\n */\nexport interface ParameterSchema {\n name: string;\n type: 'string' | 'number' | 'boolean' | 'selector' | 'element';\n optional?: boolean;\n default?: string | number | boolean;\n /** Allowed values for constrained parameters */\n enum?: string[];\n description: string;\n}\n\n/**\n * Schema for a behavior event.\n */\nexport interface EventSchema {\n name: string;\n description: string;\n}\n\n/**\n * Complete schema for a behavior.\n * This is the source of truth for all behavior metadata.\n */\nexport interface BehaviorSchema {\n /** Behavior name (PascalCase) */\n name: string;\n /** Category for organization */\n category: BehaviorCategory;\n /** Loading tier for lazy loading */\n tier: BehaviorTier;\n /** Semantic version */\n version: string;\n /** Human-readable description */\n description: string;\n /** Parameter definitions */\n parameters: ParameterSchema[];\n /** Event definitions */\n events: EventSchema[];\n /** The hyperscript behavior source code */\n source: string;\n /** Optional requirements or notes */\n requirements?: string[];\n}\n\n/**\n * Runtime behavior module interface.\n * This is what gets registered with the hyperfixi runtime.\n */\nexport interface BehaviorModule {\n source: string;\n metadata: BehaviorSchema;\n register: (hyperfixi?: LokaScriptInstance) => Promise<void>;\n}\n\n/**\n * Minimal LokaScript instance interface for behavior registration.\n */\nexport interface LokaScriptInstance {\n compileSync: (\n code: string,\n options?: { traditional?: boolean; language?: string }\n ) => {\n ok: boolean;\n ast?: unknown;\n errors?: Array<{ message: string; line?: number; column?: number }>;\n };\n execute: (ast: unknown, ctx: unknown) => Promise<unknown>;\n createContext?: () => { locals: Map<string, unknown>; globals: Map<string, unknown> };\n}\n\n/**\n * Typed window interface for behavior auto-registration.\n * Avoids `(window as any)` casts throughout the codebase.\n * Checks both `window.lokascript` and `window.hyperfixi` for the runtime instance.\n */\nexport interface LokaScriptWindow {\n lokascript?: LokaScriptInstance;\n hyperfixi?: LokaScriptInstance;\n __hyperfixi_behaviors_ready?: Promise<void> | undefined;\n}\n\n/**\n * Resolve the LokaScript runtime from window globals.\n * Checks both `window.lokascript` and `window.hyperfixi`.\n */\nexport function resolveRuntime(): LokaScriptInstance | null {\n if (typeof window === 'undefined') return null;\n const win = window as unknown as LokaScriptWindow;\n return win.lokascript || win.hyperfixi || null;\n}\n"],"mappings":"scAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,aAAAE,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,qBAAAC,ICQO,IAAMC,EAAmC,CAC9C,KAAM,aACN,SAAU,OACV,KAAM,OACN,QAAS,QACT,YAAa,+BACb,WAAY,CACV,CACE,KAAM,MACN,KAAM,SACN,SAAU,GACV,QAAS,SACT,YAAa,qBACf,EACA,CACE,KAAM,SACN,KAAM,WACN,SAAU,GACV,QAAS,KACT,YAAa,gCACf,CACF,EACA,OAAQ,CACN,CAAE,KAAM,gBAAiB,YAAa,2BAA4B,EAClE,CAAE,KAAM,iBAAkB,YAAa,6BAA8B,CACvE,EACA,OAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAiBL,KAAK,CACV,EC8DO,SAASC,GAA4C,CAC1D,GAAI,OAAO,OAAW,IAAa,OAAO,KAC1C,IAAMC,EAAM,OACZ,OAAOA,EAAI,YAAcA,EAAI,WAAa,IAC5C,CF/FO,IAAMC,EAAmBC,EAAiB,OACpCC,EAAqBD,EAMlC,SAASE,EAAkBC,EAAsBC,EAAmC,CAClF,IAAMC,EAAMD,EAAO,KAAO,SAGtBE,EAAwBH,EAC5B,GAAIC,EAAO,QAAUA,EAAO,SAAW,KACrC,GAAI,OAAOA,EAAO,QAAW,SAAU,CACrC,IAAMG,EAAQ,SAAS,cAAcH,EAAO,MAAM,EAC9CG,aAAiB,cAAaD,EAAWC,EAC/C,MAAWH,EAAO,kBAAkB,cAClCE,EAAWF,EAAO,QAItBD,EAAQ,iBAAiB,QAAS,IAAM,CACtC,IAAMK,EAAQF,EAAS,UAAU,SAASD,CAAG,EAC7CC,EAAS,UAAU,OAAOD,CAAG,EAC7BC,EAAS,cACP,IAAI,YAAYE,EAAQ,iBAAmB,gBAAiB,CAAE,QAAS,EAAK,CAAC,CAC/E,CACF,CAAC,CACH,CAMA,eAAsBC,EAAmBC,EAA+C,CACtF,IAAMC,EAAKD,GAAaE,EAAe,EAEvC,GAAI,CAACD,EACH,MAAM,IAAI,MACR,yFACF,EAGF,IAAME,EAAgB,CACpB,KAAM,WACN,KAAM,aACN,WAAY,CAAC,MAAO,QAAQ,EAC5B,cAAe,CAAC,EAChB,oBAAqBX,CACvB,EACMY,EAAMH,EAAG,cAAgBA,EAAG,cAAc,EAAI,CAAE,OAAQ,IAAI,IAAO,QAAS,IAAI,GAAM,EAC5F,MAAMA,EAAG,QAAQE,EAAeC,CAAG,CACrC,CAGIF,EAAe,GACjBH,EAAmB,EAAE,MAAM,QAAQ,KAAK,EAG1C,IAAOM,EAAQ,CACb,OAAQf,EAAiB,OACzB,SAAUA,EACV,SAAUS,CACZ","names":["toggleable_exports","__export","toggleable_default","registerToggleable","toggleableMetadata","toggleableSource","toggleableSchema","resolveRuntime","win","toggleableSource","toggleableSchema","toggleableMetadata","installToggleable","element","params","cls","targetEl","found","wasOn","registerToggleable","hyperfixi","hf","resolveRuntime","syntheticNode","ctx","toggleable_default"]}
1
+ {"version":3,"sources":["../src/behaviors/toggleable.ts","../src/schemas/toggleable.schema.ts","../src/schemas/types.ts"],"sourcesContent":["/**\n * Toggleable Behavior\n *\n * A behavior that toggles a CSS class on click.\n * Useful for accordions, dropdowns, and toggle buttons.\n *\n * Compiled from its hyperscript `source` (the single source of truth shared with\n * the CDN resolver bundle and patterns-reference). The `.{cls}` dynamic class\n * selector resolves the behavior's `cls` parameter at runtime — see\n * `packages/core/src/commands/behaviors/__tests__/template-interpolation.test.ts`.\n *\n * @example\n * ```html\n * <button _=\"install Toggleable\">Toggle</button>\n *\n * <button _=\"install Toggleable(cls: expanded, target: #menu)\">Menu</button>\n * ```\n */\n\nimport { toggleableSchema } from '../schemas/toggleable.schema';\nimport type { LokaScriptInstance } from '../schemas/types';\nimport { resolveRuntime } from '../schemas/types';\n\n// Re-export schema-derived values for backwards compatibility\nexport const toggleableSource = toggleableSchema.source;\nexport const toggleableMetadata = toggleableSchema;\n\n/**\n * Register the Toggleable behavior with LokaScript by compiling its\n * hyperscript source and executing the resulting behavior definition.\n */\nexport async function registerToggleable(hyperfixi?: LokaScriptInstance): Promise<void> {\n const hf = hyperfixi || resolveRuntime();\n\n if (!hf) {\n throw new Error(\n 'LokaScript not found. Make sure @hyperfixi/core is loaded before registering behaviors.'\n );\n }\n\n const result = hf.compileSync(toggleableSchema.source, { traditional: true });\n\n if (!result.ok) {\n throw new Error(`Failed to compile Toggleable behavior: ${JSON.stringify(result.errors)}`);\n }\n\n const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };\n await hf.execute(result.ast, ctx);\n}\n\n// Auto-register when loaded as a script tag\nif (resolveRuntime()) {\n registerToggleable().catch(console.error);\n}\n\nexport default {\n source: toggleableSchema.source,\n metadata: toggleableSchema,\n register: registerToggleable,\n};\n","import type { BehaviorSchema } from './types';\n\n/**\n * Toggleable Behavior Schema\n *\n * Toggles a CSS class on click.\n * Useful for accordions, dropdowns, and toggle buttons.\n */\nexport const toggleableSchema: BehaviorSchema = {\n name: 'Toggleable',\n category: 'form',\n tier: 'core',\n version: '1.0.0',\n description: 'Toggles a CSS class on click',\n parameters: [\n {\n name: 'cls',\n type: 'string',\n optional: true,\n default: 'active',\n description: 'CSS class to toggle',\n },\n {\n name: 'target',\n type: 'selector',\n optional: true,\n default: 'me',\n description: 'Element to toggle the class on',\n },\n ],\n events: [\n { name: 'toggleable:on', description: 'Fired when class is added' },\n { name: 'toggleable:off', description: 'Fired when class is removed' },\n ],\n source: `\nbehavior Toggleable(cls, target)\n init\n if cls is undefined\n set cls to \"active\"\n end\n if target is undefined\n set target to me\n end\n end\n on click\n toggle .{cls} on target\n js(target, cls)\n var eventName = target.classList.contains(cls) ? 'toggleable:on' : 'toggleable:off';\n target.dispatchEvent(new CustomEvent(eventName, { bubbles: true }));\n end\n end\nend`.trim(),\n};\n","/**\n * Behavior Schema Types\n *\n * Defines the structure for behavior metadata used for:\n * - Code generation (TypeScript types, documentation)\n * - Runtime categorization and lazy loading\n * - Tooling and IDE integration\n */\n\n/**\n * Behavior categories for organization.\n * - ui: Visual interaction behaviors (drag, resize, sort)\n * - data: Data manipulation behaviors (remove, transform)\n * - animation: Animation and transition behaviors\n * - form: Form-related behaviors (validation, toggle states)\n * - layout: Layout behaviors (sticky, scrollspy)\n */\nexport type BehaviorCategory = 'ui' | 'data' | 'animation' | 'form' | 'layout';\n\n/**\n * Loading tiers for lazy loading optimization.\n * - core: Always loaded (essential behaviors)\n * - common: High usage (loaded for typical apps)\n * - optional: Low usage (loaded on demand)\n */\nexport type BehaviorTier = 'core' | 'common' | 'optional';\n\n/**\n * Schema for a behavior parameter.\n */\nexport interface ParameterSchema {\n name: string;\n type: 'string' | 'number' | 'boolean' | 'selector' | 'element';\n optional?: boolean;\n default?: string | number | boolean;\n /** Allowed values for constrained parameters */\n enum?: string[];\n description: string;\n}\n\n/**\n * Schema for a behavior event.\n */\nexport interface EventSchema {\n name: string;\n description: string;\n}\n\n/**\n * Complete schema for a behavior.\n * This is the source of truth for all behavior metadata.\n */\nexport interface BehaviorSchema {\n /** Behavior name (PascalCase) */\n name: string;\n /** Category for organization */\n category: BehaviorCategory;\n /** Loading tier for lazy loading */\n tier: BehaviorTier;\n /** Semantic version */\n version: string;\n /** Human-readable description */\n description: string;\n /** Parameter definitions */\n parameters: ParameterSchema[];\n /** Event definitions */\n events: EventSchema[];\n /** The hyperscript behavior source code */\n source: string;\n /** Optional requirements or notes */\n requirements?: string[];\n}\n\n/**\n * Runtime behavior module interface.\n * This is what gets registered with the hyperfixi runtime.\n */\nexport interface BehaviorModule {\n source: string;\n metadata: BehaviorSchema;\n register: (hyperfixi?: LokaScriptInstance) => Promise<void>;\n}\n\n/**\n * Minimal LokaScript instance interface for behavior registration.\n */\nexport interface LokaScriptInstance {\n compileSync: (\n code: string,\n options?: { traditional?: boolean; language?: string }\n ) => {\n ok: boolean;\n ast?: unknown;\n errors?: Array<{ message: string; line?: number; column?: number }>;\n };\n execute: (ast: unknown, ctx: unknown) => Promise<unknown>;\n createContext?: () => { locals: Map<string, unknown>; globals: Map<string, unknown> };\n}\n\n/**\n * Typed window interface for behavior auto-registration.\n * Avoids `(window as any)` casts throughout the codebase.\n * Checks both `window.lokascript` and `window.hyperfixi` for the runtime instance.\n */\nexport interface LokaScriptWindow {\n lokascript?: LokaScriptInstance;\n hyperfixi?: LokaScriptInstance;\n __hyperfixi_behaviors_ready?: Promise<void> | undefined;\n}\n\n/**\n * Resolve the LokaScript runtime from window globals.\n * Checks both `window.lokascript` and `window.hyperfixi`.\n */\nexport function resolveRuntime(): LokaScriptInstance | null {\n if (typeof window === 'undefined') return null;\n const win = window as unknown as LokaScriptWindow;\n return win.lokascript || win.hyperfixi || null;\n}\n"],"mappings":"scAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,aAAAE,EAAA,uBAAAC,EAAA,uBAAAC,EAAA,qBAAAC,ICQO,IAAMC,EAAmC,CAC9C,KAAM,aACN,SAAU,OACV,KAAM,OACN,QAAS,QACT,YAAa,+BACb,WAAY,CACV,CACE,KAAM,MACN,KAAM,SACN,SAAU,GACV,QAAS,SACT,YAAa,qBACf,EACA,CACE,KAAM,SACN,KAAM,WACN,SAAU,GACV,QAAS,KACT,YAAa,gCACf,CACF,EACA,OAAQ,CACN,CAAE,KAAM,gBAAiB,YAAa,2BAA4B,EAClE,CAAE,KAAM,iBAAkB,YAAa,6BAA8B,CACvE,EACA,OAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAiBL,KAAK,CACV,EC8DO,SAASC,GAA4C,CAC1D,GAAI,OAAO,OAAW,IAAa,OAAO,KAC1C,IAAMC,EAAM,OACZ,OAAOA,EAAI,YAAcA,EAAI,WAAa,IAC5C,CF9FO,IAAMC,EAAmBC,EAAiB,OACpCC,EAAqBD,EAMlC,eAAsBE,EAAmBC,EAA+C,CACtF,IAAMC,EAAKD,GAAaE,EAAe,EAEvC,GAAI,CAACD,EACH,MAAM,IAAI,MACR,yFACF,EAGF,IAAME,EAASF,EAAG,YAAYJ,EAAiB,OAAQ,CAAE,YAAa,EAAK,CAAC,EAE5E,GAAI,CAACM,EAAO,GACV,MAAM,IAAI,MAAM,0CAA0C,KAAK,UAAUA,EAAO,MAAM,CAAC,EAAE,EAG3F,IAAMC,EAAMH,EAAG,cAAgBA,EAAG,cAAc,EAAI,CAAE,OAAQ,IAAI,IAAO,QAAS,IAAI,GAAM,EAC5F,MAAMA,EAAG,QAAQE,EAAO,IAAKC,CAAG,CAClC,CAGIF,EAAe,GACjBH,EAAmB,EAAE,MAAM,QAAQ,KAAK,EAG1C,IAAOM,EAAQ,CACb,OAAQR,EAAiB,OACzB,SAAUA,EACV,SAAUE,CACZ","names":["toggleable_exports","__export","toggleable_default","registerToggleable","toggleableMetadata","toggleableSource","toggleableSchema","resolveRuntime","win","toggleableSource","toggleableSchema","toggleableMetadata","registerToggleable","hyperfixi","hf","resolveRuntime","result","ctx","toggleable_default"]}
@@ -97,4 +97,4 @@ interface LokaScriptInstance {
97
97
  };
98
98
  }
99
99
 
100
- export type { BehaviorCategory as B, EventSchema as E, LokaScriptInstance as L, ParameterSchema as P, BehaviorModule as a, BehaviorSchema as b, BehaviorTier as c };
100
+ export type { BehaviorSchema as B, EventSchema as E, LokaScriptInstance as L, ParameterSchema as P, BehaviorModule as a, BehaviorCategory as b, BehaviorTier as c };
@@ -97,4 +97,4 @@ interface LokaScriptInstance {
97
97
  };
98
98
  }
99
99
 
100
- export type { BehaviorCategory as B, EventSchema as E, LokaScriptInstance as L, ParameterSchema as P, BehaviorModule as a, BehaviorSchema as b, BehaviorTier as c };
100
+ export type { BehaviorSchema as B, EventSchema as E, LokaScriptInstance as L, ParameterSchema as P, BehaviorModule as a, BehaviorCategory as b, BehaviorTier as c };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperfixi/behaviors",
3
- "version": "2.5.1",
3
+ "version": "2.7.0",
4
4
  "description": "Reusable hyperscript behaviors for LokaScript",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -108,13 +108,13 @@
108
108
  "typecheck": "tsc --noEmit",
109
109
  "test": "vitest run",
110
110
  "test:watch": "vitest",
111
- "test:check": "vitest run --reporter=dot 2>&1 | tail -5"
111
+ "test:check": "VITEST_QUIET=1 bash ../../scripts/vitest-run.sh --reporter=dot"
112
112
  },
113
113
  "peerDependencies": {
114
- "@hyperfixi/core": "^2.5.1"
114
+ "@hyperfixi/core": "^2.7.0"
115
115
  },
116
116
  "devDependencies": {
117
- "@hyperfixi/core": "^2.5.1",
117
+ "@hyperfixi/core": "^2.7.0",
118
118
  "tsup": "^8.0.0",
119
119
  "typescript": "^5.0.0",
120
120
  "vitest": "^4.1.5"
@@ -2,27 +2,33 @@ import { describe, it, expect, vi } from 'vitest';
2
2
 
3
3
  describe('AutoDismiss behavior', () => {
4
4
  describe('registerAutoDismiss', () => {
5
- it('should register with synthetic node via execute', async () => {
6
- const { registerAutoDismiss } = await import('./autodismiss');
5
+ it('should compile its hyperscript source and execute', async () => {
6
+ const { registerAutoDismiss, autoDismissSource } = await import('./autodismiss');
7
7
  const mock = {
8
- compileSync: vi.fn(),
8
+ compileSync: vi.fn().mockReturnValue({ ok: true, ast: { type: 'behavior' } }),
9
9
  execute: vi.fn().mockResolvedValue(undefined),
10
10
  createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
11
11
  };
12
12
 
13
13
  await registerAutoDismiss(mock);
14
14
 
15
- expect(mock.compileSync).not.toHaveBeenCalled();
15
+ expect(mock.compileSync).toHaveBeenCalledWith(autoDismissSource, { traditional: true });
16
16
  expect(mock.execute).toHaveBeenCalledWith(
17
- expect.objectContaining({
18
- type: 'behavior',
19
- name: 'AutoDismiss',
20
- imperativeInstaller: expect.any(Function),
21
- }),
17
+ { type: 'behavior' },
22
18
  expect.objectContaining({ locals: expect.any(Map), globals: expect.any(Map) })
23
19
  );
24
20
  });
25
21
 
22
+ it('should throw on compile failure', async () => {
23
+ const { registerAutoDismiss } = await import('./autodismiss');
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() }),
28
+ };
29
+ await expect(registerAutoDismiss(mock)).rejects.toThrowError(/Failed to compile AutoDismiss/);
30
+ });
31
+
26
32
  it('should throw when no runtime available', async () => {
27
33
  const { registerAutoDismiss } = await import('./autodismiss');
28
34
  await expect(registerAutoDismiss(undefined)).rejects.toThrowError(/LokaScript not found/);
@@ -1,8 +1,12 @@
1
1
  /**
2
- * AutoDismiss Behavior — Imperative Implementation
2
+ * AutoDismiss Behavior
3
3
  *
4
4
  * Auto-removes elements after a configurable delay.
5
- * Supports pause-on-hover and fade effect using standard setTimeout/clearTimeout.
5
+ * Supports pause-on-hover and fade effect using standard setTimeout/clearTimeout
6
+ * (in its hyperscript `source`'s js() body).
7
+ *
8
+ * Compiled from its hyperscript `source` (the single source of truth shared with
9
+ * the CDN resolver bundle and patterns-reference).
6
10
  */
7
11
 
8
12
  import { autoDismissSchema } from '../schemas/autodismiss.schema';
@@ -14,66 +18,8 @@ export const autoDismissSource = autoDismissSchema.source;
14
18
  export const autoDismissMetadata = autoDismissSchema;
15
19
 
16
20
  /**
17
- * Imperative installer for AutoDismiss behavior.
18
- */
19
- function installAutoDismiss(element: HTMLElement, params: Record<string, any>): void {
20
- const delay = typeof params.delay === 'number' ? params.delay : 5000;
21
- const pauseOnHover = params.pauseOnHover !== false;
22
- const effect = params.effect === 'fade' ? 'fade' : 'none';
23
-
24
- let timerId: ReturnType<typeof setTimeout> | null = null;
25
- let remaining = delay;
26
- let startedAt = Date.now();
27
-
28
- function dismiss() {
29
- const event = new CustomEvent('autodismiss:dismissed', {
30
- bubbles: true,
31
- cancelable: true,
32
- });
33
- element.dispatchEvent(event);
34
-
35
- if (event.defaultPrevented) return;
36
-
37
- if (effect === 'fade') {
38
- element.style.transition = 'opacity 300ms';
39
- element.style.opacity = '0';
40
- setTimeout(() => element.remove(), 300);
41
- } else {
42
- element.remove();
43
- }
44
- }
45
-
46
- function start() {
47
- startedAt = Date.now();
48
- timerId = setTimeout(dismiss, remaining);
49
- }
50
-
51
- // Start the countdown
52
- element.dispatchEvent(new CustomEvent('autodismiss:start', { bubbles: true }));
53
- start();
54
-
55
- if (pauseOnHover) {
56
- element.addEventListener('mouseenter', () => {
57
- if (timerId != null) {
58
- clearTimeout(timerId);
59
- timerId = null;
60
- remaining -= Date.now() - startedAt;
61
- if (remaining < 0) remaining = 0;
62
- element.dispatchEvent(new CustomEvent('autodismiss:paused', { bubbles: true }));
63
- }
64
- });
65
-
66
- element.addEventListener('mouseleave', () => {
67
- if (timerId == null && element.isConnected) {
68
- element.dispatchEvent(new CustomEvent('autodismiss:resumed', { bubbles: true }));
69
- start();
70
- }
71
- });
72
- }
73
- }
74
-
75
- /**
76
- * Register the AutoDismiss behavior with LokaScript.
21
+ * Register the AutoDismiss behavior with LokaScript by compiling its
22
+ * hyperscript source and executing the resulting behavior definition.
77
23
  */
78
24
  export async function registerAutoDismiss(hyperfixi?: LokaScriptInstance): Promise<void> {
79
25
  const hf = hyperfixi || resolveRuntime();
@@ -84,15 +30,14 @@ export async function registerAutoDismiss(hyperfixi?: LokaScriptInstance): Promi
84
30
  );
85
31
  }
86
32
 
87
- const syntheticNode = {
88
- type: 'behavior',
89
- name: 'AutoDismiss',
90
- parameters: ['delay', 'pauseOnHover', 'effect'],
91
- eventHandlers: [],
92
- imperativeInstaller: installAutoDismiss,
93
- };
33
+ const result = hf.compileSync(autoDismissSchema.source, { traditional: true });
34
+
35
+ if (!result.ok) {
36
+ throw new Error(`Failed to compile AutoDismiss behavior: ${JSON.stringify(result.errors)}`);
37
+ }
38
+
94
39
  const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
95
- await hf.execute(syntheticNode, ctx);
40
+ await hf.execute(result.ast, ctx);
96
41
  }
97
42
 
98
43
  // Auto-register when loaded as a script tag
@@ -2,27 +2,35 @@ import { describe, it, expect, vi } from 'vitest';
2
2
 
3
3
  describe('ClickOutside behavior', () => {
4
4
  describe('registerClickOutside', () => {
5
- it('should register with synthetic node via execute', async () => {
6
- const { registerClickOutside } = await import('./clickoutside');
5
+ it('should compile its hyperscript source and execute', async () => {
6
+ const { registerClickOutside, clickOutsideSource } = await import('./clickoutside');
7
7
  const mock = {
8
- compileSync: vi.fn(),
8
+ compileSync: vi.fn().mockReturnValue({ ok: true, ast: { type: 'behavior' } }),
9
9
  execute: vi.fn().mockResolvedValue(undefined),
10
10
  createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
11
11
  };
12
12
 
13
13
  await registerClickOutside(mock);
14
14
 
15
- expect(mock.compileSync).not.toHaveBeenCalled();
15
+ expect(mock.compileSync).toHaveBeenCalledWith(clickOutsideSource, { traditional: true });
16
16
  expect(mock.execute).toHaveBeenCalledWith(
17
- expect.objectContaining({
18
- type: 'behavior',
19
- name: 'ClickOutside',
20
- imperativeInstaller: expect.any(Function),
21
- }),
17
+ { type: 'behavior' },
22
18
  expect.objectContaining({ locals: expect.any(Map), globals: expect.any(Map) })
23
19
  );
24
20
  });
25
21
 
22
+ it('should throw on compile failure', async () => {
23
+ const { registerClickOutside } = await import('./clickoutside');
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() }),
28
+ };
29
+ await expect(registerClickOutside(mock)).rejects.toThrowError(
30
+ /Failed to compile ClickOutside/
31
+ );
32
+ });
33
+
26
34
  it('should throw when no runtime available', async () => {
27
35
  const { registerClickOutside } = await import('./clickoutside');
28
36
  await expect(registerClickOutside(undefined)).rejects.toThrowError(/LokaScript not found/);
@@ -1,8 +1,11 @@
1
1
  /**
2
- * ClickOutside Behavior — Imperative Implementation
2
+ * ClickOutside Behavior
3
3
  *
4
4
  * Fires a clickoutside event when the user clicks outside the element.
5
5
  * Uses pointerdown instead of click to avoid timing issues.
6
+ *
7
+ * Compiled from its hyperscript `source` (the single source of truth shared with
8
+ * the CDN resolver bundle and patterns-reference).
6
9
  */
7
10
 
8
11
  import { clickOutsideSchema } from '../schemas/clickoutside.schema';
@@ -14,36 +17,8 @@ export const clickOutsideSource = clickOutsideSchema.source;
14
17
  export const clickOutsideMetadata = clickOutsideSchema;
15
18
 
16
19
  /**
17
- * Imperative installer for ClickOutside behavior.
18
- */
19
- function installClickOutside(element: HTMLElement, params: Record<string, any>): void {
20
- let active = params.active !== false;
21
-
22
- function onPointerDown(e: Event) {
23
- // Auto-cleanup if element has been removed from DOM
24
- if (!element.isConnected) {
25
- document.removeEventListener('pointerdown', onPointerDown);
26
- return;
27
- }
28
-
29
- if (active && !element.contains(e.target as Node)) {
30
- element.dispatchEvent(new CustomEvent('clickoutside', { bubbles: true }));
31
- }
32
- }
33
-
34
- document.addEventListener('pointerdown', onPointerDown);
35
-
36
- // Programmatic activation/deactivation via custom events
37
- element.addEventListener('clickoutside:activate', () => {
38
- active = true;
39
- });
40
- element.addEventListener('clickoutside:deactivate', () => {
41
- active = false;
42
- });
43
- }
44
-
45
- /**
46
- * Register the ClickOutside behavior with LokaScript.
20
+ * Register the ClickOutside behavior with LokaScript by compiling its
21
+ * hyperscript source and executing the resulting behavior definition.
47
22
  */
48
23
  export async function registerClickOutside(hyperfixi?: LokaScriptInstance): Promise<void> {
49
24
  const hf = hyperfixi || resolveRuntime();
@@ -54,15 +29,14 @@ export async function registerClickOutside(hyperfixi?: LokaScriptInstance): Prom
54
29
  );
55
30
  }
56
31
 
57
- const syntheticNode = {
58
- type: 'behavior',
59
- name: 'ClickOutside',
60
- parameters: ['active'],
61
- eventHandlers: [],
62
- imperativeInstaller: installClickOutside,
63
- };
32
+ const result = hf.compileSync(clickOutsideSchema.source, { traditional: true });
33
+
34
+ if (!result.ok) {
35
+ throw new Error(`Failed to compile ClickOutside behavior: ${JSON.stringify(result.errors)}`);
36
+ }
37
+
64
38
  const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
65
- await hf.execute(syntheticNode, ctx);
39
+ await hf.execute(result.ast, ctx);
66
40
  }
67
41
 
68
42
  // Auto-register when loaded as a script tag
@@ -2,27 +2,33 @@ import { describe, it, expect, vi } from 'vitest';
2
2
 
3
3
  describe('Clipboard behavior', () => {
4
4
  describe('registerClipboard', () => {
5
- it('should register with synthetic node via execute', async () => {
6
- const { registerClipboard } = await import('./clipboard');
5
+ it('should compile its hyperscript source and execute', async () => {
6
+ const { registerClipboard, clipboardSource } = await import('./clipboard');
7
7
  const mock = {
8
- compileSync: vi.fn(),
8
+ compileSync: vi.fn().mockReturnValue({ ok: true, ast: { type: 'behavior' } }),
9
9
  execute: vi.fn().mockResolvedValue(undefined),
10
10
  createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
11
11
  };
12
12
 
13
13
  await registerClipboard(mock);
14
14
 
15
- expect(mock.compileSync).not.toHaveBeenCalled();
15
+ expect(mock.compileSync).toHaveBeenCalledWith(clipboardSource, { traditional: true });
16
16
  expect(mock.execute).toHaveBeenCalledWith(
17
- expect.objectContaining({
18
- type: 'behavior',
19
- name: 'Clipboard',
20
- imperativeInstaller: expect.any(Function),
21
- }),
17
+ { type: 'behavior' },
22
18
  expect.objectContaining({ locals: expect.any(Map), globals: expect.any(Map) })
23
19
  );
24
20
  });
25
21
 
22
+ it('should throw on compile failure', async () => {
23
+ 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() }),
28
+ };
29
+ await expect(registerClipboard(mock)).rejects.toThrowError(/Failed to compile Clipboard/);
30
+ });
31
+
26
32
  it('should throw when no runtime available', async () => {
27
33
  const { registerClipboard } = await import('./clipboard');
28
34
  await expect(registerClipboard(undefined)).rejects.toThrowError(/LokaScript not found/);
@@ -31,7 +37,7 @@ describe('Clipboard behavior', () => {
31
37
  it('should fall back to manual context when createContext is missing', async () => {
32
38
  const { registerClipboard } = await import('./clipboard');
33
39
  const mock = {
34
- compileSync: vi.fn(),
40
+ compileSync: vi.fn().mockReturnValue({ ok: true, ast: { type: 'behavior' } }),
35
41
  execute: vi.fn().mockResolvedValue(undefined),
36
42
  };
37
43
 
@@ -1,8 +1,12 @@
1
1
  /**
2
- * Clipboard Behavior — Imperative Implementation
2
+ * Clipboard Behavior
3
3
  *
4
4
  * Copies text to clipboard on click with visual feedback.
5
- * Uses navigator.clipboard.writeText() with execCommand fallback.
5
+ * Uses navigator.clipboard.writeText() with execCommand fallback (in its
6
+ * hyperscript `source`'s js() body).
7
+ *
8
+ * Compiled from its hyperscript `source` (the single source of truth shared with
9
+ * the CDN resolver bundle and patterns-reference).
6
10
  */
7
11
 
8
12
  import { clipboardSchema } from '../schemas/clipboard.schema';
@@ -14,89 +18,8 @@ export const clipboardSource = clipboardSchema.source;
14
18
  export const clipboardMetadata = clipboardSchema;
15
19
 
16
20
  /**
17
- * Copy text to clipboard using the modern Clipboard API,
18
- * falling back to execCommand for older browsers.
19
- */
20
- async function copyToClipboard(text: string): Promise<void> {
21
- if (navigator.clipboard?.writeText) {
22
- await navigator.clipboard.writeText(text);
23
- return;
24
- }
25
-
26
- // Fallback: temporary textarea + execCommand
27
- const textarea = document.createElement('textarea');
28
- textarea.value = text;
29
- textarea.style.position = 'fixed';
30
- textarea.style.opacity = '0';
31
- document.body.appendChild(textarea);
32
- textarea.select();
33
- try {
34
- document.execCommand('copy');
35
- } finally {
36
- document.body.removeChild(textarea);
37
- }
38
- }
39
-
40
- /**
41
- * Resolve a selector parameter to an HTMLElement.
42
- */
43
- function resolveElement(element: HTMLElement, param: unknown): HTMLElement | null {
44
- if (!param || param === 'me') return element;
45
- if (param instanceof HTMLElement) return param;
46
- if (typeof param === 'string') {
47
- return (
48
- (element.querySelector(param) as HTMLElement) ||
49
- (document.querySelector(param) as HTMLElement) ||
50
- null
51
- );
52
- }
53
- return null;
54
- }
55
-
56
- /**
57
- * Imperative installer for Clipboard behavior.
58
- */
59
- function installClipboard(element: HTMLElement, params: Record<string, any>): void {
60
- const feedbackDuration =
61
- typeof params.feedbackDuration === 'number' ? params.feedbackDuration : 2000;
62
-
63
- element.addEventListener('click', async () => {
64
- // Determine text to copy
65
- let copyText: string;
66
- if (params.text != null) {
67
- copyText = String(params.text);
68
- } else {
69
- const sourceEl = resolveElement(element, params.source) || element;
70
- copyText = (sourceEl as HTMLInputElement).value ?? sourceEl.textContent ?? '';
71
- }
72
-
73
- try {
74
- await copyToClipboard(copyText);
75
-
76
- // Show feedback
77
- const feedbackEl = resolveElement(element, params.feedback) || element;
78
- feedbackEl.classList.add('copied');
79
- setTimeout(() => feedbackEl.classList.remove('copied'), feedbackDuration);
80
-
81
- element.dispatchEvent(
82
- new CustomEvent('clipboard:copied', {
83
- bubbles: true,
84
- detail: { text: copyText },
85
- })
86
- );
87
- } catch (error) {
88
- element.dispatchEvent(
89
- new CustomEvent('clipboard:error', {
90
- bubbles: true,
91
- detail: { error },
92
- })
93
- );
94
- }
95
- });
96
- }
97
-
98
- /**
99
- * Register the Clipboard behavior with LokaScript.
21
+ * Register the Clipboard behavior with LokaScript by compiling its
22
+ * hyperscript source and executing the resulting behavior definition.
100
23
  */
101
24
  export async function registerClipboard(hyperfixi?: LokaScriptInstance): Promise<void> {
102
25
  const hf = hyperfixi || resolveRuntime();
@@ -107,15 +30,14 @@ export async function registerClipboard(hyperfixi?: LokaScriptInstance): Promise
107
30
  );
108
31
  }
109
32
 
110
- const syntheticNode = {
111
- type: 'behavior',
112
- name: 'Clipboard',
113
- parameters: ['text', 'source', 'feedback', 'feedbackDuration'],
114
- eventHandlers: [],
115
- imperativeInstaller: installClipboard,
116
- };
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
+
117
39
  const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
118
- await hf.execute(syntheticNode, ctx);
40
+ await hf.execute(result.ast, ctx);
119
41
  }
120
42
 
121
43
  // Auto-register when loaded as a script tag