From 4dc1c2f86a78d62c74a9acd2101e29b1c7644fd6 Mon Sep 17 00:00:00 2001 From: murashit Date: Wed, 27 May 2026 19:37:11 +0900 Subject: [PATCH] Add React infrastructure --- esbuild.config.mjs | 1 + eslint.config.mjs | 4 +- package-lock.json | 60 +++++++++++++++++++ package.json | 6 ++ src/shared/ui/react-components.tsx | 39 ++++++++++++ src/shared/ui/react-root.tsx | 55 +++++++++++++++++ tests/features/chat/ui/react-root.test.tsx | 28 +++++++++ tests/features/chat/ui/react-test-helpers.tsx | 24 ++++++++ tsconfig.json | 3 +- 9 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 src/shared/ui/react-components.tsx create mode 100644 src/shared/ui/react-root.tsx create mode 100644 tests/features/chat/ui/react-root.test.tsx create mode 100644 tests/features/chat/ui/react-test-helpers.tsx diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 336f191b..9c080204 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -10,6 +10,7 @@ const context = await esbuild.context({ format: "cjs", platform: "node", target: "es2022", + jsx: "automatic", outfile: "main.js", sourcemap: production ? false : "inline", minify: production, diff --git a/eslint.config.mjs b/eslint.config.mjs index d90f2a80..e0c65078 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,7 +4,7 @@ import eslintConfigPrettier from "eslint-config-prettier/flat"; import obsidianmd from "eslint-plugin-obsidianmd"; import tseslint from "typescript-eslint"; -const typeScriptFiles = ["src/**/*.ts", "tests/**/*.ts"]; +const typeScriptFiles = ["src/**/*.{ts,tsx}", "tests/**/*.{ts,tsx}"]; const nodeJavaScriptFiles = ["*.mjs", "scripts/**/*.mjs"]; const typeScriptConfigFiles = ["*.config.ts"]; const lintedTypeScriptFiles = [...typeScriptFiles, ...typeScriptConfigFiles]; @@ -66,7 +66,7 @@ export default defineConfig([ }, }, { - files: ["tests/**/*.ts"], + files: ["tests/**/*.{ts,tsx}"], rules: { "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-call": "off", diff --git a/package-lock.json b/package-lock.json index 0d4e709f..1b7de7aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,15 @@ "name": "codex-panel", "version": "2.2.2", "license": "Apache-2.0", + "dependencies": { + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, "devDependencies": { "@eslint/js": "^9.39.4", "@types/node": "^25.6.0", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", "esbuild": "^0.28.0", "eslint": "^10.3.0", "eslint-config-prettier": "^10.1.8", @@ -1707,6 +1713,26 @@ "undici-types": ">=7.24.0 <7.24.7" } }, + "node_modules/@types/react": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, "node_modules/@types/tern": { "version": "0.23.9", "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", @@ -2630,6 +2656,13 @@ "node": ">=4" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -6417,6 +6450,27 @@ ], "license": "MIT" }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6710,6 +6764,12 @@ "node": ">=v12.22.7" } }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", diff --git a/package.json b/package.json index f86030e2..9fdea770 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,8 @@ "devDependencies": { "@eslint/js": "^9.39.4", "@types/node": "^25.6.0", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", "esbuild": "^0.28.0", "eslint": "^10.3.0", "eslint-config-prettier": "^10.1.8", @@ -47,5 +49,9 @@ "typescript": "^6.0.3", "typescript-eslint": "^8.59.4", "vitest": "^4.1.7" + }, + "dependencies": { + "react": "^19.2.6", + "react-dom": "^19.2.6" } } diff --git a/src/shared/ui/react-components.tsx b/src/shared/ui/react-components.tsx new file mode 100644 index 00000000..af45a91d --- /dev/null +++ b/src/shared/ui/react-components.tsx @@ -0,0 +1,39 @@ +import { setIcon } from "obsidian"; +import { useLayoutEffect, useRef, type ButtonHTMLAttributes, type ReactNode } from "react"; + +export interface ObsidianIconProps { + icon: string; + className?: string; +} + +export function ObsidianIcon({ icon, className }: ObsidianIconProps): ReactNode { + const ref = useRef(null); + useLayoutEffect(() => { + const element = ref.current; + if (!element) return; + element.replaceChildren(); + setIcon(element, icon); + }, [icon]); + return