@affordant/react 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # @affordant/react
2
+
3
+ React adapter for [Affordant](https://leroy-florian.github.io/Affordant/). Gate your UI on what the server offers, and invoke affordances with **either invoker** — the vanilla Promise one or the Effect one. The framework axis (React) and the effect-system axis (Promise / Effect) stay orthogonal and composable.
4
+
5
+ ## Gating (pure, invoker-agnostic)
6
+
7
+ ```tsx
8
+ import { useAffordance } from '@affordant/react'
9
+
10
+ function CancelButton({ order }) {
11
+ const cancel = useAffordance(order, 'cancel')
12
+ if (!cancel.can) return null
13
+ return <button onClick={...}>Cancel</button>
14
+ }
15
+ ```
16
+
17
+ ## Vanilla (Promise) invoker
18
+
19
+ ```tsx
20
+ import { useAffordance, useFollow } from '@affordant/react'
21
+
22
+ const cancel = useAffordance(order, 'cancel')
23
+ const { run, running } = useFollow()
24
+
25
+ <button disabled={!cancel.can || running} onClick={() => run(cancel.action!, { token })}>
26
+ Cancel
27
+ </button>
28
+ ```
29
+
30
+ ## Effect invoker
31
+
32
+ The Effect path composes [`@affordant/effect`](https://www.npmjs.com/package/@affordant/effect) with the [`effect-react-bridge`](https://www.npmjs.com/package/effect-react-bridge) runtime — no Effect coupling leaks into the vanilla path.
33
+
34
+ ```ts
35
+ import { makeEffectHooks } from 'effect-react-bridge'
36
+ import { makeAffordanceHooks } from '@affordant/react/effect'
37
+
38
+ const bridge = makeEffectHooks({ runtime })
39
+ const { useFollow } = makeAffordanceHooks(bridge)
40
+ // useFollow().run(action, init) is now an interruptible Effect with a typed error
41
+ ```
42
+
43
+ `react` is a peer dependency; `effect`, `effect-react-bridge` and `@affordant/effect` are optional peers, needed only for `@affordant/react/effect`.
44
+
45
+ ## License
46
+
47
+ MIT
@@ -0,0 +1,29 @@
1
+ import type { EffectHooks } from 'effect-react-bridge';
2
+ import { type FollowError } from '@affordant/effect';
3
+ import type { FollowInit } from 'affordant';
4
+ import type { HateoasAction } from '@affordant/contract';
5
+ export interface EffectFollowResult {
6
+ readonly running: boolean;
7
+ readonly error: FollowError | null;
8
+ readonly run: (action: HateoasAction, init?: FollowInit) => Promise<Response>;
9
+ }
10
+ export interface AffordanceEffectHooks {
11
+ /**
12
+ * The Effect invoker as a hook, run through the supplied
13
+ * `effect-react-bridge` runtime: tracks `running` / `error` and interrupts
14
+ * the request when the component unmounts.
15
+ */
16
+ useFollow(): EffectFollowResult;
17
+ }
18
+ /**
19
+ * Compose the Effect invoker (`@affordant/effect`) with an
20
+ * `effect-react-bridge` runtime to get React hooks. The bridge stays
21
+ * domain-agnostic; this is the thin Affordant-specific glue.
22
+ *
23
+ * ```ts
24
+ * const { useEffectFn } = makeEffectHooks({ runtime })
25
+ * const affordances = makeAffordanceHooks({ useEffectFn } as EffectHooks<never>)
26
+ * ```
27
+ */
28
+ export declare function makeAffordanceHooks<R>(hooks: EffectHooks<R>): AffordanceEffectHooks;
29
+ //# sourceMappingURL=effect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effect.d.ts","sourceRoot":"","sources":["../src/effect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,EAA0B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC5E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAExD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAA;IAClC,QAAQ,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE,UAAU,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC9E;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,SAAS,IAAI,kBAAkB,CAAA;CAChC;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,qBAAqB,CASnF"}
package/dist/effect.js ADDED
@@ -0,0 +1,20 @@
1
+ import { follow as effectFollow } from '@affordant/effect';
2
+ /**
3
+ * Compose the Effect invoker (`@affordant/effect`) with an
4
+ * `effect-react-bridge` runtime to get React hooks. The bridge stays
5
+ * domain-agnostic; this is the thin Affordant-specific glue.
6
+ *
7
+ * ```ts
8
+ * const { useEffectFn } = makeEffectHooks({ runtime })
9
+ * const affordances = makeAffordanceHooks({ useEffectFn } as EffectHooks<never>)
10
+ * ```
11
+ */
12
+ export function makeAffordanceHooks(hooks) {
13
+ return {
14
+ useFollow() {
15
+ const { running, error, run } = hooks.useEffectFn((action, init) => effectFollow(action, init));
16
+ return { running, error, run };
17
+ },
18
+ };
19
+ }
20
+ //# sourceMappingURL=effect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effect.js","sourceRoot":"","sources":["../src/effect.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,IAAI,YAAY,EAAoB,MAAM,mBAAmB,CAAA;AAmB5E;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAI,KAAqB;IAC1D,OAAO;QACL,SAAS;YACP,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,KAAK,CAAC,WAAW,CAC/C,CAAC,MAAqB,EAAE,IAAiB,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,CACzE,CAAA;YACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAA;QAChC,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,35 @@
1
+ import { type FollowInit } from 'affordant';
2
+ import type { HateoasAction, HateoasResource } from '@affordant/contract';
3
+ export type { HateoasAction, HateoasMethod, HateoasResource } from '@affordant/contract';
4
+ export type { FollowInit } from 'affordant';
5
+ /** What the server currently offers for a `rel` on a resource. */
6
+ export interface Affordance {
7
+ /** Whether the server offers this affordance right now. */
8
+ readonly can: boolean;
9
+ /** The action descriptor, or `null` when not offered. */
10
+ readonly action: HateoasAction | null;
11
+ }
12
+ /**
13
+ * Gate UI on an affordance. Pure and invoker-agnostic: it only reads what
14
+ * the server offered, mirroring `can` / `actionFor` as a memoised hook.
15
+ *
16
+ * ```tsx
17
+ * const cancel = useAffordance(order, 'cancel')
18
+ * return cancel.can ? <button onClick={...}>Cancel</button> : null
19
+ * ```
20
+ */
21
+ export declare function useAffordance<T>(resource: HateoasResource<T> | null | undefined, rel: string): Affordance;
22
+ export interface FollowState {
23
+ readonly running: boolean;
24
+ readonly error: unknown;
25
+ }
26
+ export interface UseFollowResult extends FollowState {
27
+ readonly run: (action: HateoasAction, init?: FollowInit) => Promise<Response>;
28
+ }
29
+ /**
30
+ * The vanilla (Promise) invoker as a hook: tracks `running` / `error` around
31
+ * the client's `follow`. This is one of Affordant's two interchangeable
32
+ * invokers; for the Effect one, see `@affordant/react/effect`.
33
+ */
34
+ export declare function useFollow(): UseFollowResult;
35
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAA2C,KAAK,UAAU,EAAE,MAAM,WAAW,CAAA;AACpF,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAEzE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACxF,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE3C,kEAAkE;AAClE,MAAM,WAAW,UAAU;IACzB,2DAA2D;IAC3D,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;IACrB,yDAAyD;IACzD,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;CACtC;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC7B,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,EAC/C,GAAG,EAAE,MAAM,GACV,UAAU,CAKZ;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CACxB;AAED,MAAM,WAAW,eAAgB,SAAQ,WAAW;IAClD,QAAQ,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE,UAAU,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC9E;AAED;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,eAAe,CAgB3C"}
package/dist/index.js ADDED
@@ -0,0 +1,36 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import { actionFor, can, follow as vanillaFollow } from 'affordant';
3
+ /**
4
+ * Gate UI on an affordance. Pure and invoker-agnostic: it only reads what
5
+ * the server offered, mirroring `can` / `actionFor` as a memoised hook.
6
+ *
7
+ * ```tsx
8
+ * const cancel = useAffordance(order, 'cancel')
9
+ * return cancel.can ? <button onClick={...}>Cancel</button> : null
10
+ * ```
11
+ */
12
+ export function useAffordance(resource, rel) {
13
+ return useMemo(() => ({ can: can(resource, rel), action: actionFor(resource, rel) }), [resource, rel]);
14
+ }
15
+ /**
16
+ * The vanilla (Promise) invoker as a hook: tracks `running` / `error` around
17
+ * the client's `follow`. This is one of Affordant's two interchangeable
18
+ * invokers; for the Effect one, see `@affordant/react/effect`.
19
+ */
20
+ export function useFollow() {
21
+ const [state, setState] = useState({ running: false, error: null });
22
+ const run = useCallback(async (action, init) => {
23
+ setState({ running: true, error: null });
24
+ try {
25
+ const response = await vanillaFollow(action, init);
26
+ setState({ running: false, error: null });
27
+ return response;
28
+ }
29
+ catch (error) {
30
+ setState({ running: false, error });
31
+ throw error;
32
+ }
33
+ }, []);
34
+ return { ...state, run };
35
+ }
36
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACtD,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,IAAI,aAAa,EAAmB,MAAM,WAAW,CAAA;AAcpF;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAC3B,QAA+C,EAC/C,GAAW;IAEX,OAAO,OAAO,CACZ,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE,CAAC,EACrE,CAAC,QAAQ,EAAE,GAAG,CAAC,CAChB,CAAA;AACH,CAAC;AAWD;;;;GAIG;AACH,MAAM,UAAU,SAAS;IACvB,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAc,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IAEhF,MAAM,GAAG,GAAG,WAAW,CAAC,KAAK,EAAE,MAAqB,EAAE,IAAiB,EAAE,EAAE;QACzE,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACxC,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;YAClD,QAAQ,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;YACzC,OAAO,QAAQ,CAAA;QACjB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,QAAQ,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;YACnC,MAAM,KAAK,CAAA;QACb,CAAC;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,OAAO,EAAE,GAAG,KAAK,EAAE,GAAG,EAAE,CAAA;AAC1B,CAAC"}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@affordant/react",
3
+ "version": "0.2.0",
4
+ "description": "React adapter for Affordant: gate UI on the server's affordances and invoke them with either invoker — the vanilla Promise one or the Effect one.",
5
+ "license": "MIT",
6
+ "author": "Florian Leroy",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Leroy-Florian/Affordant.git",
10
+ "directory": "packages/react"
11
+ },
12
+ "homepage": "https://leroy-florian.github.io/Affordant/",
13
+ "bugs": "https://github.com/Leroy-Florian/Affordant/issues",
14
+ "keywords": [
15
+ "hateoas",
16
+ "hypermedia",
17
+ "affordance",
18
+ "react",
19
+ "hooks"
20
+ ],
21
+ "type": "module",
22
+ "sideEffects": false,
23
+ "main": "dist/index.js",
24
+ "types": "dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js"
29
+ },
30
+ "./effect": {
31
+ "types": "./dist/effect.d.ts",
32
+ "import": "./dist/effect.js"
33
+ }
34
+ },
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "dependencies": {
42
+ "affordant": "^0.2.0",
43
+ "@affordant/contract": "^0.2.0"
44
+ },
45
+ "peerDependencies": {
46
+ "react": ">=18",
47
+ "effect": "^3.21.0",
48
+ "effect-react-bridge": ">=0.1.0",
49
+ "@affordant/effect": ">=0.1.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "effect": {
53
+ "optional": true
54
+ },
55
+ "effect-react-bridge": {
56
+ "optional": true
57
+ },
58
+ "@affordant/effect": {
59
+ "optional": true
60
+ }
61
+ },
62
+ "devDependencies": {
63
+ "@affordant/effect": "*",
64
+ "effect-react-bridge": "*"
65
+ },
66
+ "scripts": {
67
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
68
+ "typecheck": "tsc -p tsconfig.json",
69
+ "test": "vitest run"
70
+ }
71
+ }