tempest-react-sdk 0.29.0 → 0.30.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/bin/lib/alias/index.mjs +9 -3
- package/bin/lib/alias/tsconfig.mjs +48 -5
- package/bin/lib/alias/typescript.mjs +72 -2
- package/bin/lib/alias/typescript.test.mjs +120 -0
- package/bin/lib/css/extract.mjs +2 -3
- package/bin/lib/doctor/doctor.e2e.test.mjs +44 -0
- package/bin/tempest.mjs +17 -2
- package/dist/components/Chat/Chat.cjs +2 -0
- package/dist/components/Chat/Chat.cjs.map +1 -0
- package/dist/components/Chat/Chat.js +126 -0
- package/dist/components/Chat/Chat.js.map +1 -0
- package/dist/components/Chat/Chat.module.cjs +2 -0
- package/dist/components/Chat/Chat.module.cjs.map +1 -0
- package/dist/components/Chat/Chat.module.js +32 -0
- package/dist/components/Chat/Chat.module.js.map +1 -0
- package/dist/components/Chat/ChatComposer.cjs +2 -0
- package/dist/components/Chat/ChatComposer.cjs.map +1 -0
- package/dist/components/Chat/ChatComposer.js +67 -0
- package/dist/components/Chat/ChatComposer.js.map +1 -0
- package/dist/components/Chat/chat-groups.cjs +2 -0
- package/dist/components/Chat/chat-groups.cjs.map +1 -0
- package/dist/components/Chat/chat-groups.js +89 -0
- package/dist/components/Chat/chat-groups.js.map +1 -0
- package/dist/components/Markdown/Markdown.cjs +2 -0
- package/dist/components/Markdown/Markdown.cjs.map +1 -0
- package/dist/components/Markdown/Markdown.js +124 -0
- package/dist/components/Markdown/Markdown.js.map +1 -0
- package/dist/components/Markdown/Markdown.module.cjs +2 -0
- package/dist/components/Markdown/Markdown.module.cjs.map +1 -0
- package/dist/components/Markdown/Markdown.module.js +23 -0
- package/dist/components/Markdown/Markdown.module.js.map +1 -0
- package/dist/components/Markdown/markdown-parse.cjs +7 -0
- package/dist/components/Markdown/markdown-parse.cjs.map +1 -0
- package/dist/components/Markdown/markdown-parse.js +269 -0
- package/dist/components/Markdown/markdown-parse.js.map +1 -0
- package/dist/components/Markdown/markdown-url.cjs +2 -0
- package/dist/components/Markdown/markdown-url.cjs.map +1 -0
- package/dist/components/Markdown/markdown-url.js +28 -0
- package/dist/components/Markdown/markdown-url.js.map +1 -0
- package/dist/components/Masonry/Masonry.cjs +2 -0
- package/dist/components/Masonry/Masonry.cjs.map +1 -0
- package/dist/components/Masonry/Masonry.js +70 -0
- package/dist/components/Masonry/Masonry.js.map +1 -0
- package/dist/components/Masonry/Masonry.module.cjs +2 -0
- package/dist/components/Masonry/Masonry.module.cjs.map +1 -0
- package/dist/components/Masonry/Masonry.module.js +10 -0
- package/dist/components/Masonry/Masonry.module.js.map +1 -0
- package/dist/components/Masonry/masonry-layout.cjs +2 -0
- package/dist/components/Masonry/masonry-layout.cjs.map +1 -0
- package/dist/components/Masonry/masonry-layout.js +20 -0
- package/dist/components/Masonry/masonry-layout.js.map +1 -0
- package/dist/components/Transfer/Transfer.cjs +2 -0
- package/dist/components/Transfer/Transfer.cjs.map +1 -0
- package/dist/components/Transfer/Transfer.js +163 -0
- package/dist/components/Transfer/Transfer.js.map +1 -0
- package/dist/components/Transfer/Transfer.module.cjs +2 -0
- package/dist/components/Transfer/Transfer.module.cjs.map +1 -0
- package/dist/components/Transfer/Transfer.module.js +19 -0
- package/dist/components/Transfer/Transfer.module.js.map +1 -0
- package/dist/components/Transfer/transfer-state.cjs +2 -0
- package/dist/components/Transfer/transfer-state.cjs.map +1 -0
- package/dist/components/Transfer/transfer-state.js +59 -0
- package/dist/components/Transfer/transfer-state.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +493 -0
- package/dist/tempest-react-sdk.js +114 -105
- package/package.json +1 -1
package/bin/lib/alias/index.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { collectFiles } from "./collect.mjs";
|
|
|
7
7
|
import { discoverAlias } from "./discover.mjs";
|
|
8
8
|
import { rewriteCss } from "./rewrite-css.mjs";
|
|
9
9
|
import { rewriteSource } from "./rewrite.mjs";
|
|
10
|
-
import { loadTypeScript } from "./typescript.mjs";
|
|
10
|
+
import { loadTypeScript, typeScriptUnavailableReason } from "./typescript.mjs";
|
|
11
11
|
|
|
12
12
|
export { collectFiles } from "./collect.mjs";
|
|
13
13
|
export { discoverAlias } from "./discover.mjs";
|
|
@@ -15,7 +15,7 @@ export { rewriteCss } from "./rewrite-css.mjs";
|
|
|
15
15
|
export { rewriteSource } from "./rewrite.mjs";
|
|
16
16
|
export { isInsideBase, toAlias } from "./specifier.mjs";
|
|
17
17
|
export { readTsconfig } from "./tsconfig.mjs";
|
|
18
|
-
export { loadTypeScript } from "./typescript.mjs";
|
|
18
|
+
export { describeTypeScript, loadTypeScript, typeScriptUnavailableReason } from "./typescript.mjs";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Convert relative imports that climb out of their directory into alias imports.
|
|
@@ -42,7 +42,13 @@ export function aliasImports({ root, targets, dryRun = false }) {
|
|
|
42
42
|
const empty = { files: [], total: 0, errors: [] };
|
|
43
43
|
|
|
44
44
|
const ts = loadTypeScript(root);
|
|
45
|
-
if (!ts)
|
|
45
|
+
if (!ts) {
|
|
46
|
+
return {
|
|
47
|
+
status: "no-typescript",
|
|
48
|
+
reason: typeScriptUnavailableReason(root),
|
|
49
|
+
...empty,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
46
52
|
|
|
47
53
|
const alias = discoverAlias({ root, ts });
|
|
48
54
|
if (!alias) return { status: "no-alias", ...empty };
|
|
@@ -6,19 +6,62 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
|
6
6
|
|
|
7
7
|
const MAX_EXTENDS_DEPTH = 16;
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Strip the JSONC a tsconfig is allowed to contain: `//` and block comments, and
|
|
11
|
+
* a trailing comma before `}` or `]`.
|
|
12
|
+
*
|
|
13
|
+
* Needed because `ts.readConfigFile` is not always available — TypeScript 7 does
|
|
14
|
+
* not ship the classic API — and a plain `JSON.parse` chokes on the comments
|
|
15
|
+
* every Vite-generated tsconfig has. Losing the whole config to a comment would
|
|
16
|
+
* silently drop the `strict`/`jsx`/`paths` checks.
|
|
17
|
+
*
|
|
18
|
+
* Comment markers inside a string are left alone, which matters: `"paths"` and
|
|
19
|
+
* `"extends"` values legitimately hold `//` (a URL, a Windows share).
|
|
20
|
+
*
|
|
21
|
+
* @param {string} text
|
|
22
|
+
* @returns {string} Plain JSON.
|
|
23
|
+
*/
|
|
24
|
+
export function stripJsonc(text) {
|
|
25
|
+
let out = "";
|
|
26
|
+
let i = 0;
|
|
27
|
+
while (i < text.length) {
|
|
28
|
+
const ch = text[i];
|
|
29
|
+
if (ch === '"') {
|
|
30
|
+
let j = i + 1;
|
|
31
|
+
while (j < text.length && text[j] !== '"') j += text[j] === "\\" ? 2 : 1;
|
|
32
|
+
out += text.slice(i, Math.min(j + 1, text.length));
|
|
33
|
+
i = j + 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (ch === "/" && text[i + 1] === "/") {
|
|
37
|
+
const end = text.indexOf("\n", i);
|
|
38
|
+
i = end < 0 ? text.length : end;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (ch === "/" && text[i + 1] === "*") {
|
|
42
|
+
const end = text.indexOf("*/", i + 2);
|
|
43
|
+
i = end < 0 ? text.length : end + 2;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
out += ch;
|
|
47
|
+
i += 1;
|
|
48
|
+
}
|
|
49
|
+
return out.replace(/,(\s*[}\]])/g, "$1");
|
|
50
|
+
}
|
|
51
|
+
|
|
9
52
|
/**
|
|
10
53
|
* Parse one tsconfig file into its raw JSON object.
|
|
11
54
|
*
|
|
12
|
-
* Prefers `ts.readConfigFile`, which accepts the
|
|
13
|
-
*
|
|
14
|
-
* TypeScript
|
|
55
|
+
* Prefers `ts.readConfigFile`, which accepts the exact dialect `tsc` accepts.
|
|
56
|
+
* Falls back to a JSONC-tolerant parse so a project without the classic
|
|
57
|
+
* TypeScript API still gets its config read instead of nothing.
|
|
15
58
|
*
|
|
16
59
|
* @param {string} path - Absolute path to a tsconfig file.
|
|
17
60
|
* @param {object | null} ts - The `typescript` module, or `null`.
|
|
18
61
|
* @returns {object | null} The raw config object, or `null` when unreadable.
|
|
19
62
|
*/
|
|
20
63
|
function parseConfigFile(path, ts) {
|
|
21
|
-
if (ts) {
|
|
64
|
+
if (ts && typeof ts.readConfigFile === "function") {
|
|
22
65
|
const { config, error } = ts.readConfigFile(path, (p) => {
|
|
23
66
|
try {
|
|
24
67
|
return readFileSync(p, "utf8");
|
|
@@ -29,7 +72,7 @@ function parseConfigFile(path, ts) {
|
|
|
29
72
|
return error || !config ? null : config;
|
|
30
73
|
}
|
|
31
74
|
try {
|
|
32
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
75
|
+
return JSON.parse(stripJsonc(readFileSync(path, "utf8")));
|
|
33
76
|
} catch {
|
|
34
77
|
return null;
|
|
35
78
|
}
|
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The classic-API members every codemod in this CLI needs.
|
|
8
|
+
*
|
|
9
|
+
* TypeScript 7 is the native port: its package still installs under the name
|
|
10
|
+
* `typescript`, but the `.` export is a version stub — the JS API moved behind
|
|
11
|
+
* `typescript/unstable/*` with a different shape. So "the module resolved" no
|
|
12
|
+
* longer implies "the API is there", and calling into it blew up with
|
|
13
|
+
* `ts.readConfigFile is not a function` — a stack trace out of `tempest doctor`,
|
|
14
|
+
* which is the command people run first.
|
|
15
|
+
*/
|
|
16
|
+
const REQUIRED_API = ["readConfigFile", "createSourceFile", "forEachChild"];
|
|
17
|
+
|
|
18
|
+
/** True when the resolved module exposes the classic compiler API. */
|
|
19
|
+
function hasClassicApi(module) {
|
|
20
|
+
return Boolean(module) && REQUIRED_API.every((name) => typeof module[name] === "function");
|
|
21
|
+
}
|
|
22
|
+
|
|
6
23
|
/**
|
|
7
24
|
* Load `typescript` from the project's `node_modules`.
|
|
8
25
|
*
|
|
@@ -11,13 +28,66 @@ import { join } from "node:path";
|
|
|
11
28
|
* compiler version than the one that type-checks it would let syntax the app
|
|
12
29
|
* accepts fail here.
|
|
13
30
|
*
|
|
31
|
+
* Returns `null` both when TypeScript is absent and when the install does not
|
|
32
|
+
* expose the classic API, because callers treat the two the same way — report it
|
|
33
|
+
* and leave the source untouched. Use {@link describeTypeScript} to say which of
|
|
34
|
+
* the two happened.
|
|
35
|
+
*
|
|
14
36
|
* @param {string} root - Project root, the directory holding `node_modules`.
|
|
15
|
-
* @returns {object | null} The `typescript` module, or `null
|
|
37
|
+
* @returns {object | null} The `typescript` module, or `null`.
|
|
16
38
|
*/
|
|
17
39
|
export function loadTypeScript(root) {
|
|
18
40
|
try {
|
|
19
|
-
|
|
41
|
+
const module = createRequire(join(root, "package.json"))("typescript");
|
|
42
|
+
return hasClassicApi(module) ? module : null;
|
|
20
43
|
} catch {
|
|
21
44
|
return null;
|
|
22
45
|
}
|
|
23
46
|
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What the project's TypeScript install is, so a message can say the truth
|
|
50
|
+
* instead of "not installed" about a package that is right there.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} root - Project root.
|
|
53
|
+
* @returns {{ status: "ok" | "missing" | "api-unavailable", version: string | null }}
|
|
54
|
+
*/
|
|
55
|
+
export function describeTypeScript(root) {
|
|
56
|
+
let resolve_;
|
|
57
|
+
try {
|
|
58
|
+
resolve_ = createRequire(join(root, "package.json"));
|
|
59
|
+
} catch {
|
|
60
|
+
return { status: "missing", version: null };
|
|
61
|
+
}
|
|
62
|
+
let version = null;
|
|
63
|
+
try {
|
|
64
|
+
version = resolve_("typescript/package.json")?.version ?? null;
|
|
65
|
+
} catch {
|
|
66
|
+
return { status: "missing", version: null };
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return {
|
|
70
|
+
status: hasClassicApi(resolve_("typescript")) ? "ok" : "api-unavailable",
|
|
71
|
+
version,
|
|
72
|
+
};
|
|
73
|
+
} catch {
|
|
74
|
+
return { status: "api-unavailable", version };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* One line explaining why a codemod cannot run, or `null` when it can.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} root - Project root.
|
|
82
|
+
* @returns {string | null}
|
|
83
|
+
*/
|
|
84
|
+
export function typeScriptUnavailableReason(root) {
|
|
85
|
+
const { status, version } = describeTypeScript(root);
|
|
86
|
+
if (status === "ok") return null;
|
|
87
|
+
if (status === "missing") return "typescript não instalado (npm i -D typescript)";
|
|
88
|
+
return (
|
|
89
|
+
`typescript ${version ?? "?"} não expõe a API clássica do compilador — o 7 publica só ` +
|
|
90
|
+
"`typescript/unstable/*`, com outra forma. Pra usar os codemods, tenha o TypeScript 6 " +
|
|
91
|
+
"instalado; o resto do comando segue normal"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { readTsconfig, stripJsonc } from "./tsconfig.mjs";
|
|
7
|
+
import { describeTypeScript, loadTypeScript, typeScriptUnavailableReason } from "./typescript.mjs";
|
|
8
|
+
|
|
9
|
+
let root;
|
|
10
|
+
|
|
11
|
+
/** Write a file under the fixture, creating parent directories. */
|
|
12
|
+
function write(rel, text) {
|
|
13
|
+
const full = join(root, rel);
|
|
14
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
15
|
+
writeFileSync(full, text);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fake an installed `typescript` whose main module exports `exports`.
|
|
20
|
+
*
|
|
21
|
+
* TypeScript 7 is exactly this shape: the package resolves, the version is there,
|
|
22
|
+
* and the classic API is not — it moved to `typescript/unstable/*`.
|
|
23
|
+
*/
|
|
24
|
+
function fakeTypeScript(version, exports) {
|
|
25
|
+
write(
|
|
26
|
+
"node_modules/typescript/package.json",
|
|
27
|
+
JSON.stringify({ name: "typescript", version, main: "index.js" }),
|
|
28
|
+
);
|
|
29
|
+
write("node_modules/typescript/index.js", `module.exports = ${exports};`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
root = mkdtempSync(join(tmpdir(), "tempest-ts-"));
|
|
34
|
+
write("package.json", JSON.stringify({ name: "fixture" }));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
rmSync(root, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("loadTypeScript", () => {
|
|
42
|
+
it("returns null when typescript is not installed", () => {
|
|
43
|
+
expect(loadTypeScript(root)).toBeNull();
|
|
44
|
+
expect(describeTypeScript(root)).toEqual({ status: "missing", version: null });
|
|
45
|
+
expect(typeScriptUnavailableReason(root)).toContain("não instalado");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns null for an install without the classic compiler API", () => {
|
|
49
|
+
fakeTypeScript("7.0.2", '{ version: "7.0.2", versionMajorMinor: "7.0" }');
|
|
50
|
+
expect(loadTypeScript(root)).toBeNull();
|
|
51
|
+
expect(describeTypeScript(root)).toEqual({ status: "api-unavailable", version: "7.0.2" });
|
|
52
|
+
const reason = typeScriptUnavailableReason(root);
|
|
53
|
+
expect(reason).toContain("7.0.2");
|
|
54
|
+
expect(reason).toContain("unstable");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns the module when the classic API is there", () => {
|
|
58
|
+
fakeTypeScript(
|
|
59
|
+
"6.0.3",
|
|
60
|
+
"{ readConfigFile: () => ({}), createSourceFile: () => ({}), forEachChild: () => {} }",
|
|
61
|
+
);
|
|
62
|
+
expect(loadTypeScript(root)).not.toBeNull();
|
|
63
|
+
expect(describeTypeScript(root)).toEqual({ status: "ok", version: "6.0.3" });
|
|
64
|
+
expect(typeScriptUnavailableReason(root)).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("treats a partial API as unavailable rather than half-usable", () => {
|
|
68
|
+
fakeTypeScript("7.1.0", "{ createSourceFile: () => ({}) }");
|
|
69
|
+
expect(loadTypeScript(root)).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("stripJsonc", () => {
|
|
74
|
+
it("removes line and block comments", () => {
|
|
75
|
+
expect(stripJsonc('{ // hi\n "a": 1 /* there */ }')).toBe('{ \n "a": 1 }');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("removes a trailing comma before a close", () => {
|
|
79
|
+
expect(JSON.parse(stripJsonc('{ "a": [1, 2,], }'))).toEqual({ a: [1, 2] });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("leaves comment markers inside strings alone", () => {
|
|
83
|
+
expect(JSON.parse(stripJsonc('{ "url": "https://x.dev/a", "p": "a/*b" }'))).toEqual({
|
|
84
|
+
url: "https://x.dev/a",
|
|
85
|
+
p: "a/*b",
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("survives an unterminated comment instead of throwing", () => {
|
|
90
|
+
expect(() => stripJsonc('{ "a": 1 } /* forever')).not.toThrow();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("readTsconfig without the compiler API", () => {
|
|
95
|
+
it("still reads a tsconfig that has comments and a trailing comma", () => {
|
|
96
|
+
write(
|
|
97
|
+
"tsconfig.json",
|
|
98
|
+
'{\n // the app\n "compilerOptions": {\n "strict": true,\n "paths": { "@/*": ["./src/*"] },\n },\n}\n',
|
|
99
|
+
);
|
|
100
|
+
const config = readTsconfig({ root, ts: null });
|
|
101
|
+
expect(config.compilerOptions.strict).toBe(true);
|
|
102
|
+
expect(config.compilerOptions.paths["@/*"]).toEqual(["./src/*"]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("follows extends with the fallback parser", () => {
|
|
106
|
+
write("base.json", '{ "compilerOptions": { "jsx": "react-jsx" } } // base\n');
|
|
107
|
+
write(
|
|
108
|
+
"tsconfig.json",
|
|
109
|
+
'{ "extends": "./base.json", "compilerOptions": { "strict": true } }',
|
|
110
|
+
);
|
|
111
|
+
const config = readTsconfig({ root, ts: null });
|
|
112
|
+
expect(config.compilerOptions).toMatchObject({ jsx: "react-jsx", strict: true });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("ignores a `ts` that cannot read a config file", () => {
|
|
116
|
+
write("tsconfig.json", '{ "compilerOptions": { "strict": true } }');
|
|
117
|
+
const config = readTsconfig({ root, ts: { version: "7.0.2" } });
|
|
118
|
+
expect(config.compilerOptions.strict).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
});
|
package/bin/lib/css/extract.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
25
25
|
import { join, relative } from "node:path";
|
|
26
26
|
|
|
27
27
|
import { discoverAlias } from "../alias/discover.mjs";
|
|
28
|
-
import { loadTypeScript } from "../alias/typescript.mjs";
|
|
28
|
+
import { loadTypeScript, typeScriptUnavailableReason } from "../alias/typescript.mjs";
|
|
29
29
|
import { collectSources, indexClassUses, resolveSpecifier } from "./references.mjs";
|
|
30
30
|
import { parseCss } from "./parse.mjs";
|
|
31
31
|
import { declSignature } from "./semantic.mjs";
|
|
@@ -192,8 +192,7 @@ export function planExtraction({ analysis, root, target, prefix = "u-" }) {
|
|
|
192
192
|
if (!ts) {
|
|
193
193
|
return {
|
|
194
194
|
status: "no-typescript",
|
|
195
|
-
message:
|
|
196
|
-
"typescript não instalado — a reescrita do TSX precisa do compilador do projeto",
|
|
195
|
+
message: `a reescrita do TSX precisa do compilador do projeto: ${typeScriptUnavailableReason(root)}`,
|
|
197
196
|
groups: [],
|
|
198
197
|
refusals: [],
|
|
199
198
|
};
|
|
@@ -155,6 +155,50 @@ describe("tempest doctor — generic findings still surface", () => {
|
|
|
155
155
|
});
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
+
describe("tempest doctor — a project on TypeScript 7", () => {
|
|
159
|
+
/**
|
|
160
|
+
* TypeScript 7 is the native port: same package name, and no classic compiler
|
|
161
|
+
* API — it moved to `typescript/unstable/*`. Before this was detected, `doctor`
|
|
162
|
+
* died with `ts.readConfigFile is not a function` on any such project, which is
|
|
163
|
+
* the worst possible failure for the command people run first.
|
|
164
|
+
*/
|
|
165
|
+
beforeEach(() => {
|
|
166
|
+
thirdPartyApp();
|
|
167
|
+
write("node_modules/typescript/package.json", {
|
|
168
|
+
name: "typescript",
|
|
169
|
+
version: "7.0.2",
|
|
170
|
+
main: "index.js",
|
|
171
|
+
});
|
|
172
|
+
write(
|
|
173
|
+
"node_modules/typescript/index.js",
|
|
174
|
+
'module.exports = { version: "7.0.2", versionMajorMinor: "7.0" };',
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("does not crash, and still reports the tsconfig checks", () => {
|
|
179
|
+
const { out, code } = doctor();
|
|
180
|
+
expect(out).not.toContain("is not a function");
|
|
181
|
+
expect(out).toContain("moduleResolution: bundler");
|
|
182
|
+
expect(code).toBe(0);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("says the codemods are unavailable, and why", () => {
|
|
186
|
+
const { out } = doctor();
|
|
187
|
+
expect(out).toContain("no classic compiler API");
|
|
188
|
+
expect(out).toContain("every other pass runs");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("reads a tsconfig with comments through the fallback parser", () => {
|
|
192
|
+
writeFileSync(
|
|
193
|
+
join(root, "tsconfig.json"),
|
|
194
|
+
'{\n // vite default\n "compilerOptions": { "strict": true, "jsx": "react-jsx", "moduleResolution": "bundler" },\n}\n',
|
|
195
|
+
);
|
|
196
|
+
const { out } = doctor();
|
|
197
|
+
expect(out).toContain("strict mode on");
|
|
198
|
+
expect(out).not.toContain("strict mode off");
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
158
202
|
describe("tempest doctor — stylesheets", () => {
|
|
159
203
|
beforeEach(thirdPartyApp);
|
|
160
204
|
|
package/bin/tempest.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { mkdir, writeFile } from "node:fs/promises";
|
|
|
12
12
|
import { dirname, join, resolve } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { aliasImports } from "./lib/alias/index.mjs";
|
|
15
|
-
import { loadTypeScript } from "./lib/alias/typescript.mjs";
|
|
15
|
+
import { describeTypeScript, loadTypeScript } from "./lib/alias/typescript.mjs";
|
|
16
16
|
import { readTsconfig } from "./lib/alias/tsconfig.mjs";
|
|
17
17
|
import { analyzeCss, applyCssFixes, applyExtraction, planExtraction } from "./lib/css/index.mjs";
|
|
18
18
|
import { checkLucide } from "./lib/doctor/lucide.mjs";
|
|
@@ -535,6 +535,21 @@ function doctor() {
|
|
|
535
535
|
const tsc = readTsconfig({ root: ROOT, ts: loadTypeScript(ROOT) });
|
|
536
536
|
if (tsc) {
|
|
537
537
|
checks.push(["section", "TypeScript"]);
|
|
538
|
+
/*
|
|
539
|
+
* TypeScript 7 installs under the same package name and does not ship the
|
|
540
|
+
* classic compiler API — it lives behind `typescript/unstable/*` with a
|
|
541
|
+
* different shape. The tsconfig checks below keep working (they fall back to
|
|
542
|
+
* a JSONC-tolerant parse), but the codemods cannot run, and saying so here is
|
|
543
|
+
* better than each pass reporting it as if TypeScript were missing.
|
|
544
|
+
*/
|
|
545
|
+
const tsInstall = describeTypeScript(ROOT);
|
|
546
|
+
if (tsInstall.status === "api-unavailable") {
|
|
547
|
+
checks.push([
|
|
548
|
+
"info",
|
|
549
|
+
`typescript ${tsInstall.version} has no classic compiler API`,
|
|
550
|
+
"`tempest fix` skips the alias and --extract-css codemods — they need the TypeScript 6 API; every other pass runs",
|
|
551
|
+
]);
|
|
552
|
+
}
|
|
538
553
|
const co = tsc.compilerOptions;
|
|
539
554
|
// The `@/*` alias is the SDK's convention, not a health property: a project
|
|
540
555
|
// that has not adopted the SDK is not wrong for lacking it.
|
|
@@ -855,7 +870,7 @@ function parseFixArgs(args) {
|
|
|
855
870
|
function reportAlias(result, dryRun) {
|
|
856
871
|
if (result.status === "no-typescript") {
|
|
857
872
|
console.log(
|
|
858
|
-
`${c.yellow}!
|
|
873
|
+
`${c.yellow}! alias pass skipped${c.reset} ${c.dim}— ${result.reason ?? "typescript não instalado"}${c.reset}`,
|
|
859
874
|
);
|
|
860
875
|
return;
|
|
861
876
|
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("../EmptyState/EmptyState.cjs"),n=require("../VisuallyHidden/VisuallyHidden.cjs"),r=require("./chat-groups.cjs"),i=require("./Chat.module.cjs"),a=require("./ChatComposer.cjs");let o=require("react"),s=require("react/jsx-runtime");var c={sending:`◌`,sent:`✓`,read:`✓✓`,failed:`!`};function l({messages:l,currentUserId:u,onSend:d,onRetry:f,typing:p=[],renderAvatar:m,header:h,emptyState:g,groupWindowMs:_,locale:v=`pt-BR`,now:y,placeholder:b,composerActions:x,composerDisabled:S,onSendError:C,className:w,...T}){let E=r.chatStrings(v),D=r.groupMessages({messages:l,currentUserId:u,windowMs:_}),O=(0,o.useRef)(null),k=(0,o.useRef)(null),A=(0,o.useRef)(!0),j=()=>{let e=k.current;if(!e)return;let t=e.scrollHeight-e.scrollTop-e.clientHeight;A.current=t<48};(0,o.useLayoutEffect)(()=>{let e=k.current;!e||!A.current||(e.scrollTop=e.scrollHeight)},[l,p]),(0,o.useEffect)(()=>{let e=k.current;e&&(e.scrollTop=e.scrollHeight)},[]);let M=r.typingLabel(p,v);return(0,s.jsxs)(`div`,{className:e.cn(i.default.panel,w),...T,children:[h&&(0,s.jsx)(`header`,{className:i.default.header,children:h}),(0,s.jsx)(`div`,{ref:k,className:i.default.thread,onScroll:j,role:`log`,"aria-live":`polite`,"aria-relevant":`additions text`,tabIndex:0,"aria-label":E.thread,children:l.length===0?g??(0,s.jsx)(t.EmptyState,{title:E.empty}):D.map(t=>t.kind===`day`?(0,s.jsx)(`div`,{className:i.default.day,children:(0,s.jsx)(`span`,{className:i.default.dayLabel,children:r.dayLabel(t.date,{locale:v,now:y})})},t.key):(0,s.jsxs)(`div`,{className:e.cn(i.default.run,t.own&&i.default.ownRun),children:[m&&(0,s.jsx)(`div`,{className:i.default.avatar,"aria-hidden":`true`,children:m(t.messages[0])}),(0,s.jsxs)(`div`,{className:i.default.runBody,children:[(0,s.jsx)(`span`,{className:i.default.author,children:t.own?E.you:t.authorName??t.authorId}),(0,s.jsx)(`ul`,{className:i.default.bubbles,children:t.messages.map(a=>(0,s.jsxs)(`li`,{className:i.default.bubbleRow,children:[(0,s.jsxs)(`div`,{className:e.cn(i.default.bubble,t.own&&i.default.ownBubble,a.status===`failed`&&i.default.failedBubble),children:[(0,s.jsx)(`div`,{className:i.default.body,children:a.body}),(0,s.jsxs)(`div`,{className:i.default.meta,children:[(0,s.jsx)(`time`,{dateTime:new Date(a.sentAt).toISOString(),children:r.timeLabel(a.sentAt,v)}),t.own&&a.status&&(0,s.jsxs)(`span`,{className:e.cn(i.default.status,a.status===`failed`&&i.default.statusFailed),title:E[a.status],children:[(0,s.jsx)(`span`,{"aria-hidden":`true`,children:c[a.status]}),(0,s.jsx)(n.VisuallyHidden,{children:E[a.status]})]})]})]}),a.status===`failed`&&f&&(0,s.jsx)(`button`,{type:`button`,className:i.default.retry,onClick:()=>f(a),children:E.retry})]},a.id))})]})]},t.key))}),M&&(0,s.jsx)(`p`,{className:i.default.typing,"aria-live":`polite`,children:M}),d&&(0,s.jsx)(a.ChatComposer,{ref:O,onSend:d,locale:v,placeholder:b,actions:x,disabled:S,onError:C})]})}exports.Chat=l;
|
|
2
|
+
//# sourceMappingURL=Chat.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Chat.cjs","names":[],"sources":["../../../src/components/Chat/Chat.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport { ChatComposer, type ChatComposerHandle } from \"./ChatComposer\";\nimport {\n chatStrings,\n dayLabel,\n groupMessages,\n timeLabel,\n typingLabel,\n type ChatMessage,\n} from \"./chat-groups\";\nimport styles from \"./Chat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface ChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The thread, **oldest first**. Never reordered by the component. */\n messages: readonly ChatMessage[];\n /** Author id treated as \"own\" — decides side, colour and status ticks. */\n currentUserId?: string;\n /** Renders the composer when given. Receives the trimmed text. */\n onSend?: (text: string) => void | Promise<void>;\n /** Enables the retry control on a `\"failed\"` message. */\n onRetry?: (message: ChatMessage) => void;\n /** Names currently typing. One, two or a count is phrased for you. */\n typing?: readonly string[];\n /** Avatar for the first message of a run — an `<Avatar>`, an `<Icon>`. */\n renderAvatar?: (message: ChatMessage) => ReactNode;\n /** Rendered above the thread, inside the panel. */\n header?: ReactNode;\n /** Shown when there are no messages. */\n emptyState?: ReactNode;\n /** Gap that still keeps consecutive messages in one run. Default 5 min. */\n groupWindowMs?: number;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Reference instant for \"Hoje\"/\"Ontem\". Default: now, at render time. */\n now?: number;\n /** Placeholder for the composer. */\n placeholder?: string;\n /** Extra controls inside the composer, before the send button. */\n composerActions?: ReactNode;\n /** Disable the composer — no permission, thread archived, offline. */\n composerDisabled?: boolean;\n /** Called when `onSend` rejects. The draft stays in the field either way. */\n onSendError?: (error: unknown) => void;\n}\n\n/** Status glyph and label for an outgoing message. */\nconst STATUS_GLYPH: Record<NonNullable<ChatMessage[\"status\"]>, string> = {\n sending: \"◌\",\n sent: \"✓\",\n read: \"✓✓\",\n failed: \"!\",\n};\n\n/**\n * A message thread: grouped by author and by day, own messages on one side, with\n * delivery state, a typing indicator and an optional composer.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and\n * emits intent (`onSend`, `onRetry`). Where messages come from — REST, the SDK's\n * `createWebSocket`, an SSE stream — and how an optimistic insert is done stay with\n * the app, because those differ per backend and a component that assumed one would\n * be wrong for most.\n *\n * It works as a comment thread too: that is the same component with\n * `currentUserId` set and no `typing`.\n *\n * @example\n * <Chat\n * messages={messages}\n * currentUserId={me.id}\n * typing={typingNames}\n * onSend={(text) => send({ text })}\n * onRetry={(message) => resend(message.id)}\n * />\n */\nexport function Chat({\n messages,\n currentUserId,\n onSend,\n onRetry,\n typing = [],\n renderAvatar,\n header,\n emptyState,\n groupWindowMs,\n locale = \"pt-BR\",\n now,\n placeholder,\n composerActions,\n composerDisabled,\n onSendError,\n className,\n ...rest\n}: ChatProps) {\n const strings = chatStrings(locale);\n const sections = groupMessages({ messages, currentUserId, windowMs: groupWindowMs });\n const composer = useRef<ChatComposerHandle | null>(null);\n const thread = useRef<HTMLDivElement | null>(null);\n const stuckToBottom = useRef(true);\n\n /**\n * Remember whether the reader is at the bottom, before the next batch lands.\n *\n * A thread that always scrolls to the newest message yanks somebody out of the\n * history they were reading, every time anyone types. So the jump only happens\n * when they were already at the bottom — the same rule every chat app converges\n * on. The 48px slack covers a partially visible last row.\n */\n const trackPosition = (): void => {\n const node = thread.current;\n if (!node) return;\n const distance = node.scrollHeight - node.scrollTop - node.clientHeight;\n stuckToBottom.current = distance < 48;\n };\n\n useLayoutEffect(() => {\n const node = thread.current;\n if (!node || !stuckToBottom.current) return;\n node.scrollTop = node.scrollHeight;\n }, [messages, typing]);\n\n useEffect(() => {\n const node = thread.current;\n if (!node) return;\n node.scrollTop = node.scrollHeight;\n // Mount lands on the newest message; from then on `trackPosition` decides.\n }, []);\n\n const typingText = typingLabel(typing, locale);\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {header && <header className={styles.header}>{header}</header>}\n\n <div\n ref={thread}\n className={styles.thread}\n onScroll={trackPosition}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions text\"\n tabIndex={0}\n aria-label={strings.thread}\n >\n {messages.length === 0\n ? (emptyState ?? <EmptyState title={strings.empty} />)\n : sections.map((section) =>\n section.kind === \"day\" ? (\n <div key={section.key} className={styles.day}>\n <span className={styles.dayLabel}>\n {dayLabel(section.date, { locale, now })}\n </span>\n </div>\n ) : (\n <div\n key={section.key}\n className={cn(styles.run, section.own && styles.ownRun)}\n >\n {renderAvatar && (\n <div className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(section.messages[0])}\n </div>\n )}\n <div className={styles.runBody}>\n <span className={styles.author}>\n {section.own\n ? strings.you\n : (section.authorName ?? section.authorId)}\n </span>\n <ul className={styles.bubbles}>\n {section.messages.map((message) => (\n <li key={message.id} className={styles.bubbleRow}>\n <div\n className={cn(\n styles.bubble,\n section.own && styles.ownBubble,\n message.status === \"failed\" &&\n styles.failedBubble,\n )}\n >\n <div className={styles.body}>\n {message.body}\n </div>\n <div className={styles.meta}>\n <time\n dateTime={new Date(\n message.sentAt,\n ).toISOString()}\n >\n {timeLabel(message.sentAt, locale)}\n </time>\n {section.own && message.status && (\n <span\n className={cn(\n styles.status,\n message.status === \"failed\" &&\n styles.statusFailed,\n )}\n title={strings[message.status]}\n >\n <span aria-hidden=\"true\">\n {STATUS_GLYPH[message.status]}\n </span>\n <VisuallyHidden>\n {strings[message.status]}\n </VisuallyHidden>\n </span>\n )}\n </div>\n </div>\n {message.status === \"failed\" && onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </li>\n ))}\n </ul>\n </div>\n </div>\n ),\n )}\n </div>\n\n {typingText && (\n <p className={styles.typing} aria-live=\"polite\">\n {typingText}\n </p>\n )}\n\n {onSend && (\n <ChatComposer\n ref={composer}\n onSend={onSend}\n locale={locale}\n placeholder={placeholder}\n actions={composerActions}\n disabled={composerDisabled}\n onError={onSendError}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { cn as e } from "../../utils/cn.js";
|
|
2
|
+
import { EmptyState as t } from "../EmptyState/EmptyState.js";
|
|
3
|
+
import { VisuallyHidden as n } from "../VisuallyHidden/VisuallyHidden.js";
|
|
4
|
+
import { chatStrings as r, dayLabel as i, groupMessages as a, timeLabel as o, typingLabel as s } from "./chat-groups.js";
|
|
5
|
+
import c from "./Chat.module.js";
|
|
6
|
+
import { ChatComposer as l } from "./ChatComposer.js";
|
|
7
|
+
import { useEffect as u, useLayoutEffect as d, useRef as f } from "react";
|
|
8
|
+
import { jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
9
|
+
//#region src/components/Chat/Chat.tsx
|
|
10
|
+
var h = {
|
|
11
|
+
sending: "◌",
|
|
12
|
+
sent: "✓",
|
|
13
|
+
read: "✓✓",
|
|
14
|
+
failed: "!"
|
|
15
|
+
};
|
|
16
|
+
function g({ messages: g, currentUserId: _, onSend: v, onRetry: y, typing: b = [], renderAvatar: x, header: S, emptyState: C, groupWindowMs: w, locale: T = "pt-BR", now: E, placeholder: D, composerActions: O, composerDisabled: k, onSendError: A, className: j, ...M }) {
|
|
17
|
+
let N = r(T), P = a({
|
|
18
|
+
messages: g,
|
|
19
|
+
currentUserId: _,
|
|
20
|
+
windowMs: w
|
|
21
|
+
}), F = f(null), I = f(null), L = f(!0), R = () => {
|
|
22
|
+
let e = I.current;
|
|
23
|
+
if (!e) return;
|
|
24
|
+
let t = e.scrollHeight - e.scrollTop - e.clientHeight;
|
|
25
|
+
L.current = t < 48;
|
|
26
|
+
};
|
|
27
|
+
d(() => {
|
|
28
|
+
let e = I.current;
|
|
29
|
+
!e || !L.current || (e.scrollTop = e.scrollHeight);
|
|
30
|
+
}, [g, b]), u(() => {
|
|
31
|
+
let e = I.current;
|
|
32
|
+
e && (e.scrollTop = e.scrollHeight);
|
|
33
|
+
}, []);
|
|
34
|
+
let z = s(b, T);
|
|
35
|
+
return /* @__PURE__ */ m("div", {
|
|
36
|
+
className: e(c.panel, j),
|
|
37
|
+
...M,
|
|
38
|
+
children: [
|
|
39
|
+
S && /* @__PURE__ */ p("header", {
|
|
40
|
+
className: c.header,
|
|
41
|
+
children: S
|
|
42
|
+
}),
|
|
43
|
+
/* @__PURE__ */ p("div", {
|
|
44
|
+
ref: I,
|
|
45
|
+
className: c.thread,
|
|
46
|
+
onScroll: R,
|
|
47
|
+
role: "log",
|
|
48
|
+
"aria-live": "polite",
|
|
49
|
+
"aria-relevant": "additions text",
|
|
50
|
+
tabIndex: 0,
|
|
51
|
+
"aria-label": N.thread,
|
|
52
|
+
children: g.length === 0 ? C ?? /* @__PURE__ */ p(t, { title: N.empty }) : P.map((t) => t.kind === "day" ? /* @__PURE__ */ p("div", {
|
|
53
|
+
className: c.day,
|
|
54
|
+
children: /* @__PURE__ */ p("span", {
|
|
55
|
+
className: c.dayLabel,
|
|
56
|
+
children: i(t.date, {
|
|
57
|
+
locale: T,
|
|
58
|
+
now: E
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
}, t.key) : /* @__PURE__ */ m("div", {
|
|
62
|
+
className: e(c.run, t.own && c.ownRun),
|
|
63
|
+
children: [x && /* @__PURE__ */ p("div", {
|
|
64
|
+
className: c.avatar,
|
|
65
|
+
"aria-hidden": "true",
|
|
66
|
+
children: x(t.messages[0])
|
|
67
|
+
}), /* @__PURE__ */ m("div", {
|
|
68
|
+
className: c.runBody,
|
|
69
|
+
children: [/* @__PURE__ */ p("span", {
|
|
70
|
+
className: c.author,
|
|
71
|
+
children: t.own ? N.you : t.authorName ?? t.authorId
|
|
72
|
+
}), /* @__PURE__ */ p("ul", {
|
|
73
|
+
className: c.bubbles,
|
|
74
|
+
children: t.messages.map((r) => /* @__PURE__ */ m("li", {
|
|
75
|
+
className: c.bubbleRow,
|
|
76
|
+
children: [/* @__PURE__ */ m("div", {
|
|
77
|
+
className: e(c.bubble, t.own && c.ownBubble, r.status === "failed" && c.failedBubble),
|
|
78
|
+
children: [/* @__PURE__ */ p("div", {
|
|
79
|
+
className: c.body,
|
|
80
|
+
children: r.body
|
|
81
|
+
}), /* @__PURE__ */ m("div", {
|
|
82
|
+
className: c.meta,
|
|
83
|
+
children: [/* @__PURE__ */ p("time", {
|
|
84
|
+
dateTime: new Date(r.sentAt).toISOString(),
|
|
85
|
+
children: o(r.sentAt, T)
|
|
86
|
+
}), t.own && r.status && /* @__PURE__ */ m("span", {
|
|
87
|
+
className: e(c.status, r.status === "failed" && c.statusFailed),
|
|
88
|
+
title: N[r.status],
|
|
89
|
+
children: [/* @__PURE__ */ p("span", {
|
|
90
|
+
"aria-hidden": "true",
|
|
91
|
+
children: h[r.status]
|
|
92
|
+
}), /* @__PURE__ */ p(n, { children: N[r.status] })]
|
|
93
|
+
})]
|
|
94
|
+
})]
|
|
95
|
+
}), r.status === "failed" && y && /* @__PURE__ */ p("button", {
|
|
96
|
+
type: "button",
|
|
97
|
+
className: c.retry,
|
|
98
|
+
onClick: () => y(r),
|
|
99
|
+
children: N.retry
|
|
100
|
+
})]
|
|
101
|
+
}, r.id))
|
|
102
|
+
})]
|
|
103
|
+
})]
|
|
104
|
+
}, t.key))
|
|
105
|
+
}),
|
|
106
|
+
z && /* @__PURE__ */ p("p", {
|
|
107
|
+
className: c.typing,
|
|
108
|
+
"aria-live": "polite",
|
|
109
|
+
children: z
|
|
110
|
+
}),
|
|
111
|
+
v && /* @__PURE__ */ p(l, {
|
|
112
|
+
ref: F,
|
|
113
|
+
onSend: v,
|
|
114
|
+
locale: T,
|
|
115
|
+
placeholder: D,
|
|
116
|
+
actions: O,
|
|
117
|
+
disabled: k,
|
|
118
|
+
onError: A
|
|
119
|
+
})
|
|
120
|
+
]
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
export { g as Chat };
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=Chat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Chat.js","names":[],"sources":["../../../src/components/Chat/Chat.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport { ChatComposer, type ChatComposerHandle } from \"./ChatComposer\";\nimport {\n chatStrings,\n dayLabel,\n groupMessages,\n timeLabel,\n typingLabel,\n type ChatMessage,\n} from \"./chat-groups\";\nimport styles from \"./Chat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface ChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The thread, **oldest first**. Never reordered by the component. */\n messages: readonly ChatMessage[];\n /** Author id treated as \"own\" — decides side, colour and status ticks. */\n currentUserId?: string;\n /** Renders the composer when given. Receives the trimmed text. */\n onSend?: (text: string) => void | Promise<void>;\n /** Enables the retry control on a `\"failed\"` message. */\n onRetry?: (message: ChatMessage) => void;\n /** Names currently typing. One, two or a count is phrased for you. */\n typing?: readonly string[];\n /** Avatar for the first message of a run — an `<Avatar>`, an `<Icon>`. */\n renderAvatar?: (message: ChatMessage) => ReactNode;\n /** Rendered above the thread, inside the panel. */\n header?: ReactNode;\n /** Shown when there are no messages. */\n emptyState?: ReactNode;\n /** Gap that still keeps consecutive messages in one run. Default 5 min. */\n groupWindowMs?: number;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Reference instant for \"Hoje\"/\"Ontem\". Default: now, at render time. */\n now?: number;\n /** Placeholder for the composer. */\n placeholder?: string;\n /** Extra controls inside the composer, before the send button. */\n composerActions?: ReactNode;\n /** Disable the composer — no permission, thread archived, offline. */\n composerDisabled?: boolean;\n /** Called when `onSend` rejects. The draft stays in the field either way. */\n onSendError?: (error: unknown) => void;\n}\n\n/** Status glyph and label for an outgoing message. */\nconst STATUS_GLYPH: Record<NonNullable<ChatMessage[\"status\"]>, string> = {\n sending: \"◌\",\n sent: \"✓\",\n read: \"✓✓\",\n failed: \"!\",\n};\n\n/**\n * A message thread: grouped by author and by day, own messages on one side, with\n * delivery state, a typing indicator and an optional composer.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and\n * emits intent (`onSend`, `onRetry`). Where messages come from — REST, the SDK's\n * `createWebSocket`, an SSE stream — and how an optimistic insert is done stay with\n * the app, because those differ per backend and a component that assumed one would\n * be wrong for most.\n *\n * It works as a comment thread too: that is the same component with\n * `currentUserId` set and no `typing`.\n *\n * @example\n * <Chat\n * messages={messages}\n * currentUserId={me.id}\n * typing={typingNames}\n * onSend={(text) => send({ text })}\n * onRetry={(message) => resend(message.id)}\n * />\n */\nexport function Chat({\n messages,\n currentUserId,\n onSend,\n onRetry,\n typing = [],\n renderAvatar,\n header,\n emptyState,\n groupWindowMs,\n locale = \"pt-BR\",\n now,\n placeholder,\n composerActions,\n composerDisabled,\n onSendError,\n className,\n ...rest\n}: ChatProps) {\n const strings = chatStrings(locale);\n const sections = groupMessages({ messages, currentUserId, windowMs: groupWindowMs });\n const composer = useRef<ChatComposerHandle | null>(null);\n const thread = useRef<HTMLDivElement | null>(null);\n const stuckToBottom = useRef(true);\n\n /**\n * Remember whether the reader is at the bottom, before the next batch lands.\n *\n * A thread that always scrolls to the newest message yanks somebody out of the\n * history they were reading, every time anyone types. So the jump only happens\n * when they were already at the bottom — the same rule every chat app converges\n * on. The 48px slack covers a partially visible last row.\n */\n const trackPosition = (): void => {\n const node = thread.current;\n if (!node) return;\n const distance = node.scrollHeight - node.scrollTop - node.clientHeight;\n stuckToBottom.current = distance < 48;\n };\n\n useLayoutEffect(() => {\n const node = thread.current;\n if (!node || !stuckToBottom.current) return;\n node.scrollTop = node.scrollHeight;\n }, [messages, typing]);\n\n useEffect(() => {\n const node = thread.current;\n if (!node) return;\n node.scrollTop = node.scrollHeight;\n // Mount lands on the newest message; from then on `trackPosition` decides.\n }, []);\n\n const typingText = typingLabel(typing, locale);\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {header && <header className={styles.header}>{header}</header>}\n\n <div\n ref={thread}\n className={styles.thread}\n onScroll={trackPosition}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions text\"\n tabIndex={0}\n aria-label={strings.thread}\n >\n {messages.length === 0\n ? (emptyState ?? <EmptyState title={strings.empty} />)\n : sections.map((section) =>\n section.kind === \"day\" ? (\n <div key={section.key} className={styles.day}>\n <span className={styles.dayLabel}>\n {dayLabel(section.date, { locale, now })}\n </span>\n </div>\n ) : (\n <div\n key={section.key}\n className={cn(styles.run, section.own && styles.ownRun)}\n >\n {renderAvatar && (\n <div className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(section.messages[0])}\n </div>\n )}\n <div className={styles.runBody}>\n <span className={styles.author}>\n {section.own\n ? strings.you\n : (section.authorName ?? section.authorId)}\n </span>\n <ul className={styles.bubbles}>\n {section.messages.map((message) => (\n <li key={message.id} className={styles.bubbleRow}>\n <div\n className={cn(\n styles.bubble,\n section.own && styles.ownBubble,\n message.status === \"failed\" &&\n styles.failedBubble,\n )}\n >\n <div className={styles.body}>\n {message.body}\n </div>\n <div className={styles.meta}>\n <time\n dateTime={new Date(\n message.sentAt,\n ).toISOString()}\n >\n {timeLabel(message.sentAt, locale)}\n </time>\n {section.own && message.status && (\n <span\n className={cn(\n styles.status,\n message.status === \"failed\" &&\n styles.statusFailed,\n )}\n title={strings[message.status]}\n >\n <span aria-hidden=\"true\">\n {STATUS_GLYPH[message.status]}\n </span>\n <VisuallyHidden>\n {strings[message.status]}\n </VisuallyHidden>\n </span>\n )}\n </div>\n </div>\n {message.status === \"failed\" && onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </li>\n ))}\n </ul>\n </div>\n </div>\n ),\n )}\n </div>\n\n {typingText && (\n <p className={styles.typing} aria-live=\"polite\">\n {typingText}\n </p>\n )}\n\n {onSend && (\n <ChatComposer\n ref={composer}\n onSend={onSend}\n locale={locale}\n placeholder={placeholder}\n actions={composerActions}\n disabled={composerDisabled}\n onError={onSendError}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e=`tempest_panel_xUVw0`,t=`tempest_header_muoPu`,n=`tempest_thread_-j8yS`,r=`tempest_day_uLsWA`,i=`tempest_dayLabel_m8rDy`,a=`tempest_run_jOUv7`,o=`tempest_ownRun_3XTG5`,s=`tempest_avatar_d9SYT`,c=`tempest_runBody_nHQC3`,l=`tempest_author_4MRkd`,u=`tempest_bubbles_VYaQ0`,d=`tempest_bubbleRow_QSc41`,f=`tempest_bubble_GGFcU`,p=`tempest_ownBubble_agOMW`,m=`tempest_failedBubble_kT3F5`,h=`tempest_body_1aH2F`,g=`tempest_meta_3fNx8`,_=`tempest_status_EeiUt`,v=`tempest_statusFailed_Bfro1`,y=`tempest_retry_DAbIj`,b=`tempest_typing_zdp3O`,x=`tempest_composer_krOGl`,S=`tempest_field_fBY-q`,C=`tempest_composerActions_MYwQx`,w=`tempest_send_NwcaR`,T={panel:e,header:t,thread:n,day:r,dayLabel:i,run:a,ownRun:o,avatar:s,runBody:c,author:l,bubbles:u,bubbleRow:d,bubble:f,ownBubble:p,failedBubble:m,body:h,meta:g,status:_,statusFailed:v,retry:y,typing:b,composer:x,field:S,composerActions:C,send:w};exports.author=l,exports.avatar=s,exports.body=h,exports.bubble=f,exports.bubbleRow=d,exports.bubbles=u,exports.composer=x,exports.composerActions=C,exports.day=r,exports.dayLabel=i,exports.default=T,exports.failedBubble=m,exports.field=S,exports.header=t,exports.meta=g,exports.ownBubble=p,exports.ownRun=o,exports.panel=e,exports.retry=y,exports.run=a,exports.runBody=c,exports.send=w,exports.status=_,exports.statusFailed=v,exports.thread=n,exports.typing=b;
|
|
2
|
+
//# sourceMappingURL=Chat.module.cjs.map
|