@gyral/core 0.2.0 → 0.3.1

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 (517) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/NOTICE +19 -0
  3. package/README.md +35 -8
  4. package/dist/call.d.ts +32 -0
  5. package/dist/call.js +63 -0
  6. package/dist/call.js.map +1 -0
  7. package/dist/children.d.ts +30 -5
  8. package/dist/children.js +26 -4
  9. package/dist/children.js.map +1 -1
  10. package/dist/cli/gyral-types.d.ts +2 -0
  11. package/dist/cli/gyral-types.js +23 -0
  12. package/dist/cli/gyral-types.js.map +1 -0
  13. package/dist/command.d.ts +54 -12
  14. package/dist/command.js +31 -11
  15. package/dist/command.js.map +1 -1
  16. package/dist/compiler/ast.d.ts +23 -0
  17. package/dist/compiler/ast.js +149 -0
  18. package/dist/compiler/ast.js.map +1 -0
  19. package/dist/compiler/compile.d.ts +41 -0
  20. package/dist/compiler/compile.js +148 -0
  21. package/dist/compiler/compile.js.map +1 -0
  22. package/dist/compiler/component-entries.d.ts +7 -0
  23. package/dist/compiler/component-entries.js +42 -0
  24. package/dist/compiler/component-entries.js.map +1 -0
  25. package/dist/compiler/components.d.ts +4 -0
  26. package/dist/compiler/components.js +182 -0
  27. package/dist/compiler/components.js.map +1 -0
  28. package/dist/compiler/facts.d.ts +31 -0
  29. package/dist/compiler/facts.js +285 -0
  30. package/dist/compiler/facts.js.map +1 -0
  31. package/dist/compiler/features.d.ts +34 -0
  32. package/dist/compiler/features.js +158 -0
  33. package/dist/compiler/features.js.map +1 -0
  34. package/dist/compiler/hooks.d.ts +22 -0
  35. package/dist/compiler/hooks.js +241 -0
  36. package/dist/compiler/hooks.js.map +1 -0
  37. package/dist/compiler/loader-source.d.ts +14 -0
  38. package/dist/compiler/loader-source.js +86 -0
  39. package/dist/compiler/loader-source.js.map +1 -0
  40. package/dist/compiler/locate.d.ts +14 -0
  41. package/dist/compiler/locate.js +78 -0
  42. package/dist/compiler/locate.js.map +1 -0
  43. package/dist/compiler/parse5-check.d.ts +21 -0
  44. package/dist/compiler/parse5-check.js +60 -0
  45. package/dist/compiler/parse5-check.js.map +1 -0
  46. package/dist/compiler/prop-schemas.d.ts +4 -0
  47. package/dist/compiler/prop-schemas.js +108 -0
  48. package/dist/compiler/prop-schemas.js.map +1 -0
  49. package/dist/compiler/scan.d.ts +41 -0
  50. package/dist/compiler/scan.js +160 -0
  51. package/dist/compiler/scan.js.map +1 -0
  52. package/dist/compiler/scope.d.ts +8 -0
  53. package/dist/compiler/scope.js +116 -0
  54. package/dist/compiler/scope.js.map +1 -0
  55. package/dist/compiler/source.d.ts +34 -0
  56. package/dist/compiler/source.js +114 -0
  57. package/dist/compiler/source.js.map +1 -0
  58. package/dist/compiler/tag-map.d.ts +41 -0
  59. package/dist/compiler/tag-map.js +126 -0
  60. package/dist/compiler/tag-map.js.map +1 -0
  61. package/dist/components-ready.d.ts +7 -0
  62. package/dist/components-ready.js +19 -0
  63. package/dist/components-ready.js.map +1 -0
  64. package/dist/define.d.ts +13 -4
  65. package/dist/define.js +35 -331
  66. package/dist/define.js.map +1 -1
  67. package/dist/devtools-events.d.ts +13 -0
  68. package/dist/devtools-events.js.map +1 -1
  69. package/dist/devtools-off.d.ts +2 -0
  70. package/dist/devtools-off.js +2 -0
  71. package/dist/devtools-off.js.map +1 -1
  72. package/dist/devtools.d.ts +4 -0
  73. package/dist/devtools.js +17 -2
  74. package/dist/devtools.js.map +1 -1
  75. package/dist/drivers-scope.d.ts +1 -1
  76. package/dist/drivers-scope.js +1 -8
  77. package/dist/drivers-scope.js.map +1 -1
  78. package/dist/element-internals.d.ts +1 -0
  79. package/dist/element-internals.js +11 -0
  80. package/dist/element-internals.js.map +1 -0
  81. package/dist/element-types.d.ts +11 -5
  82. package/dist/element-types.js.map +1 -1
  83. package/dist/element.d.ts +3 -0
  84. package/dist/element.js +322 -0
  85. package/dist/element.js.map +1 -0
  86. package/dist/error-view.d.ts +10 -0
  87. package/dist/error-view.js +30 -0
  88. package/dist/error-view.js.map +1 -0
  89. package/dist/errors.d.ts +21 -0
  90. package/dist/errors.js +47 -0
  91. package/dist/errors.js.map +1 -0
  92. package/dist/errors.json +289 -0
  93. package/dist/eslint/imports.d.ts +26 -0
  94. package/dist/eslint/imports.js +78 -0
  95. package/dist/eslint/imports.js.map +1 -0
  96. package/dist/eslint/index.d.ts +16 -0
  97. package/dist/eslint/index.js +23 -0
  98. package/dist/eslint/index.js.map +1 -0
  99. package/dist/eslint/locate.d.ts +33 -0
  100. package/dist/eslint/locate.js +132 -0
  101. package/dist/eslint/locate.js.map +1 -0
  102. package/dist/eslint/row-purity-rule.d.ts +2 -0
  103. package/dist/eslint/row-purity-rule.js +103 -0
  104. package/dist/eslint/row-purity-rule.js.map +1 -0
  105. package/dist/eslint/template-rule.d.ts +6 -0
  106. package/dist/eslint/template-rule.js +76 -0
  107. package/dist/eslint/template-rule.js.map +1 -0
  108. package/dist/eslint/unused-intent-rule.d.ts +2 -0
  109. package/dist/eslint/unused-intent-rule.js +186 -0
  110. package/dist/eslint/unused-intent-rule.js.map +1 -0
  111. package/dist/features.d.ts +39 -0
  112. package/dist/features.js +2 -0
  113. package/dist/features.js.map +1 -0
  114. package/dist/focus.d.ts +13 -12
  115. package/dist/focus.js +44 -11
  116. package/dist/focus.js.map +1 -1
  117. package/dist/form-associated.d.ts +46 -0
  118. package/dist/form-associated.js +77 -0
  119. package/dist/form-associated.js.map +1 -0
  120. package/dist/forms.d.ts +42 -30
  121. package/dist/forms.js +85 -41
  122. package/dist/forms.js.map +1 -1
  123. package/dist/frame-lane.d.ts +11 -0
  124. package/dist/frame-lane.js +28 -0
  125. package/dist/frame-lane.js.map +1 -0
  126. package/dist/head.d.ts +61 -0
  127. package/dist/head.js +85 -0
  128. package/dist/head.js.map +1 -0
  129. package/dist/hooks/invalid.d.ts +14 -0
  130. package/dist/hooks/invalid.js +72 -0
  131. package/dist/hooks/invalid.js.map +1 -0
  132. package/dist/host-model.d.ts +77 -0
  133. package/dist/host-model.js +278 -0
  134. package/dist/host-model.js.map +1 -0
  135. package/dist/hydration-client.d.ts +9 -0
  136. package/dist/hydration-client.js +35 -0
  137. package/dist/hydration-client.js.map +1 -0
  138. package/dist/hydration-loader.d.ts +13 -0
  139. package/dist/hydration-loader.js +58 -0
  140. package/dist/hydration-loader.js.map +1 -0
  141. package/dist/hydration-off.d.ts +8 -0
  142. package/dist/hydration-off.js +25 -0
  143. package/dist/hydration-off.js.map +1 -0
  144. package/dist/hydration.d.ts +3 -21
  145. package/dist/hydration.js +14 -63
  146. package/dist/hydration.js.map +1 -1
  147. package/dist/index.d.ts +24 -36
  148. package/dist/index.js +14 -32
  149. package/dist/index.js.map +1 -1
  150. package/dist/indexeddb-types.d.ts +97 -0
  151. package/dist/indexeddb-types.js +2 -0
  152. package/dist/indexeddb-types.js.map +1 -0
  153. package/dist/indexeddb.d.ts +5 -0
  154. package/dist/indexeddb.js +118 -0
  155. package/dist/indexeddb.js.map +1 -0
  156. package/dist/intent.d.ts +62 -17
  157. package/dist/intent.js +193 -100
  158. package/dist/intent.js.map +1 -1
  159. package/dist/internal/indexeddb-open.d.ts +8 -0
  160. package/dist/internal/indexeddb-open.js +96 -0
  161. package/dist/internal/indexeddb-open.js.map +1 -0
  162. package/dist/internal/indexeddb-transaction.d.ts +4 -0
  163. package/dist/internal/indexeddb-transaction.js +134 -0
  164. package/dist/internal/indexeddb-transaction.js.map +1 -0
  165. package/dist/internal/interpreter.d.ts +15 -1
  166. package/dist/internal/interpreter.js +42 -47
  167. package/dist/internal/interpreter.js.map +1 -1
  168. package/dist/internal/token.d.ts +1 -0
  169. package/dist/internal/token.js +7 -0
  170. package/dist/internal/token.js.map +1 -0
  171. package/dist/internal.d.ts +20 -0
  172. package/dist/internal.js +22 -0
  173. package/dist/internal.js.map +1 -0
  174. package/dist/invoker-fallback-used.d.ts +4 -0
  175. package/dist/invoker-fallback-used.js +10 -0
  176. package/dist/invoker-fallback-used.js.map +1 -0
  177. package/dist/invoker-fallback.d.ts +4 -0
  178. package/dist/invoker-fallback.js +15 -0
  179. package/dist/invoker-fallback.js.map +1 -0
  180. package/dist/invokers-shim.d.ts +6 -0
  181. package/dist/invokers-shim.js +32 -0
  182. package/dist/invokers-shim.js.map +1 -0
  183. package/dist/invokers.d.ts +3 -5
  184. package/dist/invokers.js +11 -51
  185. package/dist/invokers.js.map +1 -1
  186. package/dist/islands.d.ts +0 -2
  187. package/dist/islands.js +4 -11
  188. package/dist/islands.js.map +1 -1
  189. package/dist/light-dom.d.ts +4 -53
  190. package/dist/light-dom.js +5 -100
  191. package/dist/light-dom.js.map +1 -1
  192. package/dist/prop.d.ts +89 -0
  193. package/dist/prop.js +41 -0
  194. package/dist/prop.js.map +1 -0
  195. package/dist/props.d.ts +57 -21
  196. package/dist/props.js +157 -34
  197. package/dist/props.js.map +1 -1
  198. package/dist/retry.d.ts +15 -0
  199. package/dist/retry.js +48 -0
  200. package/dist/retry.js.map +1 -0
  201. package/dist/scheduler.d.ts +43 -0
  202. package/dist/scheduler.js +212 -0
  203. package/dist/scheduler.js.map +1 -0
  204. package/dist/script-json.d.ts +2 -0
  205. package/dist/script-json.js +13 -0
  206. package/dist/script-json.js.map +1 -0
  207. package/dist/server-component.d.ts +10 -0
  208. package/dist/server-component.js +133 -0
  209. package/dist/server-component.js.map +1 -0
  210. package/dist/server-specs.d.ts +6 -0
  211. package/dist/server-specs.js +13 -0
  212. package/dist/server-specs.js.map +1 -0
  213. package/dist/server.d.ts +34 -0
  214. package/dist/server.js +59 -0
  215. package/dist/server.js.map +1 -0
  216. package/dist/settled.d.ts +11 -0
  217. package/dist/settled.js +62 -0
  218. package/dist/settled.js.map +1 -0
  219. package/dist/spec-features-used.d.ts +21 -0
  220. package/dist/spec-features-used.js +50 -0
  221. package/dist/spec-features-used.js.map +1 -0
  222. package/dist/spec-features.d.ts +28 -0
  223. package/dist/spec-features.js +42 -0
  224. package/dist/spec-features.js.map +1 -0
  225. package/dist/states.d.ts +3 -5
  226. package/dist/states.js +22 -11
  227. package/dist/states.js.map +1 -1
  228. package/dist/storage.d.ts +69 -0
  229. package/dist/storage.js +158 -0
  230. package/dist/storage.js.map +1 -0
  231. package/dist/store-binding.d.ts +6 -3
  232. package/dist/store-binding.js +7 -8
  233. package/dist/store-binding.js.map +1 -1
  234. package/dist/store-scope.d.ts +8 -11
  235. package/dist/store-scope.js +15 -40
  236. package/dist/store-scope.js.map +1 -1
  237. package/dist/store.d.ts +3 -0
  238. package/dist/store.js +45 -10
  239. package/dist/store.js.map +1 -1
  240. package/dist/stores-provider.d.ts +2 -6
  241. package/dist/stores-provider.js +21 -55
  242. package/dist/stores-provider.js.map +1 -1
  243. package/dist/subscription.d.ts +35 -0
  244. package/dist/subscription.js +74 -0
  245. package/dist/subscription.js.map +1 -0
  246. package/dist/subscriptions.d.ts +26 -0
  247. package/dist/subscriptions.js +98 -0
  248. package/dist/subscriptions.js.map +1 -0
  249. package/dist/transitions.d.ts +5 -7
  250. package/dist/transitions.js +17 -20
  251. package/dist/transitions.js.map +1 -1
  252. package/dist/types.d.ts +142 -49
  253. package/dist/types.js.map +1 -1
  254. package/dist/use-dialogs.d.ts +2 -0
  255. package/dist/use-dialogs.js +9 -0
  256. package/dist/use-dialogs.js.map +1 -0
  257. package/dist/use-disposal.d.ts +2 -0
  258. package/dist/use-disposal.js +9 -0
  259. package/dist/use-disposal.js.map +1 -0
  260. package/dist/use-error-view.d.ts +2 -0
  261. package/dist/use-error-view.js +9 -0
  262. package/dist/use-error-view.js.map +1 -0
  263. package/dist/use-errored.d.ts +2 -0
  264. package/dist/use-errored.js +9 -0
  265. package/dist/use-errored.js.map +1 -0
  266. package/dist/use-form-associated.d.ts +2 -0
  267. package/dist/use-form-associated.js +7 -0
  268. package/dist/use-form-associated.js.map +1 -0
  269. package/dist/use-frame-lane.d.ts +2 -0
  270. package/dist/use-frame-lane.js +10 -0
  271. package/dist/use-frame-lane.js.map +1 -0
  272. package/dist/use-invokers.d.ts +2 -0
  273. package/dist/use-invokers.js +10 -0
  274. package/dist/use-invokers.js.map +1 -0
  275. package/dist/use-states.d.ts +2 -0
  276. package/dist/use-states.js +10 -0
  277. package/dist/use-states.js.map +1 -0
  278. package/dist/use-subscriptions.d.ts +2 -0
  279. package/dist/use-subscriptions.js +10 -0
  280. package/dist/use-subscriptions.js.map +1 -0
  281. package/dist/use-transitions.d.ts +2 -0
  282. package/dist/use-transitions.js +10 -0
  283. package/dist/use-transitions.js.map +1 -0
  284. package/dist/view/attributes.d.ts +11 -0
  285. package/dist/view/attributes.js +15 -0
  286. package/dist/view/attributes.js.map +1 -0
  287. package/dist/view/compiled.d.ts +1 -0
  288. package/dist/view/compiled.js +6 -0
  289. package/dist/view/compiled.js.map +1 -0
  290. package/dist/view/dev-off.d.ts +1 -0
  291. package/dist/view/dev-off.js +4 -0
  292. package/dist/view/dev-off.js.map +1 -0
  293. package/dist/view/dev.d.ts +1 -0
  294. package/dist/view/dev.js +5 -0
  295. package/dist/view/dev.js.map +1 -0
  296. package/dist/view/flags.d.ts +1 -0
  297. package/dist/view/flags.js +4 -0
  298. package/dist/view/flags.js.map +1 -0
  299. package/dist/view/index.d.ts +26 -0
  300. package/dist/view/index.js +27 -0
  301. package/dist/view/index.js.map +1 -0
  302. package/dist/view/loc.d.ts +9 -0
  303. package/dist/view/loc.js +50 -0
  304. package/dist/view/loc.js.map +1 -0
  305. package/dist/view/message.d.ts +17 -0
  306. package/dist/view/message.js +40 -0
  307. package/dist/view/message.js.map +1 -0
  308. package/dist/view/messages.d.ts +43 -0
  309. package/dist/view/messages.js +84 -0
  310. package/dist/view/messages.js.map +1 -0
  311. package/dist/view/normalize/check.d.ts +23 -0
  312. package/dist/view/normalize/check.js +81 -0
  313. package/dist/view/normalize/check.js.map +1 -0
  314. package/dist/view/normalize/emit.d.ts +15 -0
  315. package/dist/view/normalize/emit.js +165 -0
  316. package/dist/view/normalize/emit.js.map +1 -0
  317. package/dist/view/normalize/entities.d.ts +4 -0
  318. package/dist/view/normalize/entities.js +38 -0
  319. package/dist/view/normalize/entities.js.map +1 -0
  320. package/dist/view/normalize/errors.d.ts +9 -0
  321. package/dist/view/normalize/errors.js +19 -0
  322. package/dist/view/normalize/errors.js.map +1 -0
  323. package/dist/view/normalize/id.d.ts +10 -0
  324. package/dist/view/normalize/id.js +55 -0
  325. package/dist/view/normalize/id.js.map +1 -0
  326. package/dist/view/normalize/normalize.d.ts +18 -0
  327. package/dist/view/normalize/normalize.js +42 -0
  328. package/dist/view/normalize/normalize.js.map +1 -0
  329. package/dist/view/normalize/repairs.d.ts +24 -0
  330. package/dist/view/normalize/repairs.js +166 -0
  331. package/dist/view/normalize/repairs.js.map +1 -0
  332. package/dist/view/normalize/rules.d.ts +39 -0
  333. package/dist/view/normalize/rules.js +120 -0
  334. package/dist/view/normalize/rules.js.map +1 -0
  335. package/dist/view/normalize/shape.d.ts +5 -0
  336. package/dist/view/normalize/shape.js +38 -0
  337. package/dist/view/normalize/shape.js.map +1 -0
  338. package/dist/view/normalize/source.d.ts +13 -0
  339. package/dist/view/normalize/source.js +34 -0
  340. package/dist/view/normalize/source.js.map +1 -0
  341. package/dist/view/normalize/svg.d.ts +14 -0
  342. package/dist/view/normalize/svg.js +40 -0
  343. package/dist/view/normalize/svg.js.map +1 -0
  344. package/dist/view/normalize/tag-states.d.ts +23 -0
  345. package/dist/view/normalize/tag-states.js +192 -0
  346. package/dist/view/normalize/tag-states.js.map +1 -0
  347. package/dist/view/normalize/tokenizer.d.ts +5 -0
  348. package/dist/view/normalize/tokenizer.js +233 -0
  349. package/dist/view/normalize/tokenizer.js.map +1 -0
  350. package/dist/view/normalize/tokens.d.ts +55 -0
  351. package/dist/view/normalize/tokens.js +24 -0
  352. package/dist/view/normalize/tokens.js.map +1 -0
  353. package/dist/view/normalize/tree.d.ts +65 -0
  354. package/dist/view/normalize/tree.js +235 -0
  355. package/dist/view/normalize/tree.js.map +1 -0
  356. package/dist/view/normalize/types.d.ts +146 -0
  357. package/dist/view/normalize/types.js +29 -0
  358. package/dist/view/normalize/types.js.map +1 -0
  359. package/dist/view/normalize/whitespace.d.ts +2 -0
  360. package/dist/{template-whitespace.js → view/normalize/whitespace.js} +27 -36
  361. package/dist/view/normalize/whitespace.js.map +1 -0
  362. package/dist/view/prepare-stub.d.ts +3 -0
  363. package/dist/view/prepare-stub.js +7 -0
  364. package/dist/view/prepare-stub.js.map +1 -0
  365. package/dist/view/prepare.d.ts +11 -0
  366. package/dist/view/prepare.js +63 -0
  367. package/dist/view/prepare.js.map +1 -0
  368. package/dist/view/registry.d.ts +78 -0
  369. package/dist/view/registry.js +27 -0
  370. package/dist/view/registry.js.map +1 -0
  371. package/dist/view/render/adopt-attr.d.ts +14 -0
  372. package/dist/view/render/adopt-attr.js +83 -0
  373. package/dist/view/render/adopt-attr.js.map +1 -0
  374. package/dist/view/render/adopt-plan.d.ts +10 -0
  375. package/dist/view/render/adopt-plan.js +26 -0
  376. package/dist/view/render/adopt-plan.js.map +1 -0
  377. package/dist/view/render/adopt.d.ts +3 -0
  378. package/dist/view/render/adopt.js +286 -0
  379. package/dist/view/render/adopt.js.map +1 -0
  380. package/dist/view/render/attr-parts.d.ts +72 -0
  381. package/dist/view/render/attr-parts.js +262 -0
  382. package/dist/view/render/attr-parts.js.map +1 -0
  383. package/dist/view/render/child-part.d.ts +51 -0
  384. package/dist/view/render/child-part.js +173 -0
  385. package/dist/view/render/child-part.js.map +1 -0
  386. package/dist/view/render/dev-check.d.ts +12 -0
  387. package/dist/view/render/dev-check.js +122 -0
  388. package/dist/view/render/dev-check.js.map +1 -0
  389. package/dist/view/render/dispose.d.ts +3 -0
  390. package/dist/view/render/dispose.js +76 -0
  391. package/dist/view/render/dispose.js.map +1 -0
  392. package/dist/view/render/hook-part.d.ts +5 -0
  393. package/dist/view/render/hook-part.js +36 -0
  394. package/dist/view/render/hook-part.js.map +1 -0
  395. package/dist/view/render/hook-run.d.ts +2 -0
  396. package/dist/view/render/hook-run.js +72 -0
  397. package/dist/view/render/hook-run.js.map +1 -0
  398. package/dist/view/render/hooks.d.ts +108 -0
  399. package/dist/view/render/hooks.js +108 -0
  400. package/dist/view/render/hooks.js.map +1 -0
  401. package/dist/view/render/hydrate.d.ts +10 -0
  402. package/dist/view/render/hydrate.js +33 -0
  403. package/dist/view/render/hydrate.js.map +1 -0
  404. package/dist/view/render/instance.d.ts +33 -0
  405. package/dist/view/render/instance.js +107 -0
  406. package/dist/view/render/instance.js.map +1 -0
  407. package/dist/view/render/keyed.d.ts +6 -0
  408. package/dist/view/render/keyed.js +15 -0
  409. package/dist/view/render/keyed.js.map +1 -0
  410. package/dist/view/render/list.d.ts +20 -0
  411. package/dist/view/render/list.js +156 -0
  412. package/dist/view/render/list.js.map +1 -0
  413. package/dist/view/render/mismatch.d.ts +10 -0
  414. package/dist/view/render/mismatch.js +55 -0
  415. package/dist/view/render/mismatch.js.map +1 -0
  416. package/dist/view/render/modal.d.ts +3 -0
  417. package/dist/view/render/modal.js +39 -0
  418. package/dist/view/render/modal.js.map +1 -0
  419. package/dist/view/render/nodes.d.ts +10 -0
  420. package/dist/view/render/nodes.js +37 -0
  421. package/dist/view/render/nodes.js.map +1 -0
  422. package/dist/view/render/place.d.ts +18 -0
  423. package/dist/view/render/place.js +69 -0
  424. package/dist/view/render/place.js.map +1 -0
  425. package/dist/view/render/plan.d.ts +26 -0
  426. package/dist/view/render/plan.js +124 -0
  427. package/dist/view/render/plan.js.map +1 -0
  428. package/dist/view/render/raw.d.ts +21 -0
  429. package/dist/view/render/raw.js +72 -0
  430. package/dist/view/render/raw.js.map +1 -0
  431. package/dist/view/render/render.d.ts +15 -0
  432. package/dist/view/render/render.js +65 -0
  433. package/dist/view/render/render.js.map +1 -0
  434. package/dist/view/render/reorder.d.ts +4 -0
  435. package/dist/view/render/reorder.js +101 -0
  436. package/dist/view/render/reorder.js.map +1 -0
  437. package/dist/view/render/rows.d.ts +23 -0
  438. package/dist/view/render/rows.js +95 -0
  439. package/dist/view/render/rows.js.map +1 -0
  440. package/dist/view/render/seen.d.ts +11 -0
  441. package/dist/view/render/seen.js +17 -0
  442. package/dist/view/render/seen.js.map +1 -0
  443. package/dist/view/render/toggles.d.ts +6 -0
  444. package/dist/view/render/toggles.js +6 -0
  445. package/dist/view/render/toggles.js.map +1 -0
  446. package/dist/view/render/values.d.ts +29 -0
  447. package/dist/view/render/values.js +13 -0
  448. package/dist/view/render/values.js.map +1 -0
  449. package/dist/view/render/warn.d.ts +28 -0
  450. package/dist/view/render/warn.js +114 -0
  451. package/dist/view/render/warn.js.map +1 -0
  452. package/dist/view/server/component.d.ts +12 -0
  453. package/dist/view/server/component.js +64 -0
  454. package/dist/view/server/component.js.map +1 -0
  455. package/dist/view/server/escape.d.ts +10 -0
  456. package/dist/view/server/escape.js +34 -0
  457. package/dist/view/server/escape.js.map +1 -0
  458. package/dist/view/server/index.d.ts +48 -0
  459. package/dist/view/server/index.js +80 -0
  460. package/dist/view/server/index.js.map +1 -0
  461. package/dist/view/server/sha256.d.ts +2 -0
  462. package/dist/view/server/sha256.js +75 -0
  463. package/dist/view/server/sha256.js.map +1 -0
  464. package/dist/view/server/tags.d.ts +68 -0
  465. package/dist/view/server/tags.js +132 -0
  466. package/dist/view/server/tags.js.map +1 -0
  467. package/dist/view/server/values.d.ts +18 -0
  468. package/dist/view/server/values.js +94 -0
  469. package/dist/view/server/values.js.map +1 -0
  470. package/dist/view/server/writer.d.ts +21 -0
  471. package/dist/view/server/writer.js +170 -0
  472. package/dist/view/server/writer.js.map +1 -0
  473. package/dist/view/styles.d.ts +25 -0
  474. package/dist/view/styles.js +58 -0
  475. package/dist/view/styles.js.map +1 -0
  476. package/dist/view/template-element.d.ts +10 -0
  477. package/dist/view/template-element.js +45 -0
  478. package/dist/view/template-element.js.map +1 -0
  479. package/dist/view/template.d.ts +29 -0
  480. package/dist/view/template.js +84 -0
  481. package/dist/view/template.js.map +1 -0
  482. package/dist/view/trusted-html.d.ts +6 -0
  483. package/dist/view/trusted-html.js +34 -0
  484. package/dist/view/trusted-html.js.map +1 -0
  485. package/dist/vite.d.ts +57 -11
  486. package/dist/vite.js +319 -27
  487. package/dist/vite.js.map +1 -1
  488. package/package.json +70 -4
  489. package/dist/accessible-name.d.ts +0 -19
  490. package/dist/accessible-name.js +0 -56
  491. package/dist/accessible-name.js.map +0 -1
  492. package/dist/element-directive.d.ts +0 -20
  493. package/dist/element-directive.js +0 -31
  494. package/dist/element-directive.js.map +0 -1
  495. package/dist/invalid.d.ts +0 -21
  496. package/dist/invalid.js +0 -70
  497. package/dist/invalid.js.map +0 -1
  498. package/dist/lit-html-check.d.ts +0 -6
  499. package/dist/lit-html-check.js +0 -48
  500. package/dist/lit-html-check.js.map +0 -1
  501. package/dist/live-boolean.d.ts +0 -22
  502. package/dist/live-boolean.js +0 -34
  503. package/dist/live-boolean.js.map +0 -1
  504. package/dist/random.d.ts +0 -13
  505. package/dist/random.js +0 -19
  506. package/dist/random.js.map +0 -1
  507. package/dist/styles.d.ts +0 -12
  508. package/dist/styles.js +0 -11
  509. package/dist/styles.js.map +0 -1
  510. package/dist/template-whitespace.d.ts +0 -7
  511. package/dist/template-whitespace.js.map +0 -1
  512. package/dist/templates.d.ts +0 -5
  513. package/dist/templates.js +0 -15
  514. package/dist/templates.js.map +0 -1
  515. package/dist/textarea.d.ts +0 -26
  516. package/dist/textarea.js +0 -85
  517. package/dist/textarea.js.map +0 -1
package/dist/focus.js CHANGED
@@ -1,8 +1,17 @@
1
- /** Marker driver: `define()` handles focus commands itself. */
1
+ import { afterRender, POST_FOCUS } from './scheduler.js';
2
+ import { message } from './view/index.js';
3
+ /** Marker driver: the host runs focus commands itself (`local`). */
2
4
  export const FOCUS = {
3
5
  name: '@gyral/focus',
4
6
  run: () => undefined,
7
+ local: (host, input) => {
8
+ queueFocus(host.el, host.root, host.tag, input);
9
+ },
5
10
  };
11
+ // How long `focus(selector, { wait: true })` waits for its target to appear.
12
+ const FOCUS_WAIT_MS = 1000;
13
+ // A component's waiting focus request, so the next focus() can cancel it.
14
+ const waiting = new WeakMap();
6
15
  /**
7
16
  * A command that focuses the first element matching `selector` inside this component (its
8
17
  * shadow root, or its own children in light-DOM mode) once the current update has rendered:
@@ -15,11 +24,11 @@ export function focus(selector, options = {}) {
15
24
  const input = { selector, ...options };
16
25
  return { driver: FOCUS, input, onSuccess: () => undefined };
17
26
  }
18
- /** Runs a focus command against `root` (called by define() after the update completes). */
27
+ /** Runs a focus command against `root` (the scheduler calls it after the render). */
19
28
  export function runFocus(root, tag, input) {
20
29
  const target = root.querySelector(input.selector);
21
30
  if (!(target instanceof HTMLElement || target instanceof SVGElement)) {
22
- console.warn(`<${tag}> focus("${input.selector}") matched no focusable element.`);
31
+ console.warn(message(13, tag, input.selector));
23
32
  return;
24
33
  }
25
34
  target.focus({ preventScroll: input.preventScroll ?? false });
@@ -28,15 +37,39 @@ export function runFocus(root, tag, input) {
28
37
  }
29
38
  }
30
39
  /**
31
- * Runs a focus command after the render the same update caused, view transition included:
32
- * define() runs commands before #dispatch registers that update's view transition, so wait a
33
- * microtask first, then for updateComplete, which waits for the transition (gyral-6zz).
40
+ * Runs a focus command after the flush that renders the same update, view transition
41
+ * included: it is post-render work, so the DOM already shows the new state.
34
42
  */
35
- export function focusAfterUpdate(host, tag, input) {
36
- void Promise.resolve().then(async () => {
37
- await host.updateComplete;
38
- if (host.isConnected)
39
- runFocus(host.renderRoot, tag, input);
43
+ export function queueFocus(host, root, tag, input) {
44
+ afterRender(POST_FOCUS, () => {
45
+ waiting.get(host)?.();
46
+ const target = root();
47
+ if (!host.isConnected || target === undefined)
48
+ return;
49
+ if (input.wait !== true || target.querySelector(input.selector) !== null) {
50
+ runFocus(target, tag, input);
51
+ return;
52
+ }
53
+ // Not rendered yet: watch the component's own DOM (renders are the only writers) until the
54
+ // target appears, a newer focus() cancels, or time runs out. settled() doesn't wait for it.
55
+ const stop = () => {
56
+ observer.disconnect();
57
+ clearTimeout(timer);
58
+ waiting.delete(host);
59
+ };
60
+ const observer = new MutationObserver(() => {
61
+ if (target.querySelector(input.selector) !== null) {
62
+ stop();
63
+ if (host.isConnected)
64
+ runFocus(target, tag, input);
65
+ }
66
+ });
67
+ const timer = setTimeout(() => {
68
+ stop();
69
+ runFocus(target, tag, input);
70
+ }, FOCUS_WAIT_MS);
71
+ observer.observe(target, { childList: true, subtree: true, attributes: true });
72
+ waiting.set(host, stop);
40
73
  });
41
74
  }
42
75
  //# sourceMappingURL=focus.js.map
package/dist/focus.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"focus.js","sourceRoot":"","sources":["../src/focus.ts"],"names":[],"mappings":"AAKA,+DAA+D;AAC/D,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,IAAI,EAAE,cAAc;IACpB,GAAG,EAAE,GAAG,EAAE,CAAC,SAAS;CACZ,CAAC;AAcX;;;;;;;GAOG;AACH,MAAM,UAAU,KAAK,CAAC,QAAgB,EAAE,UAAwB,EAAE;IAChE,MAAM,KAAK,GAAe,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE,CAAC;IACnD,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC;AAC9D,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,QAAQ,CAAC,IAAgB,EAAE,GAAW,EAAE,KAAiB;IACvE,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAClD,IAAI,CAAC,CAAC,MAAM,YAAY,WAAW,IAAI,MAAM,YAAY,UAAU,CAAC,EAAE,CAAC;QACrE,OAAO,CAAC,IAAI,CAAC,IAAI,GAAG,YAAY,KAAK,CAAC,QAAQ,kCAAkC,CAAC,CAAC;QAClF,OAAO;IACT,CAAC;IACD,MAAM,CAAC,KAAK,CAAC,EAAE,aAAa,EAAE,KAAK,CAAC,aAAa,IAAI,KAAK,EAAE,CAAC,CAAC;IAC9D,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,IAAI,QAAQ,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QACtF,MAA2B,CAAC,MAAM,EAAE,CAAC;IACxC,CAAC;AACH,CAAC;AASD;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAe,EAAE,GAAW,EAAE,KAAiB;IAC9E,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;QACrC,MAAM,IAAI,CAAC,cAAc,CAAC;QAC1B,IAAI,IAAI,CAAC,WAAW;YAAE,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["// Focus management from the model (gyral-czi.28). Moving focus is a side effect, so the model\n// asks for it with a command; define() runs it against the component's own render root after\n// the render that the same reducer caused, so the element to focus already exists.\nimport type { Command } from './command.js';\n\n/** Marker driver: `define()` handles focus commands itself. */\nexport const FOCUS = {\n name: '@gyral/focus',\n run: () => undefined,\n} as const;\n\nexport interface FocusOptions {\n /** Don't scroll the element into view (default `false`: the browser scrolls if needed). */\n readonly preventScroll?: boolean;\n /** Also select the text of an input or textarea. */\n readonly select?: boolean;\n}\n\n/** The input of a `focus()` command. */\nexport interface FocusInput extends FocusOptions {\n readonly selector: string;\n}\n\n/**\n * A command that focuses the first element matching `selector` inside this component (its\n * shadow root, or its own children in light-DOM mode) once the current update has rendered:\n *\n * NextPage: (s, m) => [{ ...s, page: m.page }, [focus('h2', { preventScroll: true })]]\n *\n * Non-focusable targets (a heading) need `tabindex=\"-1\"` in the view.\n */\nexport function focus(selector: string, options: FocusOptions = {}): Command<never> {\n const input: FocusInput = { selector, ...options };\n return { driver: FOCUS, input, onSuccess: () => undefined };\n}\n\n/** Runs a focus command against `root` (called by define() after the update completes). */\nexport function runFocus(root: ParentNode, tag: string, input: FocusInput): void {\n const target = root.querySelector(input.selector);\n if (!(target instanceof HTMLElement || target instanceof SVGElement)) {\n console.warn(`<${tag}> focus(\"${input.selector}\") matched no focusable element.`);\n return;\n }\n target.focus({ preventScroll: input.preventScroll ?? false });\n if (input.select === true && 'select' in target && typeof target.select === 'function') {\n (target as HTMLInputElement).select();\n }\n}\n\n/** What focusAfterUpdate needs from a component: Lit's update promise and its render root. */\nexport interface FocusHost {\n readonly updateComplete: Promise<boolean>;\n readonly isConnected: boolean;\n readonly renderRoot: ParentNode;\n}\n\n/**\n * Runs a focus command after the render the same update caused, view transition included:\n * define() runs commands before #dispatch registers that update's view transition, so wait a\n * microtask first, then for updateComplete, which waits for the transition (gyral-6zz).\n */\nexport function focusAfterUpdate(host: FocusHost, tag: string, input: FocusInput): void {\n void Promise.resolve().then(async () => {\n await host.updateComplete;\n if (host.isConnected) runFocus(host.renderRoot, tag, input);\n });\n}\n"]}
1
+ {"version":3,"file":"focus.js","sourceRoot":"","sources":["../src/focus.ts"],"names":[],"mappings":"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","sourcesContent":["// Focus management from the model (gyral-czi.28). Moving focus is a side effect, so the model\n// asks for it with a command; define() queues it as the scheduler's first post-render work\n// (view/04-scheduler.md \"Post-render queue\"), against the component's own root, so the element\n// to focus already exists.\nimport type { Command } from './command.js';\nimport type { LocalHost } from './features.js';\nimport { afterRender, POST_FOCUS } from './scheduler.js';\nimport { message } from './view/index.js';\n\n/** Marker driver: the host runs focus commands itself (`local`). */\nexport const FOCUS = {\n name: '@gyral/focus',\n run: () => undefined,\n local: (host: LocalHost, input: unknown) => {\n queueFocus(host.el, host.root, host.tag, input as FocusInput);\n },\n} as const;\n\nexport interface FocusOptions {\n /** Don't scroll the element into view (default `false`: the browser scrolls if needed). */\n readonly preventScroll?: boolean;\n /** Also select the text of an input or textarea. */\n readonly select?: boolean;\n /**\n * If nothing matches after this render, keep the request and focus the target as soon as a\n * later render of this component produces it; a newer `focus()` from the component replaces\n * it, and after one second it gives up with the usual warning. `settled()` doesn't wait for it.\n */\n readonly wait?: boolean;\n}\n\n// How long `focus(selector, { wait: true })` waits for its target to appear.\nconst FOCUS_WAIT_MS = 1000;\n\n// A component's waiting focus request, so the next focus() can cancel it.\nconst waiting = new WeakMap<Element, () => void>();\n\n/** The input of a `focus()` command. */\nexport interface FocusInput extends FocusOptions {\n readonly selector: string;\n}\n\n/**\n * A command that focuses the first element matching `selector` inside this component (its\n * shadow root, or its own children in light-DOM mode) once the current update has rendered:\n *\n * NextPage: (s, m) => [{ ...s, page: m.page }, [focus('h2', { preventScroll: true })]]\n *\n * Non-focusable targets (a heading) need `tabindex=\"-1\"` in the view.\n */\nexport function focus(selector: string, options: FocusOptions = {}): Command<never> {\n const input: FocusInput = { selector, ...options };\n return { driver: FOCUS, input, onSuccess: () => undefined };\n}\n\n/** Runs a focus command against `root` (the scheduler calls it after the render). */\nexport function runFocus(root: ParentNode, tag: string, input: FocusInput): void {\n const target = root.querySelector(input.selector);\n if (!(target instanceof HTMLElement || target instanceof SVGElement)) {\n console.warn(message(13, tag, input.selector));\n return;\n }\n target.focus({ preventScroll: input.preventScroll ?? false });\n if (input.select === true && 'select' in target && typeof target.select === 'function') {\n (target as HTMLInputElement).select();\n }\n}\n\n/**\n * Runs a focus command after the flush that renders the same update, view transition\n * included: it is post-render work, so the DOM already shows the new state.\n */\nexport function queueFocus(\n host: Element,\n root: () => ParentNode | undefined,\n tag: string,\n input: FocusInput,\n): void {\n afterRender(POST_FOCUS, () => {\n waiting.get(host)?.();\n const target = root();\n if (!host.isConnected || target === undefined) return;\n if (input.wait !== true || target.querySelector(input.selector) !== null) {\n runFocus(target, tag, input);\n return;\n }\n // Not rendered yet: watch the component's own DOM (renders are the only writers) until the\n // target appears, a newer focus() cancels, or time runs out. settled() doesn't wait for it.\n const stop = (): void => {\n observer.disconnect();\n clearTimeout(timer);\n waiting.delete(host);\n };\n const observer = new MutationObserver(() => {\n if (target.querySelector(input.selector) !== null) {\n stop();\n if (host.isConnected) runFocus(target, tag, input);\n }\n });\n const timer = setTimeout(() => {\n stop();\n runFocus(target, tag, input);\n }, FOCUS_WAIT_MS);\n observer.observe(target, { childList: true, subtree: true, attributes: true });\n waiting.set(host, stop);\n });\n}\n"]}
@@ -0,0 +1,46 @@
1
+ import type { Ctx } from './types.js';
2
+ import type { HostModel } from './host-model.js';
3
+ /** A native submitted value, or null to omit this component from the submission. */
4
+ export type FormControlValue = string | File | FormData | null;
5
+ /** Native constraint flags and a validation bubble anchor in the component's render root. */
6
+ export interface FormControlValidity {
7
+ readonly flags: ValidityStateFlags;
8
+ readonly message: string;
9
+ readonly anchor?: string;
10
+ }
11
+ /** Pure model projections applied by Gyral to the host's ElementInternals (ADR 0008). */
12
+ export interface FormAssociation<S, P> {
13
+ readonly value: (state: S, ctx: Ctx<P>) => FormControlValue;
14
+ readonly state?: (state: S, ctx: Ctx<P>) => FormControlValue;
15
+ readonly validity?: (state: S, ctx: Ctx<P>) => FormControlValidity | undefined;
16
+ }
17
+ /** Framework message: the owning native form was reset. */
18
+ export interface FormReset {
19
+ readonly _tag: 'FormReset';
20
+ }
21
+ /** Framework message: the host or its fieldset changed the effective disabled state. */
22
+ export interface FormDisabled {
23
+ readonly _tag: 'FormDisabled';
24
+ readonly disabled: boolean;
25
+ }
26
+ /** Framework message: native session restoration or autofill supplied control state. */
27
+ export interface FormStateRestore {
28
+ readonly _tag: 'FormStateRestore';
29
+ readonly state: FormControlValue;
30
+ readonly mode: 'restore' | 'autocomplete';
31
+ }
32
+ export type FormMessage = FormReset | FormDisabled | FormStateRestore;
33
+ export interface FormControl {
34
+ receive(msg: FormMessage): void;
35
+ sync<S, P>(projection: FormAssociation<S, P>, state: S, ctx: Ctx<P>, root: ParentNode): void;
36
+ }
37
+ /** Native reactions must be installed before customElements.define() reads the prototype. */
38
+ declare function installFormCallbacks(Element: CustomElementConstructor, enabled: boolean): void;
39
+ /** Attach once in the constructor; defer pre-render reactions until hydration has committed. */
40
+ export declare function createFormControl(host: HTMLElement, model: Pick<HostModel<unknown, unknown>, 'hasReducer' | 'dispatch'>): FormControl;
41
+ export declare const formFeature: {
42
+ create<S, P>(host: HTMLElement, model: Pick<HostModel<unknown, unknown>, "hasReducer" | "dispatch">, projection: FormAssociation<S, P> | undefined): void;
43
+ sync(host: HTMLElement, state: unknown, ctx: Ctx<unknown>, root: ParentNode): void;
44
+ install: typeof installFormCallbacks;
45
+ };
46
+ export {};
@@ -0,0 +1,77 @@
1
+ import { internalsOf } from './element-internals.js';
2
+ import { afterRender, POST_HYDRATED } from './scheduler.js';
3
+ const controls = new WeakMap();
4
+ const projections = new WeakMap();
5
+ /** Native reactions must be installed before customElements.define() reads the prototype. */
6
+ function installFormCallbacks(Element, enabled) {
7
+ if (!enabled)
8
+ return;
9
+ Object.defineProperty(Element, 'formAssociated', { value: true });
10
+ Object.assign(Element.prototype, {
11
+ formResetCallback() {
12
+ controls.get(this)?.receive({ _tag: 'FormReset' });
13
+ },
14
+ formDisabledCallback(disabled) {
15
+ controls.get(this)?.receive({ _tag: 'FormDisabled', disabled });
16
+ },
17
+ formStateRestoreCallback(state, mode) {
18
+ controls.get(this)?.receive({ _tag: 'FormStateRestore', state, mode });
19
+ },
20
+ });
21
+ }
22
+ /** Attach once in the constructor; defer pre-render reactions until hydration has committed. */
23
+ export function createFormControl(host, model) {
24
+ const dispatch = (msg) => {
25
+ if (model.hasReducer(msg._tag))
26
+ model.dispatch(msg);
27
+ };
28
+ const internals = internalsOf(host);
29
+ let live = false;
30
+ const pending = [];
31
+ const control = {
32
+ receive(msg) {
33
+ if (live)
34
+ dispatch(msg);
35
+ else
36
+ pending.push(msg);
37
+ },
38
+ sync(projection, state, ctx, root) {
39
+ const value = projection.value(state, ctx);
40
+ if (projection.state === undefined)
41
+ internals.setFormValue(value);
42
+ else
43
+ internals.setFormValue(value, projection.state(state, ctx));
44
+ const validity = projection.validity?.(state, ctx);
45
+ const anchor = validity?.anchor === undefined ? undefined : root.querySelector(validity.anchor);
46
+ if (anchor != null && !(anchor instanceof HTMLElement)) {
47
+ throw new TypeError('The form validity anchor must select an HTMLElement.');
48
+ }
49
+ internals.setValidity(validity?.flags ?? {}, validity?.message ?? '', anchor ?? undefined);
50
+ live = true;
51
+ if (pending.length > 0) {
52
+ const messages = pending.splice(0);
53
+ afterRender(POST_HYDRATED, () => {
54
+ for (const msg of messages)
55
+ dispatch(msg);
56
+ });
57
+ }
58
+ },
59
+ };
60
+ controls.set(host, control);
61
+ return control;
62
+ }
63
+ export const formFeature = {
64
+ create(host, model, projection) {
65
+ if (projection === undefined)
66
+ return;
67
+ projections.set(host, projection);
68
+ createFormControl(host, model);
69
+ },
70
+ sync(host, state, ctx, root) {
71
+ const projection = projections.get(host);
72
+ if (projection !== undefined)
73
+ controls.get(host)?.sync(projection, state, ctx, root);
74
+ },
75
+ install: installFormCallbacks,
76
+ };
77
+ //# sourceMappingURL=form-associated.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-associated.js","sourceRoot":"","sources":["../src/form-associated.ts"],"names":[],"mappings":"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","sourcesContent":["import { internalsOf } from './element-internals.js';\nimport { afterRender, POST_HYDRATED } from './scheduler.js';\nimport type { Ctx } from './types.js';\nimport type { HostModel } from './host-model.js';\n\n/** A native submitted value, or null to omit this component from the submission. */\nexport type FormControlValue = string | File | FormData | null;\n\n/** Native constraint flags and a validation bubble anchor in the component's render root. */\nexport interface FormControlValidity {\n readonly flags: ValidityStateFlags;\n readonly message: string;\n readonly anchor?: string;\n}\n\n/** Pure model projections applied by Gyral to the host's ElementInternals (ADR 0008). */\nexport interface FormAssociation<S, P> {\n readonly value: (state: S, ctx: Ctx<P>) => FormControlValue;\n readonly state?: (state: S, ctx: Ctx<P>) => FormControlValue;\n readonly validity?: (state: S, ctx: Ctx<P>) => FormControlValidity | undefined;\n}\n\n/** Framework message: the owning native form was reset. */\nexport interface FormReset {\n readonly _tag: 'FormReset';\n}\n\n/** Framework message: the host or its fieldset changed the effective disabled state. */\nexport interface FormDisabled {\n readonly _tag: 'FormDisabled';\n readonly disabled: boolean;\n}\n\n/** Framework message: native session restoration or autofill supplied control state. */\nexport interface FormStateRestore {\n readonly _tag: 'FormStateRestore';\n readonly state: FormControlValue;\n readonly mode: 'restore' | 'autocomplete';\n}\n\nexport type FormMessage = FormReset | FormDisabled | FormStateRestore;\n\nexport interface FormControl {\n receive(msg: FormMessage): void;\n sync<S, P>(projection: FormAssociation<S, P>, state: S, ctx: Ctx<P>, root: ParentNode): void;\n}\n\nconst controls = new WeakMap<HTMLElement, FormControl>();\nconst projections = new WeakMap<HTMLElement, FormAssociation<unknown, unknown>>();\n\n/** Native reactions must be installed before customElements.define() reads the prototype. */\nfunction installFormCallbacks(Element: CustomElementConstructor, enabled: boolean): void {\n if (!enabled) return;\n Object.defineProperty(Element, 'formAssociated', { value: true });\n Object.assign(Element.prototype, {\n formResetCallback(this: HTMLElement): void {\n controls.get(this)?.receive({ _tag: 'FormReset' });\n },\n formDisabledCallback(this: HTMLElement, disabled: boolean): void {\n controls.get(this)?.receive({ _tag: 'FormDisabled', disabled });\n },\n formStateRestoreCallback(\n this: HTMLElement,\n state: FormControlValue,\n mode: 'restore' | 'autocomplete',\n ): void {\n controls.get(this)?.receive({ _tag: 'FormStateRestore', state, mode });\n },\n });\n}\n\n/** Attach once in the constructor; defer pre-render reactions until hydration has committed. */\nexport function createFormControl(\n host: HTMLElement,\n model: Pick<HostModel<unknown, unknown>, 'hasReducer' | 'dispatch'>,\n): FormControl {\n const dispatch = (msg: FormMessage): void => {\n if (model.hasReducer(msg._tag)) model.dispatch(msg);\n };\n const internals = internalsOf(host);\n let live = false;\n const pending: FormMessage[] = [];\n const control: FormControl = {\n receive(msg) {\n if (live) dispatch(msg);\n else pending.push(msg);\n },\n sync(projection, state, ctx, root) {\n const value = projection.value(state, ctx);\n if (projection.state === undefined) internals.setFormValue(value);\n else internals.setFormValue(value, projection.state(state, ctx));\n const validity = projection.validity?.(state, ctx);\n const anchor =\n validity?.anchor === undefined ? undefined : root.querySelector(validity.anchor);\n if (anchor != null && !(anchor instanceof HTMLElement)) {\n throw new TypeError('The form validity anchor must select an HTMLElement.');\n }\n internals.setValidity(validity?.flags ?? {}, validity?.message ?? '', anchor ?? undefined);\n live = true;\n if (pending.length > 0) {\n const messages = pending.splice(0);\n afterRender(POST_HYDRATED, () => {\n for (const msg of messages) dispatch(msg);\n });\n }\n },\n };\n controls.set(host, control);\n return control;\n}\n\nexport const formFeature = {\n create<S, P>(\n host: HTMLElement,\n model: Pick<HostModel<unknown, unknown>, 'hasReducer' | 'dispatch'>,\n projection: FormAssociation<S, P> | undefined,\n ): void {\n if (projection === undefined) return;\n projections.set(host, projection as unknown as FormAssociation<unknown, unknown>);\n createFormControl(host, model);\n },\n sync(host: HTMLElement, state: unknown, ctx: Ctx<unknown>, root: ParentNode): void {\n const projection = projections.get(host);\n if (projection !== undefined) controls.get(host)?.sync(projection, state, ctx, root);\n },\n install: installFormCallbacks,\n};\n"]}
package/dist/forms.d.ts CHANGED
@@ -1,21 +1,27 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
- import type { FieldIssue, FormFields, IntentParser, IntentRejected } from './types.js';
3
- /** A form's schema, shared by the client component and (with @gyral/ssr) the server route. */
4
- export interface FormDefinition<Schema extends StandardSchemaV1> {
5
- readonly _tag: 'FormDefinition';
6
- readonly schema: Schema;
2
+ import type { FieldIssue, FormFields, IntentInput, IntentParser, IntentRejected } from './types.js';
3
+ type In<Schema extends StandardSchemaV1> = StandardSchemaV1.InferInput<Schema>;
4
+ type Out<Schema extends StandardSchemaV1> = StandardSchemaV1.InferOutput<Schema>;
5
+ /** The definition's field names, when its input is an object; otherwise any string. */
6
+ export type FieldName<Schema extends StandardSchemaV1> = In<Schema> extends Readonly<Record<string, unknown>> ? keyof In<Schema> & string : string;
7
+ export interface FormOptions<Schema extends StandardSchemaV1> {
8
+ /**
9
+ * Fields whose submitted values are never sent back: not in `IntentRejected.values`, not in
10
+ * `values()`, not in the devtools timeline. Any posted field name may be listed, including
11
+ * fields outside the schema. Names containing `password`, `passcode`, `csrf` or `xsrf` are
12
+ * secret without being listed (the browser never restores a password field either, and a
13
+ * CSRF token must come fresh from the server, not from the echo).
14
+ */
15
+ readonly secret?: readonly (FieldName<Schema> | (string & {}))[];
7
16
  }
8
- export declare function defineForm<Schema extends StandardSchemaV1>(schema: Schema): FormDefinition<Schema>;
9
- export type FormValue = FormDataEntryValue | readonly FormDataEntryValue[];
10
17
  /**
11
- * FormData to a plain object. The same function runs on the server, so both sides agree.
12
- * Repeated names become arrays; a `name[]` suffix always yields an array (named `name`).
13
- * Values stay strings (or Files): the schema coerces them.
18
+ * A form's submitted text values, keyed by the definition's fields, for re-rendering the form
19
+ * with what was typed (sticky fields). The same shape as `IntentRejected.values`: repeated names
20
+ * (and `name[]`) are arrays; `File`s and secret fields are left out.
14
21
  */
15
- export declare function formDataToObject(data: FormData): Readonly<Record<string, FormValue>>;
16
- type Out<Schema extends StandardSchemaV1> = StandardSchemaV1.InferOutput<Schema>;
17
- /** The text fields of a parsed form, for re-filling it after a rejection. */
18
- export declare function formFields(data: Readonly<Record<string, FormValue>>): FormFields;
22
+ export type FormValues<Schema extends StandardSchemaV1> = In<Schema> extends Readonly<Record<string, unknown>> ? {
23
+ readonly [K in keyof In<Schema> & string]?: string | readonly string[];
24
+ } : FormFields;
19
25
  export type FormResult<T> = {
20
26
  readonly ok: true;
21
27
  readonly data: T;
@@ -23,22 +29,35 @@ export type FormResult<T> = {
23
29
  readonly ok: false;
24
30
  readonly rejected: IntentRejected;
25
31
  };
26
- /**
27
- * Validates submitted form data exactly as `form()` does. The no-JS server path
28
- * (`formAction` in @gyral/ssr) uses it, so both paths produce the same `IntentRejected`.
29
- */
30
- export declare function validateForm<Schema extends StandardSchemaV1>(definition: FormDefinition<Schema> | Schema, intent: string, data: FormData): FormResult<Out<Schema>> | Promise<FormResult<Out<Schema>>>;
32
+ /** A form's schema, shared by the client component and (with `@gyral/ssr`) the server route. */
33
+ export interface FormDefinition<Schema extends StandardSchemaV1> {
34
+ readonly _tag: 'FormDefinition';
35
+ readonly schema: Schema;
36
+ /** The fields listed as secret (names containing `password`, `passcode`, `csrf` or `xsrf` are secret too). */
37
+ readonly secret: readonly string[];
38
+ /**
39
+ * Validates submitted form data exactly as `form()` does; the server's `formAction` uses it,
40
+ * so both paths produce the same `IntentRejected` (`intent` defaults to `''`).
41
+ */
42
+ readonly validate: (data: FormData, intent?: string) => FormResult<Out<Schema>> | Promise<FormResult<Out<Schema>>>;
43
+ /** The submitted text values without secrets: what a re-rendered form may show again. */
44
+ readonly values: (data: FormData) => FormValues<Schema>;
45
+ }
46
+ /** Whether `name` is never echoed: declared secret by a form, or a password-like name. */
47
+ export declare function isSecretField(name: string): boolean;
48
+ export declare function defineForm<Schema extends StandardSchemaV1>(schema: Schema, options?: FormOptions<Schema>): FormDefinition<Schema>;
49
+ export type FormValue = FormDataEntryValue | readonly FormDataEntryValue[];
31
50
  /**
32
51
  * Intent parser for a `<form data-intent>` submission, validated by a schema. `toMsg` also
33
52
  * gets the raw `FormData`, for sending the submission to the server (`submitForm` in
34
- * @gyral/http) after it passed client-side validation.
53
+ * `@gyral/http`) after it passed client-side validation.
35
54
  */
36
- export declare function form<Schema extends StandardSchemaV1, M>(definition: FormDefinition<Schema> | Schema, toMsg: (data: Out<Schema>, formData: FormData) => M | undefined): IntentParser<M>;
55
+ export declare function form<Schema extends StandardSchemaV1, M>(definition: FormDefinition<Schema> | Schema, toMsg: (data: Out<Schema>, formData: FormData) => M | undefined): (input: IntentInput) => ReturnType<IntentParser<M>>;
37
56
  /**
38
57
  * Intent parser for one control (pair with `data-intent-on="input"` for live checks).
39
58
  * Validates `checked` for checkboxes and radios, otherwise `value`.
40
59
  */
41
- export declare function field<Schema extends StandardSchemaV1, M>(schema: Schema, toMsg: (value: Out<Schema>) => M | undefined): IntentParser<M>;
60
+ export declare function field<Schema extends StandardSchemaV1, M>(schema: Schema, toMsg: (value: Out<Schema>) => M | undefined): (input: IntentInput) => ReturnType<IntentParser<M>>;
42
61
  /**
43
62
  * Groups issues by field name. Call it in the `IntentRejected` reducer and keep the result in
44
63
  * state: `invalid()` re-applies an error only when it gets a new array.
@@ -46,14 +65,7 @@ export declare function field<Schema extends StandardSchemaV1, M>(schema: Schema
46
65
  export declare function fieldErrors(issues: readonly FieldIssue[]): Readonly<Record<string, readonly string[]>>;
47
66
  /**
48
67
  * A Standard Schema for `IntentRejected` as a server sends it (`formAction` answering JSON).
49
- * Used by `submitForm` in @gyral/http to recognise a 422 rejection; usable with any decoder.
68
+ * Used by `submitForm` in `@gyral/http` to recognise a 422 rejection; usable with any decoder.
50
69
  */
51
70
  export declare const intentRejectedSchema: StandardSchemaV1<unknown, IntentRejected>;
52
- /** A server's JSON answer to a valid form post that would have redirected without JS. */
53
- export interface FormRedirected {
54
- readonly _tag: 'Redirected';
55
- readonly location: string;
56
- }
57
- /** The `location` of a `FormRedirected` answer (from `formAction` in @gyral/ssr), if it is one. */
58
- export declare function redirectedTo(body: unknown): string | undefined;
59
71
  export {};
package/dist/forms.js CHANGED
@@ -1,12 +1,91 @@
1
- export function defineForm(schema) {
2
- return { _tag: 'FormDefinition', schema };
1
+ import { DEV } from '#view-dev';
2
+ // Every secret name a definition declared, for redacting previews (the devtools timeline).
3
+ const declaredSecrets = /* @__PURE__ */ new Set();
4
+ const DEFAULT_SECRET = /password|passcode|csrf|xsrf/i;
5
+ /** Whether `name` is never echoed: declared secret by a form, or a password-like name. */
6
+ export function isSecretField(name) {
7
+ return DEFAULT_SECRET.test(name) || declaredSecrets.has(name);
8
+ }
9
+ // Words that suggest a secret; an unlisted field named with one gets a development warning.
10
+ const SUSPECT = /* @__PURE__ */ new Set([
11
+ 'password',
12
+ 'passcode',
13
+ 'token',
14
+ 'secret',
15
+ 'card',
16
+ 'cvc',
17
+ 'cvv',
18
+ 'ssn',
19
+ 'pin',
20
+ ]);
21
+ const warned = /* @__PURE__ */ new Set();
22
+ function warnUnmarked(key) {
23
+ if (warned.has(key))
24
+ return;
25
+ const words = key
26
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
27
+ .toLowerCase()
28
+ .split(/[^a-z0-9]+/);
29
+ if (!words.some((w) => SUSPECT.has(w)))
30
+ return;
31
+ warned.add(key);
32
+ console.warn(`Gyral: form field "${key}" looks secret but isn't listed in defineForm(schema, { secret }); ` +
33
+ 'its value is echoed back after a rejection.');
34
+ }
35
+ /**
36
+ * The text values of a parsed submission, for re-filling the form after a rejection: Files and
37
+ * the fields `secret` names (or that look like passwords) are left out.
38
+ */
39
+ function stickyValues(data, secret) {
40
+ const out = {};
41
+ for (const [key, value] of Object.entries(data)) {
42
+ if (DEFAULT_SECRET.test(key) || secret.includes(key))
43
+ continue;
44
+ if (DEV)
45
+ warnUnmarked(key);
46
+ if (typeof value === 'string')
47
+ out[key] = value;
48
+ else if (Array.isArray(value))
49
+ out[key] = value.filter((v) => typeof v === 'string');
50
+ }
51
+ return out;
52
+ }
53
+ function validate(schema, secret, intent, data) {
54
+ const raw = formDataToObject(data);
55
+ const settle = (result) => result.issues === undefined
56
+ ? { ok: true, data: result.value }
57
+ : {
58
+ ok: false,
59
+ rejected: {
60
+ _tag: 'IntentRejected',
61
+ intent,
62
+ issues: toIssues(result.issues, ''),
63
+ values: stickyValues(raw, secret),
64
+ },
65
+ };
66
+ const result = schema['~standard'].validate(raw);
67
+ return result instanceof Promise ? result.then(settle) : settle(result);
68
+ }
69
+ export function defineForm(schema, options = {}) {
70
+ const secret = options.secret ?? [];
71
+ // Only the devtools timeline reads these, and it only runs in development.
72
+ if (DEV)
73
+ for (const name of secret)
74
+ declaredSecrets.add(name);
75
+ return {
76
+ _tag: 'FormDefinition',
77
+ schema,
78
+ secret,
79
+ validate: (data, intent = '') => validate(schema, secret, intent, data),
80
+ values: (data) => stickyValues(formDataToObject(data), secret),
81
+ };
3
82
  }
4
83
  /**
5
84
  * FormData to a plain object. The same function runs on the server, so both sides agree.
6
85
  * Repeated names become arrays; a `name[]` suffix always yields an array (named `name`).
7
86
  * Values stay strings (or Files): the schema coerces them.
8
87
  */
9
- export function formDataToObject(data) {
88
+ function formDataToObject(data) {
10
89
  const out = {};
11
90
  for (const key of new Set(data.keys())) {
12
91
  const values = data.getAll(key);
@@ -34,39 +113,10 @@ function parse(schema, value, intent, fallbackPath, toMsg) {
34
113
  const result = schema['~standard'].validate(value);
35
114
  return result instanceof Promise ? result.then(settle) : settle(result);
36
115
  }
37
- /** The text fields of a parsed form, for re-filling it after a rejection. */
38
- export function formFields(data) {
39
- const out = {};
40
- for (const [key, value] of Object.entries(data)) {
41
- if (typeof value === 'string')
42
- out[key] = value;
43
- else if (Array.isArray(value))
44
- out[key] = value.filter((v) => typeof v === 'string');
45
- }
46
- return out;
47
- }
48
- const schemaOf = (definition) => ('_tag' in definition ? definition.schema : definition);
49
- /**
50
- * Validates submitted form data exactly as `form()` does. The no-JS server path
51
- * (`formAction` in @gyral/ssr) uses it, so both paths produce the same `IntentRejected`.
52
- */
53
- export function validateForm(definition, intent, data) {
54
- const raw = formDataToObject(data);
55
- const values = formFields(raw);
56
- const rejected = (issues) => ({
57
- ok: false,
58
- rejected: { _tag: 'IntentRejected', intent, issues, values },
59
- });
60
- const settle = (result) => result.issues === undefined
61
- ? { ok: true, data: result.value }
62
- : rejected(toIssues(result.issues, ''));
63
- const result = schemaOf(definition)['~standard'].validate(raw);
64
- return result instanceof Promise ? result.then(settle) : settle(result);
65
- }
66
116
  /**
67
117
  * Intent parser for a `<form data-intent>` submission, validated by a schema. `toMsg` also
68
118
  * gets the raw `FormData`, for sending the submission to the server (`submitForm` in
69
- * @gyral/http) after it passed client-side validation.
119
+ * `@gyral/http`) after it passed client-side validation.
70
120
  */
71
121
  export function form(definition, toMsg) {
72
122
  return (input) => {
@@ -74,7 +124,7 @@ export function form(definition, toMsg) {
74
124
  if (formData === undefined)
75
125
  return undefined;
76
126
  const toParsed = (r) => r.ok ? toMsg(r.data, formData) : r.rejected;
77
- const result = validateForm(definition, input.name, formData);
127
+ const result = ('_tag' in definition ? definition : defineForm(definition)).validate(formData, input.name);
78
128
  return result instanceof Promise ? result.then(toParsed) : toParsed(result);
79
129
  };
80
130
  }
@@ -104,7 +154,7 @@ const isFields = (value) => isRecord(value) &&
104
154
  Object.values(value).every((v) => typeof v === 'string' || (Array.isArray(v) && v.every((x) => typeof x === 'string')));
105
155
  /**
106
156
  * A Standard Schema for `IntentRejected` as a server sends it (`formAction` answering JSON).
107
- * Used by `submitForm` in @gyral/http to recognise a 422 rejection; usable with any decoder.
157
+ * Used by `submitForm` in `@gyral/http` to recognise a 422 rejection; usable with any decoder.
108
158
  */
109
159
  export const intentRejectedSchema = {
110
160
  '~standard': {
@@ -129,10 +179,4 @@ export const intentRejectedSchema = {
129
179
  },
130
180
  },
131
181
  };
132
- /** The `location` of a `FormRedirected` answer (from `formAction` in @gyral/ssr), if it is one. */
133
- export function redirectedTo(body) {
134
- return isRecord(body) && body['_tag'] === 'Redirected' && typeof body['location'] === 'string'
135
- ? body['location']
136
- : undefined;
137
- }
138
182
  //# sourceMappingURL=forms.js.map
package/dist/forms.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"forms.js","sourceRoot":"","sources":["../src/forms.ts"],"names":[],"mappings":"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","sourcesContent":["// Forms, client half (docs/design-docs/0008-forms.md). Schemas are any Standard Schema v1.\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { FieldIssue, FormFields, IntentInput, IntentParser, IntentRejected } from './types.js';\n\n/** A form's schema, shared by the client component and (with @gyral/ssr) the server route. */\nexport interface FormDefinition<Schema extends StandardSchemaV1> {\n readonly _tag: 'FormDefinition';\n readonly schema: Schema;\n}\n\nexport function defineForm<Schema extends StandardSchemaV1>(\n schema: Schema,\n): FormDefinition<Schema> {\n return { _tag: 'FormDefinition', schema };\n}\n\nexport type FormValue = FormDataEntryValue | readonly FormDataEntryValue[];\n\n/**\n * FormData to a plain object. The same function runs on the server, so both sides agree.\n * Repeated names become arrays; a `name[]` suffix always yields an array (named `name`).\n * Values stay strings (or Files): the schema coerces them.\n */\nexport function formDataToObject(data: FormData): Readonly<Record<string, FormValue>> {\n const out: Record<string, FormValue> = {};\n for (const key of new Set(data.keys())) {\n const values = data.getAll(key);\n const [first] = values;\n if (key.endsWith('[]')) out[key.slice(0, -2)] = values;\n else if (values.length === 1 && first !== undefined) out[key] = first;\n else out[key] = values;\n }\n return out;\n}\n\nconst segment = (s: PropertyKey | StandardSchemaV1.PathSegment): string =>\n String(typeof s === 'object' ? s.key : s);\n\nfunction toIssues(\n issues: ReadonlyArray<StandardSchemaV1.Issue>,\n fallbackPath: string,\n): FieldIssue[] {\n return issues.map((issue) => {\n const path = issue.path?.map(segment).join('.') ?? '';\n return { path: path === '' ? fallbackPath : path, message: issue.message };\n });\n}\n\ntype Out<Schema extends StandardSchemaV1> = StandardSchemaV1.InferOutput<Schema>;\ntype Parsed<M> = M | IntentRejected | undefined;\n\nfunction parse<Schema extends StandardSchemaV1, M>(\n schema: Schema,\n value: unknown,\n intent: string,\n fallbackPath: string,\n toMsg: (data: Out<Schema>) => M | undefined,\n): Parsed<M> | Promise<Parsed<M>> {\n const settle = (result: StandardSchemaV1.Result<Out<Schema>>): Parsed<M> =>\n result.issues === undefined\n ? toMsg(result.value)\n : { _tag: 'IntentRejected', intent, issues: toIssues(result.issues, fallbackPath) };\n const result = schema['~standard'].validate(value);\n return result instanceof Promise ? result.then(settle) : settle(result);\n}\n\n/** The text fields of a parsed form, for re-filling it after a rejection. */\nexport function formFields(data: Readonly<Record<string, FormValue>>): FormFields {\n const out: Record<string, string | readonly string[]> = {};\n for (const [key, value] of Object.entries(data)) {\n if (typeof value === 'string') out[key] = value;\n else if (Array.isArray(value)) out[key] = value.filter((v) => typeof v === 'string');\n }\n return out;\n}\n\nconst schemaOf = <Schema extends StandardSchemaV1>(\n definition: FormDefinition<Schema> | Schema,\n): Schema => ('_tag' in definition ? definition.schema : definition);\n\nexport type FormResult<T> =\n | { readonly ok: true; readonly data: T }\n | { readonly ok: false; readonly rejected: IntentRejected };\n\n/**\n * Validates submitted form data exactly as `form()` does. The no-JS server path\n * (`formAction` in @gyral/ssr) uses it, so both paths produce the same `IntentRejected`.\n */\nexport function validateForm<Schema extends StandardSchemaV1>(\n definition: FormDefinition<Schema> | Schema,\n intent: string,\n data: FormData,\n): FormResult<Out<Schema>> | Promise<FormResult<Out<Schema>>> {\n const raw = formDataToObject(data);\n const values = formFields(raw);\n const rejected = (issues: readonly FieldIssue[]): FormResult<Out<Schema>> => ({\n ok: false,\n rejected: { _tag: 'IntentRejected', intent, issues, values },\n });\n const settle = (result: StandardSchemaV1.Result<Out<Schema>>): FormResult<Out<Schema>> =>\n result.issues === undefined\n ? { ok: true, data: result.value }\n : rejected(toIssues(result.issues, ''));\n const result = schemaOf(definition)['~standard'].validate(raw);\n return result instanceof Promise ? result.then(settle) : settle(result);\n}\n\n/**\n * Intent parser for a `<form data-intent>` submission, validated by a schema. `toMsg` also\n * gets the raw `FormData`, for sending the submission to the server (`submitForm` in\n * @gyral/http) after it passed client-side validation.\n */\nexport function form<Schema extends StandardSchemaV1, M>(\n definition: FormDefinition<Schema> | Schema,\n toMsg: (data: Out<Schema>, formData: FormData) => M | undefined,\n): IntentParser<M> {\n return (input: IntentInput) => {\n const { formData } = input;\n if (formData === undefined) return undefined;\n const toParsed = (r: FormResult<Out<Schema>>): Parsed<M> =>\n r.ok ? toMsg(r.data, formData) : r.rejected;\n const result = validateForm(definition, input.name, formData);\n return result instanceof Promise ? result.then(toParsed) : toParsed(result);\n };\n}\n\n/**\n * Intent parser for one control (pair with `data-intent-on=\"input\"` for live checks).\n * Validates `checked` for checkboxes and radios, otherwise `value`.\n */\nexport function field<Schema extends StandardSchemaV1, M>(\n schema: Schema,\n toMsg: (value: Out<Schema>) => M | undefined,\n): IntentParser<M> {\n return (input: IntentInput) => {\n const name = input.target.getAttribute('name') ?? input.name;\n return parse(schema, input.checked ?? input.value, input.name, name, toMsg);\n };\n}\n\n/**\n * Groups issues by field name. Call it in the `IntentRejected` reducer and keep the result in\n * state: `invalid()` re-applies an error only when it gets a new array.\n */\nexport function fieldErrors(\n issues: readonly FieldIssue[],\n): Readonly<Record<string, readonly string[]>> {\n const out: Record<string, string[]> = {};\n for (const { path, message } of issues) (out[path] ??= []).push(message);\n return out;\n}\n\nconst isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>\n typeof value === 'object' && value !== null;\n\nconst isIssue = (value: unknown): value is FieldIssue =>\n isRecord(value) && typeof value['path'] === 'string' && typeof value['message'] === 'string';\n\nconst isFields = (value: unknown): value is FormFields =>\n isRecord(value) &&\n Object.values(value).every(\n (v) => typeof v === 'string' || (Array.isArray(v) && v.every((x) => typeof x === 'string')),\n );\n\n/**\n * A Standard Schema for `IntentRejected` as a server sends it (`formAction` answering JSON).\n * Used by `submitForm` in @gyral/http to recognise a 422 rejection; usable with any decoder.\n */\nexport const intentRejectedSchema: StandardSchemaV1<unknown, IntentRejected> = {\n '~standard': {\n version: 1,\n vendor: 'gyral',\n validate: (value) => {\n if (\n !isRecord(value) ||\n value['_tag'] !== 'IntentRejected' ||\n typeof value['intent'] !== 'string' ||\n !Array.isArray(value['issues']) ||\n !value['issues'].every(isIssue) ||\n (value['values'] !== undefined && !isFields(value['values']))\n ) {\n return { issues: [{ message: 'Expected an IntentRejected message' }] };\n }\n const rejected: IntentRejected = {\n _tag: 'IntentRejected',\n intent: value['intent'],\n issues: value['issues'],\n ...(value['values'] === undefined ? {} : { values: value['values'] }),\n };\n return { value: rejected };\n },\n },\n};\n\n/** A server's JSON answer to a valid form post that would have redirected without JS. */\nexport interface FormRedirected {\n readonly _tag: 'Redirected';\n readonly location: string;\n}\n\n/** The `location` of a `FormRedirected` answer (from `formAction` in @gyral/ssr), if it is one. */\nexport function redirectedTo(body: unknown): string | undefined {\n return isRecord(body) && body['_tag'] === 'Redirected' && typeof body['location'] === 'string'\n ? body['location']\n : undefined;\n}\n"]}
1
+ {"version":3,"file":"forms.js","sourceRoot":"","sources":["../src/forms.ts"],"names":[],"mappings":"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","sourcesContent":["// Forms, client half (docs/design-docs/0008-forms.md). Schemas are any Standard Schema v1.\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { DEV } from '#view-dev';\nimport type { FieldIssue, FormFields, IntentInput, IntentParser, IntentRejected } from './types.js';\n\ntype In<Schema extends StandardSchemaV1> = StandardSchemaV1.InferInput<Schema>;\ntype Out<Schema extends StandardSchemaV1> = StandardSchemaV1.InferOutput<Schema>;\n\n/** The definition's field names, when its input is an object; otherwise any string. */\nexport type FieldName<Schema extends StandardSchemaV1> =\n In<Schema> extends Readonly<Record<string, unknown>> ? keyof In<Schema> & string : string;\n\nexport interface FormOptions<Schema extends StandardSchemaV1> {\n /**\n * Fields whose submitted values are never sent back: not in `IntentRejected.values`, not in\n * `values()`, not in the devtools timeline. Any posted field name may be listed, including\n * fields outside the schema. Names containing `password`, `passcode`, `csrf` or `xsrf` are\n * secret without being listed (the browser never restores a password field either, and a\n * CSRF token must come fresh from the server, not from the echo).\n */\n readonly secret?: readonly (FieldName<Schema> | (string & {}))[];\n}\n\n/**\n * A form's submitted text values, keyed by the definition's fields, for re-rendering the form\n * with what was typed (sticky fields). The same shape as `IntentRejected.values`: repeated names\n * (and `name[]`) are arrays; `File`s and secret fields are left out.\n */\nexport type FormValues<Schema extends StandardSchemaV1> =\n In<Schema> extends Readonly<Record<string, unknown>>\n ? { readonly [K in keyof In<Schema> & string]?: string | readonly string[] }\n : FormFields;\n\nexport type FormResult<T> =\n | { readonly ok: true; readonly data: T }\n | { readonly ok: false; readonly rejected: IntentRejected };\n\n/** A form's schema, shared by the client component and (with `@gyral/ssr`) the server route. */\nexport interface FormDefinition<Schema extends StandardSchemaV1> {\n readonly _tag: 'FormDefinition';\n readonly schema: Schema;\n /** The fields listed as secret (names containing `password`, `passcode`, `csrf` or `xsrf` are secret too). */\n readonly secret: readonly string[];\n /**\n * Validates submitted form data exactly as `form()` does; the server's `formAction` uses it,\n * so both paths produce the same `IntentRejected` (`intent` defaults to `''`).\n */\n readonly validate: (\n data: FormData,\n intent?: string,\n ) => FormResult<Out<Schema>> | Promise<FormResult<Out<Schema>>>;\n /** The submitted text values without secrets: what a re-rendered form may show again. */\n readonly values: (data: FormData) => FormValues<Schema>;\n}\n\n// Every secret name a definition declared, for redacting previews (the devtools timeline).\nconst declaredSecrets = /* @__PURE__ */ new Set<string>();\n\nconst DEFAULT_SECRET = /password|passcode|csrf|xsrf/i;\n\n/** Whether `name` is never echoed: declared secret by a form, or a password-like name. */\nexport function isSecretField(name: string): boolean {\n return DEFAULT_SECRET.test(name) || declaredSecrets.has(name);\n}\n\n// Words that suggest a secret; an unlisted field named with one gets a development warning.\nconst SUSPECT = /* @__PURE__ */ new Set([\n 'password',\n 'passcode',\n 'token',\n 'secret',\n 'card',\n 'cvc',\n 'cvv',\n 'ssn',\n 'pin',\n]);\nconst warned = /* @__PURE__ */ new Set<string>();\n\nfunction warnUnmarked(key: string): void {\n if (warned.has(key)) return;\n const words = key\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .toLowerCase()\n .split(/[^a-z0-9]+/);\n if (!words.some((w) => SUSPECT.has(w))) return;\n warned.add(key);\n console.warn(\n `Gyral: form field \"${key}\" looks secret but isn't listed in defineForm(schema, { secret }); ` +\n 'its value is echoed back after a rejection.',\n );\n}\n\n/**\n * The text values of a parsed submission, for re-filling the form after a rejection: Files and\n * the fields `secret` names (or that look like passwords) are left out.\n */\nfunction stickyValues(\n data: Readonly<Record<string, FormValue>>,\n secret: readonly string[],\n): FormFields {\n const out: Record<string, string | readonly string[]> = {};\n for (const [key, value] of Object.entries(data)) {\n if (DEFAULT_SECRET.test(key) || secret.includes(key)) continue;\n if (DEV) warnUnmarked(key);\n if (typeof value === 'string') out[key] = value;\n else if (Array.isArray(value)) out[key] = value.filter((v) => typeof v === 'string');\n }\n return out;\n}\n\nfunction validate<Schema extends StandardSchemaV1>(\n schema: Schema,\n secret: readonly string[],\n intent: string,\n data: FormData,\n): FormResult<Out<Schema>> | Promise<FormResult<Out<Schema>>> {\n const raw = formDataToObject(data);\n const settle = (result: StandardSchemaV1.Result<Out<Schema>>): FormResult<Out<Schema>> =>\n result.issues === undefined\n ? { ok: true, data: result.value }\n : {\n ok: false,\n rejected: {\n _tag: 'IntentRejected',\n intent,\n issues: toIssues(result.issues, ''),\n values: stickyValues(raw, secret),\n },\n };\n const result = schema['~standard'].validate(raw);\n return result instanceof Promise ? result.then(settle) : settle(result);\n}\n\nexport function defineForm<Schema extends StandardSchemaV1>(\n schema: Schema,\n options: FormOptions<Schema> = {},\n): FormDefinition<Schema> {\n const secret: readonly string[] = options.secret ?? [];\n // Only the devtools timeline reads these, and it only runs in development.\n if (DEV) for (const name of secret) declaredSecrets.add(name);\n return {\n _tag: 'FormDefinition',\n schema,\n secret,\n validate: (data, intent = '') => validate(schema, secret, intent, data),\n values: (data) => stickyValues(formDataToObject(data), secret) as FormValues<Schema>,\n };\n}\n\nexport type FormValue = FormDataEntryValue | readonly FormDataEntryValue[];\n\n/**\n * FormData to a plain object. The same function runs on the server, so both sides agree.\n * Repeated names become arrays; a `name[]` suffix always yields an array (named `name`).\n * Values stay strings (or Files): the schema coerces them.\n */\nfunction formDataToObject(data: FormData): Readonly<Record<string, FormValue>> {\n const out: Record<string, FormValue> = {};\n for (const key of new Set(data.keys())) {\n const values = data.getAll(key);\n const [first] = values;\n if (key.endsWith('[]')) out[key.slice(0, -2)] = values;\n else if (values.length === 1 && first !== undefined) out[key] = first;\n else out[key] = values;\n }\n return out;\n}\n\nconst segment = (s: PropertyKey | StandardSchemaV1.PathSegment): string =>\n String(typeof s === 'object' ? s.key : s);\n\nfunction toIssues(\n issues: ReadonlyArray<StandardSchemaV1.Issue>,\n fallbackPath: string,\n): FieldIssue[] {\n return issues.map((issue) => {\n const path = issue.path?.map(segment).join('.') ?? '';\n return { path: path === '' ? fallbackPath : path, message: issue.message };\n });\n}\n\ntype Parsed<M> = M | IntentRejected | undefined;\n\nfunction parse<Schema extends StandardSchemaV1, M>(\n schema: Schema,\n value: unknown,\n intent: string,\n fallbackPath: string,\n toMsg: (data: Out<Schema>) => M | undefined,\n): Parsed<M> | Promise<Parsed<M>> {\n const settle = (result: StandardSchemaV1.Result<Out<Schema>>): Parsed<M> =>\n result.issues === undefined\n ? toMsg(result.value)\n : { _tag: 'IntentRejected', intent, issues: toIssues(result.issues, fallbackPath) };\n const result = schema['~standard'].validate(value);\n return result instanceof Promise ? result.then(settle) : settle(result);\n}\n\n/**\n * Intent parser for a `<form data-intent>` submission, validated by a schema. `toMsg` also\n * gets the raw `FormData`, for sending the submission to the server (`submitForm` in\n * `@gyral/http`) after it passed client-side validation.\n */\nexport function form<Schema extends StandardSchemaV1, M>(\n definition: FormDefinition<Schema> | Schema,\n toMsg: (data: Out<Schema>, formData: FormData) => M | undefined,\n): (input: IntentInput) => ReturnType<IntentParser<M>> {\n return (input: IntentInput) => {\n const { formData } = input;\n if (formData === undefined) return undefined;\n const toParsed = (r: FormResult<Out<Schema>>): Parsed<M> =>\n r.ok ? toMsg(r.data, formData) : r.rejected;\n const result = ('_tag' in definition ? definition : defineForm(definition)).validate(\n formData,\n input.name,\n );\n return result instanceof Promise ? result.then(toParsed) : toParsed(result);\n };\n}\n\n/**\n * Intent parser for one control (pair with `data-intent-on=\"input\"` for live checks).\n * Validates `checked` for checkboxes and radios, otherwise `value`.\n */\nexport function field<Schema extends StandardSchemaV1, M>(\n schema: Schema,\n toMsg: (value: Out<Schema>) => M | undefined,\n): (input: IntentInput) => ReturnType<IntentParser<M>> {\n return (input: IntentInput) => {\n const name = input.target.getAttribute('name') ?? input.name;\n return parse(schema, input.checked ?? input.value, input.name, name, toMsg);\n };\n}\n\n/**\n * Groups issues by field name. Call it in the `IntentRejected` reducer and keep the result in\n * state: `invalid()` re-applies an error only when it gets a new array.\n */\nexport function fieldErrors(\n issues: readonly FieldIssue[],\n): Readonly<Record<string, readonly string[]>> {\n const out: Record<string, string[]> = {};\n for (const { path, message } of issues) (out[path] ??= []).push(message);\n return out;\n}\n\nconst isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>\n typeof value === 'object' && value !== null;\n\nconst isIssue = (value: unknown): value is FieldIssue =>\n isRecord(value) && typeof value['path'] === 'string' && typeof value['message'] === 'string';\n\nconst isFields = (value: unknown): value is FormFields =>\n isRecord(value) &&\n Object.values(value).every(\n (v) => typeof v === 'string' || (Array.isArray(v) && v.every((x) => typeof x === 'string')),\n );\n\n/**\n * A Standard Schema for `IntentRejected` as a server sends it (`formAction` answering JSON).\n * Used by `submitForm` in `@gyral/http` to recognise a 422 rejection; usable with any decoder.\n */\nexport const intentRejectedSchema: StandardSchemaV1<unknown, IntentRejected> = {\n '~standard': {\n version: 1,\n vendor: 'gyral',\n validate: (value) => {\n if (\n !isRecord(value) ||\n value['_tag'] !== 'IntentRejected' ||\n typeof value['intent'] !== 'string' ||\n !Array.isArray(value['issues']) ||\n !value['issues'].every(isIssue) ||\n (value['values'] !== undefined && !isFields(value['values']))\n ) {\n return { issues: [{ message: 'Expected an IntentRejected message' }] };\n }\n const rejected: IntentRejected = {\n _tag: 'IntentRejected',\n intent: value['intent'],\n issues: value['issues'],\n ...(value['values'] === undefined ? {} : { values: value['values'] }),\n };\n return { value: rejected };\n },\n },\n};\n"]}
@@ -0,0 +1,11 @@
1
+ import type { HostTask } from './scheduler.js';
2
+ /** The frame lane as the scheduler uses it. */
3
+ export interface FrameLane {
4
+ /** Marks `task` for the next frame; then `mark` moves each waiting host to a microtask flush. */
5
+ add(task: HostTask, mark: (task: HostTask) => void): void;
6
+ /** A microtask mark took `task` out of the frame lane. */
7
+ drop(task: HostTask): void;
8
+ /** Hosts are waiting for a frame (settled() waits for them). */
9
+ pending(): boolean;
10
+ }
11
+ export declare const frameLane: FrameLane;