@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.
- package/CHANGELOG.md +7 -0
- 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 +345 -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 +66 -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 +27 -0
- package/dist/focus.js +30 -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 +42 -0
- package/dist/index.js +35 -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 +128 -0
- package/dist/internal/interpreter.js.map +1 -0
- package/dist/internal/runtime.d.ts +7 -0
- package/dist/internal/runtime.js +8 -0
- package/dist/internal/runtime.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/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/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 -12
package/dist/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export { command, defineDriver } from './command.js';
|
|
2
|
+
export { child, emit } from './children.js';
|
|
3
|
+
export { focus } from './focus.js';
|
|
4
|
+
export { invokersSupported } from './invokers.js';
|
|
5
|
+
export { ISLAND_ATTRIBUTE } from './islands.js';
|
|
6
|
+
export { define } from './define.js';
|
|
7
|
+
export { changed, defineStore, send } from './store.js';
|
|
8
|
+
export { STORE_SEND } from './store.js';
|
|
9
|
+
// Store scoping: used by @gyral/ssr (server scope + page seed) and @gyral/testing.
|
|
10
|
+
export { resetDocumentStores, scriptSafeJson, STORE_SEED_ATTRIBUTE, StoreRegistry, STORES_ELEMENT, withStoreScope, } from './store-scope.js';
|
|
11
|
+
export { defineStoresProvider } from './stores-provider.js';
|
|
12
|
+
export { DRIVERS_ELEMENT, provideDrivers } from './drivers-scope.js';
|
|
13
|
+
export { jsonHazard, warnJsonHazard } from './json-safety.js';
|
|
14
|
+
export { random, randomDriver, randomInt, toInt } from './random.js';
|
|
15
|
+
export { defineForm, field, fieldErrors, form, formDataToObject, formFields, intentRejectedSchema, redirectedTo, validateForm, } from './forms.js';
|
|
16
|
+
export { ElementDirective } from './element-directive.js';
|
|
17
|
+
export { runInit } from './init.js';
|
|
18
|
+
export { invalid } from './invalid.js';
|
|
19
|
+
export { findInScope, labelledBy } from './accessible-name.js';
|
|
20
|
+
export { liveBoolean } from './live-boolean.js';
|
|
21
|
+
export { textarea, textareaMarkup } from './textarea.js';
|
|
22
|
+
// Devtools event stream (ADR 0017): emitted in development builds only.
|
|
23
|
+
export { DEVTOOLS_ENABLED as devtoolsEnabled, devLiveComponents as devtoolsLiveComponents, } from '#devtools';
|
|
24
|
+
export { DEVTOOLS_GLOBAL } from './devtools-events.js';
|
|
25
|
+
// Used by @gyral/ssr to render light-DOM components as plain children (ADR 0014).
|
|
26
|
+
export { HIDDEN_MARKER, HYDRATE_KEY, isLightComponent, LIGHT_ATTRIBUTE } from './light-dom.js';
|
|
27
|
+
// The view layer is Lit. Re-exported so most apps need a single import.
|
|
28
|
+
export { css, html, nothing, svg, unsafeCSS } from 'lit';
|
|
29
|
+
export { directive } from 'lit/directive.js';
|
|
30
|
+
export { classMap } from 'lit/directives/class-map.js';
|
|
31
|
+
export { keyed } from 'lit/directives/keyed.js';
|
|
32
|
+
export { live } from 'lit/directives/live.js';
|
|
33
|
+
export { repeat } from 'lit/directives/repeat.js';
|
|
34
|
+
export { styleMap } from 'lit/directives/style-map.js';
|
|
35
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAYrD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGhD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAexD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,mFAAmF;AACnF,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,oBAAoB,EACpB,aAAa,EACb,cAAc,EACd,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,KAAK,EAAoB,MAAM,aAAa,CAAC;AACvF,OAAO,EACL,UAAU,EACV,KAAK,EACL,WAAW,EACX,IAAI,EACJ,gBAAgB,EAChB,UAAU,EACV,oBAAoB,EACpB,YAAY,EACZ,YAAY,GACb,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACzD,wEAAwE;AACxE,OAAO,EACL,gBAAgB,IAAI,eAAe,EACnC,iBAAiB,IAAI,sBAAsB,GAC5C,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAUvD,kFAAkF;AAClF,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAqB/F,wEAAwE;AACxE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC","sourcesContent":["export { command, defineDriver } from './command.js';\nexport type {\n AnyDriver,\n Command,\n CommandHandlers,\n Concurrency,\n Driver,\n DriverContext,\n DriverOverrides,\n Next,\n RetryPolicy,\n} from './command.js';\nexport { child, emit } from './children.js';\nexport { focus } from './focus.js';\nexport type { FocusOptions } from './focus.js';\nexport { invokersSupported } from './invokers.js';\nexport type { CommandInfo } from './invokers.js';\nexport { ISLAND_ATTRIBUTE } from './islands.js';\nexport type { HydrateStrategy } from './islands.js';\nexport type { ChildSource, OutputSource } from './children.js';\nexport { define } from './define.js';\nexport { changed, defineStore, send } from './store.js';\nexport type {\n AnyStore,\n AnyStoreInstance,\n Store,\n StoreChanged,\n StoreInstance,\n StoreOverrides,\n StoreRef,\n StoreSendInput,\n SeedCheck,\n StoreResolver,\n StoreSpec,\n StoreUpdate,\n} from './store.js';\nexport { STORE_SEND } from './store.js';\n// Store scoping: used by @gyral/ssr (server scope + page seed) and @gyral/testing.\nexport {\n resetDocumentStores,\n scriptSafeJson,\n STORE_SEED_ATTRIBUTE,\n StoreRegistry,\n STORES_ELEMENT,\n withStoreScope,\n} from './store-scope.js';\nexport { defineStoresProvider } from './stores-provider.js';\nexport { DRIVERS_ELEMENT, provideDrivers } from './drivers-scope.js';\nexport { jsonHazard, warnJsonHazard } from './json-safety.js';\nexport { random, randomDriver, randomInt, toInt, type RandomInput } from './random.js';\nexport {\n defineForm,\n field,\n fieldErrors,\n form,\n formDataToObject,\n formFields,\n intentRejectedSchema,\n redirectedTo,\n validateForm,\n} from './forms.js';\nexport type { FormDefinition, FormRedirected, FormResult, FormValue } from './forms.js';\nexport { ElementDirective } from './element-directive.js';\nexport { runInit } from './init.js';\nexport { invalid } from './invalid.js';\nexport { findInScope, labelledBy } from './accessible-name.js';\nexport { liveBoolean } from './live-boolean.js';\nexport { textarea, textareaMarkup } from './textarea.js';\n// Devtools event stream (ADR 0017): emitted in development builds only.\nexport {\n DEVTOOLS_ENABLED as devtoolsEnabled,\n devLiveComponents as devtoolsLiveComponents,\n} from '#devtools';\nexport { DEVTOOLS_GLOBAL } from './devtools-events.js';\nexport type {\n CommandPhase,\n CommandTraceEvent,\n DevComponentRef,\n DevEvent,\n DevtoolsHook,\n} from './devtools-events.js';\nexport type { TextareaAttribute, TextareaOptions } from './textarea.js';\nexport type { Styles } from './styles.js';\n// Used by @gyral/ssr to render light-DOM components as plain children (ADR 0014).\nexport { HIDDEN_MARKER, HYDRATE_KEY, isLightComponent, LIGHT_ATTRIBUTE } from './light-dom.js';\nexport type { GyralElement, GyralElementClass } from './define.js';\nexport type {\n ComponentSpec,\n Ctx,\n FieldIssue,\n FormFields,\n IntentInput,\n IntentNames,\n IntentParser,\n IntentRejected,\n Intents,\n PropDeclarations,\n Hydrated,\n PropsChanged,\n Stateless,\n StoreReader,\n Tagged,\n Update,\n} from './types.js';\n\n// The view layer is Lit. Re-exported so most apps need a single import.\nexport { css, html, nothing, svg, unsafeCSS } from 'lit';\nexport { directive } from 'lit/directive.js';\nexport { classMap } from 'lit/directives/class-map.js';\nexport { keyed } from 'lit/directives/keyed.js';\nexport { live } from 'lit/directives/live.js';\nexport { repeat } from 'lit/directives/repeat.js';\nexport { styleMap } from 'lit/directives/style-map.js';\n"]}
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Next } from './command.js';
|
|
2
|
+
import type { ComponentSpec, Tagged } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Runs a spec's `init`, or starts from `{}` when a stateless component omits it. Used by
|
|
5
|
+
* `define()` and by `@gyral/testing`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function runInit<S, M extends Tagged, P>(spec: ComponentSpec<S, M, P>, props: P): Next<S, M>;
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs a spec's `init`, or starts from `{}` when a stateless component omits it. Used by
|
|
3
|
+
* `define()` and by `@gyral/testing`.
|
|
4
|
+
*/
|
|
5
|
+
export function runInit(spec, props) {
|
|
6
|
+
const { init } = spec;
|
|
7
|
+
// Sound: InitField only lets `init` be omitted when `{}` is a valid S.
|
|
8
|
+
return init === undefined ? {} : init(props);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=init.js.map
|
package/dist/init.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,UAAU,OAAO,CACrB,IAA4B,EAC5B,KAAQ;IAER,MAAM,EAAE,IAAI,EAAE,GAAG,IAAoD,CAAC;IACtE,uEAAuE;IACvE,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAE,EAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACtD,CAAC","sourcesContent":["import type { Next } from './command.js';\nimport type { ComponentSpec, Tagged } from './types.js';\n\n/**\n * Runs a spec's `init`, or starts from `{}` when a stateless component omits it. Used by\n * `define()` and by `@gyral/testing`.\n */\nexport function runInit<S, M extends Tagged, P>(\n spec: ComponentSpec<S, M, P>,\n props: P,\n): Next<S, M> {\n const { init } = spec as { readonly init?: (props: P) => Next<S, M> };\n // Sound: InitField only lets `init` be omitted when `{}` is a valid S.\n return init === undefined ? ({} as S) : init(props);\n}\n"]}
|
package/dist/intent.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IntentInput, IntentParser, Tagged } from './types.js';
|
|
2
|
+
/** Event a child component dispatches on its host to send an output up (ADR 0010). */
|
|
3
|
+
export declare const OUTPUT_EVENT = "gyral-output";
|
|
4
|
+
/**
|
|
5
|
+
* Events the intent layer listens for on each component's shadow root (capture phase, so
|
|
6
|
+
* non-bubbling events such as `toggle` are seen too). Only `click`, `submit`, `input`,
|
|
7
|
+
* `change` and outputs are default triggers; the rest fire via `data-intent-on="…"`.
|
|
8
|
+
* Components may add more with `spec.events`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const INTENT_EVENTS: readonly ["click", "submit", "input", "change", "keydown", "keyup", "focusin", "focusout", "toggle", "command", "gyral-output"];
|
|
11
|
+
/** The event that fires an intent unless `data-intent-on` overrides it. */
|
|
12
|
+
export declare function defaultTrigger(el: Element): string;
|
|
13
|
+
export declare function markGyralHost(ctor: object): void;
|
|
14
|
+
/**
|
|
15
|
+
* Finds the nearest `data-intent` element for this event that belongs to `root`. Elements
|
|
16
|
+
* inside nested components are ignored: that is component isolation.
|
|
17
|
+
*/
|
|
18
|
+
export declare function findIntentElement(event: Event, root: Node): Element | undefined;
|
|
19
|
+
/** Reads an event into an IntentInput. Form submissions are prevented and turned into FormData. */
|
|
20
|
+
export declare function readIntent(event: Event, root: Node): IntentInput | undefined;
|
|
21
|
+
export declare const intentNames: unknown;
|
|
22
|
+
/** Listens for intent events on a component root (capture phase: `toggle` doesn't bubble). */
|
|
23
|
+
export declare function listenForIntents(root: Node, extra: readonly string[], handler: (event: Event) => void): void;
|
|
24
|
+
/** Parses one event with its matching parser and delivers the message (sync or async). */
|
|
25
|
+
export declare function handleIntent<M>(event: Event, root: Node, parsers: Readonly<Record<string, IntentParser<M> | undefined>>, tag: string, deliver: (msg: Tagged | undefined) => void): void;
|
package/dist/intent.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { commandOf, shimInvokers } from './invokers.js';
|
|
2
|
+
/** Event a child component dispatches on its host to send an output up (ADR 0010). */
|
|
3
|
+
export const OUTPUT_EVENT = 'gyral-output';
|
|
4
|
+
/**
|
|
5
|
+
* Events the intent layer listens for on each component's shadow root (capture phase, so
|
|
6
|
+
* non-bubbling events such as `toggle` are seen too). Only `click`, `submit`, `input`,
|
|
7
|
+
* `change` and outputs are default triggers; the rest fire via `data-intent-on="…"`.
|
|
8
|
+
* Components may add more with `spec.events`.
|
|
9
|
+
*/
|
|
10
|
+
export const INTENT_EVENTS = [
|
|
11
|
+
'click',
|
|
12
|
+
'submit',
|
|
13
|
+
'input',
|
|
14
|
+
'change',
|
|
15
|
+
'keydown',
|
|
16
|
+
'keyup',
|
|
17
|
+
'focusin',
|
|
18
|
+
'focusout',
|
|
19
|
+
'toggle',
|
|
20
|
+
'command',
|
|
21
|
+
OUTPUT_EVENT,
|
|
22
|
+
];
|
|
23
|
+
const isToggle = (el) => el instanceof HTMLInputElement && (el.type === 'checkbox' || el.type === 'radio');
|
|
24
|
+
// ToggleEvent (popover, <details>) is not Baseline widely available; read it structurally.
|
|
25
|
+
function toggleState(event) {
|
|
26
|
+
if (event.type !== 'toggle' || !('newState' in event))
|
|
27
|
+
return undefined;
|
|
28
|
+
return event.newState === 'open' ? 'open' : 'closed';
|
|
29
|
+
}
|
|
30
|
+
const CLICK_INPUT_TYPES = new Set(['button', 'submit', 'reset', 'image']);
|
|
31
|
+
/** The event that fires an intent unless `data-intent-on` overrides it. */
|
|
32
|
+
export function defaultTrigger(el) {
|
|
33
|
+
if (el instanceof HTMLFormElement)
|
|
34
|
+
return 'submit';
|
|
35
|
+
if (el instanceof HTMLSelectElement)
|
|
36
|
+
return 'change';
|
|
37
|
+
if (el instanceof HTMLTextAreaElement)
|
|
38
|
+
return 'input';
|
|
39
|
+
if (el instanceof HTMLInputElement) {
|
|
40
|
+
if (CLICK_INPUT_TYPES.has(el.type))
|
|
41
|
+
return 'click';
|
|
42
|
+
return el.type === 'checkbox' || el.type === 'radio' ? 'change' : 'input';
|
|
43
|
+
}
|
|
44
|
+
// Custom elements (autonomous: the name has a dash) talk to their parent via outputs.
|
|
45
|
+
if (el.localName.includes('-'))
|
|
46
|
+
return OUTPUT_EVENT;
|
|
47
|
+
return 'click';
|
|
48
|
+
}
|
|
49
|
+
function triggerOf(el) {
|
|
50
|
+
return el.getAttribute('data-intent-on') ?? defaultTrigger(el);
|
|
51
|
+
}
|
|
52
|
+
/** Every class `define()` creates, so intent lookup can tell where a component begins. */
|
|
53
|
+
const hostClasses = new WeakSet();
|
|
54
|
+
export function markGyralHost(ctor) {
|
|
55
|
+
hostClasses.add(ctor);
|
|
56
|
+
}
|
|
57
|
+
const isGyralHost = (node) => hostClasses.has(node.constructor);
|
|
58
|
+
/**
|
|
59
|
+
* Does `node` belong to the component whose intent root is `root`? `root` is the component's
|
|
60
|
+
* shadow root, or the host itself for light-DOM components (ADR 0014). The node must be in the
|
|
61
|
+
* same tree (not inside a nested shadow root) with no other Gyral host between them: a nested
|
|
62
|
+
* component, light-DOM or shadow, owns its own content. The nested host element itself (with
|
|
63
|
+
* the parent's `data-intent` on it) still belongs to the parent.
|
|
64
|
+
*/
|
|
65
|
+
function ownedBy(node, root) {
|
|
66
|
+
const tree = root instanceof ShadowRoot ? root : root.getRootNode();
|
|
67
|
+
if (node.getRootNode() !== tree)
|
|
68
|
+
return false;
|
|
69
|
+
for (let p = node.parentNode; p !== null; p = p.parentNode) {
|
|
70
|
+
if (p === root)
|
|
71
|
+
return true;
|
|
72
|
+
if (isGyralHost(p))
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Finds the nearest `data-intent` element for this event that belongs to `root`. Elements
|
|
79
|
+
* inside nested components are ignored: that is component isolation.
|
|
80
|
+
*/
|
|
81
|
+
export function findIntentElement(event, root) {
|
|
82
|
+
for (const node of event.composedPath()) {
|
|
83
|
+
if (node === root)
|
|
84
|
+
return undefined;
|
|
85
|
+
if (node instanceof Element &&
|
|
86
|
+
node.hasAttribute('data-intent') &&
|
|
87
|
+
triggerOf(node) === event.type &&
|
|
88
|
+
ownedBy(node, root)) {
|
|
89
|
+
return node;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
function valueOf(el) {
|
|
95
|
+
if (el instanceof HTMLInputElement ||
|
|
96
|
+
el instanceof HTMLSelectElement ||
|
|
97
|
+
el instanceof HTMLTextAreaElement ||
|
|
98
|
+
el instanceof HTMLButtonElement) {
|
|
99
|
+
return el.value;
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
/** Reads an event into an IntentInput. Form submissions are prevented and turned into FormData. */
|
|
104
|
+
export function readIntent(event, root) {
|
|
105
|
+
const target = findIntentElement(event, root);
|
|
106
|
+
const name = target?.getAttribute('data-intent');
|
|
107
|
+
if (target === undefined || name == null)
|
|
108
|
+
return undefined;
|
|
109
|
+
let formData;
|
|
110
|
+
if (target instanceof HTMLFormElement && event instanceof SubmitEvent) {
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
formData = new FormData(target, event.submitter);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
name,
|
|
116
|
+
event,
|
|
117
|
+
target,
|
|
118
|
+
value: valueOf(target),
|
|
119
|
+
checked: isToggle(target) ? target.checked : undefined,
|
|
120
|
+
formData,
|
|
121
|
+
detail: event instanceof CustomEvent ? event.detail : undefined,
|
|
122
|
+
key: event instanceof KeyboardEvent ? event.key : undefined,
|
|
123
|
+
newState: toggleState(event),
|
|
124
|
+
command: commandOf(event),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// Any property read returns its own name, so `intents.Increment === 'Increment'`.
|
|
128
|
+
// Types restrict reads to real message tags; a tag without a parser warns at event time.
|
|
129
|
+
export const intentNames = new Proxy({}, { get: (_target, key) => (typeof key === 'string' ? key : undefined) });
|
|
130
|
+
/** Listens for intent events on a component root (capture phase: `toggle` doesn't bubble). */
|
|
131
|
+
export function listenForIntents(root, extra, handler) {
|
|
132
|
+
for (const type of new Set([...INTENT_EVENTS, ...extra])) {
|
|
133
|
+
root.addEventListener(type, handler, { capture: true });
|
|
134
|
+
}
|
|
135
|
+
shimInvokers(root); // `command` intents in browsers without invoker commands
|
|
136
|
+
}
|
|
137
|
+
/** Parses one event with its matching parser and delivers the message (sync or async). */
|
|
138
|
+
export function handleIntent(event, root, parsers, tag, deliver) {
|
|
139
|
+
const input = readIntent(event, root);
|
|
140
|
+
if (input === undefined)
|
|
141
|
+
return;
|
|
142
|
+
const parser = parsers[input.name];
|
|
143
|
+
if (parser === undefined) {
|
|
144
|
+
console.warn(`<${tag}> has no intent parser for data-intent="${input.name}".`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const result = parser(input);
|
|
148
|
+
if (result instanceof Promise) {
|
|
149
|
+
result.then(deliver, (error) => {
|
|
150
|
+
console.error(`<${tag}> intent parser for "${input.name}" failed`, error);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
deliver(result);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=intent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intent.js","sourceRoot":"","sources":["../src/intent.ts"],"names":[],"mappings":"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","sourcesContent":["import { commandOf, shimInvokers } from './invokers.js';\nimport type { IntentInput, IntentParser, Tagged } from './types.js';\n\n/** Event a child component dispatches on its host to send an output up (ADR 0010). */\nexport const OUTPUT_EVENT = 'gyral-output';\n\n/**\n * Events the intent layer listens for on each component's shadow root (capture phase, so\n * non-bubbling events such as `toggle` are seen too). Only `click`, `submit`, `input`,\n * `change` and outputs are default triggers; the rest fire via `data-intent-on=\"…\"`.\n * Components may add more with `spec.events`.\n */\nexport const INTENT_EVENTS = [\n 'click',\n 'submit',\n 'input',\n 'change',\n 'keydown',\n 'keyup',\n 'focusin',\n 'focusout',\n 'toggle',\n 'command',\n OUTPUT_EVENT,\n] as const;\n\nconst isToggle = (el: Element): el is HTMLInputElement =>\n el instanceof HTMLInputElement && (el.type === 'checkbox' || el.type === 'radio');\n\n// ToggleEvent (popover, <details>) is not Baseline widely available; read it structurally.\nfunction toggleState(event: Event): 'open' | 'closed' | undefined {\n if (event.type !== 'toggle' || !('newState' in event)) return undefined;\n return event.newState === 'open' ? 'open' : 'closed';\n}\n\nconst CLICK_INPUT_TYPES = new Set(['button', 'submit', 'reset', 'image']);\n\n/** The event that fires an intent unless `data-intent-on` overrides it. */\nexport function defaultTrigger(el: Element): string {\n if (el instanceof HTMLFormElement) return 'submit';\n if (el instanceof HTMLSelectElement) return 'change';\n if (el instanceof HTMLTextAreaElement) return 'input';\n if (el instanceof HTMLInputElement) {\n if (CLICK_INPUT_TYPES.has(el.type)) return 'click';\n return el.type === 'checkbox' || el.type === 'radio' ? 'change' : 'input';\n }\n // Custom elements (autonomous: the name has a dash) talk to their parent via outputs.\n if (el.localName.includes('-')) return OUTPUT_EVENT;\n return 'click';\n}\n\nfunction triggerOf(el: Element): string {\n return el.getAttribute('data-intent-on') ?? defaultTrigger(el);\n}\n\n/** Every class `define()` creates, so intent lookup can tell where a component begins. */\nconst hostClasses = new WeakSet<object>();\n\nexport function markGyralHost(ctor: object): void {\n hostClasses.add(ctor);\n}\n\nconst isGyralHost = (node: Node): boolean => hostClasses.has(node.constructor);\n\n/**\n * Does `node` belong to the component whose intent root is `root`? `root` is the component's\n * shadow root, or the host itself for light-DOM components (ADR 0014). The node must be in the\n * same tree (not inside a nested shadow root) with no other Gyral host between them: a nested\n * component, light-DOM or shadow, owns its own content. The nested host element itself (with\n * the parent's `data-intent` on it) still belongs to the parent.\n */\nfunction ownedBy(node: Element, root: Node): boolean {\n const tree = root instanceof ShadowRoot ? root : root.getRootNode();\n if (node.getRootNode() !== tree) return false;\n for (let p = node.parentNode; p !== null; p = p.parentNode) {\n if (p === root) return true;\n if (isGyralHost(p)) return false;\n }\n return false;\n}\n\n/**\n * Finds the nearest `data-intent` element for this event that belongs to `root`. Elements\n * inside nested components are ignored: that is component isolation.\n */\nexport function findIntentElement(event: Event, root: Node): Element | undefined {\n for (const node of event.composedPath()) {\n if (node === root) return undefined;\n if (\n node instanceof Element &&\n node.hasAttribute('data-intent') &&\n triggerOf(node) === event.type &&\n ownedBy(node, root)\n ) {\n return node;\n }\n }\n return undefined;\n}\n\nfunction valueOf(el: Element): string | undefined {\n if (\n el instanceof HTMLInputElement ||\n el instanceof HTMLSelectElement ||\n el instanceof HTMLTextAreaElement ||\n el instanceof HTMLButtonElement\n ) {\n return el.value;\n }\n return undefined;\n}\n\n/** Reads an event into an IntentInput. Form submissions are prevented and turned into FormData. */\nexport function readIntent(event: Event, root: Node): IntentInput | undefined {\n const target = findIntentElement(event, root);\n const name = target?.getAttribute('data-intent');\n if (target === undefined || name == null) return undefined;\n let formData: FormData | undefined;\n if (target instanceof HTMLFormElement && event instanceof SubmitEvent) {\n event.preventDefault();\n formData = new FormData(target, event.submitter);\n }\n return {\n name,\n event,\n target,\n value: valueOf(target),\n checked: isToggle(target) ? target.checked : undefined,\n formData,\n detail: event instanceof CustomEvent ? (event.detail as unknown) : undefined,\n key: event instanceof KeyboardEvent ? event.key : undefined,\n newState: toggleState(event),\n command: commandOf(event),\n };\n}\n\n// Any property read returns its own name, so `intents.Increment === 'Increment'`.\n// Types restrict reads to real message tags; a tag without a parser warns at event time.\nexport const intentNames: unknown = new Proxy(\n {},\n { get: (_target, key) => (typeof key === 'string' ? key : undefined) },\n);\n\n/** Listens for intent events on a component root (capture phase: `toggle` doesn't bubble). */\nexport function listenForIntents(\n root: Node,\n extra: readonly string[],\n handler: (event: Event) => void,\n): void {\n for (const type of new Set([...INTENT_EVENTS, ...extra])) {\n root.addEventListener(type, handler, { capture: true });\n }\n shimInvokers(root); // `command` intents in browsers without invoker commands\n}\n\n/** Parses one event with its matching parser and delivers the message (sync or async). */\nexport function handleIntent<M>(\n event: Event,\n root: Node,\n parsers: Readonly<Record<string, IntentParser<M> | undefined>>,\n tag: string,\n deliver: (msg: Tagged | undefined) => void,\n): void {\n const input = readIntent(event, root);\n if (input === undefined) return;\n const parser = parsers[input.name];\n if (parser === undefined) {\n console.warn(`<${tag}> has no intent parser for data-intent=\"${input.name}\".`);\n return;\n }\n const result = parser(input) as Tagged | undefined | Promise<Tagged | undefined>;\n if (result instanceof Promise) {\n result.then(deliver, (error: unknown) => {\n console.error(`<${tag}> intent parser for \"${input.name}\" failed`, error);\n });\n } else {\n deliver(result);\n }\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AnyDriver, Command } from '../command.js';
|
|
2
|
+
import type { CommandTrace } from '../devtools-events.js';
|
|
3
|
+
/** Runs commands for one connected element. Disposed on disconnect. */
|
|
4
|
+
export interface Interpreter<M> {
|
|
5
|
+
readonly run: (cmd: Command<M>) => void;
|
|
6
|
+
readonly dispose: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function makeInterpreter<M>(resolve: (driver: AnyDriver) => AnyDriver, dispatch: (msg: M) => void, trace?: CommandTrace): Interpreter<M>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Data, Duration, Effect, Fiber, Schedule } from 'effect';
|
|
2
|
+
import { DEVTOOLS_ENABLED } from '#devtools';
|
|
3
|
+
import { fork } from './runtime.js';
|
|
4
|
+
class DriverFailure extends Data.TaggedError('DriverFailure') {
|
|
5
|
+
}
|
|
6
|
+
const isRunning = (fiber) => fiber !== undefined && fiber.unsafePoll() === null;
|
|
7
|
+
const withRetry = (attempt, policy) => {
|
|
8
|
+
if (policy === undefined || policy.times <= 0)
|
|
9
|
+
return attempt;
|
|
10
|
+
const base = Duration.millis(policy.delayMs ?? 0);
|
|
11
|
+
const delays = policy.backoff === 'exponential' ? Schedule.exponential(base) : Schedule.spaced(base);
|
|
12
|
+
return Effect.retry(attempt, delays.pipe(Schedule.intersect(Schedule.recurs(policy.times))));
|
|
13
|
+
};
|
|
14
|
+
const execute = (driver, cmd, dispatch, report) => {
|
|
15
|
+
const deliver = (output) => {
|
|
16
|
+
try {
|
|
17
|
+
const msg = cmd.onSuccess(output);
|
|
18
|
+
if (msg !== undefined)
|
|
19
|
+
dispatch(msg);
|
|
20
|
+
}
|
|
21
|
+
catch (defect) {
|
|
22
|
+
console.error('gyral: command mapper threw', defect);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const attempt = Effect.tryPromise({
|
|
26
|
+
try: (signal) => {
|
|
27
|
+
let settled = false;
|
|
28
|
+
const emit = (output) => {
|
|
29
|
+
if (!settled && !signal.aborted)
|
|
30
|
+
deliver(output);
|
|
31
|
+
};
|
|
32
|
+
// The input type was erased by command(); it was built for this driver's name.
|
|
33
|
+
const result = Promise.resolve(driver.run(cmd.input, { signal, emit }));
|
|
34
|
+
return result.finally(() => {
|
|
35
|
+
settled = true;
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
catch: (cause) => new DriverFailure({ cause }),
|
|
39
|
+
});
|
|
40
|
+
const traced = DEVTOOLS_ENABLED && report !== undefined
|
|
41
|
+
? withRetry(attempt, driver.retry).pipe(Effect.onInterrupt(() => Effect.sync(() => {
|
|
42
|
+
report('interrupted');
|
|
43
|
+
})))
|
|
44
|
+
: withRetry(attempt, driver.retry);
|
|
45
|
+
return traced.pipe(Effect.matchEffect({
|
|
46
|
+
onSuccess: (output) => {
|
|
47
|
+
if (DEVTOOLS_ENABLED)
|
|
48
|
+
report?.('settled', output);
|
|
49
|
+
return Effect.succeed(cmd.onSuccess(output));
|
|
50
|
+
},
|
|
51
|
+
onFailure: (failure) => {
|
|
52
|
+
const error = driver.toError === undefined ? failure.cause : driver.toError(failure.cause);
|
|
53
|
+
if (DEVTOOLS_ENABLED)
|
|
54
|
+
report?.('failed', error);
|
|
55
|
+
return cmd.onFailure === undefined
|
|
56
|
+
? Effect.logWarning(`gyral: unhandled failure from driver "${driver.name}"`, error).pipe(Effect.as(undefined))
|
|
57
|
+
: Effect.succeed(cmd.onFailure(error));
|
|
58
|
+
},
|
|
59
|
+
}), Effect.flatMap((msg) => Effect.sync(() => {
|
|
60
|
+
if (msg !== undefined)
|
|
61
|
+
dispatch(msg);
|
|
62
|
+
})), Effect.catchAllDefect((defect) => Effect.logError('gyral: command mapper threw', defect)));
|
|
63
|
+
};
|
|
64
|
+
export function makeInterpreter(resolve, dispatch, trace) {
|
|
65
|
+
const reporter = (driver, lane, policy, input) => trace === undefined
|
|
66
|
+
? undefined
|
|
67
|
+
: (phase, result) => {
|
|
68
|
+
trace({
|
|
69
|
+
phase,
|
|
70
|
+
driver,
|
|
71
|
+
lane,
|
|
72
|
+
policy,
|
|
73
|
+
input,
|
|
74
|
+
...(result === undefined ? {} : { result }),
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const lanes = new Map();
|
|
78
|
+
const all = new Set();
|
|
79
|
+
let active = true;
|
|
80
|
+
const guardedDispatch = (msg) => {
|
|
81
|
+
if (active)
|
|
82
|
+
dispatch(msg);
|
|
83
|
+
};
|
|
84
|
+
const track = (lane, fiber) => {
|
|
85
|
+
if (!isRunning(fiber))
|
|
86
|
+
return;
|
|
87
|
+
lanes.set(lane, fiber);
|
|
88
|
+
all.add(fiber);
|
|
89
|
+
fiber.addObserver(() => {
|
|
90
|
+
all.delete(fiber);
|
|
91
|
+
if (lanes.get(lane) === fiber)
|
|
92
|
+
lanes.delete(lane);
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
const run = (cmd) => {
|
|
96
|
+
if (!active)
|
|
97
|
+
return;
|
|
98
|
+
const driver = resolve(cmd.driver);
|
|
99
|
+
const lane = cmd.key ?? driver.name;
|
|
100
|
+
const policy = cmd.concurrency ?? driver.concurrency ?? 'merge';
|
|
101
|
+
const previous = lanes.get(lane);
|
|
102
|
+
const inFlight = isRunning(previous) ? previous : undefined;
|
|
103
|
+
const report = DEVTOOLS_ENABLED ? reporter(driver.name, lane, policy, cmd.input) : undefined;
|
|
104
|
+
let program = execute(driver, cmd, guardedDispatch, report);
|
|
105
|
+
if (inFlight !== undefined && policy === 'exhaust') {
|
|
106
|
+
if (DEVTOOLS_ENABLED)
|
|
107
|
+
report?.('dropped');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (DEVTOOLS_ENABLED)
|
|
111
|
+
report?.('issued');
|
|
112
|
+
if (inFlight !== undefined) {
|
|
113
|
+
if (policy === 'switch')
|
|
114
|
+
program = Effect.zipRight(Fiber.interrupt(inFlight), program);
|
|
115
|
+
if (policy === 'queue')
|
|
116
|
+
program = Effect.zipRight(Fiber.await(inFlight), program);
|
|
117
|
+
}
|
|
118
|
+
track(lane, fork(program));
|
|
119
|
+
};
|
|
120
|
+
const dispose = () => {
|
|
121
|
+
active = false;
|
|
122
|
+
fork(Fiber.interruptAll([...all]));
|
|
123
|
+
all.clear();
|
|
124
|
+
lanes.clear();
|
|
125
|
+
};
|
|
126
|
+
return { run, dispose };
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=interpreter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interpreter.js","sourceRoot":"","sources":["../../src/internal/interpreter.ts"],"names":[],"mappings":"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","sourcesContent":["import { Data, Duration, Effect, Fiber, Schedule } from 'effect';\nimport { DEVTOOLS_ENABLED } from '#devtools';\nimport type { AnyDriver, Command, Concurrency, RetryPolicy } from '../command.js';\nimport type { CommandPhase, CommandTrace } from '../devtools-events.js';\nimport { fork } from './runtime.js';\n\nclass DriverFailure extends Data.TaggedError('DriverFailure')<{ readonly cause: unknown }> {}\n\ntype Running = Fiber.RuntimeFiber<void>;\n\n/** Runs commands for one connected element. Disposed on disconnect. */\nexport interface Interpreter<M> {\n readonly run: (cmd: Command<M>) => void;\n readonly dispose: () => void;\n}\n\nconst isRunning = (fiber: Running | undefined): fiber is Running =>\n fiber !== undefined && fiber.unsafePoll() === null;\n\nconst withRetry = <A>(\n attempt: Effect.Effect<A, DriverFailure>,\n policy: RetryPolicy | undefined,\n): Effect.Effect<A, DriverFailure> => {\n if (policy === undefined || policy.times <= 0) return attempt;\n const base = Duration.millis(policy.delayMs ?? 0);\n const delays: Schedule.Schedule<unknown> =\n policy.backoff === 'exponential' ? Schedule.exponential(base) : Schedule.spaced(base);\n return Effect.retry(attempt, delays.pipe(Schedule.intersect(Schedule.recurs(policy.times))));\n};\n\n/** Reports one command's lifecycle to devtools (ADR 0017); undefined in production. */\ntype Report = ((phase: CommandPhase, result?: unknown) => void) | undefined;\n\nconst execute = <M>(\n driver: AnyDriver,\n cmd: Command<M>,\n dispatch: (msg: M) => void,\n report: Report,\n): Effect.Effect<void> => {\n const deliver = (output: unknown): void => {\n try {\n const msg = cmd.onSuccess(output);\n if (msg !== undefined) dispatch(msg);\n } catch (defect) {\n console.error('gyral: command mapper threw', defect);\n }\n };\n const attempt = Effect.tryPromise({\n try: (signal) => {\n let settled = false;\n const emit = (output: unknown): void => {\n if (!settled && !signal.aborted) deliver(output);\n };\n // The input type was erased by command(); it was built for this driver's name.\n const result = Promise.resolve(driver.run(cmd.input as never, { signal, emit }));\n return result.finally(() => {\n settled = true;\n });\n },\n catch: (cause) => new DriverFailure({ cause }),\n });\n const traced =\n DEVTOOLS_ENABLED && report !== undefined\n ? withRetry(attempt, driver.retry).pipe(\n Effect.onInterrupt(() =>\n Effect.sync(() => {\n report('interrupted');\n }),\n ),\n )\n : withRetry(attempt, driver.retry);\n return traced.pipe(\n Effect.matchEffect({\n onSuccess: (output) => {\n if (DEVTOOLS_ENABLED) report?.('settled', output);\n return Effect.succeed(cmd.onSuccess(output));\n },\n onFailure: (failure) => {\n const error = driver.toError === undefined ? failure.cause : driver.toError(failure.cause);\n if (DEVTOOLS_ENABLED) report?.('failed', error);\n return cmd.onFailure === undefined\n ? Effect.logWarning(`gyral: unhandled failure from driver \"${driver.name}\"`, error).pipe(\n Effect.as(undefined),\n )\n : Effect.succeed(cmd.onFailure(error));\n },\n }),\n Effect.flatMap((msg) =>\n Effect.sync(() => {\n if (msg !== undefined) dispatch(msg);\n }),\n ),\n Effect.catchAllDefect((defect) => Effect.logError('gyral: command mapper threw', defect)),\n );\n};\n\nexport function makeInterpreter<M>(\n resolve: (driver: AnyDriver) => AnyDriver,\n dispatch: (msg: M) => void,\n trace?: CommandTrace,\n): Interpreter<M> {\n const reporter = (driver: string, lane: string, policy: Concurrency, input: unknown): Report =>\n trace === undefined\n ? undefined\n : (phase, result) => {\n trace({\n phase,\n driver,\n lane,\n policy,\n input,\n ...(result === undefined ? {} : { result }),\n });\n };\n const lanes = new Map<string, Running>();\n const all = new Set<Running>();\n let active = true;\n const guardedDispatch = (msg: M): void => {\n if (active) dispatch(msg);\n };\n\n const track = (lane: string, fiber: Running): void => {\n if (!isRunning(fiber)) return;\n lanes.set(lane, fiber);\n all.add(fiber);\n fiber.addObserver(() => {\n all.delete(fiber);\n if (lanes.get(lane) === fiber) lanes.delete(lane);\n });\n };\n\n const run = (cmd: Command<M>): void => {\n if (!active) return;\n const driver = resolve(cmd.driver);\n const lane = cmd.key ?? driver.name;\n const policy = cmd.concurrency ?? driver.concurrency ?? 'merge';\n const previous = lanes.get(lane);\n const inFlight = isRunning(previous) ? previous : undefined;\n const report = DEVTOOLS_ENABLED ? reporter(driver.name, lane, policy, cmd.input) : undefined;\n let program = execute(driver, cmd, guardedDispatch, report);\n if (inFlight !== undefined && policy === 'exhaust') {\n if (DEVTOOLS_ENABLED) report?.('dropped');\n return;\n }\n if (DEVTOOLS_ENABLED) report?.('issued');\n if (inFlight !== undefined) {\n if (policy === 'switch') program = Effect.zipRight(Fiber.interrupt(inFlight), program);\n if (policy === 'queue') program = Effect.zipRight(Fiber.await(inFlight), program);\n }\n track(lane, fork(program));\n };\n\n const dispose = (): void => {\n active = false;\n fork(Fiber.interruptAll([...all]));\n all.clear();\n lanes.clear();\n };\n\n return { run, dispose };\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Effect, type Fiber } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* The single place Gyral starts Effect programs (ADR 0006). There are no services to
|
|
4
|
+
* provide yet, so the default runtime is enough; a ManagedRuntime with layers goes here
|
|
5
|
+
* when drivers need shared resources.
|
|
6
|
+
*/
|
|
7
|
+
export declare const fork: <A>(program: Effect.Effect<A>) => Fiber.RuntimeFiber<A>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* The single place Gyral starts Effect programs (ADR 0006). There are no services to
|
|
4
|
+
* provide yet, so the default runtime is enough; a ManagedRuntime with layers goes here
|
|
5
|
+
* when drivers need shared resources.
|
|
6
|
+
*/
|
|
7
|
+
export const fork = (program) => Effect.runFork(program);
|
|
8
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/internal/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAc,MAAM,QAAQ,CAAC;AAE5C;;;;GAIG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAI,OAAyB,EAAyB,EAAE,CAC1E,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC","sourcesContent":["import { Effect, type Fiber } from 'effect';\n\n/**\n * The single place Gyral starts Effect programs (ADR 0006). There are no services to\n * provide yet, so the default runtime is enough; a ManagedRuntime with layers goes here\n * when drivers need shared resources.\n */\nexport const fork = <A>(program: Effect.Effect<A>): Fiber.RuntimeFiber<A> =>\n Effect.runFork(program);\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ElementDirective } from './element-directive.js';
|
|
2
|
+
type Errors = readonly string[] | string | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Mirrors model errors to native validity (ADR 0008): sets `setCustomValidity` and
|
|
5
|
+
* `aria-invalid`, so `:user-invalid` and native bubbles agree with the model.
|
|
6
|
+
*
|
|
7
|
+
* The custom error clears when:
|
|
8
|
+
* - the control fires `input` or `change` (the user edited it);
|
|
9
|
+
* - the bound errors become `undefined` or empty (the model cleared them);
|
|
10
|
+
* - a submit finds the error stale: the value changed without an event (set from code, e.g.
|
|
11
|
+
* by a test or a script). Then the error is dropped and the form is submitted again once.
|
|
12
|
+
*
|
|
13
|
+
* An error is applied again only when a new value arrives (keep `fieldErrors()` results in
|
|
14
|
+
* state). Element parts don't render on the server, so this only runs in the browser.
|
|
15
|
+
*/
|
|
16
|
+
declare class InvalidDirective extends ElementDirective<[errors?: Errors]> {
|
|
17
|
+
#private;
|
|
18
|
+
apply(el: Element, [errors]: [errors?: Errors]): void;
|
|
19
|
+
}
|
|
20
|
+
export declare const invalid: (errors?: Errors) => import("lit/directive.js").DirectiveResult<typeof InvalidDirective>;
|
|
21
|
+
export {};
|
package/dist/invalid.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { directive } from 'lit/directive.js';
|
|
2
|
+
import { ElementDirective } from './element-directive.js';
|
|
3
|
+
const isValidatable = (el) => 'setCustomValidity' in el;
|
|
4
|
+
const valueOf = (el) => ('value' in el ? el.value : undefined);
|
|
5
|
+
const formOf = (el) => {
|
|
6
|
+
const form = 'form' in el ? el.form : undefined;
|
|
7
|
+
return form instanceof HTMLFormElement ? form : undefined;
|
|
8
|
+
};
|
|
9
|
+
// Forms with a resubmit already queued: several stale fields in one validation pass resubmit once.
|
|
10
|
+
const resubmitting = new WeakSet();
|
|
11
|
+
/**
|
|
12
|
+
* Mirrors model errors to native validity (ADR 0008): sets `setCustomValidity` and
|
|
13
|
+
* `aria-invalid`, so `:user-invalid` and native bubbles agree with the model.
|
|
14
|
+
*
|
|
15
|
+
* The custom error clears when:
|
|
16
|
+
* - the control fires `input` or `change` (the user edited it);
|
|
17
|
+
* - the bound errors become `undefined` or empty (the model cleared them);
|
|
18
|
+
* - a submit finds the error stale: the value changed without an event (set from code, e.g.
|
|
19
|
+
* by a test or a script). Then the error is dropped and the form is submitted again once.
|
|
20
|
+
*
|
|
21
|
+
* An error is applied again only when a new value arrives (keep `fieldErrors()` results in
|
|
22
|
+
* state). Element parts don't render on the server, so this only runs in the browser.
|
|
23
|
+
*/
|
|
24
|
+
class InvalidDirective extends ElementDirective {
|
|
25
|
+
#last;
|
|
26
|
+
#listening;
|
|
27
|
+
apply(el, [errors]) {
|
|
28
|
+
if (errors === this.#last)
|
|
29
|
+
return;
|
|
30
|
+
this.#last = errors;
|
|
31
|
+
this.#listening?.abort();
|
|
32
|
+
this.#listening = undefined;
|
|
33
|
+
const message = typeof errors === 'string' ? errors : (errors ?? []).join(' ');
|
|
34
|
+
if (isValidatable(el))
|
|
35
|
+
el.setCustomValidity(message);
|
|
36
|
+
if (message === '') {
|
|
37
|
+
el.removeAttribute('aria-invalid');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
el.setAttribute('aria-invalid', 'true');
|
|
41
|
+
const listening = new AbortController();
|
|
42
|
+
this.#listening = listening;
|
|
43
|
+
const { signal } = listening;
|
|
44
|
+
const rejectedValue = valueOf(el);
|
|
45
|
+
const clear = () => {
|
|
46
|
+
listening.abort();
|
|
47
|
+
if (isValidatable(el))
|
|
48
|
+
el.setCustomValidity('');
|
|
49
|
+
el.removeAttribute('aria-invalid');
|
|
50
|
+
};
|
|
51
|
+
el.addEventListener('input', clear, { signal });
|
|
52
|
+
el.addEventListener('change', clear, { signal });
|
|
53
|
+
el.addEventListener('invalid', (event) => {
|
|
54
|
+
if (Object.is(valueOf(el), rejectedValue))
|
|
55
|
+
return; // still the rejected value
|
|
56
|
+
event.preventDefault(); // no bubble for an error that no longer applies
|
|
57
|
+
clear();
|
|
58
|
+
const form = formOf(el);
|
|
59
|
+
if (form === undefined || resubmitting.has(form))
|
|
60
|
+
return;
|
|
61
|
+
resubmitting.add(form);
|
|
62
|
+
queueMicrotask(() => {
|
|
63
|
+
resubmitting.delete(form);
|
|
64
|
+
form.requestSubmit();
|
|
65
|
+
});
|
|
66
|
+
}, { signal });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export const invalid = directive(InvalidDirective);
|
|
70
|
+
//# sourceMappingURL=invalid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invalid.js","sourceRoot":"","sources":["../src/invalid.ts"],"names":[],"mappings":"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","sourcesContent":["import { directive } from 'lit/directive.js';\nimport { ElementDirective } from './element-directive.js';\n\ntype Errors = readonly string[] | string | undefined;\n\ninterface Validatable extends Element {\n setCustomValidity(message: string): void;\n}\n\nconst isValidatable = (el: Element): el is Validatable => 'setCustomValidity' in el;\n\nconst valueOf = (el: Element): unknown => ('value' in el ? el.value : undefined);\n\nconst formOf = (el: Element): HTMLFormElement | undefined => {\n const form = 'form' in el ? el.form : undefined;\n return form instanceof HTMLFormElement ? form : undefined;\n};\n\n// Forms with a resubmit already queued: several stale fields in one validation pass resubmit once.\nconst resubmitting = new WeakSet<HTMLFormElement>();\n\n/**\n * Mirrors model errors to native validity (ADR 0008): sets `setCustomValidity` and\n * `aria-invalid`, so `:user-invalid` and native bubbles agree with the model.\n *\n * The custom error clears when:\n * - the control fires `input` or `change` (the user edited it);\n * - the bound errors become `undefined` or empty (the model cleared them);\n * - a submit finds the error stale: the value changed without an event (set from code, e.g.\n * by a test or a script). Then the error is dropped and the form is submitted again once.\n *\n * An error is applied again only when a new value arrives (keep `fieldErrors()` results in\n * state). Element parts don't render on the server, so this only runs in the browser.\n */\nclass InvalidDirective extends ElementDirective<[errors?: Errors]> {\n #last: Errors;\n #listening: AbortController | undefined;\n\n apply(el: Element, [errors]: [errors?: Errors]): void {\n if (errors === this.#last) return;\n this.#last = errors;\n this.#listening?.abort();\n this.#listening = undefined;\n const message = typeof errors === 'string' ? errors : (errors ?? []).join(' ');\n if (isValidatable(el)) el.setCustomValidity(message);\n if (message === '') {\n el.removeAttribute('aria-invalid');\n return;\n }\n el.setAttribute('aria-invalid', 'true');\n\n const listening = new AbortController();\n this.#listening = listening;\n const { signal } = listening;\n const rejectedValue = valueOf(el);\n const clear = (): void => {\n listening.abort();\n if (isValidatable(el)) el.setCustomValidity('');\n el.removeAttribute('aria-invalid');\n };\n el.addEventListener('input', clear, { signal });\n el.addEventListener('change', clear, { signal });\n el.addEventListener(\n 'invalid',\n (event) => {\n if (Object.is(valueOf(el), rejectedValue)) return; // still the rejected value\n event.preventDefault(); // no bubble for an error that no longer applies\n clear();\n const form = formOf(el);\n if (form === undefined || resubmitting.has(form)) return;\n resubmitting.add(form);\n queueMicrotask(() => {\n resubmitting.delete(form);\n form.requestSubmit();\n });\n },\n { signal },\n );\n }\n}\n\nexport const invalid = directive(InvalidDirective);\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface CommandInfo {
|
|
2
|
+
/** The command, e.g. `--clear`. */
|
|
3
|
+
readonly command: string;
|
|
4
|
+
/** The invoker (the button) that sent it. */
|
|
5
|
+
readonly source: Element | undefined;
|
|
6
|
+
}
|
|
7
|
+
/** Native invoker commands available? */
|
|
8
|
+
export declare const invokersSupported: () => boolean;
|
|
9
|
+
/** The command carried by a native `CommandEvent` or by Gyral's fallback event. */
|
|
10
|
+
export declare function commandOf(event: Event): CommandInfo | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Fallback for browsers without invoker commands: a click on a `command="--…"` invoker inside
|
|
13
|
+
* `root` dispatches a `command` CustomEvent on its `commandfor` target (same tree scope).
|
|
14
|
+
*/
|
|
15
|
+
export declare function shimInvokers(root: Node): void;
|