@gyral/core 0.2.0 → 0.3.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/CHANGELOG.md +87 -0
  2. package/README.md +7 -5
  3. package/dist/children.d.ts +5 -2
  4. package/dist/children.js +8 -3
  5. package/dist/children.js.map +1 -1
  6. package/dist/command.js +6 -0
  7. package/dist/command.js.map +1 -1
  8. package/dist/compiler/ast.d.ts +21 -0
  9. package/dist/compiler/ast.js +145 -0
  10. package/dist/compiler/ast.js.map +1 -0
  11. package/dist/compiler/compile.d.ts +38 -0
  12. package/dist/compiler/compile.js +129 -0
  13. package/dist/compiler/compile.js.map +1 -0
  14. package/dist/compiler/hooks.d.ts +18 -0
  15. package/dist/compiler/hooks.js +186 -0
  16. package/dist/compiler/hooks.js.map +1 -0
  17. package/dist/compiler/parse5-check.d.ts +21 -0
  18. package/dist/compiler/parse5-check.js +55 -0
  19. package/dist/compiler/parse5-check.js.map +1 -0
  20. package/dist/compiler/scan.d.ts +31 -0
  21. package/dist/compiler/scan.js +152 -0
  22. package/dist/compiler/scan.js.map +1 -0
  23. package/dist/compiler/source.d.ts +34 -0
  24. package/dist/compiler/source.js +114 -0
  25. package/dist/compiler/source.js.map +1 -0
  26. package/dist/define.d.ts +2 -2
  27. package/dist/define.js +35 -327
  28. package/dist/define.js.map +1 -1
  29. package/dist/devtools-events.d.ts +5 -0
  30. package/dist/devtools-events.js.map +1 -1
  31. package/dist/devtools-off.d.ts +1 -0
  32. package/dist/devtools-off.js +1 -0
  33. package/dist/devtools-off.js.map +1 -1
  34. package/dist/devtools.d.ts +1 -0
  35. package/dist/devtools.js +3 -2
  36. package/dist/devtools.js.map +1 -1
  37. package/dist/drivers-scope.d.ts +1 -1
  38. package/dist/drivers-scope.js +1 -8
  39. package/dist/drivers-scope.js.map +1 -1
  40. package/dist/element-types.d.ts +5 -3
  41. package/dist/element-types.js.map +1 -1
  42. package/dist/element.d.ts +3 -0
  43. package/dist/element.js +269 -0
  44. package/dist/element.js.map +1 -0
  45. package/dist/eslint/imports.d.ts +24 -0
  46. package/dist/eslint/imports.js +78 -0
  47. package/dist/eslint/imports.js.map +1 -0
  48. package/dist/eslint/index.d.ts +15 -0
  49. package/dist/eslint/index.js +15 -0
  50. package/dist/eslint/index.js.map +1 -0
  51. package/dist/eslint/locate.d.ts +33 -0
  52. package/dist/eslint/locate.js +132 -0
  53. package/dist/eslint/locate.js.map +1 -0
  54. package/dist/eslint/row-purity-rule.d.ts +2 -0
  55. package/dist/eslint/row-purity-rule.js +103 -0
  56. package/dist/eslint/row-purity-rule.js.map +1 -0
  57. package/dist/eslint/template-rule.d.ts +5 -0
  58. package/dist/eslint/template-rule.js +66 -0
  59. package/dist/eslint/template-rule.js.map +1 -0
  60. package/dist/features.d.ts +39 -0
  61. package/dist/features.js +2 -0
  62. package/dist/features.js.map +1 -0
  63. package/dist/focus.d.ts +7 -12
  64. package/dist/focus.js +13 -10
  65. package/dist/focus.js.map +1 -1
  66. package/dist/hooks/invalid.d.ts +14 -0
  67. package/dist/hooks/invalid.js +72 -0
  68. package/dist/hooks/invalid.js.map +1 -0
  69. package/dist/hooks/labelled-by.d.ts +13 -0
  70. package/dist/hooks/labelled-by.js +51 -0
  71. package/dist/hooks/labelled-by.js.map +1 -0
  72. package/dist/host-model.d.ts +48 -0
  73. package/dist/host-model.js +155 -0
  74. package/dist/host-model.js.map +1 -0
  75. package/dist/hydration-client.d.ts +9 -0
  76. package/dist/hydration-client.js +35 -0
  77. package/dist/hydration-client.js.map +1 -0
  78. package/dist/hydration.d.ts +13 -19
  79. package/dist/hydration.js +45 -45
  80. package/dist/hydration.js.map +1 -1
  81. package/dist/index.d.ts +9 -17
  82. package/dist/index.js +9 -18
  83. package/dist/index.js.map +1 -1
  84. package/dist/intent.d.ts +31 -8
  85. package/dist/intent.js +82 -27
  86. package/dist/intent.js.map +1 -1
  87. package/dist/internal/interpreter.d.ts +7 -1
  88. package/dist/internal/interpreter.js +11 -4
  89. package/dist/internal/interpreter.js.map +1 -1
  90. package/dist/invokers-shim.d.ts +6 -0
  91. package/dist/invokers-shim.js +32 -0
  92. package/dist/invokers-shim.js.map +1 -0
  93. package/dist/invokers.d.ts +3 -5
  94. package/dist/invokers.js +11 -51
  95. package/dist/invokers.js.map +1 -1
  96. package/dist/islands.d.ts +0 -2
  97. package/dist/islands.js +4 -11
  98. package/dist/islands.js.map +1 -1
  99. package/dist/light-dom.d.ts +3 -52
  100. package/dist/light-dom.js +5 -100
  101. package/dist/light-dom.js.map +1 -1
  102. package/dist/prop.d.ts +66 -0
  103. package/dist/prop.js +22 -0
  104. package/dist/prop.js.map +1 -0
  105. package/dist/props.d.ts +52 -21
  106. package/dist/props.js +150 -34
  107. package/dist/props.js.map +1 -1
  108. package/dist/scheduler.d.ts +39 -0
  109. package/dist/scheduler.js +230 -0
  110. package/dist/scheduler.js.map +1 -0
  111. package/dist/server-component.d.ts +10 -0
  112. package/dist/server-component.js +102 -0
  113. package/dist/server-component.js.map +1 -0
  114. package/dist/server-specs.d.ts +6 -0
  115. package/dist/server-specs.js +13 -0
  116. package/dist/server-specs.js.map +1 -0
  117. package/dist/server.d.ts +28 -0
  118. package/dist/server.js +50 -0
  119. package/dist/server.js.map +1 -0
  120. package/dist/settled.d.ts +10 -0
  121. package/dist/settled.js +40 -0
  122. package/dist/settled.js.map +1 -0
  123. package/dist/states.d.ts +3 -3
  124. package/dist/states.js +4 -7
  125. package/dist/states.js.map +1 -1
  126. package/dist/store-binding.d.ts +6 -3
  127. package/dist/store-binding.js +5 -6
  128. package/dist/store-binding.js.map +1 -1
  129. package/dist/store-scope.d.ts +6 -7
  130. package/dist/store-scope.js +11 -26
  131. package/dist/store-scope.js.map +1 -1
  132. package/dist/store.d.ts +3 -0
  133. package/dist/store.js +9 -2
  134. package/dist/store.js.map +1 -1
  135. package/dist/stores-provider.d.ts +2 -6
  136. package/dist/stores-provider.js +21 -55
  137. package/dist/stores-provider.js.map +1 -1
  138. package/dist/transitions.d.ts +5 -6
  139. package/dist/transitions.js +11 -20
  140. package/dist/transitions.js.map +1 -1
  141. package/dist/types.d.ts +19 -23
  142. package/dist/types.js.map +1 -1
  143. package/dist/view/attributes.d.ts +6 -0
  144. package/dist/view/attributes.js +10 -0
  145. package/dist/view/attributes.js.map +1 -0
  146. package/dist/view/compiled.d.ts +1 -0
  147. package/dist/view/compiled.js +6 -0
  148. package/dist/view/compiled.js.map +1 -0
  149. package/dist/view/dev-off.d.ts +1 -0
  150. package/dist/view/dev-off.js +4 -0
  151. package/dist/view/dev-off.js.map +1 -0
  152. package/dist/view/dev.d.ts +1 -0
  153. package/dist/view/dev.js +5 -0
  154. package/dist/view/dev.js.map +1 -0
  155. package/dist/view/flags.d.ts +1 -0
  156. package/dist/view/flags.js +4 -0
  157. package/dist/view/flags.js.map +1 -0
  158. package/dist/view/index.d.ts +21 -0
  159. package/dist/view/index.js +21 -0
  160. package/dist/view/index.js.map +1 -0
  161. package/dist/view/normalize/check.d.ts +20 -0
  162. package/dist/view/normalize/check.js +78 -0
  163. package/dist/view/normalize/check.js.map +1 -0
  164. package/dist/view/normalize/emit.d.ts +13 -0
  165. package/dist/view/normalize/emit.js +136 -0
  166. package/dist/view/normalize/emit.js.map +1 -0
  167. package/dist/view/normalize/entities.d.ts +4 -0
  168. package/dist/view/normalize/entities.js +38 -0
  169. package/dist/view/normalize/entities.js.map +1 -0
  170. package/dist/view/normalize/errors.d.ts +9 -0
  171. package/dist/view/normalize/errors.js +19 -0
  172. package/dist/view/normalize/errors.js.map +1 -0
  173. package/dist/view/normalize/id.d.ts +7 -0
  174. package/dist/view/normalize/id.js +48 -0
  175. package/dist/view/normalize/id.js.map +1 -0
  176. package/dist/view/normalize/normalize.d.ts +15 -0
  177. package/dist/view/normalize/normalize.js +36 -0
  178. package/dist/view/normalize/normalize.js.map +1 -0
  179. package/dist/view/normalize/repairs.d.ts +24 -0
  180. package/dist/view/normalize/repairs.js +166 -0
  181. package/dist/view/normalize/repairs.js.map +1 -0
  182. package/dist/view/normalize/rules.d.ts +30 -0
  183. package/dist/view/normalize/rules.js +90 -0
  184. package/dist/view/normalize/rules.js.map +1 -0
  185. package/dist/view/normalize/shape.d.ts +5 -0
  186. package/dist/view/normalize/shape.js +38 -0
  187. package/dist/view/normalize/shape.js.map +1 -0
  188. package/dist/view/normalize/source.d.ts +13 -0
  189. package/dist/view/normalize/source.js +34 -0
  190. package/dist/view/normalize/source.js.map +1 -0
  191. package/dist/view/normalize/tag-states.d.ts +23 -0
  192. package/dist/view/normalize/tag-states.js +192 -0
  193. package/dist/view/normalize/tag-states.js.map +1 -0
  194. package/dist/view/normalize/tokenizer.d.ts +5 -0
  195. package/dist/view/normalize/tokenizer.js +233 -0
  196. package/dist/view/normalize/tokenizer.js.map +1 -0
  197. package/dist/view/normalize/tokens.d.ts +51 -0
  198. package/dist/view/normalize/tokens.js +24 -0
  199. package/dist/view/normalize/tokens.js.map +1 -0
  200. package/dist/view/normalize/tree.d.ts +59 -0
  201. package/dist/view/normalize/tree.js +190 -0
  202. package/dist/view/normalize/tree.js.map +1 -0
  203. package/dist/view/normalize/types.d.ts +126 -0
  204. package/dist/view/normalize/types.js +26 -0
  205. package/dist/view/normalize/types.js.map +1 -0
  206. package/dist/view/normalize/whitespace.d.ts +2 -0
  207. package/dist/{template-whitespace.js → view/normalize/whitespace.js} +24 -34
  208. package/dist/view/normalize/whitespace.js.map +1 -0
  209. package/dist/view/prepare-stub.d.ts +3 -0
  210. package/dist/view/prepare-stub.js +7 -0
  211. package/dist/view/prepare-stub.js.map +1 -0
  212. package/dist/view/prepare.d.ts +8 -0
  213. package/dist/view/prepare.js +57 -0
  214. package/dist/view/prepare.js.map +1 -0
  215. package/dist/view/registry.d.ts +63 -0
  216. package/dist/view/registry.js +19 -0
  217. package/dist/view/registry.js.map +1 -0
  218. package/dist/view/render/adopt-attr.d.ts +14 -0
  219. package/dist/view/render/adopt-attr.js +77 -0
  220. package/dist/view/render/adopt-attr.js.map +1 -0
  221. package/dist/view/render/adopt-plan.d.ts +10 -0
  222. package/dist/view/render/adopt-plan.js +26 -0
  223. package/dist/view/render/adopt-plan.js.map +1 -0
  224. package/dist/view/render/adopt.d.ts +3 -0
  225. package/dist/view/render/adopt.js +269 -0
  226. package/dist/view/render/adopt.js.map +1 -0
  227. package/dist/view/render/attr-parts.d.ts +61 -0
  228. package/dist/view/render/attr-parts.js +229 -0
  229. package/dist/view/render/attr-parts.js.map +1 -0
  230. package/dist/view/render/child-part.d.ts +51 -0
  231. package/dist/view/render/child-part.js +170 -0
  232. package/dist/view/render/child-part.js.map +1 -0
  233. package/dist/view/render/dev-check.d.ts +12 -0
  234. package/dist/view/render/dev-check.js +122 -0
  235. package/dist/view/render/dev-check.js.map +1 -0
  236. package/dist/view/render/hook-part.d.ts +3 -0
  237. package/dist/view/render/hook-part.js +20 -0
  238. package/dist/view/render/hook-part.js.map +1 -0
  239. package/dist/view/render/hooks.d.ts +40 -0
  240. package/dist/view/render/hooks.js +46 -0
  241. package/dist/view/render/hooks.js.map +1 -0
  242. package/dist/view/render/hydrate.d.ts +10 -0
  243. package/dist/view/render/hydrate.js +33 -0
  244. package/dist/view/render/hydrate.js.map +1 -0
  245. package/dist/view/render/instance.d.ts +33 -0
  246. package/dist/view/render/instance.js +107 -0
  247. package/dist/view/render/instance.js.map +1 -0
  248. package/dist/view/render/list.d.ts +20 -0
  249. package/dist/view/render/list.js +156 -0
  250. package/dist/view/render/list.js.map +1 -0
  251. package/dist/view/render/mismatch.d.ts +10 -0
  252. package/dist/view/render/mismatch.js +53 -0
  253. package/dist/view/render/mismatch.js.map +1 -0
  254. package/dist/view/render/nodes.d.ts +10 -0
  255. package/dist/view/render/nodes.js +37 -0
  256. package/dist/view/render/nodes.js.map +1 -0
  257. package/dist/view/render/place.d.ts +18 -0
  258. package/dist/view/render/place.js +69 -0
  259. package/dist/view/render/place.js.map +1 -0
  260. package/dist/view/render/plan.d.ts +26 -0
  261. package/dist/view/render/plan.js +124 -0
  262. package/dist/view/render/plan.js.map +1 -0
  263. package/dist/view/render/raw.d.ts +18 -0
  264. package/dist/view/render/raw.js +68 -0
  265. package/dist/view/render/raw.js.map +1 -0
  266. package/dist/view/render/render.d.ts +12 -0
  267. package/dist/view/render/render.js +60 -0
  268. package/dist/view/render/render.js.map +1 -0
  269. package/dist/view/render/reorder.d.ts +4 -0
  270. package/dist/view/render/reorder.js +101 -0
  271. package/dist/view/render/reorder.js.map +1 -0
  272. package/dist/view/render/rows.d.ts +23 -0
  273. package/dist/view/render/rows.js +95 -0
  274. package/dist/view/render/rows.js.map +1 -0
  275. package/dist/view/render/seen.d.ts +11 -0
  276. package/dist/view/render/seen.js +17 -0
  277. package/dist/view/render/seen.js.map +1 -0
  278. package/dist/view/render/values.d.ts +29 -0
  279. package/dist/view/render/values.js +13 -0
  280. package/dist/view/render/values.js.map +1 -0
  281. package/dist/view/render/warn.d.ts +14 -0
  282. package/dist/view/render/warn.js +69 -0
  283. package/dist/view/render/warn.js.map +1 -0
  284. package/dist/view/server/component.d.ts +9 -0
  285. package/dist/view/server/component.js +42 -0
  286. package/dist/view/server/component.js.map +1 -0
  287. package/dist/view/server/escape.d.ts +10 -0
  288. package/dist/view/server/escape.js +34 -0
  289. package/dist/view/server/escape.js.map +1 -0
  290. package/dist/view/server/index.d.ts +28 -0
  291. package/dist/view/server/index.js +72 -0
  292. package/dist/view/server/index.js.map +1 -0
  293. package/dist/view/server/sha256.d.ts +2 -0
  294. package/dist/view/server/sha256.js +75 -0
  295. package/dist/view/server/sha256.js.map +1 -0
  296. package/dist/view/server/tags.d.ts +57 -0
  297. package/dist/view/server/tags.js +121 -0
  298. package/dist/view/server/tags.js.map +1 -0
  299. package/dist/view/server/values.d.ts +16 -0
  300. package/dist/view/server/values.js +86 -0
  301. package/dist/view/server/values.js.map +1 -0
  302. package/dist/view/server/writer.d.ts +16 -0
  303. package/dist/view/server/writer.js +155 -0
  304. package/dist/view/server/writer.js.map +1 -0
  305. package/dist/view/styles.d.ts +25 -0
  306. package/dist/view/styles.js +58 -0
  307. package/dist/view/styles.js.map +1 -0
  308. package/dist/view/template-element.d.ts +3 -0
  309. package/dist/view/template-element.js +25 -0
  310. package/dist/view/template-element.js.map +1 -0
  311. package/dist/view/template.d.ts +21 -0
  312. package/dist/view/template.js +30 -0
  313. package/dist/view/template.js.map +1 -0
  314. package/dist/vite.d.ts +45 -6
  315. package/dist/vite.js +150 -25
  316. package/dist/vite.js.map +1 -1
  317. package/package.json +39 -4
  318. package/dist/accessible-name.d.ts +0 -19
  319. package/dist/accessible-name.js +0 -56
  320. package/dist/accessible-name.js.map +0 -1
  321. package/dist/element-directive.d.ts +0 -20
  322. package/dist/element-directive.js +0 -31
  323. package/dist/element-directive.js.map +0 -1
  324. package/dist/invalid.d.ts +0 -21
  325. package/dist/invalid.js +0 -70
  326. package/dist/invalid.js.map +0 -1
  327. package/dist/lit-html-check.d.ts +0 -6
  328. package/dist/lit-html-check.js +0 -48
  329. package/dist/lit-html-check.js.map +0 -1
  330. package/dist/live-boolean.d.ts +0 -22
  331. package/dist/live-boolean.js +0 -34
  332. package/dist/live-boolean.js.map +0 -1
  333. package/dist/styles.d.ts +0 -12
  334. package/dist/styles.js +0 -11
  335. package/dist/styles.js.map +0 -1
  336. package/dist/template-whitespace.d.ts +0 -7
  337. package/dist/template-whitespace.js.map +0 -1
  338. package/dist/templates.d.ts +0 -5
  339. package/dist/templates.js +0 -15
  340. package/dist/templates.js.map +0 -1
  341. package/dist/textarea.d.ts +0 -26
  342. package/dist/textarea.js +0 -85
  343. package/dist/textarea.js.map +0 -1
@@ -0,0 +1,72 @@
1
+ // `invalid(errors)` as an element hook (view/02-bindings.md "Element hooks", ADR 0008): model
2
+ // errors mirrored to native validity, so `:user-invalid` and native bubbles agree with the
3
+ // model. The server half writes `aria-invalid` into the start tag, so it is no longer written
4
+ // twice.
5
+ import { defineHook } from '../view/index.js';
6
+ const isValidatable = (el) => 'setCustomValidity' in el;
7
+ const valueOf = (el) => ('value' in el ? el.value : undefined);
8
+ const formOf = (el) => {
9
+ const form = 'form' in el ? el.form : undefined;
10
+ return form instanceof HTMLFormElement ? form : undefined;
11
+ };
12
+ const messageOf = (errors) => typeof errors === 'string' ? errors : (errors ?? []).join(' ');
13
+ /** The listeners that clear the current error, per element (hooks keep no instance state). */
14
+ const listening = new WeakMap();
15
+ // Forms with a resubmit already queued: several stale fields in one validation pass resubmit once.
16
+ const resubmitting = new WeakSet();
17
+ function apply(el, errors) {
18
+ listening.get(el)?.abort();
19
+ listening.delete(el);
20
+ const message = messageOf(errors);
21
+ if (isValidatable(el))
22
+ el.setCustomValidity(message);
23
+ if (message === '') {
24
+ el.removeAttribute('aria-invalid');
25
+ return;
26
+ }
27
+ el.setAttribute('aria-invalid', 'true');
28
+ const controller = new AbortController();
29
+ listening.set(el, controller);
30
+ const { signal } = controller;
31
+ const rejectedValue = valueOf(el);
32
+ const clear = () => {
33
+ controller.abort();
34
+ if (isValidatable(el))
35
+ el.setCustomValidity('');
36
+ el.removeAttribute('aria-invalid');
37
+ };
38
+ el.addEventListener('input', clear, { signal });
39
+ el.addEventListener('change', clear, { signal });
40
+ el.addEventListener('invalid', (event) => {
41
+ if (Object.is(valueOf(el), rejectedValue))
42
+ return; // still the rejected value
43
+ event.preventDefault(); // no bubble for an error that no longer applies
44
+ clear();
45
+ const form = formOf(el);
46
+ if (form === undefined || resubmitting.has(form))
47
+ return;
48
+ resubmitting.add(form);
49
+ queueMicrotask(() => {
50
+ resubmitting.delete(form);
51
+ form.requestSubmit();
52
+ });
53
+ }, { signal });
54
+ }
55
+ /**
56
+ * Mirrors model errors to native validity: `setCustomValidity` and `aria-invalid`.
57
+ *
58
+ * <input name="email" ${invalid(s.errors.email)} />
59
+ *
60
+ * The custom error clears when the control fires `input` or `change` (the user edited it), when
61
+ * the bound errors become `undefined` or empty (the model cleared them), or when a submit finds
62
+ * the error stale (the value changed without an event, e.g. set from code): then the error is
63
+ * dropped and the form is submitted again once. An error is applied again only when a new value
64
+ * arrives (keep `fieldErrors()` results in state): `client` runs only when the errors change.
65
+ */
66
+ export const invalid = defineHook({
67
+ server: ([errors]) => (messageOf(errors) === '' ? {} : { 'aria-invalid': 'true' }),
68
+ client: (el, [errors]) => {
69
+ apply(el, errors);
70
+ },
71
+ });
72
+ //# sourceMappingURL=invalid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invalid.js","sourceRoot":"","sources":["../../src/hooks/invalid.ts"],"names":[],"mappings":"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","sourcesContent":["// `invalid(errors)` as an element hook (view/02-bindings.md \"Element hooks\", ADR 0008): model\n// errors mirrored to native validity, so `:user-invalid` and native bubbles agree with the\n// model. The server half writes `aria-invalid` into the start tag, so it is no longer written\n// twice.\nimport { defineHook } from '../view/index.js';\n\ntype Errors = readonly string[] | string | undefined;\n\ninterface Validatable extends Element {\n setCustomValidity(message: string): void;\n}\n\nconst isValidatable = (el: Element): el is Validatable => 'setCustomValidity' in el;\n\nconst valueOf = (el: Element): unknown => ('value' in el ? el.value : undefined);\n\nconst formOf = (el: Element): HTMLFormElement | undefined => {\n const form = 'form' in el ? el.form : undefined;\n return form instanceof HTMLFormElement ? form : undefined;\n};\n\nconst messageOf = (errors: Errors): string =>\n typeof errors === 'string' ? errors : (errors ?? []).join(' ');\n\n/** The listeners that clear the current error, per element (hooks keep no instance state). */\nconst listening = new WeakMap<Element, AbortController>();\n\n// Forms with a resubmit already queued: several stale fields in one validation pass resubmit once.\nconst resubmitting = new WeakSet<HTMLFormElement>();\n\nfunction apply(el: Element, errors: Errors): void {\n listening.get(el)?.abort();\n listening.delete(el);\n const message = messageOf(errors);\n if (isValidatable(el)) el.setCustomValidity(message);\n if (message === '') {\n el.removeAttribute('aria-invalid');\n return;\n }\n el.setAttribute('aria-invalid', 'true');\n\n const controller = new AbortController();\n listening.set(el, controller);\n const { signal } = controller;\n const rejectedValue = valueOf(el);\n const clear = (): void => {\n controller.abort();\n if (isValidatable(el)) el.setCustomValidity('');\n el.removeAttribute('aria-invalid');\n };\n el.addEventListener('input', clear, { signal });\n el.addEventListener('change', clear, { signal });\n el.addEventListener(\n 'invalid',\n (event) => {\n if (Object.is(valueOf(el), rejectedValue)) return; // still the rejected value\n event.preventDefault(); // no bubble for an error that no longer applies\n clear();\n const form = formOf(el);\n if (form === undefined || resubmitting.has(form)) return;\n resubmitting.add(form);\n queueMicrotask(() => {\n resubmitting.delete(form);\n form.requestSubmit();\n });\n },\n { signal },\n );\n}\n\n/**\n * Mirrors model errors to native validity: `setCustomValidity` and `aria-invalid`.\n *\n * <input name=\"email\" ${invalid(s.errors.email)} />\n *\n * The custom error clears when the control fires `input` or `change` (the user edited it), when\n * the bound errors become `undefined` or empty (the model cleared them), or when a submit finds\n * the error stale (the value changed without an event, e.g. set from code): then the error is\n * dropped and the form is submitted again once. An error is applied again only when a new value\n * arrives (keep `fieldErrors()` results in state): `client` runs only when the errors change.\n */\nexport const invalid = defineHook<[errors?: Errors]>({\n server: ([errors]) => (messageOf(errors) === '' ? {} : { 'aria-invalid': 'true' }),\n client: (el, [errors]) => {\n apply(el, errors);\n },\n});\n"]}
@@ -0,0 +1,13 @@
1
+ /** The element with `id`, searched from `from`'s own root outward to the document. */
2
+ export declare function findInScope(from: Element, id: string): Element | null;
3
+ /**
4
+ * Names the element after the element with `id`, even outside this component's shadow root:
5
+ *
6
+ * <form ${labelledBy('page-title', 'Sign in')}>
7
+ *
8
+ * Uses `ariaLabelledByElements` (ARIA element reflection) where the browser has it, so the name
9
+ * stays in sync with the heading. Elsewhere it sets `aria-label` from `fallback`, or from the
10
+ * target's text. It has no server half (as before): render a plain `aria-label` too (from a
11
+ * `label` prop) for the page before scripts run; the reflected `aria-labelledby` wins over it.
12
+ */
13
+ export declare const labelledBy: (id: string, fallback?: string | undefined) => import("../index.js").HookResult<[id: string, fallback?: string]>;
@@ -0,0 +1,51 @@
1
+ // `labelledBy(id, fallback?)` as an element hook (view/02-bindings.md "Element hooks",
2
+ // gyral-czi.26): accessible names across shadow boundaries. `aria-labelledby="page-title"`
3
+ // inside a shadow root can't see an id in the page, so the id is resolved outward through the
4
+ // shadow-including ancestors.
5
+ import { defineHook } from '../view/index.js';
6
+ /** ARIA element reflection: newly available Baseline, so feature-detected (ADR 0003). */
7
+ const reflects = (el) => 'ariaLabelledByElements' in el;
8
+ /** The element with `id`, searched from `from`'s own root outward to the document. */
9
+ export function findInScope(from, id) {
10
+ for (let root = from.getRootNode();;) {
11
+ const found = 'getElementById' in root ? root.getElementById(id) : null;
12
+ if (found !== null && found !== from)
13
+ return found;
14
+ if (!(root instanceof ShadowRoot))
15
+ return null;
16
+ root = root.host.getRootNode();
17
+ }
18
+ }
19
+ /** Names `el`; false when the target wasn't found (it may render later). */
20
+ function name(el, id, fallback) {
21
+ const target = findInScope(el, id);
22
+ if (target !== null && reflects(el)) {
23
+ el.ariaLabelledByElements = [target];
24
+ return true;
25
+ }
26
+ const text = fallback ?? target?.textContent.trim() ?? '';
27
+ if (text !== '')
28
+ el.setAttribute('aria-label', text);
29
+ return target !== null;
30
+ }
31
+ /**
32
+ * Names the element after the element with `id`, even outside this component's shadow root:
33
+ *
34
+ * <form ${labelledBy('page-title', 'Sign in')}>
35
+ *
36
+ * Uses `ariaLabelledByElements` (ARIA element reflection) where the browser has it, so the name
37
+ * stays in sync with the heading. Elsewhere it sets `aria-label` from `fallback`, or from the
38
+ * target's text. It has no server half (as before): render a plain `aria-label` too (from a
39
+ * `label` prop) for the page before scripts run; the reflected `aria-labelledby` wins over it.
40
+ */
41
+ export const labelledBy = defineHook({
42
+ client: (el, [id, fallback]) => {
43
+ // The labelling element may render after this one (a later sibling): try again once.
44
+ if (!name(el, id, fallback)) {
45
+ queueMicrotask(() => {
46
+ name(el, id, fallback);
47
+ });
48
+ }
49
+ },
50
+ });
51
+ //# sourceMappingURL=labelled-by.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"labelled-by.js","sourceRoot":"","sources":["../../src/hooks/labelled-by.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,2FAA2F;AAC3F,8FAA8F;AAC9F,8BAA8B;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAM9C,yFAAyF;AACzF,MAAM,QAAQ,GAAG,CAAC,EAAW,EAAoB,EAAE,CAAC,wBAAwB,IAAI,EAAE,CAAC;AAInF,sFAAsF;AACtF,MAAM,UAAU,WAAW,CAAC,IAAa,EAAE,EAAU;IACnD,KAAK,IAAI,IAAI,GAAS,IAAI,CAAC,WAAW,EAAE,IAAK,CAAC;QAC5C,MAAM,KAAK,GAAG,gBAAgB,IAAI,IAAI,CAAC,CAAC,CAAE,IAAyB,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9F,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QACnD,IAAI,CAAC,CAAC,IAAI,YAAY,UAAU,CAAC;YAAE,OAAO,IAAI,CAAC;QAC/C,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,SAAS,IAAI,CAAC,EAAW,EAAE,EAAU,EAAE,QAA4B;IACjE,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IACnC,IAAI,MAAM,KAAK,IAAI,IAAI,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;QACpC,EAAE,CAAC,sBAAsB,GAAG,CAAC,MAAM,CAAC,CAAC;QACrC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,IAAI,GAAG,QAAQ,IAAI,MAAM,EAAE,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;IAC1D,IAAI,IAAI,KAAK,EAAE;QAAE,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;IACrD,OAAO,MAAM,KAAK,IAAI,CAAC;AACzB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,UAAU,CAAkC;IACpE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE;QAC7B,qFAAqF;QACrF,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,CAAC;YAC5B,cAAc,CAAC,GAAG,EAAE;gBAClB,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC;YACzB,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;CACF,CAAC,CAAC","sourcesContent":["// `labelledBy(id, fallback?)` as an element hook (view/02-bindings.md \"Element hooks\",\n// gyral-czi.26): accessible names across shadow boundaries. `aria-labelledby=\"page-title\"`\n// inside a shadow root can't see an id in the page, so the id is resolved outward through the\n// shadow-including ancestors.\nimport { defineHook } from '../view/index.js';\n\ninterface Reflecting extends Element {\n ariaLabelledByElements: readonly Element[] | null;\n}\n\n/** ARIA element reflection: newly available Baseline, so feature-detected (ADR 0003). */\nconst reflects = (el: Element): el is Reflecting => 'ariaLabelledByElements' in el;\n\ntype DocumentOrShadow = Document | ShadowRoot;\n\n/** The element with `id`, searched from `from`'s own root outward to the document. */\nexport function findInScope(from: Element, id: string): Element | null {\n for (let root: Node = from.getRootNode(); ;) {\n const found = 'getElementById' in root ? (root as DocumentOrShadow).getElementById(id) : null;\n if (found !== null && found !== from) return found;\n if (!(root instanceof ShadowRoot)) return null;\n root = root.host.getRootNode();\n }\n}\n\n/** Names `el`; false when the target wasn't found (it may render later). */\nfunction name(el: Element, id: string, fallback: string | undefined): boolean {\n const target = findInScope(el, id);\n if (target !== null && reflects(el)) {\n el.ariaLabelledByElements = [target];\n return true;\n }\n const text = fallback ?? target?.textContent.trim() ?? '';\n if (text !== '') el.setAttribute('aria-label', text);\n return target !== null;\n}\n\n/**\n * Names the element after the element with `id`, even outside this component's shadow root:\n *\n * <form ${labelledBy('page-title', 'Sign in')}>\n *\n * Uses `ariaLabelledByElements` (ARIA element reflection) where the browser has it, so the name\n * stays in sync with the heading. Elsewhere it sets `aria-label` from `fallback`, or from the\n * target's text. It has no server half (as before): render a plain `aria-label` too (from a\n * `label` prop) for the page before scripts run; the reflected `aria-labelledby` wins over it.\n */\nexport const labelledBy = defineHook<[id: string, fallback?: string]>({\n client: (el, [id, fallback]) => {\n // The labelling element may render after this one (a later sibling): try again once.\n if (!name(el, id, fallback)) {\n queueMicrotask(() => {\n name(el, id, fallback);\n });\n }\n },\n});\n"]}
@@ -0,0 +1,48 @@
1
+ import { type Command, type Next } from './command.js';
2
+ import { type FeatureHost, type LocalHost, type StoreLink } from './features.js';
3
+ import type { Seed } from './hydration.js';
4
+ import type { ComponentSpec, Ctx, IntentRejected, Tagged } from './types.js';
5
+ type Bag = Readonly<Record<string, unknown>>;
6
+ /** A command as the model runs it. */
7
+ export type ModelCommand = Command<Tagged | IntentRejected>;
8
+ type Cmd = ModelCommand;
9
+ /** What the model needs from its element. */
10
+ export interface ModelHost {
11
+ readonly el: FeatureHost;
12
+ readonly tag: string;
13
+ /** The declared props as components see them (defaults applied). */
14
+ readonly props: () => Bag;
15
+ /** The host's root once it has one (focus commands run against it). */
16
+ readonly root: () => ParentNode | undefined;
17
+ /** Something changed that the next render must show; `onFrame`: in the frame lane (04). */
18
+ readonly invalidate: (onFrame: boolean) => void;
19
+ }
20
+ export declare class HostModel<S, P> implements LocalHost {
21
+ #private;
22
+ readonly el: FeatureHost;
23
+ readonly tag: string;
24
+ readonly root: () => ParentNode | undefined;
25
+ constructor(host: ModelHost, spec: ComponentSpec<S, Tagged, P>);
26
+ /** The store binding, created on first use. Only reached once `defineStore()` has run. */
27
+ readonly stores: () => StoreLink;
28
+ get ready(): boolean;
29
+ /** The current state, running `init` (and `initialMessages`) on first read. */
30
+ state(initialMessages?: readonly Tagged[]): S;
31
+ hasReducer(tag: string): boolean;
32
+ /**
33
+ * Resumes from a server seed (ADR 0012): the server's state, or `init(props)`'s when the
34
+ * seed left it out. Returns init's commands, to start after the first render.
35
+ */
36
+ resume(seed: Seed): readonly Cmd[];
37
+ /** PropsChanged when props differ from the last ones seen (`Object.is` per prop). */
38
+ syncProps(names: readonly string[]): void;
39
+ dispatch(msg: Tagged, schedule?: boolean): void;
40
+ apply(next: Next<S, Tagged> | Next<S, Tagged | IntentRejected>): void;
41
+ /** Starts commands kept for later (init's, after a server-rendered host's first render). */
42
+ run(commands: readonly Cmd[]): void;
43
+ ctx(): Ctx<P>;
44
+ /** Resolves stores and runs kept commands. Returns true if store instances were bound. */
45
+ connect(): boolean;
46
+ disconnect(): void;
47
+ }
48
+ export {};
@@ -0,0 +1,155 @@
1
+ // The model side of one Gyral host (docs/design-docs/view/05-element.md, ADR 0006, ADR 0013):
2
+ // state, reducers and commands. element.ts owns the DOM side (props, root, lifecycle,
3
+ // rendering) and asks this object for state and context. Reducers run at once, so `state` is
4
+ // always current; only the render waits (04). The interpreter and the store binding come from
5
+ // features.ts, registered by `command()` and `defineStore()`; marker drivers (focus, emit, store
6
+ // sends) carry their own `local` handler.
7
+ import { DEVTOOLS_ENABLED, devCommands, devOwner, devUpdate } from '#devtools';
8
+ import { splitNext } from './command.js';
9
+ import { features, } from './features.js';
10
+ import { runInit } from './init.js';
11
+ import { requestTransition } from './scheduler.js';
12
+ export class HostModel {
13
+ #host;
14
+ #spec;
15
+ #reducers;
16
+ #binding;
17
+ #model;
18
+ /** Props as of init or the last PropsChanged: the `prev` of the next PropsChanged. */
19
+ #seen;
20
+ #interpreter;
21
+ /** Commands kept while disconnected (init's, before the first connect); undefined once connected. */
22
+ #pending;
23
+ constructor(host, spec) {
24
+ /** Commands kept while disconnected (init's, before the first connect); undefined once connected. */
25
+ this.#pending = [];
26
+ /** The store binding, created on first use. Only reached once `defineStore()` has run. */
27
+ this.stores = () => (this.#binding ??= features.stores(this.#host.el, this.#host.tag, this.#spec.stores ?? [], (store, state, prev) => {
28
+ this.#storeChanged(store, state, prev);
29
+ }));
30
+ this.#host = host;
31
+ this.el = host.el;
32
+ this.tag = host.tag;
33
+ this.root = host.root;
34
+ this.#spec = spec;
35
+ // Sound: dispatch() only calls the reducer whose key equals msg._tag.
36
+ this.#reducers = spec.update;
37
+ }
38
+ get ready() {
39
+ return this.#model !== undefined;
40
+ }
41
+ /** The current state, running `init` (and `initialMessages`) on first read. */
42
+ state(initialMessages = []) {
43
+ if (this.#model === undefined) {
44
+ this.#seen = this.#host.props();
45
+ this.apply(runInit(this.#spec, this.#seen));
46
+ for (const msg of initialMessages)
47
+ this.dispatch(msg, false);
48
+ }
49
+ return this.#model.value;
50
+ }
51
+ hasReducer(tag) {
52
+ return this.#reducers[tag] !== undefined;
53
+ }
54
+ /**
55
+ * Resumes from a server seed (ADR 0012): the server's state, or `init(props)`'s when the
56
+ * seed left it out. Returns init's commands, to start after the first render.
57
+ */
58
+ resume(seed) {
59
+ this.#seen = this.#host.props();
60
+ const [initial, commands] = splitNext(runInit(this.#spec, this.#seen));
61
+ // Sound: the seed is this component's own state, written by the server.
62
+ this.#model = { value: 'state' in seed ? seed.state : initial };
63
+ return commands;
64
+ }
65
+ /** PropsChanged when props differ from the last ones seen (`Object.is` per prop). */
66
+ syncProps(names) {
67
+ const prev = this.#seen;
68
+ if (prev === undefined)
69
+ return;
70
+ const props = this.#host.props();
71
+ if (names.every((name) => Object.is(props[name], prev[name])))
72
+ return;
73
+ this.#seen = props;
74
+ this.dispatch({ _tag: 'PropsChanged', props, prev }, false);
75
+ }
76
+ dispatch(msg, schedule = true) {
77
+ const reducer = this.#reducers[msg._tag];
78
+ if (reducer === undefined) {
79
+ // Framework messages have optional reducers; props and stores stay readable as context.
80
+ if (msg._tag !== 'PropsChanged' && msg._tag !== 'StoreChanged') {
81
+ console.warn(`<${this.#host.tag}> has no update for message "${msg._tag}".`);
82
+ }
83
+ return;
84
+ }
85
+ const prev = this.state();
86
+ this.apply(reducer(prev, msg, this.ctx()));
87
+ const next = this.state();
88
+ if (DEVTOOLS_ENABLED)
89
+ devUpdate(this.#host.el, this.#host.tag, msg, prev, next);
90
+ if (!schedule)
91
+ return;
92
+ if (this.#spec.viewTransition?.(prev, next, msg) === true)
93
+ requestTransition();
94
+ this.#host.invalidate(this.#onFrame(msg._tag));
95
+ }
96
+ apply(next) {
97
+ const [state, commands] = splitNext(next);
98
+ this.#model = { value: state };
99
+ for (const cmd of commands) {
100
+ const local = cmd.driver.local;
101
+ if (local !== undefined)
102
+ local(this, cmd.input);
103
+ else if (this.#pending !== undefined)
104
+ this.#pending.push(cmd);
105
+ else
106
+ this.#commands().run(cmd);
107
+ }
108
+ }
109
+ /** Starts commands kept for later (init's, after a server-rendered host's first render). */
110
+ run(commands) {
111
+ this.apply([this.state(), commands]);
112
+ }
113
+ ctx() {
114
+ return { props: this.#host.props(), read: (store) => this.stores().read(store) };
115
+ }
116
+ /** Resolves stores and runs kept commands. Returns true if store instances were bound. */
117
+ connect() {
118
+ // The nearest <gyral-stores> may differ after a move.
119
+ const rebound = (this.#spec.stores?.length ?? 0) > 0 && this.stores().connect();
120
+ const pending = this.#pending ?? [];
121
+ this.#pending = undefined;
122
+ for (const cmd of pending)
123
+ this.#commands().run(cmd);
124
+ return rebound;
125
+ }
126
+ disconnect() {
127
+ this.#binding?.disconnect();
128
+ this.#interpreter?.dispose();
129
+ this.#interpreter = undefined;
130
+ this.#pending ??= [];
131
+ }
132
+ /**
133
+ * The interpreter, started with the first command after connecting. Only reached for a
134
+ * command built by `command()`, which registered it (features.ts).
135
+ */
136
+ #commands() {
137
+ const el = this.el;
138
+ return (this.#interpreter ??= features.commands(el, this.#spec.drivers, (msg) => {
139
+ this.dispatch(msg);
140
+ }, DEVTOOLS_ENABLED ? devCommands(() => devOwner(el, this.tag)) : undefined));
141
+ }
142
+ /** Does `tag` render in the frame lane (`spec.renderOnFrame`)? */
143
+ #onFrame(tag) {
144
+ return this.#spec.renderOnFrame?.includes(tag) === true;
145
+ }
146
+ #storeChanged(store, state, prev) {
147
+ if (this.#reducers['StoreChanged'] !== undefined) {
148
+ this.dispatch({ _tag: 'StoreChanged', store: store.name, state, prev });
149
+ }
150
+ else {
151
+ this.#host.invalidate(this.#onFrame('StoreChanged'));
152
+ }
153
+ }
154
+ }
155
+ //# sourceMappingURL=host-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-model.js","sourceRoot":"","sources":["../src/host-model.ts"],"names":[],"mappings":"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","sourcesContent":["// The model side of one Gyral host (docs/design-docs/view/05-element.md, ADR 0006, ADR 0013):\n// state, reducers and commands. element.ts owns the DOM side (props, root, lifecycle,\n// rendering) and asks this object for state and context. Reducers run at once, so `state` is\n// always current; only the render waits (04). The interpreter and the store binding come from\n// features.ts, registered by `command()` and `defineStore()`; marker drivers (focus, emit, store\n// sends) carry their own `local` handler.\nimport { DEVTOOLS_ENABLED, devCommands, devOwner, devUpdate } from '#devtools';\nimport { splitNext, type Command, type Next } from './command.js';\nimport {\n features,\n type FeatureHost,\n type LocalDriver,\n type LocalHost,\n type StoreLink,\n} from './features.js';\nimport type { Seed } from './hydration.js';\nimport { runInit } from './init.js';\nimport type { Interpreter } from './internal/interpreter.js';\nimport { requestTransition } from './scheduler.js';\nimport type { AnyStore } from './store.js';\nimport type { ComponentSpec, Ctx, IntentRejected, Tagged } from './types.js';\n\ntype Bag = Readonly<Record<string, unknown>>;\ntype Reducer<S> = (state: S, msg: Tagged, ctx: Ctx<object>) => Next<S, Tagged>;\n/** A command as the model runs it. */\nexport type ModelCommand = Command<Tagged | IntentRejected>;\ntype Cmd = ModelCommand;\n\n/** What the model needs from its element. */\nexport interface ModelHost {\n readonly el: FeatureHost;\n readonly tag: string;\n /** The declared props as components see them (defaults applied). */\n readonly props: () => Bag;\n /** The host's root once it has one (focus commands run against it). */\n readonly root: () => ParentNode | undefined;\n /** Something changed that the next render must show; `onFrame`: in the frame lane (04). */\n readonly invalidate: (onFrame: boolean) => void;\n}\n\nexport class HostModel<S, P> implements LocalHost {\n readonly el: FeatureHost;\n readonly tag: string;\n readonly root: () => ParentNode | undefined;\n readonly #host: ModelHost;\n readonly #spec: ComponentSpec<S, Tagged, P>;\n readonly #reducers: Readonly<Record<string, Reducer<S> | undefined>>;\n #binding: StoreLink | undefined;\n #model: { value: S } | undefined;\n /** Props as of init or the last PropsChanged: the `prev` of the next PropsChanged. */\n #seen: Bag | undefined;\n #interpreter: Interpreter<Tagged | IntentRejected> | undefined;\n /** Commands kept while disconnected (init's, before the first connect); undefined once connected. */\n #pending: Cmd[] | undefined = [];\n\n constructor(host: ModelHost, spec: ComponentSpec<S, Tagged, P>) {\n this.#host = host;\n this.el = host.el;\n this.tag = host.tag;\n this.root = host.root;\n this.#spec = spec;\n // Sound: dispatch() only calls the reducer whose key equals msg._tag.\n this.#reducers = spec.update as unknown as Readonly<Record<string, Reducer<S> | undefined>>;\n }\n\n /** The store binding, created on first use. Only reached once `defineStore()` has run. */\n readonly stores = (): StoreLink =>\n (this.#binding ??= (features.stores as NonNullable<typeof features.stores>)(\n this.#host.el,\n this.#host.tag,\n this.#spec.stores ?? [],\n (store, state, prev) => {\n this.#storeChanged(store, state, prev);\n },\n ));\n\n get ready(): boolean {\n return this.#model !== undefined;\n }\n\n /** The current state, running `init` (and `initialMessages`) on first read. */\n state(initialMessages: readonly Tagged[] = []): S {\n if (this.#model === undefined) {\n this.#seen = this.#host.props();\n this.apply(runInit(this.#spec, this.#seen as P));\n for (const msg of initialMessages) this.dispatch(msg, false);\n }\n return (this.#model as { value: S }).value;\n }\n\n hasReducer(tag: string): boolean {\n return this.#reducers[tag] !== undefined;\n }\n\n /**\n * Resumes from a server seed (ADR 0012): the server's state, or `init(props)`'s when the\n * seed left it out. Returns init's commands, to start after the first render.\n */\n resume(seed: Seed): readonly Cmd[] {\n this.#seen = this.#host.props();\n const [initial, commands] = splitNext(runInit(this.#spec, this.#seen as P));\n // Sound: the seed is this component's own state, written by the server.\n this.#model = { value: 'state' in seed ? (seed.state as S) : initial };\n return commands;\n }\n\n /** PropsChanged when props differ from the last ones seen (`Object.is` per prop). */\n syncProps(names: readonly string[]): void {\n const prev = this.#seen;\n if (prev === undefined) return;\n const props = this.#host.props();\n if (names.every((name) => Object.is(props[name], prev[name]))) return;\n this.#seen = props;\n this.dispatch({ _tag: 'PropsChanged', props, prev } as Tagged, false);\n }\n\n dispatch(msg: Tagged, schedule = true): void {\n const reducer = this.#reducers[msg._tag];\n if (reducer === undefined) {\n // Framework messages have optional reducers; props and stores stay readable as context.\n if (msg._tag !== 'PropsChanged' && msg._tag !== 'StoreChanged') {\n console.warn(`<${this.#host.tag}> has no update for message \"${msg._tag}\".`);\n }\n return;\n }\n const prev = this.state();\n this.apply(reducer(prev, msg, this.ctx() as Ctx<object>));\n const next = this.state();\n if (DEVTOOLS_ENABLED) devUpdate(this.#host.el, this.#host.tag, msg, prev, next);\n if (!schedule) return;\n if (this.#spec.viewTransition?.(prev, next, msg) === true) requestTransition();\n this.#host.invalidate(this.#onFrame(msg._tag));\n }\n\n apply(next: Next<S, Tagged> | Next<S, Tagged | IntentRejected>): void {\n const [state, commands] = splitNext(next as Next<S, Tagged>);\n this.#model = { value: state };\n for (const cmd of commands as readonly Cmd[]) {\n const local = (cmd.driver as Partial<LocalDriver>).local;\n if (local !== undefined) local(this, cmd.input);\n else if (this.#pending !== undefined) this.#pending.push(cmd);\n else this.#commands().run(cmd);\n }\n }\n\n /** Starts commands kept for later (init's, after a server-rendered host's first render). */\n run(commands: readonly Cmd[]): void {\n this.apply([this.state(), commands]);\n }\n\n ctx(): Ctx<P> {\n return { props: this.#host.props() as P, read: (store) => this.stores().read(store) };\n }\n\n /** Resolves stores and runs kept commands. Returns true if store instances were bound. */\n connect(): boolean {\n // The nearest <gyral-stores> may differ after a move.\n const rebound = (this.#spec.stores?.length ?? 0) > 0 && this.stores().connect();\n const pending = this.#pending ?? [];\n this.#pending = undefined;\n for (const cmd of pending) this.#commands().run(cmd);\n return rebound;\n }\n\n disconnect(): void {\n this.#binding?.disconnect();\n this.#interpreter?.dispose();\n this.#interpreter = undefined;\n this.#pending ??= [];\n }\n\n /**\n * The interpreter, started with the first command after connecting. Only reached for a\n * command built by `command()`, which registered it (features.ts).\n */\n #commands(): Interpreter<Tagged | IntentRejected> {\n const el = this.el;\n return (this.#interpreter ??= (features.commands as NonNullable<typeof features.commands>)(\n el,\n this.#spec.drivers,\n (msg) => {\n this.dispatch(msg);\n },\n DEVTOOLS_ENABLED ? devCommands(() => devOwner(el, this.tag)) : undefined,\n ));\n }\n\n /** Does `tag` render in the frame lane (`spec.renderOnFrame`)? */\n #onFrame(tag: string): boolean {\n return this.#spec.renderOnFrame?.includes(tag) === true;\n }\n\n #storeChanged(store: AnyStore, state: unknown, prev: unknown): void {\n if (this.#reducers['StoreChanged'] !== undefined) {\n this.dispatch({ _tag: 'StoreChanged', store: store.name, state, prev } as Tagged);\n } else {\n this.#host.invalidate(this.#onFrame('StoreChanged'));\n }\n }\n}\n"]}
@@ -0,0 +1,9 @@
1
+ import { type ChildValue, type SeenMarkup } from './view/index.js';
2
+ export { scheduleIsland } from './islands.js';
3
+ /**
4
+ * Hydrates a server-rendered host's root with its first view (view/07-hydration.md "Steps").
5
+ * A shadow root swaps styles in the same step (08 "Hydration"): the shared sheets are adopted
6
+ * and the server's `<style>` removed. On a mismatch development throws; production recovers
7
+ * this component only: it clears the root, renders fresh and warns.
8
+ */
9
+ export declare function hydrateRoot(host: HTMLElement, tag: string, view: ChildValue, root: ShadowRoot | HTMLElement, sheets: CSSStyleSheet[] | undefined, seen: SeenMarkup): void;
@@ -0,0 +1,35 @@
1
+ // The lazily loaded half of hydration (view/07-hydration.md "Loading"): the parallel walk and
2
+ // islands. element.ts imports this module with import() when the first host that needs it
3
+ // connects (one with a seed or `defer-hydration`), so client-only pages never fetch it.
4
+ import { DEVTOOLS_ENABLED, devMismatch } from '#devtools';
5
+ import { DEV, hydrate, HydrationMismatch, render, } from './view/index.js';
6
+ export { scheduleIsland } from './islands.js';
7
+ /**
8
+ * Hydrates a server-rendered host's root with its first view (view/07-hydration.md "Steps").
9
+ * A shadow root swaps styles in the same step (08 "Hydration"): the shared sheets are adopted
10
+ * and the server's `<style>` removed. On a mismatch development throws; production recovers
11
+ * this component only: it clears the root, renders fresh and warns.
12
+ */
13
+ export function hydrateRoot(host, tag, view, root, sheets, seen) {
14
+ if (sheets !== undefined) {
15
+ root.adoptedStyleSheets = sheets;
16
+ const first = root.firstChild;
17
+ if (sheets.length > 0 && first?.nodeName === 'STYLE')
18
+ first.remove();
19
+ }
20
+ try {
21
+ hydrate(view, root, `<${tag}>`, seen);
22
+ }
23
+ catch (error) {
24
+ if (!(error instanceof HydrationMismatch))
25
+ throw error;
26
+ if (DEVTOOLS_ENABLED)
27
+ devMismatch(host, tag, error.message);
28
+ if (DEV)
29
+ throw error;
30
+ console.warn(`${error.message} <${tag}> was rendered fresh.`);
31
+ root.replaceChildren();
32
+ render(view, root, seen);
33
+ }
34
+ }
35
+ //# sourceMappingURL=hydration-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hydration-client.js","sourceRoot":"","sources":["../src/hydration-client.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,0FAA0F;AAC1F,wFAAwF;AACxF,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EACL,GAAG,EACH,OAAO,EACP,iBAAiB,EACjB,MAAM,GAGP,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,IAAiB,EACjB,GAAW,EACX,IAAgB,EAChB,IAA8B,EAC9B,MAAmC,EACnC,IAAgB;IAEhB,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACxB,IAAmB,CAAC,kBAAkB,GAAG,MAAM,CAAC;QACjD,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC;QAC9B,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,EAAE,QAAQ,KAAK,OAAO;YAAE,KAAK,CAAC,MAAM,EAAE,CAAC;IACvE,CAAC;IACD,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,GAAG,GAAG,EAAE,IAAI,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,CAAC,KAAK,YAAY,iBAAiB,CAAC;YAAE,MAAM,KAAK,CAAC;QACvD,IAAI,gBAAgB;YAAE,WAAW,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAC5D,IAAI,GAAG;YAAE,MAAM,KAAK,CAAC;QACrB,OAAO,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,OAAO,KAAK,GAAG,uBAAuB,CAAC,CAAC;QAC9D,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC3B,CAAC;AACH,CAAC","sourcesContent":["// The lazily loaded half of hydration (view/07-hydration.md \"Loading\"): the parallel walk and\n// islands. element.ts imports this module with import() when the first host that needs it\n// connects (one with a seed or `defer-hydration`), so client-only pages never fetch it.\nimport { DEVTOOLS_ENABLED, devMismatch } from '#devtools';\nimport {\n DEV,\n hydrate,\n HydrationMismatch,\n render,\n type ChildValue,\n type SeenMarkup,\n} from './view/index.js';\n\nexport { scheduleIsland } from './islands.js';\n\n/**\n * Hydrates a server-rendered host's root with its first view (view/07-hydration.md \"Steps\").\n * A shadow root swaps styles in the same step (08 \"Hydration\"): the shared sheets are adopted\n * and the server's `<style>` removed. On a mismatch development throws; production recovers\n * this component only: it clears the root, renders fresh and warns.\n */\nexport function hydrateRoot(\n host: HTMLElement,\n tag: string,\n view: ChildValue,\n root: ShadowRoot | HTMLElement,\n sheets: CSSStyleSheet[] | undefined,\n seen: SeenMarkup,\n): void {\n if (sheets !== undefined) {\n (root as ShadowRoot).adoptedStyleSheets = sheets;\n const first = root.firstChild;\n if (sheets.length > 0 && first?.nodeName === 'STYLE') first.remove();\n }\n try {\n hydrate(view, root, `<${tag}>`, seen);\n } catch (error) {\n if (!(error instanceof HydrationMismatch)) throw error;\n if (DEVTOOLS_ENABLED) devMismatch(host, tag, error.message);\n if (DEV) throw error;\n console.warn(`${error.message} <${tag}> was rendered fresh.`);\n root.replaceChildren();\n render(view, root, seen);\n }\n}\n"]}
@@ -1,4 +1,3 @@
1
- import type { PropertyDeclaration } from 'lit';
2
1
  /** Host attribute holding the JSON seed. Removed once the client has read it. */
3
2
  export declare const SEED_ATTRIBUTE = "data-gyral-seed";
4
3
  export interface Seed {
@@ -12,25 +11,20 @@ export interface Seed {
12
11
  }
13
12
  /** JSON equality: seeds are JSON, so two values that serialize alike hydrate alike. */
14
13
  export declare const sameJson: (a: unknown, b: unknown) => boolean;
15
- type Declarations = Readonly<Record<string, PropertyDeclaration>>;
16
14
  /**
17
- * Server side: records the element's state and its props that are not already present as
18
- * attributes (property bindings such as `.items=${data}` are not in the HTML otherwise).
19
- * The SSR renderer escapes attribute values, so the JSON is HTML-safe.
15
+ * Server side: the seed for a component whose props `carried` travel in no attribute
16
+ * (property holes) and whose state is `state`; `initial` is `init(props)`'s state.
20
17
  */
21
- export declare function writeSeed(host: Element, state: unknown, props: Readonly<Record<string, unknown>>, declarations: Declarations, initialState?: {
22
- readonly value: unknown;
23
- }): void;
24
- /**
25
- * Client side, right after hydration (gyral-4k7.12, ADR 0012): Lit SSR writes nothing for
26
- * an empty primitive (`''`, `null`, `undefined`), so `<!--lit-part--><!--/lit-part-->`
27
- * has no Text node between the markers. Hydration still records the primitive as the
28
- * part's committed value, and the next text commit writes `.data` into the
29
- * `<!--/lit-part-->` comment instead. Restore the empty Text node a client render would
30
- * have created. Harmless for `nothing` and empty iterables, which clear or insert before
31
- * the end marker. Shadow roots below `root` are handled by their own hosts.
32
- */
33
- export declare function fillEmptyTextParts(root: Node): void;
18
+ export declare function makeSeed(tag: string, state: unknown, initial: unknown, carried: Readonly<Record<string, unknown>>): Seed;
34
19
  /** Client side: reads and removes the seed, if this element was server-rendered by Gyral. */
35
20
  export declare function takeSeed(host: Element): Seed | undefined;
36
- export {};
21
+ /**
22
+ * The walk and islands (hydration-client.ts) once loaded; `null` if loading failed (a stale
23
+ * deployment: server-rendered hosts then render fresh); undefined until then.
24
+ */
25
+ export declare let hydrationCode: typeof import('./hydration-client.js') | null | undefined;
26
+ /**
27
+ * Loads the hydration code (once) and runs `next` when it has loaded or failed, in call
28
+ * order. settled() waits meanwhile (view/07-hydration.md "Loading").
29
+ */
30
+ export declare function whenHydrationLoads(next: () => void): void;
package/dist/hydration.js CHANGED
@@ -1,55 +1,24 @@
1
+ // Server-render seeds (docs/design-docs/0012-ssr.md, view/06-server.md "Components"). The
2
+ // server writes each component's state, plus the props an attribute can't carry, into one
3
+ // attribute; the client reads it on connect, before its first render, so both sides start from
4
+ // the same state. The walk that then hydrates the server's DOM is hydration-client.ts, which
5
+ // core loads lazily (view/07-hydration.md "Loading").
1
6
  import { warnJsonHazard } from './json-safety.js';
7
+ import { hold } from './scheduler.js';
8
+ import { DEV } from './view/index.js';
2
9
  /** Host attribute holding the JSON seed. Removed once the client has read it. */
3
10
  export const SEED_ATTRIBUTE = 'data-gyral-seed';
4
11
  /** JSON equality: seeds are JSON, so two values that serialize alike hydrate alike. */
5
12
  export const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
6
- function attributeName(name, decl) {
7
- if (decl.attribute === false)
8
- return undefined;
9
- return typeof decl.attribute === 'string' ? decl.attribute : name.toLowerCase();
10
- }
11
13
  /**
12
- * Server side: records the element's state and its props that are not already present as
13
- * attributes (property bindings such as `.items=${data}` are not in the HTML otherwise).
14
- * The SSR renderer escapes attribute values, so the JSON is HTML-safe.
14
+ * Server side: the seed for a component whose props `carried` travel in no attribute
15
+ * (property holes) and whose state is `state`; `initial` is `init(props)`'s state.
15
16
  */
16
- export function writeSeed(host, state, props, declarations, initialState) {
17
- const carried = {};
18
- for (const [name, value] of Object.entries(props)) {
19
- const decl = declarations[name] ?? {};
20
- const attr = attributeName(name, decl);
21
- if (value !== undefined && (attr === undefined || !host.hasAttribute(attr))) {
22
- carried[name] = value;
23
- }
24
- }
25
- const derivable = initialState !== undefined && sameJson(state, initialState.value);
26
- const seed = derivable ? { props: carried } : { state, props: carried };
27
- warnJsonHazard(`<${host.localName}>`, seed, 'seed');
28
- host.setAttribute(SEED_ATTRIBUTE, JSON.stringify(seed));
29
- }
30
- /**
31
- * Client side, right after hydration (gyral-4k7.12, ADR 0012): Lit SSR writes nothing for
32
- * an empty primitive (`''`, `null`, `undefined`), so `<!--lit-part--><!--/lit-part-->`
33
- * has no Text node between the markers. Hydration still records the primitive as the
34
- * part's committed value, and the next text commit writes `.data` into the
35
- * `<!--/lit-part-->` comment instead. Restore the empty Text node a client render would
36
- * have created. Harmless for `nothing` and empty iterables, which clear or insert before
37
- * the end marker. Shadow roots below `root` are handled by their own hosts.
38
- */
39
- export function fillEmptyTextParts(root) {
40
- const doc = root.ownerDocument ?? root;
41
- const walker = doc.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
42
- const empty = [];
43
- for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
44
- const next = node.nextSibling;
45
- if (node.data === 'lit-part' &&
46
- next instanceof Comment &&
47
- next.data === '/lit-part') {
48
- empty.push(node);
49
- }
50
- }
51
- for (const start of empty)
52
- start.after(doc.createTextNode(''));
17
+ export function makeSeed(tag, state, initial, carried) {
18
+ const seed = sameJson(state, initial) ? { props: carried } : { state, props: carried };
19
+ if (DEV)
20
+ warnJsonHazard(`<${tag}>`, seed, 'seed'); // view/06-server.md "Seed": development
21
+ return seed;
53
22
  }
54
23
  /** Client side: reads and removes the seed, if this element was server-rendered by Gyral. */
55
24
  export function takeSeed(host) {
@@ -69,4 +38,35 @@ export function takeSeed(host) {
69
38
  return undefined;
70
39
  }
71
40
  }
41
+ /**
42
+ * The walk and islands (hydration-client.ts) once loaded; `null` if loading failed (a stale
43
+ * deployment: server-rendered hosts then render fresh); undefined until then.
44
+ */
45
+ export let hydrationCode;
46
+ /** Hosts waiting for the code, in connection order; undefined when no load is pending. */
47
+ let waiting;
48
+ /**
49
+ * Loads the hydration code (once) and runs `next` when it has loaded or failed, in call
50
+ * order. settled() waits meanwhile (view/07-hydration.md "Loading").
51
+ */
52
+ export function whenHydrationLoads(next) {
53
+ if (waiting !== undefined) {
54
+ waiting.push(next);
55
+ return;
56
+ }
57
+ waiting = [next];
58
+ const done = (code) => {
59
+ hydrationCode = code;
60
+ const hosts = waiting ?? [];
61
+ waiting = undefined;
62
+ // Each in its own microtask, in order: one host throwing doesn't stop the others. They all
63
+ // run before `hold` counts the load as done.
64
+ for (const host of hosts)
65
+ queueMicrotask(host);
66
+ };
67
+ hold(import('./hydration-client.js').then(done, (error) => {
68
+ console.error('gyral: hydration code failed to load; rendering fresh', error);
69
+ done(null);
70
+ }));
71
+ }
72
72
  //# sourceMappingURL=hydration.js.map