@gyral/core 0.0.0 → 0.2.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.
- package/CHANGELOG.md +32 -0
- package/LICENSE +1 -1
- package/NOTICE +11 -0
- package/README.md +45 -5
- package/dist/accessible-name.d.ts +19 -0
- package/dist/accessible-name.js +56 -0
- package/dist/accessible-name.js.map +1 -0
- package/dist/children.d.ts +37 -0
- package/dist/children.js +49 -0
- package/dist/children.js.map +1 -0
- package/dist/command.d.ts +63 -0
- package/dist/command.js +26 -0
- package/dist/command.js.map +1 -0
- package/dist/define.d.ts +9 -0
- package/dist/define.js +338 -0
- package/dist/define.js.map +1 -0
- package/dist/devtools-events.d.ts +51 -0
- package/dist/devtools-events.js +3 -0
- package/dist/devtools-events.js.map +1 -0
- package/dist/devtools-off.d.ts +9 -0
- package/dist/devtools-off.js +10 -0
- package/dist/devtools-off.js.map +1 -0
- package/dist/devtools.d.ts +13 -0
- package/dist/devtools.js +68 -0
- package/dist/devtools.js.map +1 -0
- package/dist/drivers-scope.d.ts +15 -0
- package/dist/drivers-scope.js +48 -0
- package/dist/drivers-scope.js.map +1 -0
- package/dist/element-directive.d.ts +20 -0
- package/dist/element-directive.js +31 -0
- package/dist/element-directive.js.map +1 -0
- package/dist/element-types.d.ts +34 -0
- package/dist/element-types.js +2 -0
- package/dist/element-types.js.map +1 -0
- package/dist/focus.d.ts +39 -0
- package/dist/focus.js +42 -0
- package/dist/focus.js.map +1 -0
- package/dist/forms.d.ts +59 -0
- package/dist/forms.js +138 -0
- package/dist/forms.js.map +1 -0
- package/dist/hydration.d.ts +36 -0
- package/dist/hydration.js +72 -0
- package/dist/hydration.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +7 -0
- package/dist/init.js +10 -0
- package/dist/init.js.map +1 -0
- package/dist/intent.d.ts +25 -0
- package/dist/intent.js +157 -0
- package/dist/intent.js.map +1 -0
- package/dist/internal/interpreter.d.ts +8 -0
- package/dist/internal/interpreter.js +166 -0
- package/dist/internal/interpreter.js.map +1 -0
- package/dist/invalid.d.ts +21 -0
- package/dist/invalid.js +70 -0
- package/dist/invalid.js.map +1 -0
- package/dist/invokers.d.ts +15 -0
- package/dist/invokers.js +64 -0
- package/dist/invokers.js.map +1 -0
- package/dist/islands.d.ts +11 -0
- package/dist/islands.js +63 -0
- package/dist/islands.js.map +1 -0
- package/dist/json-safety.d.ts +8 -0
- package/dist/json-safety.js +68 -0
- package/dist/json-safety.js.map +1 -0
- package/dist/light-dom.d.ts +61 -0
- package/dist/light-dom.js +109 -0
- package/dist/light-dom.js.map +1 -0
- package/dist/lit-html-check.d.ts +6 -0
- package/dist/lit-html-check.js +48 -0
- package/dist/lit-html-check.js.map +1 -0
- package/dist/live-boolean.d.ts +22 -0
- package/dist/live-boolean.js +34 -0
- package/dist/live-boolean.js.map +1 -0
- package/dist/props.d.ts +26 -0
- package/dist/props.js +44 -0
- package/dist/props.js.map +1 -0
- package/dist/random.d.ts +13 -0
- package/dist/random.js +19 -0
- package/dist/random.js.map +1 -0
- package/dist/states.d.ts +7 -0
- package/dist/states.js +23 -0
- package/dist/states.js.map +1 -0
- package/dist/store-binding.d.ts +14 -0
- package/dist/store-binding.js +63 -0
- package/dist/store-binding.js.map +1 -0
- package/dist/store-scope.d.ts +40 -0
- package/dist/store-scope.js +146 -0
- package/dist/store-scope.js.map +1 -0
- package/dist/store.d.ts +114 -0
- package/dist/store.js +129 -0
- package/dist/store.js.map +1 -0
- package/dist/stores-provider.d.ts +6 -0
- package/dist/stores-provider.js +57 -0
- package/dist/stores-provider.js.map +1 -0
- package/dist/styles.d.ts +12 -0
- package/dist/styles.js +11 -0
- package/dist/styles.js.map +1 -0
- package/dist/template-whitespace.d.ts +7 -0
- package/dist/template-whitespace.js +192 -0
- package/dist/template-whitespace.js.map +1 -0
- package/dist/templates.d.ts +5 -0
- package/dist/templates.js +15 -0
- package/dist/templates.js.map +1 -0
- package/dist/textarea.d.ts +26 -0
- package/dist/textarea.js +85 -0
- package/dist/textarea.js.map +1 -0
- package/dist/transitions.d.ts +9 -0
- package/dist/transitions.js +26 -0
- package/dist/transitions.js.map +1 -0
- package/dist/types.d.ts +186 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/vite.d.ts +25 -0
- package/dist/vite.js +37 -0
- package/dist/vite.js.map +1 -0
- package/package.json +41 -13
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @gyral/core
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 74054b3: Smaller and faster, with no public API change:
|
|
8
|
+
|
|
9
|
+
- **No Effect.** The command interpreter is now plain TypeScript with no runtime dependencies,
|
|
10
|
+
so `@gyral/core` depends only on Lit. An empty app drops from about 49 KiB to about 11 KiB
|
|
11
|
+
gzipped, starts faster and uses less memory; rendering speed is unchanged (ADR 0015).
|
|
12
|
+
- **Template whitespace minification.** `html` and `svg` remove indentation whitespace between
|
|
13
|
+
tags (once per template, on server and client alike), so lists build far fewer DOM nodes and
|
|
14
|
+
bulk list updates are 9–22% faster. Inline spacing is kept as one space; `<pre>`,
|
|
15
|
+
`<textarea>`, `<script>`, `<style>`, attributes and comments are unchanged. For exact
|
|
16
|
+
whitespace under CSS `white-space: pre*`, use `html` from `lit`.
|
|
17
|
+
- **lit-html leak guidance.** lit-html 3.3.1 and later leak a comment node per removed
|
|
18
|
+
`repeat()` item (lit/lit#5298). Development builds warn once when an affected version is
|
|
19
|
+
loaded; pin lit-html to 3.3.0 with an override until it is fixed upstream
|
|
20
|
+
(docs/references/consumer-setup.md). New `create-gyral` apps are pinned.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- a7275e8: Copyright and package author are Mike Zupper.
|
|
25
|
+
- fa0156a: `focus()` returned by a reducer whose update renders inside a View Transition now runs after
|
|
26
|
+
the new view has rendered, instead of before it (it used to warn "matched no focusable element").
|
|
27
|
+
|
|
28
|
+
## 0.1.0
|
|
29
|
+
|
|
30
|
+
### Minor Changes
|
|
31
|
+
|
|
32
|
+
- 8b5f5ca: First public release.
|
package/LICENSE
CHANGED
package/NOTICE
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Gyral
|
|
2
|
+
Copyright (c) 2026 Mike Zupper
|
|
3
|
+
|
|
4
|
+
Gyral is inspired by Cycle.js (https://github.com/cyclejs/cyclejs), released
|
|
5
|
+
under the MIT License:
|
|
6
|
+
|
|
7
|
+
The MIT License (MIT)
|
|
8
|
+
Copyright (c) 2015-2019 The Cycle.js Core Team
|
|
9
|
+
|
|
10
|
+
Gyral is a new implementation. Where ideas, APIs or example programs are adapted
|
|
11
|
+
from Cycle.js, this notice applies.
|
package/README.md
CHANGED
|
@@ -1,10 +1,50 @@
|
|
|
1
1
|
# @gyral/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Model-View-Intent web components on [Lit](https://lit.dev) and the web platform. _Inspired by [Cycle.js](https://cycle.js.org)._ Intent parses platform events into typed messages, the model is a set of pure reducers, and the view is a pure template that names intents. Side effects are commands: data that drivers run at the edges.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @gyral/core lit
|
|
9
|
+
```
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
## Example
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { define, html } from '@gyral/core';
|
|
15
|
+
|
|
16
|
+
type Msg = { _tag: 'Increment' } | { _tag: 'Decrement' };
|
|
17
|
+
|
|
18
|
+
define<{ count: number }, Msg>('gy-counter', {
|
|
19
|
+
init: () => ({ count: 0 }),
|
|
20
|
+
intent: {
|
|
21
|
+
Increment: () => ({ _tag: 'Increment' }),
|
|
22
|
+
Decrement: () => ({ _tag: 'Decrement' }),
|
|
23
|
+
},
|
|
24
|
+
update: {
|
|
25
|
+
Increment: (s) => ({ count: s.count + 1 }),
|
|
26
|
+
Decrement: (s) => ({ count: s.count - 1 }),
|
|
27
|
+
},
|
|
28
|
+
view: (s, i) => html`
|
|
29
|
+
<output>${s.count}</output>
|
|
30
|
+
<button type="button" data-intent=${i.Decrement}>Decrement</button>
|
|
31
|
+
<button type="button" data-intent=${i.Increment}>Increment</button>
|
|
32
|
+
`,
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`@gyral/core/vite` exports `gyralVitePreset()`, the Vite/Vitest settings every Gyral app needs
|
|
37
|
+
(one copy of Lit, Lit modules pre-bundled).
|
|
38
|
+
|
|
39
|
+
## Documentation
|
|
40
|
+
|
|
41
|
+
Guides and API reference: **[gyral.dev](https://gyral.dev)**. Source, issues and the
|
|
42
|
+
consumer setup guide (peer dependencies, Vite preset, SSR checklist):
|
|
43
|
+
[github.com/gyraljs/gyral](https://github.com/gyraljs/gyral).
|
|
44
|
+
|
|
45
|
+
> Status: pre-alpha. APIs change between 0.x releases.
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
MIT © Mike Zupper. See LICENSE and NOTICE (Cycle.js attribution). Gyral, gyraljs and
|
|
50
|
+
the Gyral logo are trademarks of Mike Zupper.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ElementDirective } from './element-directive.js';
|
|
2
|
+
/** The element with `id`, searched from `from`'s own root outward to the document. */
|
|
3
|
+
export declare function findInScope(from: Element, id: string): Element | null;
|
|
4
|
+
/**
|
|
5
|
+
* Names the element after the element with `id`, even outside this component's shadow root:
|
|
6
|
+
*
|
|
7
|
+
* <form aria-label=${props.label} ${labelledBy('page-title')}>
|
|
8
|
+
*
|
|
9
|
+
* Uses `ariaLabelledByElements` (ARIA element reflection) where the browser has it, so the name
|
|
10
|
+
* stays in sync with the heading. Elsewhere it sets `aria-label` from `fallback`, or from the
|
|
11
|
+
* target's text. Element directives don't run on the server, so render a plain `aria-label` too
|
|
12
|
+
* (from a `label` prop) for the no-JS page: `aria-labelledby` wins over it once set.
|
|
13
|
+
*/
|
|
14
|
+
declare class LabelledBy extends ElementDirective<[id: string, fallback?: string]> {
|
|
15
|
+
#private;
|
|
16
|
+
apply(el: Element, [id, fallback]: [id: string, fallback?: string]): void;
|
|
17
|
+
}
|
|
18
|
+
export declare const labelledBy: (id: string, fallback?: string | undefined) => import("lit/directive.js").DirectiveResult<typeof LabelledBy>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Accessible names across shadow boundaries (gyral-czi.26). `aria-labelledby="page-title"` inside
|
|
2
|
+
// a shadow root can't see an id in the page, so a form or landmark in a component would be
|
|
3
|
+
// nameless. `labelledBy(id)` resolves the id outward through the shadow-including ancestors.
|
|
4
|
+
import { directive } from 'lit/directive.js';
|
|
5
|
+
import { ElementDirective } from './element-directive.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
|
+
/**
|
|
20
|
+
* Names the element after the element with `id`, even outside this component's shadow root:
|
|
21
|
+
*
|
|
22
|
+
* <form aria-label=${props.label} ${labelledBy('page-title')}>
|
|
23
|
+
*
|
|
24
|
+
* Uses `ariaLabelledByElements` (ARIA element reflection) where the browser has it, so the name
|
|
25
|
+
* stays in sync with the heading. Elsewhere it sets `aria-label` from `fallback`, or from the
|
|
26
|
+
* target's text. Element directives don't run on the server, so render a plain `aria-label` too
|
|
27
|
+
* (from a `label` prop) for the no-JS page: `aria-labelledby` wins over it once set.
|
|
28
|
+
*/
|
|
29
|
+
class LabelledBy extends ElementDirective {
|
|
30
|
+
#applied = '';
|
|
31
|
+
apply(el, [id, fallback]) {
|
|
32
|
+
const key = `${id}\u0000${fallback ?? ''}`;
|
|
33
|
+
if (key === this.#applied)
|
|
34
|
+
return;
|
|
35
|
+
this.#applied = key;
|
|
36
|
+
// The labelling element may render after this one (a later sibling); try again once.
|
|
37
|
+
if (!this.#name(el, id, fallback)) {
|
|
38
|
+
queueMicrotask(() => {
|
|
39
|
+
this.#name(el, id, fallback);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
#name(el, id, fallback) {
|
|
44
|
+
const target = findInScope(el, id);
|
|
45
|
+
if (target !== null && reflects(el)) {
|
|
46
|
+
el.ariaLabelledByElements = [target];
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
const text = fallback ?? target?.textContent.trim() ?? '';
|
|
50
|
+
if (text !== '')
|
|
51
|
+
el.setAttribute('aria-label', text);
|
|
52
|
+
return target !== null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export const labelledBy = directive(LabelledBy);
|
|
56
|
+
//# sourceMappingURL=accessible-name.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accessible-name.js","sourceRoot":"","sources":["../src/accessible-name.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,2FAA2F;AAC3F,6FAA6F;AAC7F,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAM1D,yFAAyF;AACzF,MAAM,QAAQ,GAAG,CAAC,EAAW,EAAoB,EAAE,CAAC,wBAAwB,IAAI,EAAE,CAAC;AAEnF,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;AAID;;;;;;;;;GASG;AACH,MAAM,UAAW,SAAQ,gBAAiD;IACxE,QAAQ,GAAG,EAAE,CAAC;IAEd,KAAK,CAAC,EAAW,EAAE,CAAC,EAAE,EAAE,QAAQ,CAAkC;QAChE,MAAM,GAAG,GAAG,GAAG,EAAE,SAAS,QAAQ,IAAI,EAAE,EAAE,CAAC;QAC3C,IAAI,GAAG,KAAK,IAAI,CAAC,QAAQ;YAAE,OAAO;QAClC,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC;QACpB,qFAAqF;QACrF,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,CAAC;YAClC,cAAc,CAAC,GAAG,EAAE;gBAClB,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC;YAC/B,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,KAAK,CAAC,EAAW,EAAE,EAAU,EAAE,QAA4B;QACzD,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QACnC,IAAI,MAAM,KAAK,IAAI,IAAI,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;YACpC,EAAE,CAAC,sBAAsB,GAAG,CAAC,MAAM,CAAC,CAAC;YACrC,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,IAAI,GAAG,QAAQ,IAAI,MAAM,EAAE,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;QAC1D,IAAI,IAAI,KAAK,EAAE;YAAE,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;QACrD,OAAO,MAAM,KAAK,IAAI,CAAC;IACzB,CAAC;CACF;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC","sourcesContent":["// Accessible names across shadow boundaries (gyral-czi.26). `aria-labelledby=\"page-title\"` inside\n// a shadow root can't see an id in the page, so a form or landmark in a component would be\n// nameless. `labelledBy(id)` resolves the id outward through the shadow-including ancestors.\nimport { directive } from 'lit/directive.js';\nimport { ElementDirective } from './element-directive.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\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\ntype DocumentOrShadow = Document | ShadowRoot;\n\n/**\n * Names the element after the element with `id`, even outside this component's shadow root:\n *\n * <form aria-label=${props.label} ${labelledBy('page-title')}>\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. Element directives don't run on the server, so render a plain `aria-label` too\n * (from a `label` prop) for the no-JS page: `aria-labelledby` wins over it once set.\n */\nclass LabelledBy extends ElementDirective<[id: string, fallback?: string]> {\n #applied = '';\n\n apply(el: Element, [id, fallback]: [id: string, fallback?: string]): void {\n const key = `${id}\\u0000${fallback ?? ''}`;\n if (key === this.#applied) return;\n this.#applied = key;\n // The labelling element may render after this one (a later sibling); try again once.\n if (!this.#name(el, id, fallback)) {\n queueMicrotask(() => {\n this.#name(el, id, fallback);\n });\n }\n }\n\n #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\nexport const labelledBy = directive(LabelledBy);\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Command } from './command.js';
|
|
2
|
+
import type { IntentParser, Tagged } from './types.js';
|
|
3
|
+
/** Marker driver: `define()` handles emit commands itself, synchronously ordered. */
|
|
4
|
+
export declare const EMIT: {
|
|
5
|
+
readonly name: "@gyral/emit";
|
|
6
|
+
readonly run: () => undefined;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* A command that sends `output` to the parent component. The parent receives it as an
|
|
10
|
+
* intent on the child element: `<my-child data-intent=${i.Picked}>` + `child(MyChild, …)`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function emit(output: Tagged & Readonly<Record<string, unknown>>): Command<never>;
|
|
13
|
+
/**
|
|
14
|
+
* Sends an output to the parent component. A microtask keeps outputs in order and out of the
|
|
15
|
+
* parent's render pass; the event bubbles through the parent's shadow tree only (not composed).
|
|
16
|
+
*/
|
|
17
|
+
export declare function dispatchOutput(host: Element, output: unknown): void;
|
|
18
|
+
/** Anything `child()` can read an output type from: a `define()` class. */
|
|
19
|
+
export interface OutputSource<O extends Tagged, E extends Element> {
|
|
20
|
+
new (): E;
|
|
21
|
+
/** Type-only marker for the component's output union. Never set at runtime. */
|
|
22
|
+
readonly outputs?: O;
|
|
23
|
+
}
|
|
24
|
+
/** A child class, or a function returning it (for recursive or later-defined components). */
|
|
25
|
+
export type ChildSource<O extends Tagged, E extends Element> = OutputSource<O, E> | (() => OutputSource<O, E>);
|
|
26
|
+
/**
|
|
27
|
+
* Intent parser for a child component's outputs, typed by the child's output union.
|
|
28
|
+
* `el` is the child element, for reading its props (an item id, for example).
|
|
29
|
+
*
|
|
30
|
+
* Pass `() => Child` when the class isn't defined yet, e.g. a component that contains
|
|
31
|
+
* itself; annotate the constant's type so TypeScript accepts the self-reference:
|
|
32
|
+
*
|
|
33
|
+
* const Folder: GyralElementClass<State, Msg, Props, Out> = define('x-folder', {
|
|
34
|
+
* intent: { Child: child(() => Folder, (out, el) => …) }, …
|
|
35
|
+
* });
|
|
36
|
+
*/
|
|
37
|
+
export declare function child<O extends Tagged, E extends Element, M>(source: ChildSource<O, E>, toMsg: (output: O, el: E) => M | undefined): IntentParser<M>;
|
package/dist/children.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { OUTPUT_EVENT } from './intent.js';
|
|
2
|
+
/** Marker driver: `define()` handles emit commands itself, synchronously ordered. */
|
|
3
|
+
export const EMIT = {
|
|
4
|
+
name: '@gyral/emit',
|
|
5
|
+
run: () => undefined,
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* A command that sends `output` to the parent component. The parent receives it as an
|
|
9
|
+
* intent on the child element: `<my-child data-intent=${i.Picked}>` + `child(MyChild, …)`.
|
|
10
|
+
*/
|
|
11
|
+
export function emit(output) {
|
|
12
|
+
return { driver: EMIT, input: output, onSuccess: () => undefined };
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Sends an output to the parent component. A microtask keeps outputs in order and out of the
|
|
16
|
+
* parent's render pass; the event bubbles through the parent's shadow tree only (not composed).
|
|
17
|
+
*/
|
|
18
|
+
export function dispatchOutput(host, output) {
|
|
19
|
+
queueMicrotask(() => {
|
|
20
|
+
if (!host.isConnected)
|
|
21
|
+
return;
|
|
22
|
+
host.dispatchEvent(new CustomEvent(OUTPUT_EVENT, { detail: output, bubbles: true, composed: false }));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
// Classes have a prototype; arrow functions (the lazy form) do not.
|
|
26
|
+
const resolveSource = (source) => 'prototype' in source && source.prototype !== undefined
|
|
27
|
+
? source
|
|
28
|
+
: source();
|
|
29
|
+
/**
|
|
30
|
+
* Intent parser for a child component's outputs, typed by the child's output union.
|
|
31
|
+
* `el` is the child element, for reading its props (an item id, for example).
|
|
32
|
+
*
|
|
33
|
+
* Pass `() => Child` when the class isn't defined yet, e.g. a component that contains
|
|
34
|
+
* itself; annotate the constant's type so TypeScript accepts the self-reference:
|
|
35
|
+
*
|
|
36
|
+
* const Folder: GyralElementClass<State, Msg, Props, Out> = define('x-folder', {
|
|
37
|
+
* intent: { Child: child(() => Folder, (out, el) => …) }, …
|
|
38
|
+
* });
|
|
39
|
+
*/
|
|
40
|
+
export function child(source, toMsg) {
|
|
41
|
+
return (input) => {
|
|
42
|
+
const type = resolveSource(source); // resolved per event, so the lazy form never hits TDZ
|
|
43
|
+
if (!(input.target instanceof type) || input.detail === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
// Sound: only `emit()` from a component of this class dispatches this detail.
|
|
46
|
+
return toMsg(input.detail, input.target);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=children.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"children.js","sourceRoot":"","sources":["../src/children.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,qFAAqF;AACrF,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,IAAI,EAAE,aAAa;IACnB,GAAG,EAAE,GAAG,EAAE,CAAC,SAAS;CACZ,CAAC;AAEX;;;GAGG;AACH,MAAM,UAAU,IAAI,CAAC,MAAkD;IACrE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC;AACrE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,IAAa,EAAE,MAAe;IAC3D,cAAc,CAAC,GAAG,EAAE;QAClB,IAAI,CAAC,IAAI,CAAC,WAAW;YAAE,OAAO;QAC9B,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAClF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAaD,oEAAoE;AACpE,MAAM,aAAa,GAAG,CACpB,MAAyB,EACL,EAAE,CACtB,WAAW,IAAI,MAAM,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS;IACrD,CAAC,CAAE,MAA6B;IAChC,CAAC,CAAE,MAAmC,EAAE,CAAC;AAE7C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,KAAK,CACnB,MAAyB,EACzB,KAA0C;IAE1C,OAAO,CAAC,KAAkB,EAAE,EAAE;QAC5B,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,sDAAsD;QAC1F,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,YAAY,IAAI,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QACpF,8EAA8E;QAC9E,OAAO,KAAK,CAAC,KAAK,CAAC,MAAW,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAChD,CAAC,CAAC;AACJ,CAAC","sourcesContent":["// Child components: props down, outputs up (docs/design-docs/0010-child-components.md).\nimport type { Command } from './command.js';\nimport { OUTPUT_EVENT } from './intent.js';\nimport type { IntentInput, IntentParser, Tagged } from './types.js';\n\n/** Marker driver: `define()` handles emit commands itself, synchronously ordered. */\nexport const EMIT = {\n name: '@gyral/emit',\n run: () => undefined,\n} as const;\n\n/**\n * A command that sends `output` to the parent component. The parent receives it as an\n * intent on the child element: `<my-child data-intent=${i.Picked}>` + `child(MyChild, …)`.\n */\nexport function emit(output: Tagged & Readonly<Record<string, unknown>>): Command<never> {\n return { driver: EMIT, input: output, onSuccess: () => undefined };\n}\n\n/**\n * Sends an output to the parent component. A microtask keeps outputs in order and out of the\n * parent's render pass; the event bubbles through the parent's shadow tree only (not composed).\n */\nexport function dispatchOutput(host: Element, output: unknown): void {\n queueMicrotask(() => {\n if (!host.isConnected) return;\n host.dispatchEvent(\n new CustomEvent(OUTPUT_EVENT, { detail: output, bubbles: true, composed: false }),\n );\n });\n}\n\n/** Anything `child()` can read an output type from: a `define()` class. */\nexport interface OutputSource<O extends Tagged, E extends Element> {\n new (): E;\n /** Type-only marker for the component's output union. Never set at runtime. */\n readonly outputs?: O;\n}\n\n/** A child class, or a function returning it (for recursive or later-defined components). */\nexport type ChildSource<O extends Tagged, E extends Element> =\n OutputSource<O, E> | (() => OutputSource<O, E>);\n\n// Classes have a prototype; arrow functions (the lazy form) do not.\nconst resolveSource = <O extends Tagged, E extends Element>(\n source: ChildSource<O, E>,\n): OutputSource<O, E> =>\n 'prototype' in source && source.prototype !== undefined\n ? (source as OutputSource<O, E>)\n : (source as () => OutputSource<O, E>)();\n\n/**\n * Intent parser for a child component's outputs, typed by the child's output union.\n * `el` is the child element, for reading its props (an item id, for example).\n *\n * Pass `() => Child` when the class isn't defined yet, e.g. a component that contains\n * itself; annotate the constant's type so TypeScript accepts the self-reference:\n *\n * const Folder: GyralElementClass<State, Msg, Props, Out> = define('x-folder', {\n * intent: { Child: child(() => Folder, (out, el) => …) }, …\n * });\n */\nexport function child<O extends Tagged, E extends Element, M>(\n source: ChildSource<O, E>,\n toMsg: (output: O, el: E) => M | undefined,\n): IntentParser<M> {\n return (input: IntentInput) => {\n const type = resolveSource(source); // resolved per event, so the lazy form never hits TDZ\n if (!(input.target instanceof type) || input.detail === undefined) return undefined;\n // Sound: only `emit()` from a component of this class dispatches this detail.\n return toMsg(input.detail as O, input.target);\n };\n}\n"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { IntentRejected } from './types.js';
|
|
2
|
+
/** How commands in the same lane interact. See ADR 0006 for the table. */
|
|
3
|
+
export type Concurrency = 'merge' | 'switch' | 'exhaust' | 'queue';
|
|
4
|
+
export interface RetryPolicy {
|
|
5
|
+
/** Retries after the first failure. */
|
|
6
|
+
readonly times: number;
|
|
7
|
+
/** Delay before each retry (the base delay for exponential backoff). Default 0. */
|
|
8
|
+
readonly delayMs?: number;
|
|
9
|
+
readonly backoff?: 'fixed' | 'exponential';
|
|
10
|
+
}
|
|
11
|
+
export interface DriverContext<O = unknown> {
|
|
12
|
+
/** Aborts when the command is switched away or its component disconnects. */
|
|
13
|
+
readonly signal: AbortSignal;
|
|
14
|
+
/**
|
|
15
|
+
* Delivers an extra result while the command runs (streaming drivers: routers, timers,
|
|
16
|
+
* sockets). Each value goes through the command's `onSuccess`. Ignored once the command
|
|
17
|
+
* has settled or been aborted. A streaming `run` usually never resolves; abort ends it.
|
|
18
|
+
*/
|
|
19
|
+
readonly emit: (output: O) => void;
|
|
20
|
+
}
|
|
21
|
+
/** Performs one kind of side effect. A plain object: easy to fake in tests. */
|
|
22
|
+
export interface Driver<I, O, E = unknown> {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly run: (input: I, ctx: DriverContext<O>) => O | Promise<O>;
|
|
25
|
+
/** Default policy for this driver's commands. Default `'merge'`. */
|
|
26
|
+
readonly concurrency?: Concurrency;
|
|
27
|
+
readonly retry?: RetryPolicy;
|
|
28
|
+
/** Turns a thrown or rejected value into this driver's typed error. */
|
|
29
|
+
readonly toError?: (cause: unknown) => E;
|
|
30
|
+
}
|
|
31
|
+
/** Any driver, with its input type erased. */
|
|
32
|
+
export type AnyDriver = Driver<never, unknown>;
|
|
33
|
+
/** Drivers substituted by name (test fakes, configured instances). */
|
|
34
|
+
export type DriverOverrides = Readonly<Record<string, AnyDriver>>;
|
|
35
|
+
/** A side effect described as data, plus pure mappers from its outcome to messages. */
|
|
36
|
+
export interface Command<M> {
|
|
37
|
+
readonly driver: AnyDriver;
|
|
38
|
+
readonly input: unknown;
|
|
39
|
+
/** Concurrency lane. Default: the driver's name. */
|
|
40
|
+
readonly key?: string;
|
|
41
|
+
readonly concurrency?: Concurrency;
|
|
42
|
+
readonly onSuccess: (output: unknown) => M | undefined;
|
|
43
|
+
/** If omitted, failures are logged and dropped. */
|
|
44
|
+
readonly onFailure?: (error: unknown) => M | undefined;
|
|
45
|
+
}
|
|
46
|
+
export interface CommandHandlers<O, E, MS, MF = MS> {
|
|
47
|
+
readonly onSuccess: (output: O) => MS | undefined;
|
|
48
|
+
readonly onFailure?: (error: E) => MF | undefined;
|
|
49
|
+
readonly key?: string;
|
|
50
|
+
readonly concurrency?: Concurrency;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* What a reducer (or `init`) returns: new state, optionally with commands to run. Commands may
|
|
54
|
+
* also answer with the framework message `IntentRejected` (e.g. `submitForm` in @gyral/http
|
|
55
|
+
* when the server rejects a form), which goes to the optional `IntentRejected` reducer.
|
|
56
|
+
*/
|
|
57
|
+
export type Next<S, M> = S | readonly [S, ReadonlyArray<Command<M | IntentRejected>>];
|
|
58
|
+
/** Identity helper so driver literals infer `I`, `O` and `E`. */
|
|
59
|
+
export declare function defineDriver<I, O, E = unknown>(driver: Driver<I, O, E>): Driver<I, O, E>;
|
|
60
|
+
/** Builds a command whose mappers are typed by the driver's input, output and error. */
|
|
61
|
+
export declare function command<I, O, E, MS, MF = MS>(driver: Driver<I, O, E>, input: I, handlers: CommandHandlers<O, E, MS, MF>): Command<MS | MF>;
|
|
62
|
+
/** Splits a reducer result into state and commands. State is never an array (ADR 0006). */
|
|
63
|
+
export declare function splitNext<S, M>(next: Next<S, M>): readonly [S, ReadonlyArray<Command<M | IntentRejected>>];
|
package/dist/command.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Identity helper so driver literals infer `I`, `O` and `E`. */
|
|
2
|
+
export function defineDriver(driver) {
|
|
3
|
+
return driver;
|
|
4
|
+
}
|
|
5
|
+
/** Builds a command whose mappers are typed by the driver's input, output and error. */
|
|
6
|
+
// Two message parameters so success and failure may produce different variants.
|
|
7
|
+
export function command(driver, input, handlers) {
|
|
8
|
+
const { onFailure, key, concurrency } = handlers;
|
|
9
|
+
return {
|
|
10
|
+
driver: driver,
|
|
11
|
+
input,
|
|
12
|
+
// Sound: the interpreter only passes this driver's output and toError() result.
|
|
13
|
+
onSuccess: (output) => handlers.onSuccess(output),
|
|
14
|
+
...(onFailure === undefined ? {} : { onFailure: (error) => onFailure(error) }),
|
|
15
|
+
...(key === undefined ? {} : { key }),
|
|
16
|
+
...(concurrency === undefined ? {} : { concurrency }),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Splits a reducer result into state and commands. State is never an array (ADR 0006). */
|
|
20
|
+
export function splitNext(next) {
|
|
21
|
+
if (Array.isArray(next) && next.length === 2 && Array.isArray(next[1])) {
|
|
22
|
+
return next;
|
|
23
|
+
}
|
|
24
|
+
return [next, []];
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command.js","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAqEA,iEAAiE;AACjE,MAAM,UAAU,YAAY,CAAoB,MAAuB;IACrE,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,wFAAwF;AACxF,gFAAgF;AAChF,MAAM,UAAU,OAAO,CACrB,MAAuB,EACvB,KAAQ,EACR,QAAuC;IAEvC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,QAAQ,CAAC;IACjD,OAAO;QACL,MAAM,EAAE,MAAM;QACd,KAAK;QACL,gFAAgF;QAChF,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAW,CAAC;QACtD,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,KAAc,EAAE,EAAE,CAAC,SAAS,CAAC,KAAU,CAAC,EAAE,CAAC;QAC5F,GAAG,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC;QACrC,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;KACtD,CAAC;AACJ,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,SAAS,CACvB,IAAgB;IAEhB,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACvE,OAAO,IAAgE,CAAC;IAC1E,CAAC;IACD,OAAO,CAAC,IAAS,EAAE,EAAE,CAAC,CAAC;AACzB,CAAC","sourcesContent":["// Effects as data (docs/design-docs/0006-effects-and-drivers.md). Plain TypeScript only:\nimport type { IntentRejected } from './types.js';\n// the interpreter lives in ./internal/ (ADR 0015).\n\n/** How commands in the same lane interact. See ADR 0006 for the table. */\nexport type Concurrency = 'merge' | 'switch' | 'exhaust' | 'queue';\n\nexport interface RetryPolicy {\n /** Retries after the first failure. */\n readonly times: number;\n /** Delay before each retry (the base delay for exponential backoff). Default 0. */\n readonly delayMs?: number;\n readonly backoff?: 'fixed' | 'exponential';\n}\n\nexport interface DriverContext<O = unknown> {\n /** Aborts when the command is switched away or its component disconnects. */\n readonly signal: AbortSignal;\n /**\n * Delivers an extra result while the command runs (streaming drivers: routers, timers,\n * sockets). Each value goes through the command's `onSuccess`. Ignored once the command\n * has settled or been aborted. A streaming `run` usually never resolves; abort ends it.\n */\n readonly emit: (output: O) => void;\n}\n\n/** Performs one kind of side effect. A plain object: easy to fake in tests. */\nexport interface Driver<I, O, E = unknown> {\n readonly name: string;\n readonly run: (input: I, ctx: DriverContext<O>) => O | Promise<O>;\n /** Default policy for this driver's commands. Default `'merge'`. */\n readonly concurrency?: Concurrency;\n readonly retry?: RetryPolicy;\n /** Turns a thrown or rejected value into this driver's typed error. */\n readonly toError?: (cause: unknown) => E;\n}\n\n/** Any driver, with its input type erased. */\nexport type AnyDriver = Driver<never, unknown>;\n\n/** Drivers substituted by name (test fakes, configured instances). */\nexport type DriverOverrides = Readonly<Record<string, AnyDriver>>;\n\n/** A side effect described as data, plus pure mappers from its outcome to messages. */\nexport interface Command<M> {\n readonly driver: AnyDriver;\n readonly input: unknown;\n /** Concurrency lane. Default: the driver's name. */\n readonly key?: string;\n readonly concurrency?: Concurrency;\n readonly onSuccess: (output: unknown) => M | undefined;\n /** If omitted, failures are logged and dropped. */\n readonly onFailure?: (error: unknown) => M | undefined;\n}\n\nexport interface CommandHandlers<O, E, MS, MF = MS> {\n readonly onSuccess: (output: O) => MS | undefined;\n readonly onFailure?: (error: E) => MF | undefined;\n readonly key?: string;\n readonly concurrency?: Concurrency;\n}\n\n/**\n * What a reducer (or `init`) returns: new state, optionally with commands to run. Commands may\n * also answer with the framework message `IntentRejected` (e.g. `submitForm` in @gyral/http\n * when the server rejects a form), which goes to the optional `IntentRejected` reducer.\n */\nexport type Next<S, M> = S | readonly [S, ReadonlyArray<Command<M | IntentRejected>>];\n\n/** Identity helper so driver literals infer `I`, `O` and `E`. */\nexport function defineDriver<I, O, E = unknown>(driver: Driver<I, O, E>): Driver<I, O, E> {\n return driver;\n}\n\n/** Builds a command whose mappers are typed by the driver's input, output and error. */\n// Two message parameters so success and failure may produce different variants.\nexport function command<I, O, E, MS, MF = MS>(\n driver: Driver<I, O, E>,\n input: I,\n handlers: CommandHandlers<O, E, MS, MF>,\n): Command<MS | MF> {\n const { onFailure, key, concurrency } = handlers;\n return {\n driver: driver,\n input,\n // Sound: the interpreter only passes this driver's output and toError() result.\n onSuccess: (output) => handlers.onSuccess(output as O),\n ...(onFailure === undefined ? {} : { onFailure: (error: unknown) => onFailure(error as E) }),\n ...(key === undefined ? {} : { key }),\n ...(concurrency === undefined ? {} : { concurrency }),\n };\n}\n\n/** Splits a reducer result into state and commands. State is never an array (ADR 0006). */\nexport function splitNext<S, M>(\n next: Next<S, M>,\n): readonly [S, ReadonlyArray<Command<M | IntentRejected>>] {\n if (Array.isArray(next) && next.length === 2 && Array.isArray(next[1])) {\n return next as readonly [S, ReadonlyArray<Command<M | IntentRejected>>];\n }\n return [next as S, []];\n}\n"]}
|
package/dist/define.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GyralElementClass } from './element-types.js';
|
|
2
|
+
import type { ComponentSpec, Tagged } from './types.js';
|
|
3
|
+
export type { GyralElement, GyralElementClass } from './element-types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Compiles a Model-View-Intent spec into a custom element and registers it under `tag`.
|
|
6
|
+
* See docs/design-docs/0001-mvi-parsed-intent.md, 0006-effects-and-drivers.md, 0007-props.md
|
|
7
|
+
* 0008-forms.md and 0012-ssr.md.
|
|
8
|
+
*/
|
|
9
|
+
export declare function define<S, M extends Tagged, P extends object = object, O extends Tagged = never>(tag: string, spec: ComponentSpec<S, M, P>): GyralElementClass<S, M, P, O>;
|