@gyral/core 0.0.0 → 0.1.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 (112) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/NOTICE +11 -0
  3. package/README.md +45 -5
  4. package/dist/accessible-name.d.ts +19 -0
  5. package/dist/accessible-name.js +56 -0
  6. package/dist/accessible-name.js.map +1 -0
  7. package/dist/children.d.ts +37 -0
  8. package/dist/children.js +49 -0
  9. package/dist/children.js.map +1 -0
  10. package/dist/command.d.ts +63 -0
  11. package/dist/command.js +26 -0
  12. package/dist/command.js.map +1 -0
  13. package/dist/define.d.ts +9 -0
  14. package/dist/define.js +345 -0
  15. package/dist/define.js.map +1 -0
  16. package/dist/devtools-events.d.ts +51 -0
  17. package/dist/devtools-events.js +3 -0
  18. package/dist/devtools-events.js.map +1 -0
  19. package/dist/devtools-off.d.ts +9 -0
  20. package/dist/devtools-off.js +10 -0
  21. package/dist/devtools-off.js.map +1 -0
  22. package/dist/devtools.d.ts +13 -0
  23. package/dist/devtools.js +66 -0
  24. package/dist/devtools.js.map +1 -0
  25. package/dist/drivers-scope.d.ts +15 -0
  26. package/dist/drivers-scope.js +48 -0
  27. package/dist/drivers-scope.js.map +1 -0
  28. package/dist/element-directive.d.ts +20 -0
  29. package/dist/element-directive.js +31 -0
  30. package/dist/element-directive.js.map +1 -0
  31. package/dist/element-types.d.ts +34 -0
  32. package/dist/element-types.js +2 -0
  33. package/dist/element-types.js.map +1 -0
  34. package/dist/focus.d.ts +27 -0
  35. package/dist/focus.js +30 -0
  36. package/dist/focus.js.map +1 -0
  37. package/dist/forms.d.ts +59 -0
  38. package/dist/forms.js +138 -0
  39. package/dist/forms.js.map +1 -0
  40. package/dist/hydration.d.ts +36 -0
  41. package/dist/hydration.js +72 -0
  42. package/dist/hydration.js.map +1 -0
  43. package/dist/index.d.ts +42 -0
  44. package/dist/index.js +35 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/init.d.ts +7 -0
  47. package/dist/init.js +10 -0
  48. package/dist/init.js.map +1 -0
  49. package/dist/intent.d.ts +25 -0
  50. package/dist/intent.js +157 -0
  51. package/dist/intent.js.map +1 -0
  52. package/dist/internal/interpreter.d.ts +8 -0
  53. package/dist/internal/interpreter.js +128 -0
  54. package/dist/internal/interpreter.js.map +1 -0
  55. package/dist/internal/runtime.d.ts +7 -0
  56. package/dist/internal/runtime.js +8 -0
  57. package/dist/internal/runtime.js.map +1 -0
  58. package/dist/invalid.d.ts +21 -0
  59. package/dist/invalid.js +70 -0
  60. package/dist/invalid.js.map +1 -0
  61. package/dist/invokers.d.ts +15 -0
  62. package/dist/invokers.js +64 -0
  63. package/dist/invokers.js.map +1 -0
  64. package/dist/islands.d.ts +11 -0
  65. package/dist/islands.js +63 -0
  66. package/dist/islands.js.map +1 -0
  67. package/dist/json-safety.d.ts +8 -0
  68. package/dist/json-safety.js +68 -0
  69. package/dist/json-safety.js.map +1 -0
  70. package/dist/light-dom.d.ts +61 -0
  71. package/dist/light-dom.js +109 -0
  72. package/dist/light-dom.js.map +1 -0
  73. package/dist/live-boolean.d.ts +22 -0
  74. package/dist/live-boolean.js +34 -0
  75. package/dist/live-boolean.js.map +1 -0
  76. package/dist/props.d.ts +26 -0
  77. package/dist/props.js +44 -0
  78. package/dist/props.js.map +1 -0
  79. package/dist/random.d.ts +13 -0
  80. package/dist/random.js +19 -0
  81. package/dist/random.js.map +1 -0
  82. package/dist/states.d.ts +7 -0
  83. package/dist/states.js +23 -0
  84. package/dist/states.js.map +1 -0
  85. package/dist/store-binding.d.ts +14 -0
  86. package/dist/store-binding.js +63 -0
  87. package/dist/store-binding.js.map +1 -0
  88. package/dist/store-scope.d.ts +40 -0
  89. package/dist/store-scope.js +146 -0
  90. package/dist/store-scope.js.map +1 -0
  91. package/dist/store.d.ts +114 -0
  92. package/dist/store.js +129 -0
  93. package/dist/store.js.map +1 -0
  94. package/dist/stores-provider.d.ts +6 -0
  95. package/dist/stores-provider.js +57 -0
  96. package/dist/stores-provider.js.map +1 -0
  97. package/dist/styles.d.ts +12 -0
  98. package/dist/styles.js +11 -0
  99. package/dist/styles.js.map +1 -0
  100. package/dist/textarea.d.ts +26 -0
  101. package/dist/textarea.js +85 -0
  102. package/dist/textarea.js.map +1 -0
  103. package/dist/transitions.d.ts +9 -0
  104. package/dist/transitions.js +26 -0
  105. package/dist/transitions.js.map +1 -0
  106. package/dist/types.d.ts +186 -0
  107. package/dist/types.js +2 -0
  108. package/dist/types.js.map +1 -0
  109. package/dist/vite.d.ts +25 -0
  110. package/dist/vite.js +37 -0
  111. package/dist/vite.js.map +1 -0
  112. package/package.json +41 -12
@@ -0,0 +1,64 @@
1
+ // Invoker commands as an intent source (gyral-czi.6, ADR 0001 addendum, ADR 0003).
2
+ //
3
+ // <button type="button" commandfor="cart" command="--clear">Clear</button>
4
+ // <section id="cart" data-intent=${i.Clear} data-intent-on="command">…</section>
5
+ //
6
+ // Browsers with invoker commands dispatch a `CommandEvent` on the target. Elsewhere (invokers
7
+ // are Baseline newly available, not widely) Gyral dispatches an equivalent `command` event when
8
+ // a custom-command (`--…`) invoker inside a component is clicked, so the same markup works.
9
+ // Built-in commands (`show-modal`, `toggle-popover`, …) are left to the browser.
10
+ /** Native invoker commands available? */
11
+ export const invokersSupported = () => 'CommandEvent' in globalThis;
12
+ /** The command carried by a native `CommandEvent` or by Gyral's fallback event. */
13
+ export function commandOf(event) {
14
+ if (event.type !== 'command')
15
+ return undefined;
16
+ if (event instanceof CustomEvent) {
17
+ const detail = event.detail;
18
+ return typeof detail?.command === 'string'
19
+ ? { command: detail.command, source: detail.source }
20
+ : undefined;
21
+ }
22
+ const native = event;
23
+ return typeof native.command === 'string'
24
+ ? {
25
+ command: native.command,
26
+ source: native.source instanceof Element ? native.source : undefined,
27
+ }
28
+ : undefined;
29
+ }
30
+ /** The custom-command invoker an event passed through, if any. */
31
+ function invokerIn(event) {
32
+ for (const node of event.composedPath()) {
33
+ if (node instanceof Element &&
34
+ node.getAttribute('command')?.startsWith('--') === true &&
35
+ node.hasAttribute('commandfor')) {
36
+ return node;
37
+ }
38
+ }
39
+ return undefined;
40
+ }
41
+ /**
42
+ * Fallback for browsers without invoker commands: a click on a `command="--…"` invoker inside
43
+ * `root` dispatches a `command` CustomEvent on its `commandfor` target (same tree scope).
44
+ */
45
+ export function shimInvokers(root) {
46
+ root.addEventListener('click', (event) => {
47
+ if (invokersSupported())
48
+ return;
49
+ const invoker = invokerIn(event);
50
+ const id = invoker?.getAttribute('commandfor');
51
+ const command = invoker?.getAttribute('command');
52
+ if (invoker === undefined || id == null || command == null)
53
+ return;
54
+ const scope = invoker.getRootNode();
55
+ const target = scope.getElementById(id);
56
+ if (target === null)
57
+ return;
58
+ target.dispatchEvent(new CustomEvent('command', {
59
+ detail: { command, source: invoker },
60
+ cancelable: true,
61
+ }));
62
+ });
63
+ }
64
+ //# sourceMappingURL=invokers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invokers.js","sourceRoot":"","sources":["../src/invokers.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,EAAE;AACF,6EAA6E;AAC7E,mFAAmF;AACnF,EAAE;AACF,8FAA8F;AAC9F,gGAAgG;AAChG,4FAA4F;AAC5F,iFAAiF;AASjF,yCAAyC;AACzC,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAY,EAAE,CAAC,cAAc,IAAI,UAAU,CAAC;AAE7E,mFAAmF;AACnF,MAAM,UAAU,SAAS,CAAC,KAAY;IACpC,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC/C,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;QACjC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAqC,CAAC;QAC3D,OAAO,OAAO,MAAM,EAAE,OAAO,KAAK,QAAQ;YACxC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;YACpD,CAAC,CAAC,SAAS,CAAC;IAChB,CAAC;IACD,MAAM,MAAM,GAAG,KAAwD,CAAC;IACxE,OAAO,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ;QACvC,CAAC,CAAC;YACE,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,MAAM,EAAE,MAAM,CAAC,MAAM,YAAY,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;SACrE;QACH,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED,kEAAkE;AAClE,SAAS,SAAS,CAAC,KAAY;IAC7B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,YAAY,EAAE,EAAE,CAAC;QACxC,IACE,IAAI,YAAY,OAAO;YACvB,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,EAAE,UAAU,CAAC,IAAI,CAAC,KAAK,IAAI;YACvD,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,EAC/B,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,IAAU;IACrC,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;QACvC,IAAI,iBAAiB,EAAE;YAAE,OAAO;QAChC,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;QACjC,MAAM,EAAE,GAAG,OAAO,EAAE,YAAY,CAAC,YAAY,CAAC,CAAC;QAC/C,MAAM,OAAO,GAAG,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC;QACjD,IAAI,OAAO,KAAK,SAAS,IAAI,EAAE,IAAI,IAAI,IAAI,OAAO,IAAI,IAAI;YAAE,OAAO;QACnE,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,EAA2B,CAAC;QAC7D,MAAM,MAAM,GAAG,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;QACxC,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO;QAC5B,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAc,SAAS,EAAE;YACtC,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;YACpC,UAAU,EAAE,IAAI;SACjB,CAAC,CACH,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["// Invoker commands as an intent source (gyral-czi.6, ADR 0001 addendum, ADR 0003).\n//\n// <button type=\"button\" commandfor=\"cart\" command=\"--clear\">Clear</button>\n// <section id=\"cart\" data-intent=${i.Clear} data-intent-on=\"command\">…</section>\n//\n// Browsers with invoker commands dispatch a `CommandEvent` on the target. Elsewhere (invokers\n// are Baseline newly available, not widely) Gyral dispatches an equivalent `command` event when\n// a custom-command (`--…`) invoker inside a component is clicked, so the same markup works.\n// Built-in commands (`show-modal`, `toggle-popover`, …) are left to the browser.\n\nexport interface CommandInfo {\n /** The command, e.g. `--clear`. */\n readonly command: string;\n /** The invoker (the button) that sent it. */\n readonly source: Element | undefined;\n}\n\n/** Native invoker commands available? */\nexport const invokersSupported = (): boolean => 'CommandEvent' in globalThis;\n\n/** The command carried by a native `CommandEvent` or by Gyral's fallback event. */\nexport function commandOf(event: Event): CommandInfo | undefined {\n if (event.type !== 'command') return undefined;\n if (event instanceof CustomEvent) {\n const detail = event.detail as Partial<CommandInfo> | null;\n return typeof detail?.command === 'string'\n ? { command: detail.command, source: detail.source }\n : undefined;\n }\n const native = event as Event & { command?: unknown; source?: unknown };\n return typeof native.command === 'string'\n ? {\n command: native.command,\n source: native.source instanceof Element ? native.source : undefined,\n }\n : undefined;\n}\n\n/** The custom-command invoker an event passed through, if any. */\nfunction invokerIn(event: Event): Element | undefined {\n for (const node of event.composedPath()) {\n if (\n node instanceof Element &&\n node.getAttribute('command')?.startsWith('--') === true &&\n node.hasAttribute('commandfor')\n ) {\n return node;\n }\n }\n return undefined;\n}\n\n/**\n * Fallback for browsers without invoker commands: a click on a `command=\"--…\"` invoker inside\n * `root` dispatches a `command` CustomEvent on its `commandfor` target (same tree scope).\n */\nexport function shimInvokers(root: Node): void {\n root.addEventListener('click', (event) => {\n if (invokersSupported()) return;\n const invoker = invokerIn(event);\n const id = invoker?.getAttribute('commandfor');\n const command = invoker?.getAttribute('command');\n if (invoker === undefined || id == null || command == null) return;\n const scope = invoker.getRootNode() as Document | ShadowRoot;\n const target = scope.getElementById(id);\n if (target === null) return;\n target.dispatchEvent(\n new CustomEvent<CommandInfo>('command', {\n detail: { command, source: invoker },\n cancelable: true,\n }),\n );\n });\n}\n"]}
@@ -0,0 +1,11 @@
1
+ /** When a server-rendered component hydrates. `load` (the default) means right away. */
2
+ export type HydrateStrategy = 'load' | 'idle' | 'visible' | 'interaction';
3
+ /** Server-written strategy for a deferred island; removed when the island is released. */
4
+ export declare const ISLAND_ATTRIBUTE = "data-gyral-hydrate";
5
+ /** Server side: marks the host as an island unless it hydrates on load. */
6
+ export declare function markIsland(host: Element, strategy: HydrateStrategy | undefined): void;
7
+ /**
8
+ * Client side: if `host` is a deferred island, releases it when its strategy fires. Called on
9
+ * connect; scheduling happens once per element.
10
+ */
11
+ export declare function scheduleIsland(host: HTMLElement): void;
@@ -0,0 +1,63 @@
1
+ // Lazy hydration islands (gyral-4k7.4, ADR 0012 addendum). A server-rendered component may
2
+ // hydrate later than page load: when the browser is idle, when it scrolls into view, or when
3
+ // the user first reaches for it. The server marks it with Lit's `defer-hydration` plus the
4
+ // strategy; the client removes `defer-hydration` when the strategy fires, and Lit's hydrate
5
+ // support hydrates it in place (define() finishes connecting from attributeChangedCallback).
6
+ /** Server-written strategy for a deferred island; removed when the island is released. */
7
+ export const ISLAND_ATTRIBUTE = 'data-gyral-hydrate';
8
+ const DEFER = 'defer-hydration';
9
+ /** Events that mean the user is about to interact: they arrive before the click itself. */
10
+ const INTERACTION_EVENTS = ['pointerover', 'pointerdown', 'focusin', 'touchstart'];
11
+ /** Server side: marks the host as an island unless it hydrates on load. */
12
+ export function markIsland(host, strategy) {
13
+ if (strategy === undefined || strategy === 'load')
14
+ return;
15
+ host.setAttribute(DEFER, '');
16
+ host.setAttribute(ISLAND_ATTRIBUTE, strategy);
17
+ }
18
+ const scheduled = new WeakSet();
19
+ /** requestIdleCallback is not Baseline widely available; fall back to a short timeout. */
20
+ function whenIdle(run) {
21
+ const idle = globalThis.requestIdleCallback;
22
+ if (idle === undefined)
23
+ setTimeout(run, 200);
24
+ else
25
+ idle(run, { timeout: 2000 });
26
+ }
27
+ /**
28
+ * Client side: if `host` is a deferred island, releases it when its strategy fires. Called on
29
+ * connect; scheduling happens once per element.
30
+ */
31
+ export function scheduleIsland(host) {
32
+ const strategy = host.getAttribute(ISLAND_ATTRIBUTE);
33
+ if (strategy === null || !host.hasAttribute(DEFER) || scheduled.has(host))
34
+ return;
35
+ scheduled.add(host);
36
+ const release = () => {
37
+ host.removeAttribute(ISLAND_ATTRIBUTE);
38
+ host.removeAttribute(DEFER); // Lit's hydrate support hydrates it now
39
+ };
40
+ if (strategy === 'idle') {
41
+ whenIdle(release);
42
+ }
43
+ else if (strategy === 'visible') {
44
+ const observer = new IntersectionObserver((entries) => {
45
+ if (entries.some((entry) => entry.isIntersecting)) {
46
+ observer.disconnect();
47
+ release();
48
+ }
49
+ });
50
+ observer.observe(host);
51
+ }
52
+ else {
53
+ const controller = new AbortController();
54
+ const once = () => {
55
+ controller.abort();
56
+ release();
57
+ };
58
+ for (const type of INTERACTION_EVENTS) {
59
+ host.addEventListener(type, once, { capture: true, signal: controller.signal });
60
+ }
61
+ }
62
+ }
63
+ //# sourceMappingURL=islands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"islands.js","sourceRoot":"","sources":["../src/islands.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,6FAA6F;AAC7F,2FAA2F;AAC3F,4FAA4F;AAC5F,6FAA6F;AAK7F,0FAA0F;AAC1F,MAAM,CAAC,MAAM,gBAAgB,GAAG,oBAAoB,CAAC;AACrD,MAAM,KAAK,GAAG,iBAAiB,CAAC;AAEhC,2FAA2F;AAC3F,MAAM,kBAAkB,GAAG,CAAC,aAAa,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,CAAU,CAAC;AAE5F,2EAA2E;AAC3E,MAAM,UAAU,UAAU,CAAC,IAAa,EAAE,QAAqC;IAC7E,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,MAAM;QAAE,OAAO;IAC1D,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC7B,IAAI,CAAC,YAAY,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,OAAO,EAAW,CAAC;AAIzC,0FAA0F;AAC1F,SAAS,QAAQ,CAAC,GAAe;IAC/B,MAAM,IAAI,GAAI,UAAqD,CAAC,mBAAmB,CAAC;IACxF,IAAI,IAAI,KAAK,SAAS;QAAE,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;;QACxC,IAAI,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;AACpC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,IAAiB;IAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,gBAAgB,CAAC,CAAC;IACrD,IAAI,QAAQ,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO;IAClF,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpB,MAAM,OAAO,GAAG,GAAS,EAAE;QACzB,IAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC,CAAC;QACvC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,wCAAwC;IACvE,CAAC,CAAC;IACF,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;QACxB,QAAQ,CAAC,OAAO,CAAC,CAAC;IACpB,CAAC;SAAM,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAClC,MAAM,QAAQ,GAAG,IAAI,oBAAoB,CAAC,CAAC,OAAO,EAAE,EAAE;YACpD,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC;gBAClD,QAAQ,CAAC,UAAU,EAAE,CAAC;gBACtB,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;SAAM,CAAC;QACN,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,GAAS,EAAE;YACtB,UAAU,CAAC,KAAK,EAAE,CAAC;YACnB,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC;QACF,KAAK,MAAM,IAAI,IAAI,kBAAkB,EAAE,CAAC;YACtC,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;QAClF,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["// Lazy hydration islands (gyral-4k7.4, ADR 0012 addendum). A server-rendered component may\n// hydrate later than page load: when the browser is idle, when it scrolls into view, or when\n// the user first reaches for it. The server marks it with Lit's `defer-hydration` plus the\n// strategy; the client removes `defer-hydration` when the strategy fires, and Lit's hydrate\n// support hydrates it in place (define() finishes connecting from attributeChangedCallback).\n\n/** When a server-rendered component hydrates. `load` (the default) means right away. */\nexport type HydrateStrategy = 'load' | 'idle' | 'visible' | 'interaction';\n\n/** Server-written strategy for a deferred island; removed when the island is released. */\nexport const ISLAND_ATTRIBUTE = 'data-gyral-hydrate';\nconst DEFER = 'defer-hydration';\n\n/** Events that mean the user is about to interact: they arrive before the click itself. */\nconst INTERACTION_EVENTS = ['pointerover', 'pointerdown', 'focusin', 'touchstart'] as const;\n\n/** Server side: marks the host as an island unless it hydrates on load. */\nexport function markIsland(host: Element, strategy: HydrateStrategy | undefined): void {\n if (strategy === undefined || strategy === 'load') return;\n host.setAttribute(DEFER, '');\n host.setAttribute(ISLAND_ATTRIBUTE, strategy);\n}\n\nconst scheduled = new WeakSet<Element>();\n\ntype IdleCallback = (callback: () => void, options?: { timeout: number }) => number;\n\n/** requestIdleCallback is not Baseline widely available; fall back to a short timeout. */\nfunction whenIdle(run: () => void): void {\n const idle = (globalThis as { requestIdleCallback?: IdleCallback }).requestIdleCallback;\n if (idle === undefined) setTimeout(run, 200);\n else idle(run, { timeout: 2000 });\n}\n\n/**\n * Client side: if `host` is a deferred island, releases it when its strategy fires. Called on\n * connect; scheduling happens once per element.\n */\nexport function scheduleIsland(host: HTMLElement): void {\n const strategy = host.getAttribute(ISLAND_ATTRIBUTE);\n if (strategy === null || !host.hasAttribute(DEFER) || scheduled.has(host)) return;\n scheduled.add(host);\n const release = (): void => {\n host.removeAttribute(ISLAND_ATTRIBUTE);\n host.removeAttribute(DEFER); // Lit's hydrate support hydrates it now\n };\n if (strategy === 'idle') {\n whenIdle(release);\n } else if (strategy === 'visible') {\n const observer = new IntersectionObserver((entries) => {\n if (entries.some((entry) => entry.isIntersecting)) {\n observer.disconnect();\n release();\n }\n });\n observer.observe(host);\n } else {\n const controller = new AbortController();\n const once = (): void => {\n controller.abort();\n release();\n };\n for (const type of INTERACTION_EVENTS) {\n host.addEventListener(type, once, { capture: true, signal: controller.signal });\n }\n }\n}\n"]}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The first place where `value` would change across a JSON round trip, as `path: reason`, or
3
+ * `undefined` when it is JSON-safe. `undefined` object properties are allowed (they vanish, and
4
+ * reading them still gives `undefined`).
5
+ */
6
+ export declare function jsonHazard(value: unknown, path: string, seen?: Set<object>): string | undefined;
7
+ /** Warns (server side) when a seed would hydrate differently from what was rendered. */
8
+ export declare function warnJsonHazard(owner: string, value: unknown, path: string): void;
@@ -0,0 +1,68 @@
1
+ // Seeds are JSON (ADR 0012). A value that doesn't survive JSON.stringify → JSON.parse unchanged
2
+ // makes the client hydrate from different state than the server rendered (gyral-4k7.5).
3
+ const plain = (value) => {
4
+ const proto = Object.getPrototypeOf(value);
5
+ return proto === Object.prototype || proto === null;
6
+ };
7
+ /**
8
+ * The first place where `value` would change across a JSON round trip, as `path: reason`, or
9
+ * `undefined` when it is JSON-safe. `undefined` object properties are allowed (they vanish, and
10
+ * reading them still gives `undefined`).
11
+ */
12
+ export function jsonHazard(value, path, seen = new Set()) {
13
+ switch (typeof value) {
14
+ case 'string':
15
+ case 'boolean':
16
+ return undefined;
17
+ case 'number':
18
+ return Number.isFinite(value) ? undefined : `${path} is ${String(value)} (becomes null)`;
19
+ case 'undefined':
20
+ return `${path} is undefined (dropped, or null in an array)`;
21
+ case 'function':
22
+ case 'symbol':
23
+ return `${path} is a ${typeof value} (dropped)`;
24
+ case 'bigint':
25
+ return `${path} is a bigint (JSON can't encode it)`;
26
+ case 'object':
27
+ break;
28
+ }
29
+ if (value === null)
30
+ return undefined;
31
+ if (seen.has(value))
32
+ return `${path} is a circular reference`;
33
+ if (value instanceof Date)
34
+ return `${path} is a Date (becomes a string)`;
35
+ if (Array.isArray(value)) {
36
+ seen.add(value);
37
+ for (const [index, item] of value.entries()) {
38
+ const found = jsonHazard(item, `${path}[${String(index)}]`, seen);
39
+ if (found !== undefined)
40
+ return found;
41
+ }
42
+ seen.delete(value);
43
+ return undefined;
44
+ }
45
+ if (!plain(value)) {
46
+ const name = value.constructor?.name ?? 'object';
47
+ return `${path} is a ${name} (becomes a plain object)`;
48
+ }
49
+ seen.add(value);
50
+ for (const [key, item] of Object.entries(value)) {
51
+ if (item === undefined)
52
+ continue;
53
+ const found = jsonHazard(item, `${path}.${key}`, seen);
54
+ if (found !== undefined)
55
+ return found;
56
+ }
57
+ seen.delete(value);
58
+ return undefined;
59
+ }
60
+ /** Warns (server side) when a seed would hydrate differently from what was rendered. */
61
+ export function warnJsonHazard(owner, value, path) {
62
+ const found = jsonHazard(value, path);
63
+ if (found !== undefined) {
64
+ console.warn(`${owner}: ${found}, so the client would hydrate from different data than the ` +
65
+ 'server rendered. Keep seeded state and props JSON-safe (ADR 0012).');
66
+ }
67
+ }
68
+ //# sourceMappingURL=json-safety.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-safety.js","sourceRoot":"","sources":["../src/json-safety.ts"],"names":[],"mappings":"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","sourcesContent":["// Seeds are JSON (ADR 0012). A value that doesn't survive JSON.stringify → JSON.parse unchanged\n// makes the client hydrate from different state than the server rendered (gyral-4k7.5).\n\nconst plain = (value: object): boolean => {\n const proto: unknown = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n};\n\n/**\n * The first place where `value` would change across a JSON round trip, as `path: reason`, or\n * `undefined` when it is JSON-safe. `undefined` object properties are allowed (they vanish, and\n * reading them still gives `undefined`).\n */\nexport function jsonHazard(\n value: unknown,\n path: string,\n seen = new Set<object>(),\n): string | undefined {\n switch (typeof value) {\n case 'string':\n case 'boolean':\n return undefined;\n case 'number':\n return Number.isFinite(value) ? undefined : `${path} is ${String(value)} (becomes null)`;\n case 'undefined':\n return `${path} is undefined (dropped, or null in an array)`;\n case 'function':\n case 'symbol':\n return `${path} is a ${typeof value} (dropped)`;\n case 'bigint':\n return `${path} is a bigint (JSON can't encode it)`;\n case 'object':\n break;\n }\n if (value === null) return undefined;\n if (seen.has(value)) return `${path} is a circular reference`;\n if (value instanceof Date) return `${path} is a Date (becomes a string)`;\n if (Array.isArray(value)) {\n seen.add(value);\n for (const [index, item] of value.entries()) {\n const found = jsonHazard(item, `${path}[${String(index)}]`, seen);\n if (found !== undefined) return found;\n }\n seen.delete(value);\n return undefined;\n }\n if (!plain(value)) {\n const name = (value.constructor as { name?: string } | undefined)?.name ?? 'object';\n return `${path} is a ${name} (becomes a plain object)`;\n }\n seen.add(value);\n for (const [key, item] of Object.entries(value)) {\n if (item === undefined) continue;\n const found = jsonHazard(item, `${path}.${key}`, seen);\n if (found !== undefined) return found;\n }\n seen.delete(value);\n return undefined;\n}\n\n/** Warns (server side) when a seed would hydrate differently from what was rendered. */\nexport function warnJsonHazard(owner: string, value: unknown, path: string): void {\n const found = jsonHazard(value, path);\n if (found !== undefined) {\n console.warn(\n `${owner}: ${found}, so the client would hydrate from different data than the ` +\n 'server rendered. Keep seeded state and props JSON-safe (ADR 0012).',\n );\n }\n}\n"]}
@@ -0,0 +1,61 @@
1
+ import { type CSSResultGroup, type CSSResultOrNative } from 'lit';
2
+ import { type Styles } from './styles.js';
3
+ interface SpecLike {
4
+ readonly shadow?: boolean;
5
+ readonly styles?: Styles;
6
+ }
7
+ export declare const isLight: (spec: SpecLike) => boolean;
8
+ /** Shadow components keep their styles; light ones use the document's (and warn if given). */
9
+ export declare function componentStyles(spec: SpecLike, tag: string): CSSResultGroup;
10
+ /**
11
+ * Is this class a light-DOM Gyral component? Used by @gyral/ssr to render its view as plain
12
+ * children instead of Declarative Shadow DOM.
13
+ */
14
+ export declare function isLightComponent(ctor: unknown): boolean;
15
+ /**
16
+ * Server-rendered light hosts carry this attribute (ADR 0014 addendum). Hydration uses it to
17
+ * tell a host's own markers from those of light hosts nested inside it.
18
+ */
19
+ export declare const LIGHT_ATTRIBUTE = "data-gyral-light";
20
+ /** Prefix @gyral/ssr puts on Lit's markers inside light views so no hydrate() walk sees them. */
21
+ export declare const HIDDEN_MARKER = "gyral:";
22
+ /** Server side: marks a light host so the client can scope its markers. */
23
+ export declare function markLightHost(host: Element): void;
24
+ /**
25
+ * Client side, on a server-rendered light host's first update: turns the host's own hidden
26
+ * markers back into Lit's, leaving nested light hosts' markers hidden until they hydrate.
27
+ * Returns whether the host's root part marker was found (so Lit can hydrate in place).
28
+ */
29
+ export declare function revealLightMarkers(host: Element): boolean;
30
+ /** Lit's public `hydrate()` from `@lit-labs/ssr-client`. */
31
+ export type Hydrate = (value: unknown, container: HTMLElement | DocumentFragment, options?: object) => void;
32
+ /**
33
+ * Where `@gyral/ssr/hydrate` registers Lit's public `hydrate()` (gyral-czi.38). A global
34
+ * symbol, not an import, so core doesn't depend on `@lit-labs/ssr-client` and apps without
35
+ * SSR don't ship it.
36
+ */
37
+ export declare const HYDRATE_KEY: unique symbol;
38
+ /**
39
+ * Lit's `hydrate()`, if a server-rendered app loaded `@gyral/ssr/hydrate`. Used for light hosts
40
+ * and, since gyral-czi.41, for server-rendered shadow roots too: Gyral hydrates both itself, so
41
+ * it never depends on Lit's hydrate support having patched LitElement before Lit evaluated.
42
+ */
43
+ export declare const hydrator: () => Hydrate | undefined;
44
+ /**
45
+ * First update of a server-rendered (declarative) shadow root: hydrate the server's view in
46
+ * place with Lit's public hydrate(), which leaves the root part on the shadow root so the
47
+ * render() inside LitElement's update() updates it instead of appending a second copy
48
+ * (gyral-czi.41). Without `@gyral/ssr/hydrate` loaded, clear the server's view and let Lit
49
+ * render fresh, adopting the styles Lit's own createRenderRoot would have.
50
+ */
51
+ export declare function hydrateShadow(root: ShadowRoot, view: unknown, options: object, styles: CSSResultOrNative[]): void;
52
+ /**
53
+ * First update of a server-rendered light host: reveal this host's own hidden markers now (not
54
+ * on connect: a deferred child connects mid-way through its parent's hydrate walk), then call
55
+ * Lit's public hydrate(). It leaves the root part on the host, so the render() inside
56
+ * LitElement's update() updates it in place instead of appending a second copy. No Lit private
57
+ * fields: their names are mangled in Lit's production build (ADR 0014, gyral-czi.38). Without
58
+ * `@gyral/ssr/hydrate` loaded, clear the server's view and let Lit render fresh.
59
+ */
60
+ export declare function hydrateLight(host: HTMLElement, view: unknown, options: object): void;
61
+ export {};
@@ -0,0 +1,109 @@
1
+ // Light-DOM render mode (docs/design-docs/0014-light-dom.md).
2
+ import { adoptStyles } from 'lit';
3
+ import { toCssResultGroup } from './styles.js';
4
+ export const isLight = (spec) => spec.shadow === false;
5
+ /** Shadow components keep their styles; light ones use the document's (and warn if given). */
6
+ export function componentStyles(spec, tag) {
7
+ if (!isLight(spec))
8
+ return spec.styles === undefined ? [] : toCssResultGroup(spec.styles);
9
+ if (spec.styles !== undefined) {
10
+ console.warn(`<${tag}> has shadow: false, so its styles are ignored; use document CSS.`);
11
+ }
12
+ return [];
13
+ }
14
+ /**
15
+ * Is this class a light-DOM Gyral component? Used by @gyral/ssr to render its view as plain
16
+ * children instead of Declarative Shadow DOM.
17
+ */
18
+ export function isLightComponent(ctor) {
19
+ const spec = ctor?.spec;
20
+ return spec !== undefined && isLight(spec);
21
+ }
22
+ /**
23
+ * Server-rendered light hosts carry this attribute (ADR 0014 addendum). Hydration uses it to
24
+ * tell a host's own markers from those of light hosts nested inside it.
25
+ */
26
+ export const LIGHT_ATTRIBUTE = 'data-gyral-light';
27
+ /** Prefix @gyral/ssr puts on Lit's markers inside light views so no hydrate() walk sees them. */
28
+ export const HIDDEN_MARKER = 'gyral:';
29
+ const LIT_MARKER = /^(?:lit-part|\/lit-part|lit-node)\b/;
30
+ /** Server side: marks a light host so the client can scope its markers. */
31
+ export function markLightHost(host) {
32
+ host.setAttribute(LIGHT_ATTRIBUTE, '');
33
+ }
34
+ /** Is the comment inside `host`'s own light view, not a nested light host's? */
35
+ function ownedBy(comment, host) {
36
+ for (let el = comment.parentElement; el !== null; el = el.parentElement) {
37
+ if (el === host)
38
+ return true;
39
+ if (el.hasAttribute(LIGHT_ATTRIBUTE))
40
+ return false;
41
+ }
42
+ return false;
43
+ }
44
+ /**
45
+ * Client side, on a server-rendered light host's first update: turns the host's own hidden
46
+ * markers back into Lit's, leaving nested light hosts' markers hidden until they hydrate.
47
+ * Returns whether the host's root part marker was found (so Lit can hydrate in place).
48
+ */
49
+ export function revealLightMarkers(host) {
50
+ const walker = document.createTreeWalker(host, NodeFilter.SHOW_COMMENT);
51
+ const own = [];
52
+ for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
53
+ const comment = node;
54
+ const data = comment.data.slice(HIDDEN_MARKER.length);
55
+ if (comment.data.startsWith(HIDDEN_MARKER) && LIT_MARKER.test(data) && ownedBy(comment, host)) {
56
+ own.push(comment);
57
+ }
58
+ }
59
+ for (const comment of own)
60
+ comment.data = comment.data.slice(HIDDEN_MARKER.length);
61
+ return own[0]?.data.startsWith('lit-part') === true;
62
+ }
63
+ /**
64
+ * Where `@gyral/ssr/hydrate` registers Lit's public `hydrate()` (gyral-czi.38). A global
65
+ * symbol, not an import, so core doesn't depend on `@lit-labs/ssr-client` and apps without
66
+ * SSR don't ship it.
67
+ */
68
+ export const HYDRATE_KEY = Symbol.for('gyral.hydrate');
69
+ /**
70
+ * Lit's `hydrate()`, if a server-rendered app loaded `@gyral/ssr/hydrate`. Used for light hosts
71
+ * and, since gyral-czi.41, for server-rendered shadow roots too: Gyral hydrates both itself, so
72
+ * it never depends on Lit's hydrate support having patched LitElement before Lit evaluated.
73
+ */
74
+ export const hydrator = () => {
75
+ const fn = globalThis[HYDRATE_KEY];
76
+ return typeof fn === 'function' ? fn : undefined;
77
+ };
78
+ /**
79
+ * First update of a server-rendered (declarative) shadow root: hydrate the server's view in
80
+ * place with Lit's public hydrate(), which leaves the root part on the shadow root so the
81
+ * render() inside LitElement's update() updates it instead of appending a second copy
82
+ * (gyral-czi.41). Without `@gyral/ssr/hydrate` loaded, clear the server's view and let Lit
83
+ * render fresh, adopting the styles Lit's own createRenderRoot would have.
84
+ */
85
+ export function hydrateShadow(root, view, options, styles) {
86
+ const hydrate = hydrator();
87
+ if (hydrate !== undefined) {
88
+ hydrate(view, root, options);
89
+ return;
90
+ }
91
+ root.replaceChildren();
92
+ adoptStyles(root, styles);
93
+ }
94
+ /**
95
+ * First update of a server-rendered light host: reveal this host's own hidden markers now (not
96
+ * on connect: a deferred child connects mid-way through its parent's hydrate walk), then call
97
+ * Lit's public hydrate(). It leaves the root part on the host, so the render() inside
98
+ * LitElement's update() updates it in place instead of appending a second copy. No Lit private
99
+ * fields: their names are mangled in Lit's production build (ADR 0014, gyral-czi.38). Without
100
+ * `@gyral/ssr/hydrate` loaded, clear the server's view and let Lit render fresh.
101
+ */
102
+ export function hydrateLight(host, view, options) {
103
+ const hydrate = hydrator();
104
+ if (hydrate !== undefined && revealLightMarkers(host))
105
+ hydrate(view, host, options);
106
+ else
107
+ host.replaceChildren();
108
+ }
109
+ //# sourceMappingURL=light-dom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"light-dom.js","sourceRoot":"","sources":["../src/light-dom.ts"],"names":[],"mappings":"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","sourcesContent":["// Light-DOM render mode (docs/design-docs/0014-light-dom.md).\nimport { adoptStyles, type CSSResultGroup, type CSSResultOrNative } from 'lit';\nimport { toCssResultGroup, type Styles } from './styles.js';\n\ninterface SpecLike {\n readonly shadow?: boolean;\n readonly styles?: Styles;\n}\n\nexport const isLight = (spec: SpecLike): boolean => spec.shadow === false;\n\n/** Shadow components keep their styles; light ones use the document's (and warn if given). */\nexport function componentStyles(spec: SpecLike, tag: string): CSSResultGroup {\n if (!isLight(spec)) return spec.styles === undefined ? [] : toCssResultGroup(spec.styles);\n if (spec.styles !== undefined) {\n console.warn(`<${tag}> has shadow: false, so its styles are ignored; use document CSS.`);\n }\n return [];\n}\n\n/**\n * Is this class a light-DOM Gyral component? Used by @gyral/ssr to render its view as plain\n * children instead of Declarative Shadow DOM.\n */\nexport function isLightComponent(ctor: unknown): boolean {\n const spec = (ctor as { readonly spec?: SpecLike } | null)?.spec;\n return spec !== undefined && isLight(spec);\n}\n\n/**\n * Server-rendered light hosts carry this attribute (ADR 0014 addendum). Hydration uses it to\n * tell a host's own markers from those of light hosts nested inside it.\n */\nexport const LIGHT_ATTRIBUTE = 'data-gyral-light';\n\n/** Prefix @gyral/ssr puts on Lit's markers inside light views so no hydrate() walk sees them. */\nexport const HIDDEN_MARKER = 'gyral:';\n\nconst LIT_MARKER = /^(?:lit-part|\\/lit-part|lit-node)\\b/;\n\n/** Server side: marks a light host so the client can scope its markers. */\nexport function markLightHost(host: Element): void {\n host.setAttribute(LIGHT_ATTRIBUTE, '');\n}\n\n/** Is the comment inside `host`'s own light view, not a nested light host's? */\nfunction ownedBy(comment: Comment, host: Element): boolean {\n for (let el = comment.parentElement; el !== null; el = el.parentElement) {\n if (el === host) return true;\n if (el.hasAttribute(LIGHT_ATTRIBUTE)) return false;\n }\n return false;\n}\n\n/**\n * Client side, on a server-rendered light host's first update: turns the host's own hidden\n * markers back into Lit's, leaving nested light hosts' markers hidden until they hydrate.\n * Returns whether the host's root part marker was found (so Lit can hydrate in place).\n */\nexport function revealLightMarkers(host: Element): boolean {\n const walker = document.createTreeWalker(host, NodeFilter.SHOW_COMMENT);\n const own: Comment[] = [];\n for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {\n const comment = node as Comment;\n const data = comment.data.slice(HIDDEN_MARKER.length);\n if (comment.data.startsWith(HIDDEN_MARKER) && LIT_MARKER.test(data) && ownedBy(comment, host)) {\n own.push(comment);\n }\n }\n for (const comment of own) comment.data = comment.data.slice(HIDDEN_MARKER.length);\n return own[0]?.data.startsWith('lit-part') === true;\n}\n\n/** Lit's public `hydrate()` from `@lit-labs/ssr-client`. */\nexport type Hydrate = (\n value: unknown,\n container: HTMLElement | DocumentFragment,\n options?: object,\n) => void;\n\n/**\n * Where `@gyral/ssr/hydrate` registers Lit's public `hydrate()` (gyral-czi.38). A global\n * symbol, not an import, so core doesn't depend on `@lit-labs/ssr-client` and apps without\n * SSR don't ship it.\n */\nexport const HYDRATE_KEY: unique symbol = Symbol.for('gyral.hydrate') as never;\n\n/**\n * Lit's `hydrate()`, if a server-rendered app loaded `@gyral/ssr/hydrate`. Used for light hosts\n * and, since gyral-czi.41, for server-rendered shadow roots too: Gyral hydrates both itself, so\n * it never depends on Lit's hydrate support having patched LitElement before Lit evaluated.\n */\nexport const hydrator = (): Hydrate | undefined => {\n const fn = (globalThis as { [HYDRATE_KEY]?: unknown })[HYDRATE_KEY];\n return typeof fn === 'function' ? (fn as Hydrate) : undefined;\n};\n\n/**\n * First update of a server-rendered (declarative) shadow root: hydrate the server's view in\n * place with Lit's public hydrate(), which leaves the root part on the shadow root so the\n * render() inside LitElement's update() updates it instead of appending a second copy\n * (gyral-czi.41). Without `@gyral/ssr/hydrate` loaded, clear the server's view and let Lit\n * render fresh, adopting the styles Lit's own createRenderRoot would have.\n */\nexport function hydrateShadow(\n root: ShadowRoot,\n view: unknown,\n options: object,\n styles: CSSResultOrNative[],\n): void {\n const hydrate = hydrator();\n if (hydrate !== undefined) {\n hydrate(view, root, options);\n return;\n }\n root.replaceChildren();\n adoptStyles(root, styles);\n}\n\n/**\n * First update of a server-rendered light host: reveal this host's own hidden markers now (not\n * on connect: a deferred child connects mid-way through its parent's hydrate walk), then call\n * Lit's public hydrate(). It leaves the root part on the host, so the render() inside\n * LitElement's update() updates it in place instead of appending a second copy. No Lit private\n * fields: their names are mangled in Lit's production build (ADR 0014, gyral-czi.38). Without\n * `@gyral/ssr/hydrate` loaded, clear the server's view and let Lit render fresh.\n */\nexport function hydrateLight(host: HTMLElement, view: unknown, options: object): void {\n const hydrate = hydrator();\n if (hydrate !== undefined && revealLightMarkers(host)) hydrate(view, host, options);\n else host.replaceChildren();\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import { Directive, type AttributePart, type PartInfo } from 'lit/directive.js';
2
+ /**
3
+ * A boolean that is right both in server-rendered markup and in the live DOM (ADR 0012).
4
+ * Use it in a boolean attribute binding:
5
+ *
6
+ * <input type="checkbox" ?checked=${liveBoolean(s.on)} />
7
+ * <option ?selected=${liveBoolean(s.sort === 'price')}>Price</option>
8
+ *
9
+ * - On the server it renders the attribute when true and omits it when false.
10
+ * (A property binding, `.checked=${false}`, is serialized by Lit SSR as `checked="false"`,
11
+ * which *checks* the box. Never bind boolean form state as a property in SSR'd views.)
12
+ * - In the browser it also sets the element's property of the same name (`checked`,
13
+ * `selected`, `open`, …), so the control follows model state even after the user has
14
+ * changed it. The attribute alone stops controlling `checked` once a box has been clicked.
15
+ */
16
+ declare class LiveBoolean extends Directive {
17
+ constructor(info: PartInfo);
18
+ render(value: boolean): boolean;
19
+ update(part: AttributePart, [value]: [boolean]): boolean;
20
+ }
21
+ export declare const liveBoolean: (value: boolean) => import("lit/directive.js").DirectiveResult<typeof LiveBoolean>;
22
+ export {};
@@ -0,0 +1,34 @@
1
+ import { Directive, directive, PartType, } from 'lit/directive.js';
2
+ /**
3
+ * A boolean that is right both in server-rendered markup and in the live DOM (ADR 0012).
4
+ * Use it in a boolean attribute binding:
5
+ *
6
+ * <input type="checkbox" ?checked=${liveBoolean(s.on)} />
7
+ * <option ?selected=${liveBoolean(s.sort === 'price')}>Price</option>
8
+ *
9
+ * - On the server it renders the attribute when true and omits it when false.
10
+ * (A property binding, `.checked=${false}`, is serialized by Lit SSR as `checked="false"`,
11
+ * which *checks* the box. Never bind boolean form state as a property in SSR'd views.)
12
+ * - In the browser it also sets the element's property of the same name (`checked`,
13
+ * `selected`, `open`, …), so the control follows model state even after the user has
14
+ * changed it. The attribute alone stops controlling `checked` once a box has been clicked.
15
+ */
16
+ class LiveBoolean extends Directive {
17
+ constructor(info) {
18
+ super(info);
19
+ if (info.type !== PartType.BOOLEAN_ATTRIBUTE) {
20
+ throw new Error('liveBoolean() must be used in a boolean attribute: ?checked=${…}');
21
+ }
22
+ }
23
+ render(value) {
24
+ return value;
25
+ }
26
+ update(part, [value]) {
27
+ const element = part.element;
28
+ if (part.name in part.element && element[part.name] !== value)
29
+ element[part.name] = value;
30
+ return value;
31
+ }
32
+ }
33
+ export const liveBoolean = directive(LiveBoolean);
34
+ //# sourceMappingURL=live-boolean.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-boolean.js","sourceRoot":"","sources":["../src/live-boolean.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,SAAS,EACT,QAAQ,GAGT,MAAM,kBAAkB,CAAC;AAE1B;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAY,SAAQ,SAAS;IACjC,YAAY,IAAc;QACxB,KAAK,CAAC,IAAI,CAAC,CAAC;QACZ,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,iBAAiB,EAAE,CAAC;YAC7C,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;QACtF,CAAC;IACH,CAAC;IAEQ,MAAM,CAAC,KAAc;QAC5B,OAAO,KAAK,CAAC;IACf,CAAC;IAEQ,MAAM,CAAC,IAAmB,EAAE,CAAC,KAAK,CAAY;QACrD,MAAM,OAAO,GAAG,IAAI,CAAC,OAA6C,CAAC;QACnE,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,KAAK;YAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QAC1F,OAAO,KAAK,CAAC;IACf,CAAC;CACF;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC,WAAW,CAAC,CAAC","sourcesContent":["import {\n Directive,\n directive,\n PartType,\n type AttributePart,\n type PartInfo,\n} from 'lit/directive.js';\n\n/**\n * A boolean that is right both in server-rendered markup and in the live DOM (ADR 0012).\n * Use it in a boolean attribute binding:\n *\n * <input type=\"checkbox\" ?checked=${liveBoolean(s.on)} />\n * <option ?selected=${liveBoolean(s.sort === 'price')}>Price</option>\n *\n * - On the server it renders the attribute when true and omits it when false.\n * (A property binding, `.checked=${false}`, is serialized by Lit SSR as `checked=\"false\"`,\n * which *checks* the box. Never bind boolean form state as a property in SSR'd views.)\n * - In the browser it also sets the element's property of the same name (`checked`,\n * `selected`, `open`, …), so the control follows model state even after the user has\n * changed it. The attribute alone stops controlling `checked` once a box has been clicked.\n */\nclass LiveBoolean extends Directive {\n constructor(info: PartInfo) {\n super(info);\n if (info.type !== PartType.BOOLEAN_ATTRIBUTE) {\n throw new Error('liveBoolean() must be used in a boolean attribute: ?checked=${…}');\n }\n }\n\n override render(value: boolean): boolean {\n return value;\n }\n\n override update(part: AttributePart, [value]: [boolean]): boolean {\n const element = part.element as unknown as Record<string, unknown>;\n if (part.name in part.element && element[part.name] !== value) element[part.name] = value;\n return value;\n }\n}\n\nexport const liveBoolean = directive(LiveBoolean);\n"]}
@@ -0,0 +1,26 @@
1
+ import type { PropertyDeclaration } from 'lit';
2
+ type Bag = Readonly<Record<string, unknown>>;
3
+ /** Gyral's view of a declaration: Lit's options plus `required` and `default`. */
4
+ export interface PropInfo extends PropertyDeclaration {
5
+ readonly required?: boolean;
6
+ readonly default?: unknown;
7
+ }
8
+ export type PropTable = Readonly<Record<string, PropInfo>>;
9
+ /** The element's declared props as set on it (no defaults): what a seed must carry. */
10
+ export declare function readRawProps(el: object, table: PropTable): Bag;
11
+ /** The element's declared props as components see them: `default` fills `undefined`. */
12
+ export declare function readProps(el: object, table: PropTable): Bag;
13
+ /** Required props that are still `undefined` (ADR 0007 addendum). */
14
+ export declare function missingRequired(el: object, table: PropTable): string[];
15
+ /** True when every declared prop is identical (`Object.is`) in both snapshots. */
16
+ export declare function sameProps<P>(names: readonly string[], a: P, b: P): boolean;
17
+ /** Hydration: sets props that only travelled in the seed, unless the page already set them. */
18
+ export declare function restoreProps(el: object, names: readonly string[], seeded: Bag): void;
19
+ /**
20
+ * Declared props that collide with a built-in element property (`hidden`, `title`, `id`, …).
21
+ * Lit's reactive accessor replaces the built-in, so setting the prop silently changes what the
22
+ * platform does with it: a prop named `hidden` hides the element (gyral-czi.33). Browser only:
23
+ * the server's DOM shim doesn't model the full prototype chain.
24
+ */
25
+ export declare function shadowedBuiltins(names: readonly string[]): string[];
26
+ export {};
package/dist/props.js ADDED
@@ -0,0 +1,44 @@
1
+ /** The element's declared props as set on it (no defaults): what a seed must carry. */
2
+ export function readRawProps(el, table) {
3
+ const self = el;
4
+ return Object.fromEntries(Object.keys(table).map((name) => [name, self[name]]));
5
+ }
6
+ /** The element's declared props as components see them: `default` fills `undefined`. */
7
+ export function readProps(el, table) {
8
+ const self = el;
9
+ return Object.fromEntries(Object.entries(table).map(([name, info]) => {
10
+ const value = self[name];
11
+ return [name, value === undefined && 'default' in info ? info.default : value];
12
+ }));
13
+ }
14
+ /** Required props that are still `undefined` (ADR 0007 addendum). */
15
+ export function missingRequired(el, table) {
16
+ const self = el;
17
+ return Object.entries(table)
18
+ .filter(([name, info]) => info.required === true && self[name] === undefined)
19
+ .map(([name]) => name);
20
+ }
21
+ /** True when every declared prop is identical (`Object.is`) in both snapshots. */
22
+ export function sameProps(names, a, b) {
23
+ return names.every((name) => Object.is(a[name], b[name]));
24
+ }
25
+ /** Hydration: sets props that only travelled in the seed, unless the page already set them. */
26
+ export function restoreProps(el, names, seeded) {
27
+ const self = el;
28
+ for (const [name, value] of Object.entries(seeded)) {
29
+ if (names.includes(name) && self[name] === undefined)
30
+ self[name] = value;
31
+ }
32
+ }
33
+ /**
34
+ * Declared props that collide with a built-in element property (`hidden`, `title`, `id`, …).
35
+ * Lit's reactive accessor replaces the built-in, so setting the prop silently changes what the
36
+ * platform does with it: a prop named `hidden` hides the element (gyral-czi.33). Browser only:
37
+ * the server's DOM shim doesn't model the full prototype chain.
38
+ */
39
+ export function shadowedBuiltins(names) {
40
+ if (typeof HTMLElement === 'undefined')
41
+ return [];
42
+ return names.filter((name) => name in HTMLElement.prototype);
43
+ }
44
+ //# sourceMappingURL=props.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"props.js","sourceRoot":"","sources":["../src/props.ts"],"names":[],"mappings":"AAaA,uFAAuF;AACvF,MAAM,UAAU,YAAY,CAAC,EAAU,EAAE,KAAgB;IACvD,MAAM,IAAI,GAAG,EAAS,CAAC;IACvB,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAClF,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,SAAS,CAAC,EAAU,EAAE,KAAgB;IACpD,MAAM,IAAI,GAAG,EAAS,CAAC;IACvB,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE;QACzC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;QACzB,OAAO,CAAC,IAAI,EAAE,KAAK,KAAK,SAAS,IAAI,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACjF,CAAC,CAAC,CACH,CAAC;AACJ,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,eAAe,CAAC,EAAU,EAAE,KAAgB;IAC1D,MAAM,IAAI,GAAG,EAAS,CAAC;IACvB,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;SACzB,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,KAAK,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;SAC5E,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,SAAS,CAAI,KAAwB,EAAE,CAAI,EAAE,CAAI;IAC/D,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAE,CAAS,CAAC,IAAI,CAAC,EAAG,CAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC9E,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,YAAY,CAAC,EAAU,EAAE,KAAwB,EAAE,MAAW;IAC5E,MAAM,IAAI,GAAG,EAA6B,CAAC;IAC3C,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACnD,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,SAAS;YAAE,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;IAC3E,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAwB;IACvD,IAAI,OAAO,WAAW,KAAK,WAAW;QAAE,OAAO,EAAE,CAAC;IAClD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,WAAW,CAAC,SAAS,CAAC,CAAC;AAC/D,CAAC","sourcesContent":["// Prop bookkeeping for define() (ADR 0007): snapshots, defaults, change detection, seed restore.\nimport type { PropertyDeclaration } from 'lit';\n\ntype Bag = Readonly<Record<string, unknown>>;\n\n/** Gyral's view of a declaration: Lit's options plus `required` and `default`. */\nexport interface PropInfo extends PropertyDeclaration {\n readonly required?: boolean;\n readonly default?: unknown;\n}\n\nexport type PropTable = Readonly<Record<string, PropInfo>>;\n\n/** The element's declared props as set on it (no defaults): what a seed must carry. */\nexport function readRawProps(el: object, table: PropTable): Bag {\n const self = el as Bag;\n return Object.fromEntries(Object.keys(table).map((name) => [name, self[name]]));\n}\n\n/** The element's declared props as components see them: `default` fills `undefined`. */\nexport function readProps(el: object, table: PropTable): Bag {\n const self = el as Bag;\n return Object.fromEntries(\n Object.entries(table).map(([name, info]) => {\n const value = self[name];\n return [name, value === undefined && 'default' in info ? info.default : value];\n }),\n );\n}\n\n/** Required props that are still `undefined` (ADR 0007 addendum). */\nexport function missingRequired(el: object, table: PropTable): string[] {\n const self = el as Bag;\n return Object.entries(table)\n .filter(([name, info]) => info.required === true && self[name] === undefined)\n .map(([name]) => name);\n}\n\n/** True when every declared prop is identical (`Object.is`) in both snapshots. */\nexport function sameProps<P>(names: readonly string[], a: P, b: P): boolean {\n return names.every((name) => Object.is((a as Bag)[name], (b as Bag)[name]));\n}\n\n/** Hydration: sets props that only travelled in the seed, unless the page already set them. */\nexport function restoreProps(el: object, names: readonly string[], seeded: Bag): void {\n const self = el as Record<string, unknown>;\n for (const [name, value] of Object.entries(seeded)) {\n if (names.includes(name) && self[name] === undefined) self[name] = value;\n }\n}\n\n/**\n * Declared props that collide with a built-in element property (`hidden`, `title`, `id`, …).\n * Lit's reactive accessor replaces the built-in, so setting the prop silently changes what the\n * platform does with it: a prop named `hidden` hides the element (gyral-czi.33). Browser only:\n * the server's DOM shim doesn't model the full prototype chain.\n */\nexport function shadowedBuiltins(names: readonly string[]): string[] {\n if (typeof HTMLElement === 'undefined') return [];\n return names.filter((name) => name in HTMLElement.prototype);\n}\n"]}
@@ -0,0 +1,13 @@
1
+ import { type Command, type Driver } from './command.js';
2
+ export interface RandomInput {
3
+ /** How many numbers to draw. */
4
+ readonly count: number;
5
+ }
6
+ /** Uniform numbers in [0, 1). Substitute it in tests: `el.drivers = { random: fake }`. */
7
+ export declare const randomDriver: Driver<RandomInput, readonly number[]>;
8
+ /** Draws `count` uniform numbers in [0, 1) and turns them into a message. */
9
+ export declare function random<M>(count: number, toMsg: (values: readonly number[]) => M | undefined): Command<M>;
10
+ /** Maps a uniform number in [0, 1) to an integer in `min..max` (inclusive). Pure. */
11
+ export declare const toInt: (u: number, min: number, max: number) => number;
12
+ /** Draws one integer in `min..max` (inclusive). */
13
+ export declare function randomInt<M>(min: number, max: number, toMsg: (value: number) => M | undefined): Command<M>;
package/dist/random.js ADDED
@@ -0,0 +1,19 @@
1
+ // Randomness as an effect (gyral-czi.10): reducers ask for random numbers with a command,
2
+ // so models stay pure and tests substitute a deterministic driver by name.
3
+ import { command, defineDriver } from './command.js';
4
+ /** Uniform numbers in [0, 1). Substitute it in tests: `el.drivers = { random: fake }`. */
5
+ export const randomDriver = defineDriver({
6
+ name: 'random',
7
+ run: ({ count }) => Array.from({ length: count }, () => Math.random()),
8
+ });
9
+ /** Draws `count` uniform numbers in [0, 1) and turns them into a message. */
10
+ export function random(count, toMsg) {
11
+ return command(randomDriver, { count }, { onSuccess: toMsg });
12
+ }
13
+ /** Maps a uniform number in [0, 1) to an integer in `min..max` (inclusive). Pure. */
14
+ export const toInt = (u, min, max) => min + Math.min(max - min, Math.floor(u * (max - min + 1)));
15
+ /** Draws one integer in `min..max` (inclusive). */
16
+ export function randomInt(min, max, toMsg) {
17
+ return random(1, ([u = 0]) => toMsg(toInt(u, min, max)));
18
+ }
19
+ //# sourceMappingURL=random.js.map