@zeix/le-truc 0.15.0 → 0.16.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 (343) hide show
  1. package/.ai-context.md +16 -15
  2. package/.github/copilot-instructions.md +3 -2
  3. package/.github/workflows/ci-cd.yml +101 -0
  4. package/.github/workflows/publish.yml +59 -0
  5. package/.github/workflows/release.yml +98 -0
  6. package/.zed/settings.json +3 -0
  7. package/ARCHITECTURE.md +272 -0
  8. package/CHANGELOG.md +44 -0
  9. package/CLAUDE.md +15 -11
  10. package/LICENSE +1 -1
  11. package/README.md +63 -19
  12. package/TASKS.md +41 -0
  13. package/docs/about.html +12 -9
  14. package/docs/assets/main.css +1 -1
  15. package/docs/assets/main.js +9 -9
  16. package/docs/assets/main.js.map +47 -43
  17. package/docs/components.html +77 -33
  18. package/docs/data-flow.html +17 -14
  19. package/docs/examples/basic-number.html +4 -4
  20. package/docs/examples/basic-pluralize.html +6 -2
  21. package/docs/examples/card-mediaqueries.html +1 -1
  22. package/docs/examples/form-checkbox.html +2 -2
  23. package/docs/examples/form-combobox.html +9 -6
  24. package/docs/examples/form-listbox.html +19 -12
  25. package/docs/examples/form-radiogroup.html +12 -9
  26. package/docs/examples/form-spinbutton.html +7 -7
  27. package/docs/examples/form-textbox.html +2 -2
  28. package/docs/examples/module-carousel.html +145 -198
  29. package/docs/examples/module-catalog.html +6 -6
  30. package/docs/examples/module-dialog.html +3 -1
  31. package/docs/examples/module-lazyload.html +11 -11
  32. package/docs/examples/module-list.html +12 -4
  33. package/docs/examples/module-tabgroup.html +11 -9
  34. package/docs/examples/module-todo.html +12 -11
  35. package/docs/favicon.ico +0 -0
  36. package/docs/getting-started.html +12 -9
  37. package/docs/index.html +15 -12
  38. package/docs/sitemap.xml +6 -6
  39. package/docs/styling.html +13 -9
  40. package/docs/sw.js +4 -4
  41. package/docs-src/api/README.md +369 -325
  42. package/docs-src/api/_media/LICENSE +1 -1
  43. package/docs-src/api/classes/CircularDependencyError.md +11 -17
  44. package/docs-src/api/classes/ContextRequestEvent.md +15 -15
  45. package/docs-src/api/classes/DependencyTimeoutError.md +6 -6
  46. package/docs-src/api/classes/InvalidCallbackError.md +14 -18
  47. package/docs-src/api/classes/InvalidComponentNameError.md +6 -6
  48. package/docs-src/api/classes/InvalidCustomElementError.md +6 -6
  49. package/docs-src/api/classes/InvalidEffectsError.md +6 -6
  50. package/docs-src/api/classes/InvalidPropertyNameError.md +6 -6
  51. package/docs-src/api/classes/InvalidReactivesError.md +6 -6
  52. package/docs-src/api/classes/InvalidSignalValueError.md +14 -18
  53. package/docs-src/api/classes/MissingElementError.md +6 -6
  54. package/docs-src/api/classes/NullishSignalValueError.md +11 -17
  55. package/docs-src/api/functions/asBoolean.md +3 -3
  56. package/docs-src/api/functions/asEnum.md +3 -3
  57. package/docs-src/api/functions/asInteger.md +3 -3
  58. package/docs-src/api/functions/asJSON.md +3 -3
  59. package/docs-src/api/functions/asNumber.md +3 -3
  60. package/docs-src/api/functions/asString.md +3 -3
  61. package/docs-src/api/functions/batch.md +41 -0
  62. package/docs-src/api/functions/createCollection.md +22 -60
  63. package/docs-src/api/functions/createComputed.md +87 -0
  64. package/docs-src/api/functions/createEffect.md +53 -0
  65. package/docs-src/api/functions/createElementsMemo.md +87 -0
  66. package/docs-src/api/functions/createEventsSensor.md +71 -0
  67. package/docs-src/api/functions/createList.md +43 -0
  68. package/docs-src/api/functions/createMemo.md +129 -0
  69. package/docs-src/api/functions/createMutableSignal.md +127 -0
  70. package/docs-src/api/functions/createScope.md +45 -0
  71. package/docs-src/api/functions/createSensor.md +41 -41
  72. package/docs-src/api/functions/createSlot.md +50 -0
  73. package/docs-src/api/functions/createState.md +60 -0
  74. package/docs-src/api/functions/createStore.md +53 -0
  75. package/docs-src/api/functions/createTask.md +139 -0
  76. package/docs-src/api/functions/dangerouslySetInnerHTML.md +5 -5
  77. package/docs-src/api/functions/defineComponent.md +7 -7
  78. package/docs-src/api/{variables → functions}/isAsyncFunction.md +9 -13
  79. package/docs-src/api/functions/isCollection.md +11 -11
  80. package/docs-src/api/functions/isComputed.md +37 -0
  81. package/docs-src/api/{variables → functions}/isEqual.md +5 -5
  82. package/docs-src/api/{variables → functions}/isFunction.md +9 -13
  83. package/docs-src/api/functions/isList.md +37 -0
  84. package/docs-src/api/functions/isMemo.md +37 -0
  85. package/docs-src/api/functions/isMutableSignal.md +31 -0
  86. package/docs-src/api/functions/isParser.md +3 -3
  87. package/docs-src/api/{variables → functions}/isRecord.md +9 -13
  88. package/docs-src/api/functions/isSensor.md +37 -0
  89. package/docs-src/api/{variables → functions}/isSignal.md +7 -7
  90. package/docs-src/api/functions/isSlot.md +37 -0
  91. package/docs-src/api/functions/isState.md +46 -0
  92. package/docs-src/api/functions/isStore.md +37 -0
  93. package/docs-src/api/functions/isTask.md +37 -0
  94. package/docs-src/api/functions/match.md +16 -21
  95. package/docs-src/api/functions/on.md +5 -5
  96. package/docs-src/api/functions/pass.md +8 -8
  97. package/docs-src/api/functions/provideContexts.md +3 -3
  98. package/docs-src/api/functions/read.md +3 -3
  99. package/docs-src/api/functions/requestContext.md +5 -5
  100. package/docs-src/api/functions/runEffects.md +3 -3
  101. package/docs-src/api/functions/runElementEffects.md +3 -3
  102. package/docs-src/api/functions/schedule.md +3 -3
  103. package/docs-src/api/functions/setAttribute.md +5 -5
  104. package/docs-src/api/functions/setProperty.md +5 -5
  105. package/docs-src/api/functions/setStyle.md +5 -5
  106. package/docs-src/api/functions/setText.md +3 -3
  107. package/docs-src/api/functions/show.md +3 -3
  108. package/docs-src/api/functions/toggleAttribute.md +5 -5
  109. package/docs-src/api/functions/toggleClass.md +5 -5
  110. package/docs-src/api/functions/updateElement.md +3 -3
  111. package/docs-src/api/functions/valueString.md +29 -0
  112. package/docs-src/api/globals.md +60 -37
  113. package/docs-src/api/type-aliases/AllElements.md +59 -0
  114. package/docs-src/api/type-aliases/Cleanup.md +5 -15
  115. package/docs-src/api/type-aliases/Collection.md +141 -27
  116. package/docs-src/api/type-aliases/CollectionChanges.md +49 -0
  117. package/docs-src/api/type-aliases/CollectionOptions.md +59 -0
  118. package/docs-src/api/type-aliases/Component.md +3 -3
  119. package/docs-src/api/type-aliases/ComponentProp.md +3 -3
  120. package/docs-src/api/type-aliases/ComponentProps.md +3 -3
  121. package/docs-src/api/type-aliases/ComponentSetup.md +3 -3
  122. package/docs-src/api/type-aliases/ComponentUI.md +3 -3
  123. package/docs-src/api/type-aliases/ComputedOptions.md +56 -0
  124. package/docs-src/api/type-aliases/Context.md +3 -3
  125. package/docs-src/api/type-aliases/ContextCallback.md +34 -0
  126. package/docs-src/api/type-aliases/ContextType.md +3 -3
  127. package/docs-src/api/type-aliases/DangerouslySetInnerHTMLOptions.md +5 -5
  128. package/docs-src/api/type-aliases/Effect.md +3 -3
  129. package/docs-src/api/type-aliases/EffectCallback.md +9 -13
  130. package/docs-src/api/type-aliases/Effects.md +3 -3
  131. package/docs-src/api/type-aliases/ElementEffects.md +3 -3
  132. package/docs-src/api/type-aliases/ElementFromKey.md +4 -4
  133. package/docs-src/api/type-aliases/ElementFromSelector.md +17 -0
  134. package/docs-src/api/type-aliases/ElementFromSingleSelector.md +17 -0
  135. package/docs-src/api/type-aliases/ElementQueries.md +7 -7
  136. package/docs-src/api/type-aliases/ElementUpdater.md +11 -11
  137. package/docs-src/api/type-aliases/ElementsFromSelectorArray.md +17 -0
  138. package/docs-src/api/type-aliases/EventHandler.md +3 -3
  139. package/docs-src/api/type-aliases/EventHandlers.md +25 -0
  140. package/docs-src/api/type-aliases/EventType.md +3 -3
  141. package/docs-src/api/type-aliases/ExtractRightmostSelector.md +17 -0
  142. package/docs-src/api/type-aliases/ExtractTag.md +17 -0
  143. package/docs-src/api/type-aliases/ExtractTagFromSimpleSelector.md +17 -0
  144. package/docs-src/api/type-aliases/Fallback.md +3 -3
  145. package/docs-src/api/type-aliases/FirstElement.md +99 -0
  146. package/docs-src/api/type-aliases/Initializers.md +3 -3
  147. package/docs-src/api/type-aliases/KnownTag.md +17 -0
  148. package/docs-src/api/type-aliases/List.md +321 -0
  149. package/docs-src/api/type-aliases/ListOptions.md +45 -0
  150. package/docs-src/api/type-aliases/LooseReader.md +3 -3
  151. package/docs-src/api/type-aliases/MatchHandlers.md +18 -22
  152. package/docs-src/api/type-aliases/MaybeCleanup.md +4 -8
  153. package/docs-src/api/type-aliases/MaybeSignal.md +4 -4
  154. package/docs-src/api/type-aliases/Memo.md +52 -0
  155. package/docs-src/api/type-aliases/MemoCallback.md +35 -0
  156. package/docs-src/api/type-aliases/MethodProducer.md +31 -0
  157. package/docs-src/api/type-aliases/Parser.md +3 -3
  158. package/docs-src/api/type-aliases/ParserOrFallback.md +3 -3
  159. package/docs-src/api/type-aliases/PassedProp.md +3 -3
  160. package/docs-src/api/type-aliases/PassedProps.md +3 -3
  161. package/docs-src/api/type-aliases/Reactive.md +3 -3
  162. package/docs-src/api/type-aliases/Reader.md +3 -3
  163. package/docs-src/api/type-aliases/ReservedWords.md +3 -3
  164. package/docs-src/api/type-aliases/Sensor.md +50 -0
  165. package/docs-src/api/type-aliases/SensorEventHandler.md +53 -0
  166. package/docs-src/api/type-aliases/SensorOptions.md +38 -0
  167. package/docs-src/api/type-aliases/Signal.md +5 -9
  168. package/docs-src/api/type-aliases/SignalOptions.md +58 -0
  169. package/docs-src/api/type-aliases/Slot.md +129 -0
  170. package/docs-src/api/type-aliases/SplitByComma.md +17 -0
  171. package/docs-src/api/type-aliases/State.md +31 -23
  172. package/docs-src/api/type-aliases/Store.md +9 -13
  173. package/docs-src/api/type-aliases/StoreOptions.md +31 -0
  174. package/docs-src/api/type-aliases/Task.md +84 -0
  175. package/docs-src/api/type-aliases/TaskCallback.md +41 -0
  176. package/docs-src/api/type-aliases/TrimWhitespace.md +17 -0
  177. package/docs-src/api/type-aliases/UI.md +4 -4
  178. package/docs-src/api/type-aliases/UnknownContext.md +3 -3
  179. package/docs-src/api/type-aliases/UpdateOperation.md +11 -0
  180. package/docs-src/api/variables/CONTEXT_REQUEST.md +3 -3
  181. package/{server → docs-src}/layouts/api.html +1 -1
  182. package/{server → docs-src}/layouts/blog.html +1 -1
  183. package/{server → docs-src}/layouts/example.html +1 -1
  184. package/{server → docs-src}/layouts/overview.html +1 -1
  185. package/docs-src/layouts/page.html +50 -0
  186. package/docs-src/layouts/test.html +29 -0
  187. package/docs-src/pages/about.md +1 -1
  188. package/docs-src/pages/components.md +123 -27
  189. package/docs-src/pages/data-flow.md +6 -6
  190. package/docs-src/pages/getting-started.md +1 -1
  191. package/docs-src/pages/index.md +9 -9
  192. package/docs-src/pages/styling.md +16 -0
  193. package/eslint.config.js +0 -3
  194. package/examples/_common/clear.ts +8 -1
  195. package/examples/_common/focus.ts +15 -12
  196. package/examples/_global.css +6 -6
  197. package/examples/basic-button/basic-button.spec.ts +1 -1
  198. package/examples/basic-counter/basic-counter.spec.ts +1 -1
  199. package/examples/basic-hello/basic-hello.spec.ts +1 -1
  200. package/examples/basic-number/basic-number.spec.ts +3 -3
  201. package/examples/basic-number/basic-number.ts +4 -4
  202. package/examples/basic-pluralize/basic-pluralize.spec.ts +1 -1
  203. package/examples/basic-pluralize/basic-pluralize.ts +6 -2
  204. package/examples/card-mediaqueries/card-mediaqueries.spec.ts +1 -1
  205. package/examples/card-mediaqueries/card-mediaqueries.ts +1 -1
  206. package/examples/form-checkbox/form-checkbox.spec.ts +1 -1
  207. package/examples/form-checkbox/form-checkbox.ts +2 -2
  208. package/examples/form-combobox/form-combobox.html +4 -1
  209. package/examples/form-combobox/form-combobox.spec.ts +21 -18
  210. package/examples/form-combobox/form-combobox.ts +5 -5
  211. package/examples/form-listbox/form-listbox.html +1 -1
  212. package/examples/form-listbox/form-listbox.spec.ts +21 -23
  213. package/examples/form-listbox/form-listbox.ts +18 -11
  214. package/examples/form-listbox/mocks/simple-options.json +5 -0
  215. package/examples/form-radiogroup/form-radiogroup.spec.ts +1 -1
  216. package/examples/form-radiogroup/form-radiogroup.ts +12 -9
  217. package/examples/form-spinbutton/form-spinbutton.spec.ts +1 -1
  218. package/examples/form-spinbutton/form-spinbutton.ts +7 -7
  219. package/examples/form-textbox/form-textbox.spec.ts +1 -1
  220. package/examples/form-textbox/form-textbox.ts +2 -2
  221. package/examples/main.ts +3 -0
  222. package/examples/module-carousel/module-carousel.css +68 -4
  223. package/examples/module-carousel/module-carousel.html +3 -154
  224. package/examples/module-carousel/module-carousel.spec.ts +107 -83
  225. package/examples/module-carousel/module-carousel.ts +73 -39
  226. package/examples/module-catalog/module-catalog.spec.ts +1 -1
  227. package/examples/module-catalog/module-catalog.ts +6 -6
  228. package/examples/module-dialog/module-dialog.spec.ts +1 -1
  229. package/examples/module-dialog/module-dialog.ts +3 -1
  230. package/examples/module-lazyload/mocks/nested-components.html +1 -3
  231. package/examples/module-lazyload/mocks/recursive.html +1 -1
  232. package/examples/module-lazyload/module-lazyload.html +8 -8
  233. package/examples/module-lazyload/module-lazyload.spec.ts +35 -17
  234. package/examples/module-lazyload/module-lazyload.ts +3 -3
  235. package/examples/module-list/module-list.spec.ts +1 -1
  236. package/examples/module-list/module-list.ts +12 -4
  237. package/examples/module-pagination/module-pagination.spec.ts +1 -1
  238. package/examples/module-scrollarea/module-scrollarea.spec.ts +105 -24
  239. package/examples/module-tabgroup/module-tabgroup.spec.ts +1 -1
  240. package/examples/module-tabgroup/module-tabgroup.ts +11 -9
  241. package/examples/module-todo/module-todo.spec.ts +1 -1
  242. package/examples/module-todo/module-todo.ts +12 -11
  243. package/examples/test-setup.md +1 -1
  244. package/index.dev.js +1662 -895
  245. package/index.js +2 -2
  246. package/index.js.map +30 -28
  247. package/index.ts +57 -27
  248. package/package.json +18 -16
  249. package/scripts/test-component.ts +176 -0
  250. package/server/BUILD_SYSTEM.md +244 -90
  251. package/server/SERVER.md +409 -242
  252. package/server/build.ts +74 -7
  253. package/server/config.ts +41 -48
  254. package/server/dev.ts +116 -0
  255. package/server/effects/api.ts +2 -2
  256. package/server/effects/css.ts +17 -23
  257. package/server/effects/examples.ts +19 -11
  258. package/server/effects/js.ts +2 -6
  259. package/server/effects/menu.ts +3 -3
  260. package/server/effects/pages.ts +50 -55
  261. package/server/effects/service-worker.ts +8 -9
  262. package/server/effects/sitemap.ts +13 -18
  263. package/server/file-signals.ts +42 -28
  264. package/server/file-watcher.ts +62 -46
  265. package/server/schema/carousel.markdoc.ts +4 -5
  266. package/server/serve.ts +321 -554
  267. package/server/templates/sitemap.ts +1 -1
  268. package/server/templates/utils.ts +4 -1
  269. package/skills/changelog-keeper/SKILL.md +59 -0
  270. package/src/component.ts +38 -23
  271. package/src/context.ts +7 -7
  272. package/src/effects/event.ts +2 -5
  273. package/src/effects/html.ts +23 -6
  274. package/src/effects/pass.ts +38 -40
  275. package/src/effects/property.ts +2 -3
  276. package/src/effects.ts +25 -110
  277. package/src/errors.ts +0 -19
  278. package/src/events.ts +117 -0
  279. package/src/internal.ts +18 -0
  280. package/src/parsers.ts +2 -2
  281. package/src/ui.ts +135 -41
  282. package/src/util.ts +2 -2
  283. package/tsconfig.build.json +11 -0
  284. package/tsconfig.json +9 -6
  285. package/types/index.d.ts +11 -10
  286. package/types/index.dev.d.ts +5 -6
  287. package/types/src/component.d.ts +3 -3
  288. package/types/src/context.d.ts +4 -4
  289. package/types/src/effects/event.d.ts +2 -2
  290. package/types/src/effects/pass.d.ts +6 -3
  291. package/types/src/effects.d.ts +1 -87
  292. package/types/src/errors.d.ts +1 -13
  293. package/types/src/events.d.ts +12 -12
  294. package/types/src/internal.d.ts +4 -0
  295. package/types/src/ui.d.ts +18 -6
  296. package/bun.lock +0 -239
  297. package/docs-src/api/classes/CircularMutationError.md +0 -301
  298. package/docs-src/api/classes/StoreKeyExistsError.md +0 -303
  299. package/docs-src/api/classes/StoreKeyRangeError.md +0 -299
  300. package/docs-src/api/classes/StoreKeyReadonlyError.md +0 -303
  301. package/docs-src/api/functions/resolve.md +0 -40
  302. package/docs-src/api/functions/toSignal.md +0 -37
  303. package/docs-src/api/type-aliases/CollectionListener.md +0 -27
  304. package/docs-src/api/type-aliases/Computed.md +0 -49
  305. package/docs-src/api/type-aliases/ComputedCallback.md +0 -29
  306. package/docs-src/api/type-aliases/DiffResult.md +0 -61
  307. package/docs-src/api/type-aliases/ResolveResult.md +0 -29
  308. package/docs-src/api/type-aliases/SensorEvents.md +0 -25
  309. package/docs-src/api/variables/UNSET.md +0 -23
  310. package/docs-src/api/variables/batch.md +0 -25
  311. package/docs-src/api/variables/createComputed.md +0 -41
  312. package/docs-src/api/variables/createEffect.md +0 -35
  313. package/docs-src/api/variables/createState.md +0 -37
  314. package/docs-src/api/variables/createStore.md +0 -42
  315. package/docs-src/api/variables/diff.md +0 -43
  316. package/docs-src/api/variables/isAbortError.md +0 -33
  317. package/docs-src/api/variables/isComputed.md +0 -37
  318. package/docs-src/api/variables/isMutableSignal.md +0 -37
  319. package/docs-src/api/variables/isNumber.md +0 -33
  320. package/docs-src/api/variables/isRecordOrArray.md +0 -39
  321. package/docs-src/api/variables/isState.md +0 -37
  322. package/docs-src/api/variables/isStore.md +0 -37
  323. package/docs-src/api/variables/isString.md +0 -33
  324. package/docs-src/api/variables/isSymbol.md +0 -33
  325. package/docs-src/api/variables/toError.md +0 -33
  326. package/docs-src/api/variables/valueString.md +0 -33
  327. package/examples/form-checkbox/vanilla-checkbox.ts +0 -101
  328. package/examples/server.ts +0 -95
  329. package/index.dev.ts +0 -127
  330. package/server/layout-engine.ts +0 -470
  331. package/server/layout-utils.ts +0 -615
  332. package/server/layouts/base.html +0 -37
  333. package/server/layouts/page.html +0 -36
  334. package/server/layouts/test.html +0 -24
  335. package/src/effects/method.ts +0 -57
  336. package/src/signals/collection.ts +0 -253
  337. package/src/signals/sensor.ts +0 -131
  338. package/types/examples/basic-button/basic-button.d.ts +0 -16
  339. package/types/examples/basic-hello/basic-hello.d.ts +0 -18
  340. package/types/src/collection.d.ts +0 -27
  341. package/types/src/sensor.d.ts +0 -27
  342. package/types/src/signals/collection.d.ts +0 -32
  343. package/types/src/signals/sensor.d.ts +0 -27
package/.ai-context.md CHANGED
@@ -36,12 +36,12 @@ The selector function provides type-safe DOM queries:
36
36
  ({ first, all }) => ({
37
37
  button?: first('button'), // HTMLButtonElement | undefined
38
38
  input: first('input', 'required'), // HTMLInputElement (throws if missing)
39
- items: all('.item'), // Collection<HTMLElement>
39
+ items: all('.item'), // Memo<HTMLElement[]>
40
40
  custom?: first<MyElement>('my-el') // Custom typing
41
41
  })
42
42
  ```
43
43
 
44
- Collections are reactive arrays that emit `add`/`remove` events and track DOM mutations automatically.
44
+ `all()` returns `Memo<E[]>` backed by a lazy `MutationObserver` that tracks DOM mutations automatically.
45
45
 
46
46
  ### Effect System
47
47
  Effects run reactively when dependencies change:
@@ -77,9 +77,10 @@ Effects run reactively when dependencies change:
77
77
  - `setStyle(property, reactive)` - Update inline styles
78
78
 
79
79
  ### Advanced
80
- - `pass(props)` - Pass properties to child components
81
- - `dangerouslySetInnerHTML(reactive)` - Set innerHTML (use carefully)
82
- - `insertOrRemoveElement(reactive, inserter)` - Dynamic element creation/removal
80
+ - `pass(props)` - Pass reactive values to child Le Truc components via Slot signal replacement
81
+ - `dangerouslySetInnerHTML(reactive, opts?)` - Set innerHTML (use only for trusted sources)
82
+ - `callMethod(name, reactive, args?)` - Call a method when reactive is truthy
83
+ - `focus(reactive)` - Focus element when truthy
83
84
 
84
85
  ## Parsers
85
86
 
@@ -114,23 +115,23 @@ const countSignal = createState(0)
114
115
  setText(() => countSignal.get())
115
116
  ```
116
117
 
117
- ## Collections
118
+ ## Element Memos
118
119
 
119
- Read-only reactive element arrays with array-like interface:
120
+ `all()` returns `Memo<E[]>` a lazily observed collection of elements:
120
121
 
121
122
  ```typescript
122
123
  // Creation
123
- ({ all }) => ({ items: all('.item') }) // In UI query, will be ui.items
124
- const items = createCollection(parent, '.item') // Elsewhere with arbitrary parent
124
+ ({ all }) => ({ items: all('.item') }) // In UI query, will be ui.items
125
+ const items = createElementsMemo(parent, '.item') // Elsewhere with arbitrary parent
125
126
 
126
127
  // Access
127
- items.get() // Get current elements
128
- items.length // Reactive length
129
- items[0] // Index access
130
- items.on('add', fn) // Listen for additions
131
- items.on('remove', fn) // Listen for removals
128
+ items.get() // Get current elements array
129
+ items.get().length // Current count
130
+ items.get()[0] // Index access
132
131
  ```
133
132
 
133
+ The `MutationObserver` backing the memo activates lazily — only when the memo is read from within a reactive effect. This avoids unnecessary observation overhead for memos that aren't actively consumed.
134
+
134
135
  ## Common Patterns
135
136
 
136
137
  ### Form Components
@@ -222,7 +223,7 @@ export default defineComponent<MyComponentProps, MyComponentUI>(...)
222
223
 
223
224
  ### Performance
224
225
  - Effects automatically optimize re-runs when dependencies don't change
225
- - Collections efficiently track only actual DOM changes
226
+ - Element memos efficiently track only actual DOM changes via lazy MutationObserver
226
227
  - Use `schedule()` for non-critical updates in passive event handlers
227
228
  - Proper cleanup prevents memory leaks
228
229
 
@@ -32,7 +32,8 @@ Files to consult for examples and authoritative patterns
32
32
  - Selector helpers & mutation-observer logic: `src/ui.ts`
33
33
  - Parser implementations: `src/parsers/*.ts` (e.g. `json.ts`, `number.ts`, `string.ts`)
34
34
  - Effect implementations: `src/effects/*.ts` (exported from root `index.ts`)
35
- - Signal helpers: `src/signals/*.ts` (collection, sensor)
35
+ - Event-driven sensors: `src/events.ts` (createEventsSensor)
36
+ - Element memos: `createElementsMemo` in `src/ui.ts`
36
37
  - Examples demonstrating usage: `examples/*` (start from `basic-hello` and `basic-counter`)
37
38
 
38
39
  Developer workflows (essential commands)
@@ -45,7 +46,7 @@ Developer workflows (essential commands)
45
46
  What to change (and what to avoid)
46
47
  - Change: small refactors that preserve exported API in `index.ts` and `.d.ts` files.
47
48
  - Change: add or update examples in `examples/` to demonstrate new or changed behavior.
48
- - Avoid: breaking changes to public exports without updating `index.ts` / `index.dev.ts` and re-building (`bun run build:dev`).
49
+ - Avoid: breaking changes to public exports without updating `index.ts` and re-building (`bun run build`).
49
50
  - Avoid: changing the custom element registration pattern (i.e., calling `customElements.define`) in a way that prevents `getHelpers` dependency detection.
50
51
 
51
52
  PR guidance / descriptions
@@ -0,0 +1,101 @@
1
+ name: CI/CD Pipeline
2
+
3
+ on:
4
+ push:
5
+ branches: [main, develop]
6
+ tags: ['v*']
7
+ pull_request:
8
+ branches: [main, develop]
9
+ release:
10
+ types: [published]
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - name: Checkout code
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Setup Bun
20
+ uses: oven-sh/setup-bun@v2
21
+ with:
22
+ bun-version: latest
23
+
24
+ - name: Install dependencies
25
+ run: bun install --frozen-lockfile
26
+
27
+ - name: Lint
28
+ run: bun run lint
29
+
30
+ - name: Build
31
+ run: bun run build
32
+
33
+ - name: Install Playwright browsers
34
+ run: bunx playwright install --with-deps
35
+
36
+ - name: Run tests
37
+ run: bun run test
38
+
39
+ - name: Upload test results
40
+ uses: actions/upload-artifact@v4
41
+ if: always()
42
+ with:
43
+ name: playwright-report
44
+ path: playwright-report/
45
+ retention-days: 30
46
+
47
+ publish:
48
+ needs: test
49
+ runs-on: ubuntu-latest
50
+ if: github.event_name == 'release' && github.event.action == 'published'
51
+ permissions:
52
+ contents: read
53
+ id-token: write # Required for npm provenance
54
+
55
+ steps:
56
+ - name: Checkout code
57
+ uses: actions/checkout@v4
58
+
59
+ - name: Setup Bun
60
+ uses: oven-sh/setup-bun@v2
61
+ with:
62
+ bun-version: latest
63
+
64
+ - name: Install dependencies
65
+ run: bun install --frozen-lockfile
66
+
67
+ - name: Build package
68
+ run: bun run build
69
+
70
+ - name: Setup Node.js for npm
71
+ uses: actions/setup-node@v4
72
+ with:
73
+ node-version: '20'
74
+ registry-url: 'https://registry.npmjs.org'
75
+
76
+ - name: Verify package version matches tag
77
+ run: |
78
+ PACKAGE_VERSION=$(node -p "require('./package.json').version")
79
+ TAG_VERSION=${GITHUB_REF#refs/tags/v}
80
+ if [ "$PACKAGE_VERSION" != "$TAG_VERSION" ]; then
81
+ echo "Package version ($PACKAGE_VERSION) doesn't match tag version ($TAG_VERSION)"
82
+ exit 1
83
+ fi
84
+
85
+ - name: Publish to npm
86
+ run: npm publish --access public --provenance
87
+ env:
88
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
89
+
90
+ - name: Create deployment status
91
+ uses: actions/github-script@v7
92
+ with:
93
+ script: |
94
+ github.rest.repos.createDeploymentStatus({
95
+ owner: context.repo.owner,
96
+ repo: context.repo.repo,
97
+ deployment_id: context.payload.deployment?.id || 0,
98
+ state: 'success',
99
+ environment: 'npm',
100
+ target_url: 'https://www.npmjs.com/package/@zeix/le-truc'
101
+ });
@@ -0,0 +1,59 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+ inputs:
8
+ version:
9
+ description: 'Version to publish (leave empty to use package.json version)'
10
+ required: false
11
+ type: string
12
+
13
+ jobs:
14
+ publish:
15
+ runs-on: ubuntu-latest
16
+ permissions:
17
+ contents: read
18
+ id-token: write # Required for npm provenance
19
+
20
+ steps:
21
+ - name: Checkout code
22
+ uses: actions/checkout@v4
23
+
24
+ - name: Setup Bun
25
+ uses: oven-sh/setup-bun@v2
26
+ with:
27
+ bun-version: latest
28
+
29
+ - name: Install dependencies
30
+ run: bun install --frozen-lockfile
31
+
32
+ - name: Build package
33
+ run: bun run build
34
+
35
+ - name: Setup Node.js for npm
36
+ uses: actions/setup-node@v4
37
+ with:
38
+ node-version: '20'
39
+ registry-url: 'https://registry.npmjs.org'
40
+
41
+ - name: Update version (if specified)
42
+ if: github.event.inputs.version != ''
43
+ run: npm version ${{ github.event.inputs.version }} --no-git-tag-version
44
+
45
+ - name: Publish to npm
46
+ run: npm publish --access public --provenance
47
+ env:
48
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
49
+
50
+ - name: Create GitHub release (if manual dispatch)
51
+ if: github.event_name == 'workflow_dispatch'
52
+ uses: actions/create-release@v1
53
+ env:
54
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55
+ with:
56
+ tag_name: v${{ steps.version.outputs.version || fromJson(steps.package.outputs.json).version }}
57
+ release_name: Release v${{ steps.version.outputs.version || fromJson(steps.package.outputs.json).version }}
58
+ draft: false
59
+ prerelease: false
@@ -0,0 +1,98 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ version_type:
7
+ description: 'Version bump type'
8
+ required: true
9
+ type: choice
10
+ options:
11
+ - patch
12
+ - minor
13
+ - major
14
+ default: 'patch'
15
+ prerelease:
16
+ description: 'Create prerelease'
17
+ required: false
18
+ type: boolean
19
+ default: false
20
+
21
+ jobs:
22
+ release:
23
+ runs-on: ubuntu-latest
24
+ permissions:
25
+ contents: write
26
+ id-token: write
27
+
28
+ steps:
29
+ - name: Checkout code
30
+ uses: actions/checkout@v4
31
+ with:
32
+ token: ${{ secrets.GITHUB_TOKEN }}
33
+ fetch-depth: 0
34
+
35
+ - name: Setup Bun
36
+ uses: oven-sh/setup-bun@v2
37
+ with:
38
+ bun-version: latest
39
+
40
+ - name: Install dependencies
41
+ run: bun install --frozen-lockfile
42
+
43
+ - name: Run tests and build
44
+ run: bun run build
45
+
46
+ - name: Configure Git
47
+ run: |
48
+ git config user.name "github-actions[bot]"
49
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
50
+
51
+ - name: Bump version
52
+ id: version
53
+ run: |
54
+ if [ "${{ github.event.inputs.prerelease }}" = "true" ]; then
55
+ NEW_VERSION=$(npm version pre${{ github.event.inputs.version_type }} --preid=beta --no-git-tag-version)
56
+ else
57
+ NEW_VERSION=$(npm version ${{ github.event.inputs.version_type }} --no-git-tag-version)
58
+ fi
59
+ echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
60
+ echo "version_number=${NEW_VERSION#v}" >> $GITHUB_OUTPUT
61
+
62
+ - name: Update changelog
63
+ run: |
64
+ echo "## ${{ steps.version.outputs.new_version }} - $(date +%Y-%m-%d)" >> CHANGELOG_NEW.md
65
+ echo "" >> CHANGELOG_NEW.md
66
+ echo "- Version bump: ${{ github.event.inputs.version_type }}" >> CHANGELOG_NEW.md
67
+ echo "" >> CHANGELOG_NEW.md
68
+ if [ -f CHANGELOG.md ]; then
69
+ cat CHANGELOG.md >> CHANGELOG_NEW.md
70
+ fi
71
+ mv CHANGELOG_NEW.md CHANGELOG.md
72
+
73
+ - name: Commit changes
74
+ run: |
75
+ git add package.json CHANGELOG.md
76
+ git commit -m "chore: bump version to ${{ steps.version.outputs.new_version }}"
77
+ git tag ${{ steps.version.outputs.new_version }}
78
+ git push origin main
79
+ git push origin ${{ steps.version.outputs.new_version }}
80
+
81
+ - name: Create GitHub Release
82
+ uses: actions/create-release@v1
83
+ env:
84
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
85
+ with:
86
+ tag_name: ${{ steps.version.outputs.new_version }}
87
+ release_name: Release ${{ steps.version.outputs.new_version }}
88
+ draft: false
89
+ prerelease: ${{ github.event.inputs.prerelease }}
90
+ body: |
91
+ ## Changes in ${{ steps.version.outputs.new_version }}
92
+
93
+ This release was created automatically via GitHub Actions.
94
+
95
+ **Version bump:** ${{ github.event.inputs.version_type }}
96
+ **Prerelease:** ${{ github.event.inputs.prerelease }}
97
+
98
+ View the full changelog at [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md)
@@ -0,0 +1,3 @@
1
+ {
2
+ "project_name": "Le Truc",
3
+ }
@@ -0,0 +1,272 @@
1
+ # Architecture
2
+
3
+ Le Truc is a reactive custom elements library. This document describes how the pieces in `src/` fit together.
4
+
5
+ ## File Map
6
+
7
+ ```
8
+ src/
9
+ component.ts The heart: defineComponent() and the Truc class
10
+ effects.ts Effect orchestration: runEffects, updateElement
11
+ ui.ts DOM queries (first/all), dependency resolution, selector type inference
12
+ parsers.ts Parser/Reader type system and composition
13
+ events.ts Event-driven sensor factory (createEventsSensor)
14
+ context.ts Context protocol (provide/request) for dependency injection
15
+ scheduler.ts rAF-based task deduplication
16
+ errors.ts Domain-specific error classes
17
+ internal.ts Internal signal map (getSignals) — shared by component.ts and pass.ts
18
+ util.ts Logging, element introspection, property validation
19
+
20
+ effects/
21
+ attribute.ts setAttribute, toggleAttribute
22
+ class.ts toggleClass
23
+ event.ts on() — event listener effect
24
+ html.ts dangerouslySetInnerHTML
25
+ pass.ts pass() — inter-component reactive property binding
26
+ property.ts setProperty, show
27
+ style.ts setStyle
28
+ text.ts setText
29
+
30
+ parsers/
31
+ boolean.ts asBoolean
32
+ json.ts asJSON
33
+ number.ts asInteger, asNumber
34
+ string.ts asString, asEnum
35
+ ```
36
+
37
+ ## Dependency Graph
38
+
39
+ Arrows mean "imports from". The graph flows bottom-up from leaf utilities to `component.ts`.
40
+
41
+ ```
42
+ util.ts ─────────────────────────────────────────────┐
43
+ errors.ts ──────── util.ts │
44
+ scheduler.ts ──── (leaf, no internal imports) │
45
+ parsers.ts ─────── ui.ts (types only) │
46
+ parsers/* ──────── parsers.ts, ui.ts (types) │
47
+
48
+ internal.ts ────── (leaf, signal storage) │
49
+ ui.ts ──────────── errors.ts, util.ts │
50
+
51
+ effects.ts ─────── component.ts (types), errors.ts, │
52
+ ui.ts, util.ts │
53
+
54
+ effects/* ──────── component.ts (types), effects.ts │
55
+ + scheduler.ts, util.ts, etc. │
56
+
57
+ events.ts ──────── component.ts (types), parsers.ts, │
58
+ scheduler.ts, ui.ts │
59
+
60
+ context.ts ─────── component.ts (types), parsers.ts, │
61
+ ui.ts │
62
+
63
+ component.ts ───── effects.ts, errors.ts, parsers.ts,│
64
+ ui.ts, util.ts │
65
+ ```
66
+
67
+ The single external dependency is `@zeix/cause-effect`, which provides the reactive primitives: `createState`, `createComputed`, `createEffect`, `createMemo`, `createSensor`, `Signal`, `Memo`, `Sensor`, `batch`, and various type guards.
68
+
69
+ ## The Component Lifecycle
70
+
71
+ `defineComponent(name, props, select, setup)` is the main entry point. It creates a class `Truc extends HTMLElement`, registers it via `customElements.define()`, and returns the class.
72
+
73
+ ### connectedCallback — initialization
74
+
75
+ ```
76
+ connectedCallback()
77
+
78
+ ├─ 1. getHelpers(this) → [{ first, all }, resolveDependencies]
79
+ │ Determines query root (shadowRoot ?? this).
80
+ │ Tracks custom element dependencies found during queries.
81
+
82
+ ├─ 2. ui = { ...select({ first, all }), host: this }
83
+ │ User-provided select function queries DOM elements.
84
+ │ Object is frozen — immutable after creation.
85
+
86
+ ├─ 3. Initialize signals for each property:
87
+ │ ├─ Parser (≥2 args)? → parser(ui, this.getAttribute(key))
88
+ │ ├─ Function (1 arg)? → reader(ui) or methodProducer(ui)
89
+ │ └─ Otherwise → use value directly (static or Signal)
90
+ │ Each result is passed to #setAccessor(key, value).
91
+
92
+ └─ 4. resolveDependencies(() => {
93
+ this.#cleanup = runEffects(ui, setup(ui))
94
+ })
95
+ Waits for child custom elements to be defined (50ms timeout),
96
+ then runs the setup function and activates effects.
97
+ ```
98
+
99
+ ### #setAccessor — signal creation
100
+
101
+ Takes a key and a value and creates the appropriate signal:
102
+
103
+ - Already a `Signal` → use directly
104
+ - A function → `createComputed(fn)` (read-only)
105
+ - Anything else → `createState(value)` (read-write)
106
+
107
+ For mutable signals, the value is wrapped in a `createSlot(signal)` — a Slot from `@zeix/cause-effect` that acts as an indirection layer. The Slot's `get`/`set` are used as the property descriptor on the component instance, which is what makes `host.count` reactive. Reading calls `signal.get()` inside effects, registering the dependency automatically.
108
+
109
+ The Slot enables signal swapping: if `#setAccessor` is called again for an existing key (e.g., via `attributeChangedCallback`), it calls `slot.replace(newSignal)` instead of redefining the property. This is also the mechanism used by `pass()` to inject parent signals into a child component.
110
+
111
+ ### attributeChangedCallback — attribute sync
112
+
113
+ Only fires for properties whose initializer `isParser` (function with ≥2 parameters). These are collected into `static observedAttributes` at class creation time.
114
+
115
+ When an attribute changes: parse the new value through the parser, then assign it to the component property (which triggers `signal.set()`). Computed (read-only) signals are skipped.
116
+
117
+ ### disconnectedCallback — cleanup
118
+
119
+ Calls the cleanup function returned by `runEffects()`, which tears down all effects and event listeners.
120
+
121
+ ## The Effect System
122
+
123
+ ### Three layers
124
+
125
+ 1. **`runEffects(ui, effects)`** — top-level orchestrator. Iterates the keys of the effects record. For each key, checks whether `ui[key]` is a `Memo` (from `all()`) or a single `Element` (from `first()`), and delegates accordingly.
126
+
127
+ 2. **`runElementsEffects(host, elements, effects)`** — handles dynamic collections. Creates a `createEffect()` that watches `Memo<E[]>`, computes added/removed elements by diffing against currently attached cleanups, and attaches/detaches per-element effects.
128
+
129
+ 3. **`runElementEffects(host, target, effects)`** — runs one or many effect functions against a single target element, collecting their cleanup functions.
130
+
131
+ ### updateElement — the shared abstraction
132
+
133
+ Every built-in effect (`setAttribute`, `toggleClass`, `setText`, `setProperty`, `setStyle`, `toggleAttribute`, `dangerouslySetInnerHTML`, `callMethod`, `focus`, `show`) follows the same pattern via `updateElement(reactive, updater)`:
134
+
135
+ ```
136
+ updateElement(reactive, { op, name, read, update, delete? })
137
+
138
+ ├─ Captures fallback = read(target) ← current DOM value
139
+
140
+ └─ createEffect(() => {
141
+ value = resolveReactive(reactive) ← auto-tracks signal deps
142
+ if value === RESET → use fallback
143
+ if value === null → delete(target) if available, else use fallback
144
+ if value !== current → update(target, value)
145
+ })
146
+ ```
147
+
148
+ The `Reactive<T>` type is a union of three forms:
149
+ - `keyof P` — a string property name on the host (reads `host[name]`)
150
+ - `Signal<T>` — a signal (calls `.get()`)
151
+ - `(target: E) => T` — a reader function
152
+
153
+ `resolveReactive()` handles all three and returns the concrete value. Because it calls `.get()` inside a `createEffect`, signal dependencies are automatically tracked.
154
+
155
+ ### The RESET sentinel
156
+
157
+ `RESET` is a `Symbol('RESET')` typed as `any`. When a reactive resolves to `RESET` (e.g., the reader function threw an error), the effect restores the original DOM value captured at setup time.
158
+
159
+ ### Built-in effects at a glance
160
+
161
+ | Effect | Op | What it does |
162
+ |---|---|---|
163
+ | `setAttribute(name, reactive?)` | `a` | Sets an attribute with URL safety validation |
164
+ | `toggleAttribute(name, reactive?)` | `a` | Boolean attribute: present when truthy |
165
+ | `toggleClass(token, reactive?)` | `c` | Adds/removes a CSS class |
166
+ | `setText(reactive)` | `t` | Replaces non-comment child nodes with a text node |
167
+ | `setProperty(key, reactive?)` | `p` | Sets a DOM property directly |
168
+ | `show(reactive)` | `p` | Controls `el.hidden` |
169
+ | `setStyle(prop, reactive?)` | `s` | Sets/removes an inline style |
170
+ | `dangerouslySetInnerHTML(reactive, opts?)` | `h` | Sets innerHTML, optionally in a shadow root |
171
+
172
+ All default their `reactive` parameter to the effect name (e.g., `setAttribute('href')` reads `host.href`).
173
+
174
+ ### on() — event listener effect
175
+
176
+ `on(type, handler, options?)` is different from `updateElement`-based effects. It directly attaches an event listener to the target element. The handler receives the event and may return a partial property update object like `{ count: host.count + 1 }`. If it does, the updates are applied to the host in a `batch()`. For passive events (scroll, resize, touch, wheel), execution is deferred via `schedule()`.
177
+
178
+ ### pass() — inter-component binding
179
+
180
+ `pass(props)` replaces the backing signal of a child Le Truc component's Slot properties. It uses `getSignals(target)` to access the child's internal signal map, then for each passed prop calls `slot.replace(signal)` with a new signal derived from the parent's reactive value. This creates a live reactive binding between parent and child without the child needing to know about the parent. No cleanup/restore is needed: when the parent unmounts, the child is torn down as well.
181
+
182
+ ## The UI Query System
183
+
184
+ `getHelpers(host)` returns `[{ first, all }, resolveDependencies]`.
185
+
186
+ ### first(selector, required?)
187
+
188
+ Calls `root.querySelector()`. If the matched element is an undefined custom element, its tag name is added to the dependency set. Returns the element or `undefined` (throws `MissingElementError` if `required` is provided and element is missing).
189
+
190
+ ### all(selector, required?)
191
+
192
+ Returns a `Memo<E[]>` created by `createElementsMemo()`. This sets up a `MutationObserver` (lazily, via the `watched` option on `createMemo`) that watches for `childList`, `subtree`, and relevant attribute changes. The memo always contains the current matching elements; added/removed diffs are derived downstream where needed (for example in `runElementsEffects`).
193
+
194
+ The `MutationObserver` config is smart about which attributes to watch: `extractAttributes(selector)` parses the CSS selector to find attribute names implied by `.class`, `#id`, and `[attr]` patterns.
195
+
196
+ ### Dependency resolution
197
+
198
+ During `first()` and `all()` calls, any matched custom element that isn't yet defined (matches `:not(:defined)`) is collected. `resolveDependencies(callback)` then awaits `customElements.whenDefined()` for all of them with a 50ms timeout. On timeout, it logs a `DependencyTimeoutError` but still runs the callback — effects proceed even if dependencies aren't ready.
199
+
200
+ ### Compile-time selector type inference
201
+
202
+ The file contains a type-level CSS selector parser that infers the correct `HTMLElement` subtype from selector strings at compile time. `first('button')` returns `HTMLButtonElement`, `first('input[type="text"]')` returns `HTMLInputElement`, `first('.foo')` returns `HTMLElement`. This works through template literal types that split combinators, extract tag names, and look them up in `HTMLElementTagNameMap` / `SVGElementTagNameMap` / `MathMLElementTagNameMap`.
203
+
204
+ ## The Parser System
205
+
206
+ Parsers transform HTML attribute strings into typed JavaScript values. The key design choice: **a Parser is a function with ≥2 parameters** (`(ui, value, old?) => T`), while a **Reader is any function with 1 parameter** (`(ui) => T`). This distinction is checked at runtime via `value.length >= 2` in `isParser()`.
207
+
208
+ Parsers serve dual duty:
209
+ 1. As property initializers — `{ config: asJSON({ theme: 'light' }) }` — called during `connectedCallback` with the attribute's initial value
210
+ 2. As attribute watchers — automatically added to `observedAttributes` and called in `attributeChangedCallback`
211
+
212
+ The `read(reader, fallback)` function composes a `LooseReader` (which may return `string | null | undefined`) with a parser/fallback into a clean `Reader<T>`. This is useful for reading DOM state and parsing it: `read(ui => ui.input.value, asInteger())`.
213
+
214
+ ## Event-Driven Sensors
215
+
216
+ `createEventsSensor(init, key, events)` returns a Reader that creates a `Sensor<T>` — a signal driven by DOM events. It uses event delegation: all listeners are attached to the host, and when an event fires, the sensor finds the matching target element via `Node.contains()`.
217
+
218
+ This is more declarative than `on()`: instead of imperatively updating host properties, the sensor produces a single reactive value from multiple event types. Use case: combining `input`, `change`, `focus`, `blur` into a single state value.
219
+
220
+ The sensor is created via `createSensor(set => ...)` from `@zeix/cause-effect`, which manages the lifecycle (activate when read, deactivate when unwatched).
221
+
222
+ ## The Context Protocol
223
+
224
+ Implements the [W3C Community Protocol for Context](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md) for dependency injection between components.
225
+
226
+ ### Provider side
227
+
228
+ `provideContexts(['theme', 'user'])` returns a function `(host) => Cleanup` that adds a `context-request` event listener. When a matching request arrives, it stops propagation and provides a getter `() => host[context]` to the callback.
229
+
230
+ This is used as a `MethodProducer` — a property initializer that returns `void` and exists only for its side effects (setting up the listener).
231
+
232
+ ### Consumer side
233
+
234
+ `requestContext('theme', 'light')` returns a `Reader<Memo<T>>` used as a property initializer. During `connectedCallback`, it dispatches a `ContextRequestEvent` that bubbles up the DOM. If an ancestor provider intercepts it, the consumer receives a getter and wraps it in a `createMemo()`, creating a live reactive binding. If no provider responds, it falls back to the provided default value.
235
+
236
+ ## The Scheduler
237
+
238
+ `schedule(element, task)` deduplicates high-frequency DOM updates using `requestAnimationFrame`. A `WeakMap<Element, () => void>` stores the latest task per element. If the same element schedules multiple tasks before the next frame, only the last one runs. This is used by `on()` for passive events and by `dangerouslySetInnerHTML`.
239
+
240
+ ## Security
241
+
242
+ `setAttribute()` includes security validation:
243
+ - Blocks `on*` event handler attributes (prevents XSS via attribute injection)
244
+ - Validates URLs against an allowlist of safe protocols (`http:`, `https:`, `ftp:`, `mailto:`, `tel:`) — blocks `javascript:`, `data:`, etc.
245
+
246
+ ---
247
+
248
+ ## Open Questions
249
+
250
+ ### Parser/Reader distinction via `function.length`
251
+
252
+ The distinction between Parser (≥2 params) and Reader (1 param) is detected at runtime via `value.length >= 2`. This is fragile — default parameters, rest parameters, and destructuring all affect `function.length` in non-obvious ways. A function `(ui, value = '') => ...` has `length === 1` and would be misclassified as a Reader. This is a potential source of subtle bugs. Would a branded type, a wrapper function, or a static property be a more robust marker?
253
+
254
+ ### MethodProducer is invisible in the type system
255
+
256
+ `MethodProducer<P, U>` is defined as `(ui) => void`, but `isReaderOrMethodProducer` just checks `isFunction`. There's no way to distinguish a `Reader` from a `MethodProducer` at runtime — the only difference is that a MethodProducer returns `void` and relies on side effects (like `provideContexts`). Since `#setAccessor` is only called when the result is non-null, this works by convention, but the flow is non-obvious: the MethodProducer's return value (`undefined`) causes `#setAccessor` to be silently skipped, which is the desired behavior but isn't explicitly documented in the code.
257
+
258
+ ### Dependency timeout of 50ms
259
+
260
+ `DEPENDENCY_TIMEOUT` is hardcoded at 50ms. This seems very short — on slower devices or with lazy-loaded component definitions, this could fire frequently. The error is logged but effects still run, so it's non-fatal, but it could cause effects to run against not-yet-upgraded elements. Is this timeout well-calibrated? Should it be configurable?
261
+
262
+ ### `resolveDependencies` uses Promise.race with error swallowing
263
+
264
+ The dependency resolution catches all errors and runs the callback anyway. The `.catch(() => { callback() })` pattern means even unexpected errors (not just timeouts) are silently swallowed. The `DependencyTimeoutError` is constructed and passed to `reject`, but the actual logging happens... nowhere visible. The error is created inside a `new Promise((_, reject) => { reject(new DependencyTimeoutError(...)) })`, which rejects the race, but the `.catch` just calls `callback()` without logging the error.
265
+
266
+ ### `createEventsSensor` captures `targets` once
267
+
268
+ In `createEventsSensor`, the `targets` array is computed once at sensor creation time from the current state of the `Memo`. If the collection changes later (elements added/removed), the sensor won't pick up new targets. For `Memo`-based collections that are specifically designed to be dynamic, this seems like a gap.
269
+
270
+ ### `dangerouslySetInnerHTML` script handling
271
+
272
+ The script re-execution logic clones scripts by copying only `textContent` and `type`. This drops `src`, `async`, `defer`, `crossorigin`, `integrity`, `nomodule`, and other attributes. External scripts (`<script src="...">`) will silently become empty inline scripts. Is this intentional (security boundary) or an oversight?