@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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Tree-scoped driver overrides (gyral-czi.35, ADR 0006 addendum): which driver a command uses.
|
|
2
|
+
// Resolution: el.drivers → nearest driver provider (across shadow roots) → spec.drivers →
|
|
3
|
+
// the command's own driver. Mirrors <gyral-stores> (ADR 0013) for stores.
|
|
4
|
+
import { isServer } from 'lit';
|
|
5
|
+
const onServer = isServer;
|
|
6
|
+
/** Provider element name: `<gyral-drivers .drivers=${{ http: fake }}>…</gyral-drivers>`. */
|
|
7
|
+
export const DRIVERS_ELEMENT = 'gyral-drivers';
|
|
8
|
+
// Elements registered with provideDrivers() (any element can act as a provider).
|
|
9
|
+
const provided = new WeakMap();
|
|
10
|
+
/**
|
|
11
|
+
* Makes `element` a driver provider for every Gyral component below it (including inside
|
|
12
|
+
* shadow roots). Returns a function that removes the overrides. Use it on a test container
|
|
13
|
+
* or an app root; a `<gyral-drivers>` element with a `.drivers` property works the same way.
|
|
14
|
+
*/
|
|
15
|
+
export function provideDrivers(element, drivers) {
|
|
16
|
+
provided.set(element, drivers);
|
|
17
|
+
return () => {
|
|
18
|
+
if (provided.get(element) === drivers)
|
|
19
|
+
provided.delete(element);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function overridesOf(node) {
|
|
23
|
+
const registered = provided.get(node);
|
|
24
|
+
if (registered !== undefined)
|
|
25
|
+
return registered;
|
|
26
|
+
if (node.localName !== DRIVERS_ELEMENT)
|
|
27
|
+
return undefined;
|
|
28
|
+
return node.drivers;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The override for `name` from the nearest provider that has one. Providers nest: an inner
|
|
32
|
+
* provider without that driver lets an outer one supply it. Commands never run on the server,
|
|
33
|
+
* so there is nothing to resolve there.
|
|
34
|
+
*/
|
|
35
|
+
export function providedDriver(host, name) {
|
|
36
|
+
if (onServer)
|
|
37
|
+
return undefined;
|
|
38
|
+
for (let node = host.parentNode; node !== null;) {
|
|
39
|
+
if (node instanceof Element) {
|
|
40
|
+
const driver = overridesOf(node)?.[name];
|
|
41
|
+
if (driver !== undefined)
|
|
42
|
+
return driver;
|
|
43
|
+
}
|
|
44
|
+
node = node instanceof ShadowRoot ? node.host : node.parentNode;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=drivers-scope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drivers-scope.js","sourceRoot":"","sources":["../src/drivers-scope.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,0FAA0F;AAC1F,0EAA0E;AAC1E,OAAO,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC;AAG/B,MAAM,QAAQ,GAAY,QAAQ,CAAC;AAEnC,4FAA4F;AAC5F,MAAM,CAAC,MAAM,eAAe,GAAG,eAAe,CAAC;AAE/C,iFAAiF;AACjF,MAAM,QAAQ,GAAG,IAAI,OAAO,EAA4B,CAAC;AAEzD;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,OAAgB,EAAE,OAAwB;IACvE,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,GAAG,EAAE;QACV,IAAI,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,OAAO;YAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAClE,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,IAAa;IAChC,MAAM,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC;IAChD,IAAI,IAAI,CAAC,SAAS,KAAK,eAAe;QAAE,OAAO,SAAS,CAAC;IACzD,OAAQ,IAAgD,CAAC,OAAO,CAAC;AACnE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,IAAa,EAAE,IAAY;IACxD,IAAI,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC/B,KAAK,IAAI,IAAI,GAAgB,IAAI,CAAC,UAAU,EAAE,IAAI,KAAK,IAAI,GAAG,CAAC;QAC7D,IAAI,IAAI,YAAY,OAAO,EAAE,CAAC;YAC5B,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;YACzC,IAAI,MAAM,KAAK,SAAS;gBAAE,OAAO,MAAM,CAAC;QAC1C,CAAC;QACD,IAAI,GAAG,IAAI,YAAY,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;IAClE,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC","sourcesContent":["// Tree-scoped driver overrides (gyral-czi.35, ADR 0006 addendum): which driver a command uses.\n// Resolution: el.drivers → nearest driver provider (across shadow roots) → spec.drivers →\n// the command's own driver. Mirrors <gyral-stores> (ADR 0013) for stores.\nimport { isServer } from 'lit';\nimport type { AnyDriver, DriverOverrides } from './command.js';\n\nconst onServer: boolean = isServer;\n\n/** Provider element name: `<gyral-drivers .drivers=${{ http: fake }}>…</gyral-drivers>`. */\nexport const DRIVERS_ELEMENT = 'gyral-drivers';\n\n// Elements registered with provideDrivers() (any element can act as a provider).\nconst provided = new WeakMap<Element, DriverOverrides>();\n\n/**\n * Makes `element` a driver provider for every Gyral component below it (including inside\n * shadow roots). Returns a function that removes the overrides. Use it on a test container\n * or an app root; a `<gyral-drivers>` element with a `.drivers` property works the same way.\n */\nexport function provideDrivers(element: Element, drivers: DriverOverrides): () => void {\n provided.set(element, drivers);\n return () => {\n if (provided.get(element) === drivers) provided.delete(element);\n };\n}\n\nfunction overridesOf(node: Element): DriverOverrides | undefined {\n const registered = provided.get(node);\n if (registered !== undefined) return registered;\n if (node.localName !== DRIVERS_ELEMENT) return undefined;\n return (node as Element & { drivers?: DriverOverrides }).drivers;\n}\n\n/**\n * The override for `name` from the nearest provider that has one. Providers nest: an inner\n * provider without that driver lets an outer one supply it. Commands never run on the server,\n * so there is nothing to resolve there.\n */\nexport function providedDriver(host: Element, name: string): AnyDriver | undefined {\n if (onServer) return undefined;\n for (let node: Node | null = host.parentNode; node !== null;) {\n if (node instanceof Element) {\n const driver = overridesOf(node)?.[name];\n if (driver !== undefined) return driver;\n }\n node = node instanceof ShadowRoot ? node.host : node.parentNode;\n }\n return undefined;\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { nothing } from 'lit';
|
|
2
|
+
import { Directive, type ElementPart, type PartInfo } from 'lit/directive.js';
|
|
3
|
+
/**
|
|
4
|
+
* Base for element directives (`<input ${invalid(errors)}>`): they act on the element they are
|
|
5
|
+
* placed on and render nothing. Subclasses declare their arguments as `Args` and implement
|
|
6
|
+
* `apply`. Wrap the class with `directive()` (re-exported from core) to get the template
|
|
7
|
+
* function. Element parts are not rendered on the server, so `apply` only runs in the browser.
|
|
8
|
+
*
|
|
9
|
+
* class Focus extends ElementDirective<[when: boolean]> {
|
|
10
|
+
* apply(el: Element, [when]: [boolean]) { if (when && el instanceof HTMLElement) el.focus(); }
|
|
11
|
+
* }
|
|
12
|
+
* export const focusWhen = directive(Focus);
|
|
13
|
+
*/
|
|
14
|
+
export declare abstract class ElementDirective<Args extends unknown[]> extends Directive {
|
|
15
|
+
constructor(info: PartInfo);
|
|
16
|
+
/** Called on every render with the directive's current arguments. */
|
|
17
|
+
abstract apply(element: Element, args: Args): void;
|
|
18
|
+
render(...args: Args): typeof nothing;
|
|
19
|
+
update(part: ElementPart, args: Args): typeof nothing;
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { nothing } from 'lit';
|
|
2
|
+
import { Directive, PartType } from 'lit/directive.js';
|
|
3
|
+
/**
|
|
4
|
+
* Base for element directives (`<input ${invalid(errors)}>`): they act on the element they are
|
|
5
|
+
* placed on and render nothing. Subclasses declare their arguments as `Args` and implement
|
|
6
|
+
* `apply`. Wrap the class with `directive()` (re-exported from core) to get the template
|
|
7
|
+
* function. Element parts are not rendered on the server, so `apply` only runs in the browser.
|
|
8
|
+
*
|
|
9
|
+
* class Focus extends ElementDirective<[when: boolean]> {
|
|
10
|
+
* apply(el: Element, [when]: [boolean]) { if (when && el instanceof HTMLElement) el.focus(); }
|
|
11
|
+
* }
|
|
12
|
+
* export const focusWhen = directive(Focus);
|
|
13
|
+
*/
|
|
14
|
+
export class ElementDirective extends Directive {
|
|
15
|
+
constructor(info) {
|
|
16
|
+
super(info);
|
|
17
|
+
if (info.type !== PartType.ELEMENT) {
|
|
18
|
+
throw new Error(`${new.target.name} must be used on an element: <el \${directive(…)}>`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Lit reads the directive's arguments from render(); update() does the work.
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
23
|
+
render(...args) {
|
|
24
|
+
return nothing;
|
|
25
|
+
}
|
|
26
|
+
update(part, args) {
|
|
27
|
+
this.apply(part.element, args);
|
|
28
|
+
return nothing;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=element-directive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"element-directive.js","sourceRoot":"","sources":["../src/element-directive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAmC,MAAM,kBAAkB,CAAC;AAExF;;;;;;;;;;GAUG;AACH,MAAM,OAAgB,gBAAyC,SAAQ,SAAS;IAC9E,YAAY,IAAc;QACxB,KAAK,CAAC,IAAI,CAAC,CAAC;QACZ,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,OAAO,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,oDAAoD,CAAC,CAAC;QAC1F,CAAC;IACH,CAAC;IAKD,6EAA6E;IAC7E,6DAA6D;IACpD,MAAM,CAAC,GAAG,IAAU;QAC3B,OAAO,OAAO,CAAC;IACjB,CAAC;IAEQ,MAAM,CAAC,IAAiB,EAAE,IAAU;QAC3C,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAC/B,OAAO,OAAO,CAAC;IACjB,CAAC;CACF","sourcesContent":["import { nothing } from 'lit';\nimport { Directive, PartType, type ElementPart, type PartInfo } from 'lit/directive.js';\n\n/**\n * Base for element directives (`<input ${invalid(errors)}>`): they act on the element they are\n * placed on and render nothing. Subclasses declare their arguments as `Args` and implement\n * `apply`. Wrap the class with `directive()` (re-exported from core) to get the template\n * function. Element parts are not rendered on the server, so `apply` only runs in the browser.\n *\n * class Focus extends ElementDirective<[when: boolean]> {\n * apply(el: Element, [when]: [boolean]) { if (when && el instanceof HTMLElement) el.focus(); }\n * }\n * export const focusWhen = directive(Focus);\n */\nexport abstract class ElementDirective<Args extends unknown[]> extends Directive {\n constructor(info: PartInfo) {\n super(info);\n if (info.type !== PartType.ELEMENT) {\n throw new Error(`${new.target.name} must be used on an element: <el \\${directive(…)}>`);\n }\n }\n\n /** Called on every render with the directive's current arguments. */\n abstract apply(element: Element, args: Args): void;\n\n // Lit reads the directive's arguments from render(); update() does the work.\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n override render(...args: Args): typeof nothing {\n return nothing;\n }\n\n override update(part: ElementPart, args: Args): typeof nothing {\n this.apply(part.element, args);\n return nothing;\n }\n}\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { LitElement } from 'lit';
|
|
2
|
+
import type { DriverOverrides } from './command.js';
|
|
3
|
+
import type { StoreOverrides } from './store.js';
|
|
4
|
+
import type { ComponentSpec, Tagged } from './types.js';
|
|
5
|
+
/** The custom element class produced by `define()`. */
|
|
6
|
+
export interface GyralElement<S, M extends Tagged> extends LitElement {
|
|
7
|
+
/** Current model state. */
|
|
8
|
+
readonly state: S;
|
|
9
|
+
/** Feeds a message through `update`, as if an intent had produced it. */
|
|
10
|
+
send(msg: M): void;
|
|
11
|
+
/** Per-instance driver substitutions by name (test fakes). Checked before the spec's. */
|
|
12
|
+
drivers: DriverOverrides;
|
|
13
|
+
/**
|
|
14
|
+
* Per-instance store instances by store name (tests, islands). Checked before the nearest
|
|
15
|
+
* `<gyral-stores>` provider and the document default (ADR 0013).
|
|
16
|
+
*/
|
|
17
|
+
stores: StoreOverrides;
|
|
18
|
+
/**
|
|
19
|
+
* Messages applied through `update` right after `init`, before the first render. The server
|
|
20
|
+
* uses it to render a rejected form with the same reducer as the JS path (ADR 0008). Ignored
|
|
21
|
+
* when the element resumes from a hydration seed (the seed already contains their effect).
|
|
22
|
+
*/
|
|
23
|
+
initialMessages: readonly Tagged[];
|
|
24
|
+
}
|
|
25
|
+
export interface GyralElementClass<S, M extends Tagged, P, O extends Tagged = never> {
|
|
26
|
+
/** Instances expose their declared props as settable properties. */
|
|
27
|
+
new (): GyralElement<S, M> & {
|
|
28
|
+
-readonly [K in keyof P]: P[K];
|
|
29
|
+
};
|
|
30
|
+
readonly spec: ComponentSpec<S, M, P>;
|
|
31
|
+
readonly tagName: string;
|
|
32
|
+
/** Type-only: the outputs this component emits (read by `child()`). */
|
|
33
|
+
readonly outputs?: O;
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"element-types.js","sourceRoot":"","sources":["../src/element-types.ts"],"names":[],"mappings":"","sourcesContent":["import type { LitElement } from 'lit';\nimport type { DriverOverrides } from './command.js';\nimport type { StoreOverrides } from './store.js';\nimport type { ComponentSpec, Tagged } from './types.js';\n\n/** The custom element class produced by `define()`. */\nexport interface GyralElement<S, M extends Tagged> extends LitElement {\n /** Current model state. */\n readonly state: S;\n /** Feeds a message through `update`, as if an intent had produced it. */\n send(msg: M): void;\n /** Per-instance driver substitutions by name (test fakes). Checked before the spec's. */\n drivers: DriverOverrides;\n /**\n * Per-instance store instances by store name (tests, islands). Checked before the nearest\n * `<gyral-stores>` provider and the document default (ADR 0013).\n */\n stores: StoreOverrides;\n /**\n * Messages applied through `update` right after `init`, before the first render. The server\n * uses it to render a rejected form with the same reducer as the JS path (ADR 0008). Ignored\n * when the element resumes from a hydration seed (the seed already contains their effect).\n */\n initialMessages: readonly Tagged[];\n}\n\nexport interface GyralElementClass<S, M extends Tagged, P, O extends Tagged = never> {\n /** Instances expose their declared props as settable properties. */\n new (): GyralElement<S, M> & { -readonly [K in keyof P]: P[K] };\n readonly spec: ComponentSpec<S, M, P>;\n readonly tagName: string;\n /** Type-only: the outputs this component emits (read by `child()`). */\n readonly outputs?: O;\n}\n"]}
|
package/dist/focus.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Command } from './command.js';
|
|
2
|
+
/** Marker driver: `define()` handles focus commands itself. */
|
|
3
|
+
export declare const FOCUS: {
|
|
4
|
+
readonly name: "@gyral/focus";
|
|
5
|
+
readonly run: () => undefined;
|
|
6
|
+
};
|
|
7
|
+
export interface FocusOptions {
|
|
8
|
+
/** Don't scroll the element into view (default `false`: the browser scrolls if needed). */
|
|
9
|
+
readonly preventScroll?: boolean;
|
|
10
|
+
/** Also select the text of an input or textarea. */
|
|
11
|
+
readonly select?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** The input of a `focus()` command. */
|
|
14
|
+
export interface FocusInput extends FocusOptions {
|
|
15
|
+
readonly selector: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A command that focuses the first element matching `selector` inside this component (its
|
|
19
|
+
* shadow root, or its own children in light-DOM mode) once the current update has rendered:
|
|
20
|
+
*
|
|
21
|
+
* NextPage: (s, m) => [{ ...s, page: m.page }, [focus('h2', { preventScroll: true })]]
|
|
22
|
+
*
|
|
23
|
+
* Non-focusable targets (a heading) need `tabindex="-1"` in the view.
|
|
24
|
+
*/
|
|
25
|
+
export declare function focus(selector: string, options?: FocusOptions): Command<never>;
|
|
26
|
+
/** Runs a focus command against `root` (called by define() after the update completes). */
|
|
27
|
+
export declare function runFocus(root: ParentNode, tag: string, input: FocusInput): void;
|
package/dist/focus.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Marker driver: `define()` handles focus commands itself. */
|
|
2
|
+
export const FOCUS = {
|
|
3
|
+
name: '@gyral/focus',
|
|
4
|
+
run: () => undefined,
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* A command that focuses the first element matching `selector` inside this component (its
|
|
8
|
+
* shadow root, or its own children in light-DOM mode) once the current update has rendered:
|
|
9
|
+
*
|
|
10
|
+
* NextPage: (s, m) => [{ ...s, page: m.page }, [focus('h2', { preventScroll: true })]]
|
|
11
|
+
*
|
|
12
|
+
* Non-focusable targets (a heading) need `tabindex="-1"` in the view.
|
|
13
|
+
*/
|
|
14
|
+
export function focus(selector, options = {}) {
|
|
15
|
+
const input = { selector, ...options };
|
|
16
|
+
return { driver: FOCUS, input, onSuccess: () => undefined };
|
|
17
|
+
}
|
|
18
|
+
/** Runs a focus command against `root` (called by define() after the update completes). */
|
|
19
|
+
export function runFocus(root, tag, input) {
|
|
20
|
+
const target = root.querySelector(input.selector);
|
|
21
|
+
if (!(target instanceof HTMLElement || target instanceof SVGElement)) {
|
|
22
|
+
console.warn(`<${tag}> focus("${input.selector}") matched no focusable element.`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
target.focus({ preventScroll: input.preventScroll ?? false });
|
|
26
|
+
if (input.select === true && 'select' in target && typeof target.select === 'function') {
|
|
27
|
+
target.select();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=focus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focus.js","sourceRoot":"","sources":["../src/focus.ts"],"names":[],"mappings":"AAKA,+DAA+D;AAC/D,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,IAAI,EAAE,cAAc;IACpB,GAAG,EAAE,GAAG,EAAE,CAAC,SAAS;CACZ,CAAC;AAcX;;;;;;;GAOG;AACH,MAAM,UAAU,KAAK,CAAC,QAAgB,EAAE,UAAwB,EAAE;IAChE,MAAM,KAAK,GAAe,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE,CAAC;IACnD,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC;AAC9D,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,QAAQ,CAAC,IAAgB,EAAE,GAAW,EAAE,KAAiB;IACvE,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAClD,IAAI,CAAC,CAAC,MAAM,YAAY,WAAW,IAAI,MAAM,YAAY,UAAU,CAAC,EAAE,CAAC;QACrE,OAAO,CAAC,IAAI,CAAC,IAAI,GAAG,YAAY,KAAK,CAAC,QAAQ,kCAAkC,CAAC,CAAC;QAClF,OAAO;IACT,CAAC;IACD,MAAM,CAAC,KAAK,CAAC,EAAE,aAAa,EAAE,KAAK,CAAC,aAAa,IAAI,KAAK,EAAE,CAAC,CAAC;IAC9D,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,IAAI,QAAQ,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QACtF,MAA2B,CAAC,MAAM,EAAE,CAAC;IACxC,CAAC;AACH,CAAC","sourcesContent":["// Focus management from the model (gyral-czi.28). Moving focus is a side effect, so the model\n// asks for it with a command; define() runs it against the component's own render root after\n// the render that the same reducer caused, so the element to focus already exists.\nimport type { Command } from './command.js';\n\n/** Marker driver: `define()` handles focus commands itself. */\nexport const FOCUS = {\n name: '@gyral/focus',\n run: () => undefined,\n} as const;\n\nexport interface FocusOptions {\n /** Don't scroll the element into view (default `false`: the browser scrolls if needed). */\n readonly preventScroll?: boolean;\n /** Also select the text of an input or textarea. */\n readonly select?: boolean;\n}\n\n/** The input of a `focus()` command. */\nexport interface FocusInput extends FocusOptions {\n readonly selector: string;\n}\n\n/**\n * A command that focuses the first element matching `selector` inside this component (its\n * shadow root, or its own children in light-DOM mode) once the current update has rendered:\n *\n * NextPage: (s, m) => [{ ...s, page: m.page }, [focus('h2', { preventScroll: true })]]\n *\n * Non-focusable targets (a heading) need `tabindex=\"-1\"` in the view.\n */\nexport function focus(selector: string, options: FocusOptions = {}): Command<never> {\n const input: FocusInput = { selector, ...options };\n return { driver: FOCUS, input, onSuccess: () => undefined };\n}\n\n/** Runs a focus command against `root` (called by define() after the update completes). */\nexport function runFocus(root: ParentNode, tag: string, input: FocusInput): void {\n const target = root.querySelector(input.selector);\n if (!(target instanceof HTMLElement || target instanceof SVGElement)) {\n console.warn(`<${tag}> focus(\"${input.selector}\") matched no focusable element.`);\n return;\n }\n target.focus({ preventScroll: input.preventScroll ?? false });\n if (input.select === true && 'select' in target && typeof target.select === 'function') {\n (target as HTMLInputElement).select();\n }\n}\n"]}
|
package/dist/forms.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import type { FieldIssue, FormFields, IntentParser, IntentRejected } from './types.js';
|
|
3
|
+
/** A form's schema, shared by the client component and (with @gyral/ssr) the server route. */
|
|
4
|
+
export interface FormDefinition<Schema extends StandardSchemaV1> {
|
|
5
|
+
readonly _tag: 'FormDefinition';
|
|
6
|
+
readonly schema: Schema;
|
|
7
|
+
}
|
|
8
|
+
export declare function defineForm<Schema extends StandardSchemaV1>(schema: Schema): FormDefinition<Schema>;
|
|
9
|
+
export type FormValue = FormDataEntryValue | readonly FormDataEntryValue[];
|
|
10
|
+
/**
|
|
11
|
+
* FormData to a plain object. The same function runs on the server, so both sides agree.
|
|
12
|
+
* Repeated names become arrays; a `name[]` suffix always yields an array (named `name`).
|
|
13
|
+
* Values stay strings (or Files): the schema coerces them.
|
|
14
|
+
*/
|
|
15
|
+
export declare function formDataToObject(data: FormData): Readonly<Record<string, FormValue>>;
|
|
16
|
+
type Out<Schema extends StandardSchemaV1> = StandardSchemaV1.InferOutput<Schema>;
|
|
17
|
+
/** The text fields of a parsed form, for re-filling it after a rejection. */
|
|
18
|
+
export declare function formFields(data: Readonly<Record<string, FormValue>>): FormFields;
|
|
19
|
+
export type FormResult<T> = {
|
|
20
|
+
readonly ok: true;
|
|
21
|
+
readonly data: T;
|
|
22
|
+
} | {
|
|
23
|
+
readonly ok: false;
|
|
24
|
+
readonly rejected: IntentRejected;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Validates submitted form data exactly as `form()` does. The no-JS server path
|
|
28
|
+
* (`formAction` in @gyral/ssr) uses it, so both paths produce the same `IntentRejected`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function validateForm<Schema extends StandardSchemaV1>(definition: FormDefinition<Schema> | Schema, intent: string, data: FormData): FormResult<Out<Schema>> | Promise<FormResult<Out<Schema>>>;
|
|
31
|
+
/**
|
|
32
|
+
* Intent parser for a `<form data-intent>` submission, validated by a schema. `toMsg` also
|
|
33
|
+
* gets the raw `FormData`, for sending the submission to the server (`submitForm` in
|
|
34
|
+
* @gyral/http) after it passed client-side validation.
|
|
35
|
+
*/
|
|
36
|
+
export declare function form<Schema extends StandardSchemaV1, M>(definition: FormDefinition<Schema> | Schema, toMsg: (data: Out<Schema>, formData: FormData) => M | undefined): IntentParser<M>;
|
|
37
|
+
/**
|
|
38
|
+
* Intent parser for one control (pair with `data-intent-on="input"` for live checks).
|
|
39
|
+
* Validates `checked` for checkboxes and radios, otherwise `value`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function field<Schema extends StandardSchemaV1, M>(schema: Schema, toMsg: (value: Out<Schema>) => M | undefined): IntentParser<M>;
|
|
42
|
+
/**
|
|
43
|
+
* Groups issues by field name. Call it in the `IntentRejected` reducer and keep the result in
|
|
44
|
+
* state: `invalid()` re-applies an error only when it gets a new array.
|
|
45
|
+
*/
|
|
46
|
+
export declare function fieldErrors(issues: readonly FieldIssue[]): Readonly<Record<string, readonly string[]>>;
|
|
47
|
+
/**
|
|
48
|
+
* A Standard Schema for `IntentRejected` as a server sends it (`formAction` answering JSON).
|
|
49
|
+
* Used by `submitForm` in @gyral/http to recognise a 422 rejection; usable with any decoder.
|
|
50
|
+
*/
|
|
51
|
+
export declare const intentRejectedSchema: StandardSchemaV1<unknown, IntentRejected>;
|
|
52
|
+
/** A server's JSON answer to a valid form post that would have redirected without JS. */
|
|
53
|
+
export interface FormRedirected {
|
|
54
|
+
readonly _tag: 'Redirected';
|
|
55
|
+
readonly location: string;
|
|
56
|
+
}
|
|
57
|
+
/** The `location` of a `FormRedirected` answer (from `formAction` in @gyral/ssr), if it is one. */
|
|
58
|
+
export declare function redirectedTo(body: unknown): string | undefined;
|
|
59
|
+
export {};
|
package/dist/forms.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export function defineForm(schema) {
|
|
2
|
+
return { _tag: 'FormDefinition', schema };
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* FormData to a plain object. The same function runs on the server, so both sides agree.
|
|
6
|
+
* Repeated names become arrays; a `name[]` suffix always yields an array (named `name`).
|
|
7
|
+
* Values stay strings (or Files): the schema coerces them.
|
|
8
|
+
*/
|
|
9
|
+
export function formDataToObject(data) {
|
|
10
|
+
const out = {};
|
|
11
|
+
for (const key of new Set(data.keys())) {
|
|
12
|
+
const values = data.getAll(key);
|
|
13
|
+
const [first] = values;
|
|
14
|
+
if (key.endsWith('[]'))
|
|
15
|
+
out[key.slice(0, -2)] = values;
|
|
16
|
+
else if (values.length === 1 && first !== undefined)
|
|
17
|
+
out[key] = first;
|
|
18
|
+
else
|
|
19
|
+
out[key] = values;
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
const segment = (s) => String(typeof s === 'object' ? s.key : s);
|
|
24
|
+
function toIssues(issues, fallbackPath) {
|
|
25
|
+
return issues.map((issue) => {
|
|
26
|
+
const path = issue.path?.map(segment).join('.') ?? '';
|
|
27
|
+
return { path: path === '' ? fallbackPath : path, message: issue.message };
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function parse(schema, value, intent, fallbackPath, toMsg) {
|
|
31
|
+
const settle = (result) => result.issues === undefined
|
|
32
|
+
? toMsg(result.value)
|
|
33
|
+
: { _tag: 'IntentRejected', intent, issues: toIssues(result.issues, fallbackPath) };
|
|
34
|
+
const result = schema['~standard'].validate(value);
|
|
35
|
+
return result instanceof Promise ? result.then(settle) : settle(result);
|
|
36
|
+
}
|
|
37
|
+
/** The text fields of a parsed form, for re-filling it after a rejection. */
|
|
38
|
+
export function formFields(data) {
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const [key, value] of Object.entries(data)) {
|
|
41
|
+
if (typeof value === 'string')
|
|
42
|
+
out[key] = value;
|
|
43
|
+
else if (Array.isArray(value))
|
|
44
|
+
out[key] = value.filter((v) => typeof v === 'string');
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
const schemaOf = (definition) => ('_tag' in definition ? definition.schema : definition);
|
|
49
|
+
/**
|
|
50
|
+
* Validates submitted form data exactly as `form()` does. The no-JS server path
|
|
51
|
+
* (`formAction` in @gyral/ssr) uses it, so both paths produce the same `IntentRejected`.
|
|
52
|
+
*/
|
|
53
|
+
export function validateForm(definition, intent, data) {
|
|
54
|
+
const raw = formDataToObject(data);
|
|
55
|
+
const values = formFields(raw);
|
|
56
|
+
const rejected = (issues) => ({
|
|
57
|
+
ok: false,
|
|
58
|
+
rejected: { _tag: 'IntentRejected', intent, issues, values },
|
|
59
|
+
});
|
|
60
|
+
const settle = (result) => result.issues === undefined
|
|
61
|
+
? { ok: true, data: result.value }
|
|
62
|
+
: rejected(toIssues(result.issues, ''));
|
|
63
|
+
const result = schemaOf(definition)['~standard'].validate(raw);
|
|
64
|
+
return result instanceof Promise ? result.then(settle) : settle(result);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Intent parser for a `<form data-intent>` submission, validated by a schema. `toMsg` also
|
|
68
|
+
* gets the raw `FormData`, for sending the submission to the server (`submitForm` in
|
|
69
|
+
* @gyral/http) after it passed client-side validation.
|
|
70
|
+
*/
|
|
71
|
+
export function form(definition, toMsg) {
|
|
72
|
+
return (input) => {
|
|
73
|
+
const { formData } = input;
|
|
74
|
+
if (formData === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
const toParsed = (r) => r.ok ? toMsg(r.data, formData) : r.rejected;
|
|
77
|
+
const result = validateForm(definition, input.name, formData);
|
|
78
|
+
return result instanceof Promise ? result.then(toParsed) : toParsed(result);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Intent parser for one control (pair with `data-intent-on="input"` for live checks).
|
|
83
|
+
* Validates `checked` for checkboxes and radios, otherwise `value`.
|
|
84
|
+
*/
|
|
85
|
+
export function field(schema, toMsg) {
|
|
86
|
+
return (input) => {
|
|
87
|
+
const name = input.target.getAttribute('name') ?? input.name;
|
|
88
|
+
return parse(schema, input.checked ?? input.value, input.name, name, toMsg);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Groups issues by field name. Call it in the `IntentRejected` reducer and keep the result in
|
|
93
|
+
* state: `invalid()` re-applies an error only when it gets a new array.
|
|
94
|
+
*/
|
|
95
|
+
export function fieldErrors(issues) {
|
|
96
|
+
const out = {};
|
|
97
|
+
for (const { path, message } of issues)
|
|
98
|
+
(out[path] ??= []).push(message);
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
const isRecord = (value) => typeof value === 'object' && value !== null;
|
|
102
|
+
const isIssue = (value) => isRecord(value) && typeof value['path'] === 'string' && typeof value['message'] === 'string';
|
|
103
|
+
const isFields = (value) => isRecord(value) &&
|
|
104
|
+
Object.values(value).every((v) => typeof v === 'string' || (Array.isArray(v) && v.every((x) => typeof x === 'string')));
|
|
105
|
+
/**
|
|
106
|
+
* A Standard Schema for `IntentRejected` as a server sends it (`formAction` answering JSON).
|
|
107
|
+
* Used by `submitForm` in @gyral/http to recognise a 422 rejection; usable with any decoder.
|
|
108
|
+
*/
|
|
109
|
+
export const intentRejectedSchema = {
|
|
110
|
+
'~standard': {
|
|
111
|
+
version: 1,
|
|
112
|
+
vendor: 'gyral',
|
|
113
|
+
validate: (value) => {
|
|
114
|
+
if (!isRecord(value) ||
|
|
115
|
+
value['_tag'] !== 'IntentRejected' ||
|
|
116
|
+
typeof value['intent'] !== 'string' ||
|
|
117
|
+
!Array.isArray(value['issues']) ||
|
|
118
|
+
!value['issues'].every(isIssue) ||
|
|
119
|
+
(value['values'] !== undefined && !isFields(value['values']))) {
|
|
120
|
+
return { issues: [{ message: 'Expected an IntentRejected message' }] };
|
|
121
|
+
}
|
|
122
|
+
const rejected = {
|
|
123
|
+
_tag: 'IntentRejected',
|
|
124
|
+
intent: value['intent'],
|
|
125
|
+
issues: value['issues'],
|
|
126
|
+
...(value['values'] === undefined ? {} : { values: value['values'] }),
|
|
127
|
+
};
|
|
128
|
+
return { value: rejected };
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
/** The `location` of a `FormRedirected` answer (from `formAction` in @gyral/ssr), if it is one. */
|
|
133
|
+
export function redirectedTo(body) {
|
|
134
|
+
return isRecord(body) && body['_tag'] === 'Redirected' && typeof body['location'] === 'string'
|
|
135
|
+
? body['location']
|
|
136
|
+
: undefined;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=forms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forms.js","sourceRoot":"","sources":["../src/forms.ts"],"names":[],"mappings":"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","sourcesContent":["// Forms, client half (docs/design-docs/0008-forms.md). Schemas are any Standard Schema v1.\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { FieldIssue, FormFields, IntentInput, IntentParser, IntentRejected } from './types.js';\n\n/** A form's schema, shared by the client component and (with @gyral/ssr) the server route. */\nexport interface FormDefinition<Schema extends StandardSchemaV1> {\n readonly _tag: 'FormDefinition';\n readonly schema: Schema;\n}\n\nexport function defineForm<Schema extends StandardSchemaV1>(\n schema: Schema,\n): FormDefinition<Schema> {\n return { _tag: 'FormDefinition', schema };\n}\n\nexport type FormValue = FormDataEntryValue | readonly FormDataEntryValue[];\n\n/**\n * FormData to a plain object. The same function runs on the server, so both sides agree.\n * Repeated names become arrays; a `name[]` suffix always yields an array (named `name`).\n * Values stay strings (or Files): the schema coerces them.\n */\nexport function formDataToObject(data: FormData): Readonly<Record<string, FormValue>> {\n const out: Record<string, FormValue> = {};\n for (const key of new Set(data.keys())) {\n const values = data.getAll(key);\n const [first] = values;\n if (key.endsWith('[]')) out[key.slice(0, -2)] = values;\n else if (values.length === 1 && first !== undefined) out[key] = first;\n else out[key] = values;\n }\n return out;\n}\n\nconst segment = (s: PropertyKey | StandardSchemaV1.PathSegment): string =>\n String(typeof s === 'object' ? s.key : s);\n\nfunction toIssues(\n issues: ReadonlyArray<StandardSchemaV1.Issue>,\n fallbackPath: string,\n): FieldIssue[] {\n return issues.map((issue) => {\n const path = issue.path?.map(segment).join('.') ?? '';\n return { path: path === '' ? fallbackPath : path, message: issue.message };\n });\n}\n\ntype Out<Schema extends StandardSchemaV1> = StandardSchemaV1.InferOutput<Schema>;\ntype Parsed<M> = M | IntentRejected | undefined;\n\nfunction parse<Schema extends StandardSchemaV1, M>(\n schema: Schema,\n value: unknown,\n intent: string,\n fallbackPath: string,\n toMsg: (data: Out<Schema>) => M | undefined,\n): Parsed<M> | Promise<Parsed<M>> {\n const settle = (result: StandardSchemaV1.Result<Out<Schema>>): Parsed<M> =>\n result.issues === undefined\n ? toMsg(result.value)\n : { _tag: 'IntentRejected', intent, issues: toIssues(result.issues, fallbackPath) };\n const result = schema['~standard'].validate(value);\n return result instanceof Promise ? result.then(settle) : settle(result);\n}\n\n/** The text fields of a parsed form, for re-filling it after a rejection. */\nexport function formFields(data: Readonly<Record<string, FormValue>>): FormFields {\n const out: Record<string, string | readonly string[]> = {};\n for (const [key, value] of Object.entries(data)) {\n if (typeof value === 'string') out[key] = value;\n else if (Array.isArray(value)) out[key] = value.filter((v) => typeof v === 'string');\n }\n return out;\n}\n\nconst schemaOf = <Schema extends StandardSchemaV1>(\n definition: FormDefinition<Schema> | Schema,\n): Schema => ('_tag' in definition ? definition.schema : definition);\n\nexport type FormResult<T> =\n | { readonly ok: true; readonly data: T }\n | { readonly ok: false; readonly rejected: IntentRejected };\n\n/**\n * Validates submitted form data exactly as `form()` does. The no-JS server path\n * (`formAction` in @gyral/ssr) uses it, so both paths produce the same `IntentRejected`.\n */\nexport function validateForm<Schema extends StandardSchemaV1>(\n definition: FormDefinition<Schema> | Schema,\n intent: string,\n data: FormData,\n): FormResult<Out<Schema>> | Promise<FormResult<Out<Schema>>> {\n const raw = formDataToObject(data);\n const values = formFields(raw);\n const rejected = (issues: readonly FieldIssue[]): FormResult<Out<Schema>> => ({\n ok: false,\n rejected: { _tag: 'IntentRejected', intent, issues, values },\n });\n const settle = (result: StandardSchemaV1.Result<Out<Schema>>): FormResult<Out<Schema>> =>\n result.issues === undefined\n ? { ok: true, data: result.value }\n : rejected(toIssues(result.issues, ''));\n const result = schemaOf(definition)['~standard'].validate(raw);\n return result instanceof Promise ? result.then(settle) : settle(result);\n}\n\n/**\n * Intent parser for a `<form data-intent>` submission, validated by a schema. `toMsg` also\n * gets the raw `FormData`, for sending the submission to the server (`submitForm` in\n * @gyral/http) after it passed client-side validation.\n */\nexport function form<Schema extends StandardSchemaV1, M>(\n definition: FormDefinition<Schema> | Schema,\n toMsg: (data: Out<Schema>, formData: FormData) => M | undefined,\n): IntentParser<M> {\n return (input: IntentInput) => {\n const { formData } = input;\n if (formData === undefined) return undefined;\n const toParsed = (r: FormResult<Out<Schema>>): Parsed<M> =>\n r.ok ? toMsg(r.data, formData) : r.rejected;\n const result = validateForm(definition, input.name, formData);\n return result instanceof Promise ? result.then(toParsed) : toParsed(result);\n };\n}\n\n/**\n * Intent parser for one control (pair with `data-intent-on=\"input\"` for live checks).\n * Validates `checked` for checkboxes and radios, otherwise `value`.\n */\nexport function field<Schema extends StandardSchemaV1, M>(\n schema: Schema,\n toMsg: (value: Out<Schema>) => M | undefined,\n): IntentParser<M> {\n return (input: IntentInput) => {\n const name = input.target.getAttribute('name') ?? input.name;\n return parse(schema, input.checked ?? input.value, input.name, name, toMsg);\n };\n}\n\n/**\n * Groups issues by field name. Call it in the `IntentRejected` reducer and keep the result in\n * state: `invalid()` re-applies an error only when it gets a new array.\n */\nexport function fieldErrors(\n issues: readonly FieldIssue[],\n): Readonly<Record<string, readonly string[]>> {\n const out: Record<string, string[]> = {};\n for (const { path, message } of issues) (out[path] ??= []).push(message);\n return out;\n}\n\nconst isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>\n typeof value === 'object' && value !== null;\n\nconst isIssue = (value: unknown): value is FieldIssue =>\n isRecord(value) && typeof value['path'] === 'string' && typeof value['message'] === 'string';\n\nconst isFields = (value: unknown): value is FormFields =>\n isRecord(value) &&\n Object.values(value).every(\n (v) => typeof v === 'string' || (Array.isArray(v) && v.every((x) => typeof x === 'string')),\n );\n\n/**\n * A Standard Schema for `IntentRejected` as a server sends it (`formAction` answering JSON).\n * Used by `submitForm` in @gyral/http to recognise a 422 rejection; usable with any decoder.\n */\nexport const intentRejectedSchema: StandardSchemaV1<unknown, IntentRejected> = {\n '~standard': {\n version: 1,\n vendor: 'gyral',\n validate: (value) => {\n if (\n !isRecord(value) ||\n value['_tag'] !== 'IntentRejected' ||\n typeof value['intent'] !== 'string' ||\n !Array.isArray(value['issues']) ||\n !value['issues'].every(isIssue) ||\n (value['values'] !== undefined && !isFields(value['values']))\n ) {\n return { issues: [{ message: 'Expected an IntentRejected message' }] };\n }\n const rejected: IntentRejected = {\n _tag: 'IntentRejected',\n intent: value['intent'],\n issues: value['issues'],\n ...(value['values'] === undefined ? {} : { values: value['values'] }),\n };\n return { value: rejected };\n },\n },\n};\n\n/** A server's JSON answer to a valid form post that would have redirected without JS. */\nexport interface FormRedirected {\n readonly _tag: 'Redirected';\n readonly location: string;\n}\n\n/** The `location` of a `FormRedirected` answer (from `formAction` in @gyral/ssr), if it is one. */\nexport function redirectedTo(body: unknown): string | undefined {\n return isRecord(body) && body['_tag'] === 'Redirected' && typeof body['location'] === 'string'\n ? body['location']\n : undefined;\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PropertyDeclaration } from 'lit';
|
|
2
|
+
/** Host attribute holding the JSON seed. Removed once the client has read it. */
|
|
3
|
+
export declare const SEED_ATTRIBUTE = "data-gyral-seed";
|
|
4
|
+
export interface Seed {
|
|
5
|
+
/**
|
|
6
|
+
* The server's state. Omitted when it equals what `init(props)` returns for the seeded
|
|
7
|
+
* props (gyral-czi 4k7.10): the client recomputes it instead of reading a second copy of
|
|
8
|
+
* data the props already carry.
|
|
9
|
+
*/
|
|
10
|
+
readonly state?: unknown;
|
|
11
|
+
readonly props: Readonly<Record<string, unknown>>;
|
|
12
|
+
}
|
|
13
|
+
/** JSON equality: seeds are JSON, so two values that serialize alike hydrate alike. */
|
|
14
|
+
export declare const sameJson: (a: unknown, b: unknown) => boolean;
|
|
15
|
+
type Declarations = Readonly<Record<string, PropertyDeclaration>>;
|
|
16
|
+
/**
|
|
17
|
+
* Server side: records the element's state and its props that are not already present as
|
|
18
|
+
* attributes (property bindings such as `.items=${data}` are not in the HTML otherwise).
|
|
19
|
+
* The SSR renderer escapes attribute values, so the JSON is HTML-safe.
|
|
20
|
+
*/
|
|
21
|
+
export declare function writeSeed(host: Element, state: unknown, props: Readonly<Record<string, unknown>>, declarations: Declarations, initialState?: {
|
|
22
|
+
readonly value: unknown;
|
|
23
|
+
}): void;
|
|
24
|
+
/**
|
|
25
|
+
* Client side, right after hydration (gyral-4k7.12, ADR 0012): Lit SSR writes nothing for
|
|
26
|
+
* an empty primitive (`''`, `null`, `undefined`), so `<!--lit-part--><!--/lit-part-->`
|
|
27
|
+
* has no Text node between the markers. Hydration still records the primitive as the
|
|
28
|
+
* part's committed value, and the next text commit writes `.data` into the
|
|
29
|
+
* `<!--/lit-part-->` comment instead. Restore the empty Text node a client render would
|
|
30
|
+
* have created. Harmless for `nothing` and empty iterables, which clear or insert before
|
|
31
|
+
* the end marker. Shadow roots below `root` are handled by their own hosts.
|
|
32
|
+
*/
|
|
33
|
+
export declare function fillEmptyTextParts(root: Node): void;
|
|
34
|
+
/** Client side: reads and removes the seed, if this element was server-rendered by Gyral. */
|
|
35
|
+
export declare function takeSeed(host: Element): Seed | undefined;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { warnJsonHazard } from './json-safety.js';
|
|
2
|
+
/** Host attribute holding the JSON seed. Removed once the client has read it. */
|
|
3
|
+
export const SEED_ATTRIBUTE = 'data-gyral-seed';
|
|
4
|
+
/** JSON equality: seeds are JSON, so two values that serialize alike hydrate alike. */
|
|
5
|
+
export const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
6
|
+
function attributeName(name, decl) {
|
|
7
|
+
if (decl.attribute === false)
|
|
8
|
+
return undefined;
|
|
9
|
+
return typeof decl.attribute === 'string' ? decl.attribute : name.toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Server side: records the element's state and its props that are not already present as
|
|
13
|
+
* attributes (property bindings such as `.items=${data}` are not in the HTML otherwise).
|
|
14
|
+
* The SSR renderer escapes attribute values, so the JSON is HTML-safe.
|
|
15
|
+
*/
|
|
16
|
+
export function writeSeed(host, state, props, declarations, initialState) {
|
|
17
|
+
const carried = {};
|
|
18
|
+
for (const [name, value] of Object.entries(props)) {
|
|
19
|
+
const decl = declarations[name] ?? {};
|
|
20
|
+
const attr = attributeName(name, decl);
|
|
21
|
+
if (value !== undefined && (attr === undefined || !host.hasAttribute(attr))) {
|
|
22
|
+
carried[name] = value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const derivable = initialState !== undefined && sameJson(state, initialState.value);
|
|
26
|
+
const seed = derivable ? { props: carried } : { state, props: carried };
|
|
27
|
+
warnJsonHazard(`<${host.localName}>`, seed, 'seed');
|
|
28
|
+
host.setAttribute(SEED_ATTRIBUTE, JSON.stringify(seed));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Client side, right after hydration (gyral-4k7.12, ADR 0012): Lit SSR writes nothing for
|
|
32
|
+
* an empty primitive (`''`, `null`, `undefined`), so `<!--lit-part--><!--/lit-part-->`
|
|
33
|
+
* has no Text node between the markers. Hydration still records the primitive as the
|
|
34
|
+
* part's committed value, and the next text commit writes `.data` into the
|
|
35
|
+
* `<!--/lit-part-->` comment instead. Restore the empty Text node a client render would
|
|
36
|
+
* have created. Harmless for `nothing` and empty iterables, which clear or insert before
|
|
37
|
+
* the end marker. Shadow roots below `root` are handled by their own hosts.
|
|
38
|
+
*/
|
|
39
|
+
export function fillEmptyTextParts(root) {
|
|
40
|
+
const doc = root.ownerDocument ?? root;
|
|
41
|
+
const walker = doc.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
|
42
|
+
const empty = [];
|
|
43
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
44
|
+
const next = node.nextSibling;
|
|
45
|
+
if (node.data === 'lit-part' &&
|
|
46
|
+
next instanceof Comment &&
|
|
47
|
+
next.data === '/lit-part') {
|
|
48
|
+
empty.push(node);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const start of empty)
|
|
52
|
+
start.after(doc.createTextNode(''));
|
|
53
|
+
}
|
|
54
|
+
/** Client side: reads and removes the seed, if this element was server-rendered by Gyral. */
|
|
55
|
+
export function takeSeed(host) {
|
|
56
|
+
const raw = host.getAttribute(SEED_ATTRIBUTE);
|
|
57
|
+
if (raw === null)
|
|
58
|
+
return undefined;
|
|
59
|
+
host.removeAttribute(SEED_ATTRIBUTE);
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(raw);
|
|
62
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
63
|
+
return undefined;
|
|
64
|
+
const props = parsed.props ?? {};
|
|
65
|
+
return 'state' in parsed ? { state: parsed.state, props } : { props };
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error(`<${host.localName}> has an unreadable ${SEED_ATTRIBUTE}`, error);
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=hydration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hydration.js","sourceRoot":"","sources":["../src/hydration.ts"],"names":[],"mappings":"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","sourcesContent":["// Server-render seeds (docs/design-docs/0012-ssr.md). The server writes each element's\n// state, plus the props an attribute can't carry, into one attribute; the client reads it\n// before its first (hydrating) render so both sides render the same template.\nimport type { PropertyDeclaration } from 'lit';\nimport { warnJsonHazard } from './json-safety.js';\n\n/** Host attribute holding the JSON seed. Removed once the client has read it. */\nexport const SEED_ATTRIBUTE = 'data-gyral-seed';\n\nexport interface Seed {\n /**\n * The server's state. Omitted when it equals what `init(props)` returns for the seeded\n * props (gyral-czi 4k7.10): the client recomputes it instead of reading a second copy of\n * data the props already carry.\n */\n readonly state?: unknown;\n readonly props: Readonly<Record<string, unknown>>;\n}\n\n/** JSON equality: seeds are JSON, so two values that serialize alike hydrate alike. */\nexport const sameJson = (a: unknown, b: unknown): boolean =>\n JSON.stringify(a) === JSON.stringify(b);\n\ntype Declarations = Readonly<Record<string, PropertyDeclaration>>;\n\nfunction attributeName(name: string, decl: PropertyDeclaration): string | undefined {\n if (decl.attribute === false) return undefined;\n return typeof decl.attribute === 'string' ? decl.attribute : name.toLowerCase();\n}\n\n/**\n * Server side: records the element's state and its props that are not already present as\n * attributes (property bindings such as `.items=${data}` are not in the HTML otherwise).\n * The SSR renderer escapes attribute values, so the JSON is HTML-safe.\n */\nexport function writeSeed(\n host: Element,\n state: unknown,\n props: Readonly<Record<string, unknown>>,\n declarations: Declarations,\n initialState?: { readonly value: unknown },\n): void {\n const carried: Record<string, unknown> = {};\n for (const [name, value] of Object.entries(props)) {\n const decl = declarations[name] ?? {};\n const attr = attributeName(name, decl);\n if (value !== undefined && (attr === undefined || !host.hasAttribute(attr))) {\n carried[name] = value;\n }\n }\n const derivable = initialState !== undefined && sameJson(state, initialState.value);\n const seed: Seed = derivable ? { props: carried } : { state, props: carried };\n warnJsonHazard(`<${host.localName}>`, seed, 'seed');\n host.setAttribute(SEED_ATTRIBUTE, JSON.stringify(seed));\n}\n\n/**\n * Client side, right after hydration (gyral-4k7.12, ADR 0012): Lit SSR writes nothing for\n * an empty primitive (`''`, `null`, `undefined`), so `<!--lit-part--><!--/lit-part-->`\n * has no Text node between the markers. Hydration still records the primitive as the\n * part's committed value, and the next text commit writes `.data` into the\n * `<!--/lit-part-->` comment instead. Restore the empty Text node a client render would\n * have created. Harmless for `nothing` and empty iterables, which clear or insert before\n * the end marker. Shadow roots below `root` are handled by their own hosts.\n */\nexport function fillEmptyTextParts(root: Node): void {\n const doc = root.ownerDocument ?? (root as Document);\n const walker = doc.createTreeWalker(root, NodeFilter.SHOW_COMMENT);\n const empty: Comment[] = [];\n for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {\n const next = node.nextSibling;\n if (\n (node as Comment).data === 'lit-part' &&\n next instanceof Comment &&\n next.data === '/lit-part'\n ) {\n empty.push(node as Comment);\n }\n }\n for (const start of empty) start.after(doc.createTextNode(''));\n}\n\n/** Client side: reads and removes the seed, if this element was server-rendered by Gyral. */\nexport function takeSeed(host: Element): Seed | undefined {\n const raw = host.getAttribute(SEED_ATTRIBUTE);\n if (raw === null) return undefined;\n host.removeAttribute(SEED_ATTRIBUTE);\n try {\n const parsed = JSON.parse(raw) as Partial<Seed> | null;\n if (parsed === null || typeof parsed !== 'object') return undefined;\n const props = parsed.props ?? {};\n return 'state' in parsed ? { state: parsed.state, props } : { props };\n } catch (error) {\n console.error(`<${host.localName}> has an unreadable ${SEED_ATTRIBUTE}`, error);\n return undefined;\n }\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export { command, defineDriver } from './command.js';
|
|
2
|
+
export type { AnyDriver, Command, CommandHandlers, Concurrency, Driver, DriverContext, DriverOverrides, Next, RetryPolicy, } from './command.js';
|
|
3
|
+
export { child, emit } from './children.js';
|
|
4
|
+
export { focus } from './focus.js';
|
|
5
|
+
export type { FocusOptions } from './focus.js';
|
|
6
|
+
export { invokersSupported } from './invokers.js';
|
|
7
|
+
export type { CommandInfo } from './invokers.js';
|
|
8
|
+
export { ISLAND_ATTRIBUTE } from './islands.js';
|
|
9
|
+
export type { HydrateStrategy } from './islands.js';
|
|
10
|
+
export type { ChildSource, OutputSource } from './children.js';
|
|
11
|
+
export { define } from './define.js';
|
|
12
|
+
export { changed, defineStore, send } from './store.js';
|
|
13
|
+
export type { AnyStore, AnyStoreInstance, Store, StoreChanged, StoreInstance, StoreOverrides, StoreRef, StoreSendInput, SeedCheck, StoreResolver, StoreSpec, StoreUpdate, } from './store.js';
|
|
14
|
+
export { STORE_SEND } from './store.js';
|
|
15
|
+
export { resetDocumentStores, scriptSafeJson, STORE_SEED_ATTRIBUTE, StoreRegistry, STORES_ELEMENT, withStoreScope, } from './store-scope.js';
|
|
16
|
+
export { defineStoresProvider } from './stores-provider.js';
|
|
17
|
+
export { DRIVERS_ELEMENT, provideDrivers } from './drivers-scope.js';
|
|
18
|
+
export { jsonHazard, warnJsonHazard } from './json-safety.js';
|
|
19
|
+
export { random, randomDriver, randomInt, toInt, type RandomInput } from './random.js';
|
|
20
|
+
export { defineForm, field, fieldErrors, form, formDataToObject, formFields, intentRejectedSchema, redirectedTo, validateForm, } from './forms.js';
|
|
21
|
+
export type { FormDefinition, FormRedirected, FormResult, FormValue } from './forms.js';
|
|
22
|
+
export { ElementDirective } from './element-directive.js';
|
|
23
|
+
export { runInit } from './init.js';
|
|
24
|
+
export { invalid } from './invalid.js';
|
|
25
|
+
export { findInScope, labelledBy } from './accessible-name.js';
|
|
26
|
+
export { liveBoolean } from './live-boolean.js';
|
|
27
|
+
export { textarea, textareaMarkup } from './textarea.js';
|
|
28
|
+
export { DEVTOOLS_ENABLED as devtoolsEnabled, devLiveComponents as devtoolsLiveComponents, } from '#devtools';
|
|
29
|
+
export { DEVTOOLS_GLOBAL } from './devtools-events.js';
|
|
30
|
+
export type { CommandPhase, CommandTraceEvent, DevComponentRef, DevEvent, DevtoolsHook, } from './devtools-events.js';
|
|
31
|
+
export type { TextareaAttribute, TextareaOptions } from './textarea.js';
|
|
32
|
+
export type { Styles } from './styles.js';
|
|
33
|
+
export { HIDDEN_MARKER, HYDRATE_KEY, isLightComponent, LIGHT_ATTRIBUTE } from './light-dom.js';
|
|
34
|
+
export type { GyralElement, GyralElementClass } from './define.js';
|
|
35
|
+
export type { ComponentSpec, Ctx, FieldIssue, FormFields, IntentInput, IntentNames, IntentParser, IntentRejected, Intents, PropDeclarations, Hydrated, PropsChanged, Stateless, StoreReader, Tagged, Update, } from './types.js';
|
|
36
|
+
export { css, html, nothing, svg, unsafeCSS } from 'lit';
|
|
37
|
+
export { directive } from 'lit/directive.js';
|
|
38
|
+
export { classMap } from 'lit/directives/class-map.js';
|
|
39
|
+
export { keyed } from 'lit/directives/keyed.js';
|
|
40
|
+
export { live } from 'lit/directives/live.js';
|
|
41
|
+
export { repeat } from 'lit/directives/repeat.js';
|
|
42
|
+
export { styleMap } from 'lit/directives/style-map.js';
|