@haikit/react 0.8.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/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
- package/src/index.ts +98 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hai contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @haikit/react
|
|
2
|
+
|
|
3
|
+
Write haikit surface components in React.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @haikit/react react react-dom
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
import { reactSurface, type SurfaceProps } from "@haikit/react";
|
|
12
|
+
import type { greetingPicker } from "../shared/surfaces.ts";
|
|
13
|
+
|
|
14
|
+
function GreetingPicker({ props, send, mode, state, selection }: SurfaceProps<typeof greetingPicker>) {
|
|
15
|
+
const [script, setScript] = useState<string | null>(null); // local state: never reaches the server
|
|
16
|
+
const live = state === "live" && mode === "elicit";
|
|
17
|
+
return (
|
|
18
|
+
<div className="rows">
|
|
19
|
+
{props.greetings
|
|
20
|
+
.filter((g) => !script || g.script === script)
|
|
21
|
+
.map((g) =>
|
|
22
|
+
live ? (
|
|
23
|
+
<button key={g.code} type="button" onClick={() => send("choose", g.code)}>
|
|
24
|
+
{g.text}
|
|
25
|
+
</button>
|
|
26
|
+
) : (
|
|
27
|
+
<div key={g.code} className={g.code === selection ? "picked" : undefined}>
|
|
28
|
+
{g.text}
|
|
29
|
+
</div>
|
|
30
|
+
),
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const registry = { greeting_picker: reactSurface(GreetingPicker) };
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Pass `registry` to `mountChat` or `createChat` as usual. The client's contract
|
|
40
|
+
doesn't change: a registry entry is anything with `mount(element, props, ctx)`,
|
|
41
|
+
and `reactSurface` builds one around a React component.
|
|
42
|
+
|
|
43
|
+
## What the component gets
|
|
44
|
+
|
|
45
|
+
`SurfaceProps<typeof yourSurface>` is typed from the same `defineSurface`
|
|
46
|
+
contract the server implements:
|
|
47
|
+
|
|
48
|
+
| prop | |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `props` | the surface's props, as its `props` schema produced them |
|
|
51
|
+
| `send(action, value)` | the only channel to the server: an action the contract declares, with a value of its input type. Anything else is a compile error |
|
|
52
|
+
| `mode` | how the surface was opened: `elicit` as a question the turn waits on, `display` as something shown. It stays `elicit` once answered |
|
|
53
|
+
| `state` | `live`, or `frozen` once answered: a resolved question can't be answered again |
|
|
54
|
+
| `selection` | what the user picked, once frozen |
|
|
55
|
+
| `expired` | the conversation is out of date; nothing here can reach the server again |
|
|
56
|
+
|
|
57
|
+
Import the contract with `import type`. Only the types are needed, so the
|
|
58
|
+
schemas and their library stay out of the browser bundle.
|
|
59
|
+
|
|
60
|
+
## Lifecycle
|
|
61
|
+
|
|
62
|
+
The default transcript mounts a surface **once**, so `useState` survives the
|
|
63
|
+
conversation streaming on around it. The root re-renders when the surface
|
|
64
|
+
freezes or the conversation goes out of date. It is unmounted when the surface
|
|
65
|
+
goes away, on `chat.reset()` or `chat.close()`, so effects clean up after
|
|
66
|
+
themselves. The first render is synchronous, so the surface already has its
|
|
67
|
+
height when the transcript scrolls to it.
|
|
68
|
+
|
|
69
|
+
React escapes text by default. Keep it that way: tool payloads are untrusted
|
|
70
|
+
input, so never pass them to `dangerouslySetInnerHTML`.
|
|
71
|
+
|
|
72
|
+
## Building
|
|
73
|
+
|
|
74
|
+
React needs a bundler. `examples/hello-react` in the haikit repo runs Vite
|
|
75
|
+
inside its own node server while developing, so the app and `/hai` share one
|
|
76
|
+
port with no proxy, and serves `vite build`'s output in production.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @haikit/react — write surface components in React.
|
|
3
|
+
*
|
|
4
|
+
* function GreetingPicker({ props, send, state }: SurfaceProps<typeof greetingPicker>) { … }
|
|
5
|
+
* export const registry = { greeting_picker: reactSurface(GreetingPicker) };
|
|
6
|
+
*
|
|
7
|
+
* The client's contract is unchanged: a registry entry is something with a
|
|
8
|
+
* `mount(element, props, ctx)`. This builds one around a React component, and
|
|
9
|
+
* types it from the same `defineSurface` contract the server implements, so a
|
|
10
|
+
* prop or an action that changes there stops this side compiling.
|
|
11
|
+
*/
|
|
12
|
+
import { type ComponentType } from "react";
|
|
13
|
+
import type { ComponentDef, SurfaceMode, SurfaceState } from "@haikit/client";
|
|
14
|
+
import type { ActionMap, Infer, QueryMap, Surface } from "@haikit/core";
|
|
15
|
+
type AnySurface = Surface<any, ActionMap, QueryMap>;
|
|
16
|
+
type PropsOf<S> = S extends Surface<infer P, any, any> ? P : never;
|
|
17
|
+
type ActionsOf<S> = S extends Surface<any, infer A, any> ? A : never;
|
|
18
|
+
/** What a React surface component receives, typed from its surface contract. */
|
|
19
|
+
export interface SurfaceProps<S extends AnySurface> {
|
|
20
|
+
/** The surface's props, as its `props` schema produced them. */
|
|
21
|
+
props: PropsOf<S>;
|
|
22
|
+
/**
|
|
23
|
+
* How the surface was opened: `elicit` as a question the turn waits on,
|
|
24
|
+
* `display` as something shown. It doesn't change once the question is
|
|
25
|
+
* answered; `state` does.
|
|
26
|
+
*/
|
|
27
|
+
mode: SurfaceMode;
|
|
28
|
+
/** `frozen` once answered: a resolved question cannot be answered again. */
|
|
29
|
+
state: SurfaceState;
|
|
30
|
+
/** What the user picked, once the surface is frozen. */
|
|
31
|
+
selection?: unknown;
|
|
32
|
+
/** The conversation is out of date: nothing here can reach the server again. */
|
|
33
|
+
expired: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The only channel to the server: an action the contract declares, with a
|
|
36
|
+
* value of its input type. Never a tool, never a handler.
|
|
37
|
+
*/
|
|
38
|
+
send<K extends keyof ActionsOf<S> & string>(action: K, value: Infer<ActionsOf<S>[K]["input"]>): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A registry entry that renders `Component` into the surface's element.
|
|
42
|
+
*
|
|
43
|
+
* The first render is synchronous, so the surface has its height when the
|
|
44
|
+
* transcript scrolls to it; later ones are React's. It re-renders when the
|
|
45
|
+
* surface freezes or the conversation goes out of date, and its root is
|
|
46
|
+
* unmounted when the surface goes away, so effects clean up after themselves.
|
|
47
|
+
*/
|
|
48
|
+
export declare function reactSurface<S extends AnySurface>(Component: ComponentType<SurfaceProps<S>>): ComponentDef<PropsOf<S>>;
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAG1D,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAExE,KAAK,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;AACpD,KAAK,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,OAAO,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AACnE,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAErE,gFAAgF;AAChF,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,UAAU;IAChD,gEAAgE;IAChE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAClB;;;;OAIG;IACH,IAAI,EAAE,WAAW,CAAC;IAClB,4EAA4E;IAC5E,KAAK,EAAE,YAAY,CAAC;IACpB,wDAAwD;IACxD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gFAAgF;IAChF,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,SAAS,CAAC,CAAC,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/G;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,UAAU,EAAE,SAAS,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CA4CtH"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @haikit/react — write surface components in React.
|
|
3
|
+
*
|
|
4
|
+
* function GreetingPicker({ props, send, state }: SurfaceProps<typeof greetingPicker>) { … }
|
|
5
|
+
* export const registry = { greeting_picker: reactSurface(GreetingPicker) };
|
|
6
|
+
*
|
|
7
|
+
* The client's contract is unchanged: a registry entry is something with a
|
|
8
|
+
* `mount(element, props, ctx)`. This builds one around a React component, and
|
|
9
|
+
* types it from the same `defineSurface` contract the server implements, so a
|
|
10
|
+
* prop or an action that changes there stops this side compiling.
|
|
11
|
+
*/
|
|
12
|
+
import { createElement } from "react";
|
|
13
|
+
import { createRoot } from "react-dom/client";
|
|
14
|
+
import { flushSync } from "react-dom";
|
|
15
|
+
/**
|
|
16
|
+
* A registry entry that renders `Component` into the surface's element.
|
|
17
|
+
*
|
|
18
|
+
* The first render is synchronous, so the surface has its height when the
|
|
19
|
+
* transcript scrolls to it; later ones are React's. It re-renders when the
|
|
20
|
+
* surface freezes or the conversation goes out of date, and its root is
|
|
21
|
+
* unmounted when the surface goes away, so effects clean up after themselves.
|
|
22
|
+
*/
|
|
23
|
+
export function reactSurface(Component) {
|
|
24
|
+
return {
|
|
25
|
+
mount(element, props, ctx) {
|
|
26
|
+
const root = createRoot(element);
|
|
27
|
+
let state = ctx.state;
|
|
28
|
+
// set when it is mounted already frozen
|
|
29
|
+
let selection = ctx.selection;
|
|
30
|
+
let expired = false;
|
|
31
|
+
const draw = () => root.render(createElement(Component, {
|
|
32
|
+
props,
|
|
33
|
+
mode: ctx.mode,
|
|
34
|
+
state,
|
|
35
|
+
selection,
|
|
36
|
+
expired,
|
|
37
|
+
send: ctx.send,
|
|
38
|
+
}));
|
|
39
|
+
// A component that throws on its first render leaves no instance behind
|
|
40
|
+
// to unmount later, so its root is let go of here, before the error goes
|
|
41
|
+
// on; otherwise a remount would stack a second root on this element.
|
|
42
|
+
try {
|
|
43
|
+
flushSync(draw);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
root.unmount();
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
freeze(picked) {
|
|
51
|
+
state = "frozen";
|
|
52
|
+
selection = picked;
|
|
53
|
+
draw();
|
|
54
|
+
},
|
|
55
|
+
expire() {
|
|
56
|
+
expired = true;
|
|
57
|
+
draw();
|
|
58
|
+
},
|
|
59
|
+
unmount() {
|
|
60
|
+
root.unmount();
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,aAAa,EAAsB,MAAM,OAAO,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AA+BtC;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAuB,SAAyC;IAC1F,OAAO;QACL,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG;YACvB,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;YACjC,IAAI,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC;YACtB,wCAAwC;YACxC,IAAI,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;YAC9B,IAAI,OAAO,GAAG,KAAK,CAAC;YACpB,MAAM,IAAI,GAAG,GAAG,EAAE,CAChB,IAAI,CAAC,MAAM,CACT,aAAa,CAAC,SAAS,EAAE;gBACvB,KAAK;gBACL,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,KAAK;gBACL,SAAS;gBACT,OAAO;gBACP,IAAI,EAAE,GAAG,CAAC,IAA+B;aAC1C,CAAC,CACH,CAAC;YACJ,wEAAwE;YACxE,yEAAyE;YACzE,qEAAqE;YACrE,IAAI,CAAC;gBACH,SAAS,CAAC,IAAI,CAAC,CAAC;YAClB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,CAAC,OAAO,EAAE,CAAC;gBACf,MAAM,GAAG,CAAC;YACZ,CAAC;YACD,OAAO;gBACL,MAAM,CAAC,MAAM;oBACX,KAAK,GAAG,QAAQ,CAAC;oBACjB,SAAS,GAAG,MAAM,CAAC;oBACnB,IAAI,EAAE,CAAC;gBACT,CAAC;gBACD,MAAM;oBACJ,OAAO,GAAG,IAAI,CAAC;oBACf,IAAI,EAAE,CAAC;gBACT,CAAC;gBACD,OAAO;oBACL,IAAI,CAAC,OAAO,EAAE,CAAC;gBACjB,CAAC;aACF,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@haikit/react",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Write haikit surface components in React: typed props and actions from the shared surface contract.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"haikit",
|
|
7
|
+
"react",
|
|
8
|
+
"llm",
|
|
9
|
+
"agent",
|
|
10
|
+
"agentic-ui",
|
|
11
|
+
"generative-ui",
|
|
12
|
+
"tool-use",
|
|
13
|
+
"ai"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"src"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"prepack": "npm --prefix ../.. run build"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@haikit/client": "0.8.0",
|
|
32
|
+
"@haikit/core": "0.8.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
36
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"homepage": "https://github.com/wfoxd/haikit/tree/main/packages/react#readme",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/wfoxd/haikit/issues"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/wfoxd/haikit.git",
|
|
49
|
+
"directory": "packages/react"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @haikit/react — write surface components in React.
|
|
3
|
+
*
|
|
4
|
+
* function GreetingPicker({ props, send, state }: SurfaceProps<typeof greetingPicker>) { … }
|
|
5
|
+
* export const registry = { greeting_picker: reactSurface(GreetingPicker) };
|
|
6
|
+
*
|
|
7
|
+
* The client's contract is unchanged: a registry entry is something with a
|
|
8
|
+
* `mount(element, props, ctx)`. This builds one around a React component, and
|
|
9
|
+
* types it from the same `defineSurface` contract the server implements, so a
|
|
10
|
+
* prop or an action that changes there stops this side compiling.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createElement, type ComponentType } from "react";
|
|
14
|
+
import { createRoot } from "react-dom/client";
|
|
15
|
+
import { flushSync } from "react-dom";
|
|
16
|
+
import type { ComponentDef, SurfaceMode, SurfaceState } from "@haikit/client";
|
|
17
|
+
import type { ActionMap, Infer, QueryMap, Surface } from "@haikit/core";
|
|
18
|
+
|
|
19
|
+
type AnySurface = Surface<any, ActionMap, QueryMap>;
|
|
20
|
+
type PropsOf<S> = S extends Surface<infer P, any, any> ? P : never;
|
|
21
|
+
type ActionsOf<S> = S extends Surface<any, infer A, any> ? A : never;
|
|
22
|
+
|
|
23
|
+
/** What a React surface component receives, typed from its surface contract. */
|
|
24
|
+
export interface SurfaceProps<S extends AnySurface> {
|
|
25
|
+
/** The surface's props, as its `props` schema produced them. */
|
|
26
|
+
props: PropsOf<S>;
|
|
27
|
+
/**
|
|
28
|
+
* How the surface was opened: `elicit` as a question the turn waits on,
|
|
29
|
+
* `display` as something shown. It doesn't change once the question is
|
|
30
|
+
* answered; `state` does.
|
|
31
|
+
*/
|
|
32
|
+
mode: SurfaceMode;
|
|
33
|
+
/** `frozen` once answered: a resolved question cannot be answered again. */
|
|
34
|
+
state: SurfaceState;
|
|
35
|
+
/** What the user picked, once the surface is frozen. */
|
|
36
|
+
selection?: unknown;
|
|
37
|
+
/** The conversation is out of date: nothing here can reach the server again. */
|
|
38
|
+
expired: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* The only channel to the server: an action the contract declares, with a
|
|
41
|
+
* value of its input type. Never a tool, never a handler.
|
|
42
|
+
*/
|
|
43
|
+
send<K extends keyof ActionsOf<S> & string>(action: K, value: Infer<ActionsOf<S>[K]["input"]>): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A registry entry that renders `Component` into the surface's element.
|
|
48
|
+
*
|
|
49
|
+
* The first render is synchronous, so the surface has its height when the
|
|
50
|
+
* transcript scrolls to it; later ones are React's. It re-renders when the
|
|
51
|
+
* surface freezes or the conversation goes out of date, and its root is
|
|
52
|
+
* unmounted when the surface goes away, so effects clean up after themselves.
|
|
53
|
+
*/
|
|
54
|
+
export function reactSurface<S extends AnySurface>(Component: ComponentType<SurfaceProps<S>>): ComponentDef<PropsOf<S>> {
|
|
55
|
+
return {
|
|
56
|
+
mount(element, props, ctx) {
|
|
57
|
+
const root = createRoot(element);
|
|
58
|
+
let state = ctx.state;
|
|
59
|
+
// set when it is mounted already frozen
|
|
60
|
+
let selection = ctx.selection;
|
|
61
|
+
let expired = false;
|
|
62
|
+
const draw = () =>
|
|
63
|
+
root.render(
|
|
64
|
+
createElement(Component, {
|
|
65
|
+
props,
|
|
66
|
+
mode: ctx.mode,
|
|
67
|
+
state,
|
|
68
|
+
selection,
|
|
69
|
+
expired,
|
|
70
|
+
send: ctx.send as SurfaceProps<S>["send"],
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
// A component that throws on its first render leaves no instance behind
|
|
74
|
+
// to unmount later, so its root is let go of here, before the error goes
|
|
75
|
+
// on; otherwise a remount would stack a second root on this element.
|
|
76
|
+
try {
|
|
77
|
+
flushSync(draw);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
root.unmount();
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
freeze(picked) {
|
|
84
|
+
state = "frozen";
|
|
85
|
+
selection = picked;
|
|
86
|
+
draw();
|
|
87
|
+
},
|
|
88
|
+
expire() {
|
|
89
|
+
expired = true;
|
|
90
|
+
draw();
|
|
91
|
+
},
|
|
92
|
+
unmount() {
|
|
93
|
+
root.unmount();
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|