From ebf48f89bc42677eae1a71cfb40e92ea53bb14a8 Mon Sep 17 00:00:00 2001 From: soryu Date: Mon, 27 Apr 2026 17:21:45 +0100 Subject: feat: soryu-co/soryu - makima: Create custom Lexical step diagram block --- frontend/package-lock.json | 386 +++++++++++++++++++++ frontend/package.json | 9 + .../src/components/document/AutoSavePlugin.tsx | 140 ++++++++ .../src/components/document/DocumentEditor.css | 246 +++++++++++++ .../src/components/document/DocumentEditor.tsx | 135 ++++++- frontend/src/components/document/EditorTheme.ts | 30 ++ .../src/components/document/nodes/StepsDiagram.css | 360 +++++++++++++++++++ .../document/nodes/StepsDiagramComponent.tsx | 180 ++++++++++ .../components/document/nodes/StepsDiagramNode.tsx | 91 +++++ frontend/tsconfig.tsbuildinfo | 2 +- 10 files changed, 1569 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/document/AutoSavePlugin.tsx create mode 100644 frontend/src/components/document/DocumentEditor.css create mode 100644 frontend/src/components/document/EditorTheme.ts create mode 100644 frontend/src/components/document/nodes/StepsDiagram.css create mode 100644 frontend/src/components/document/nodes/StepsDiagramComponent.tsx create mode 100644 frontend/src/components/document/nodes/StepsDiagramNode.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2114b2c..4c4bcbc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,8 +8,17 @@ "name": "pc98-vn", "version": "0.0.1", "dependencies": { + "@lexical/html": "^0.44.0", + "@lexical/link": "^0.44.0", + "@lexical/list": "^0.44.0", + "@lexical/markdown": "^0.44.0", + "@lexical/react": "^0.44.0", + "@lexical/rich-text": "^0.44.0", + "@lexical/selection": "^0.44.0", + "@lexical/utils": "^0.44.0", "@nanostores/react": "^1.0.0", "@types/three": "^0.180.0", + "lexical": "^0.44.0", "nanostores": "^1.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -674,6 +683,54 @@ "node": ">=12" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.19", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz", + "integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==", + "dependencies": { + "@floating-ui/react-dom": "^2.1.8", + "@floating-ui/utils": "^0.2.11", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==" + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -709,6 +766,255 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lexical/clipboard": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.44.0.tgz", + "integrity": "sha512-nfmNIs7uENqlDI7cm2E4I1Yp8mDJGMhEQIrIV2rNWnL1oeHVXQ7yuYdyoPdcY1zuj/9nvkYBQYUEh0QiGwpETA==", + "dependencies": { + "@lexical/extension": "0.44.0", + "@lexical/html": "0.44.0", + "@lexical/list": "0.44.0", + "@lexical/selection": "0.44.0", + "@lexical/utils": "0.44.0", + "@types/trusted-types": "^2.0.7", + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/code-core": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/code-core/-/code-core-0.44.0.tgz", + "integrity": "sha512-m57JyXTIvW1tsqw/Vuogk8jqWCZZIeFQbWybRc46ytR8ReDgzPRODpN8+dacIIeRH5yC5UC3lAa743mtdNkxqg==", + "dependencies": { + "@lexical/extension": "0.44.0", + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/devtools-core": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.44.0.tgz", + "integrity": "sha512-X3uNG3P1vOsdzmEcy+7m9DxAcIVtVUZnvskmLqqLs6VluVVwH9xy7h1bPsvlDKvj1Nj73tWJ3TW0qXQWDTo5tw==", + "dependencies": { + "@lexical/html": "0.44.0", + "@lexical/link": "0.44.0", + "@lexical/mark": "0.44.0", + "@lexical/table": "0.44.0", + "@lexical/utils": "0.44.0", + "lexical": "0.44.0" + }, + "peerDependencies": { + "react": ">=17.x", + "react-dom": ">=17.x" + } + }, + "node_modules/@lexical/dragon": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.44.0.tgz", + "integrity": "sha512-RhlsjVDket9k1+YFEkDE0/7Qyrh2BI0vxBMzrWwPJTXX/4YFanYN9su8RSabkIukBBJ3QiNOOoC8FKK4Lkr4qg==", + "dependencies": { + "@lexical/extension": "0.44.0", + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/extension": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/extension/-/extension-0.44.0.tgz", + "integrity": "sha512-BsYtoc+0EU0pqcOpf/lIUDU6LQVO6zX2AawZoUWJzT3Wzfov23qsqZWvl2WGM9dnRTN5iISJL3Fl53bQVxiXxw==", + "dependencies": { + "@lexical/utils": "0.44.0", + "@preact/signals-core": "^1.14.1", + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/hashtag": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.44.0.tgz", + "integrity": "sha512-0WATahDSqYKVTudQv3KpFbLeCpmrCpRptPFbjxOMckAX2MRpYlrExlqKfgfpri5BSQPtG49EPSGeNfSx/Faavw==", + "dependencies": { + "@lexical/text": "0.44.0", + "@lexical/utils": "0.44.0", + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/history": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.44.0.tgz", + "integrity": "sha512-RGXcbFTgYL1GIWaReBI26mNSsJTfiA9EAtDY4LBeZ14NrIQhYNokKgNiOxq5Bn8xXrl2+mawQEqoMfgpWp/5YA==", + "dependencies": { + "@lexical/extension": "0.44.0", + "@lexical/utils": "0.44.0", + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/html": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.44.0.tgz", + "integrity": "sha512-5X6eGsgwtqPxABsuShUxF7ZfyB/U4GwSEyeonvwH1Vc/5Q2uQVjlB+FAYd+MNwWMHMh4d4+yZ3l70AtIuhr5eg==", + "dependencies": { + "@lexical/extension": "0.44.0", + "@lexical/selection": "0.44.0", + "@lexical/utils": "0.44.0", + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/link": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.44.0.tgz", + "integrity": "sha512-uvEqEol/mLEzGVQd8Rok9I48RgYPKokM/nsclI9nYcEdccVOM2Nri4ntoRwodhbccFLtjMPl8OBldwXbfc77tQ==", + "dependencies": { + "@lexical/extension": "0.44.0", + "@lexical/utils": "0.44.0", + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/list": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.44.0.tgz", + "integrity": "sha512-ZTCWxDz1okPrC9FBXi1yV3W5fbQQeMUlFIcSVF9HibcVPmCsPa900IxthuiQbGiTycUyXDTOB3IUYRtlJNtpjw==", + "dependencies": { + "@lexical/extension": "0.44.0", + "@lexical/utils": "0.44.0", + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/mark": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.44.0.tgz", + "integrity": "sha512-bWMowllwe6BcgYMAkrsZx6Z+CX/72qCQpFKhlkR4ael92yOWSBkz68xp1wxxkSnQX9zoI1gYTeWBofVsSDKcsQ==", + "dependencies": { + "@lexical/utils": "0.44.0", + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/markdown": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.44.0.tgz", + "integrity": "sha512-DwlXdp85pYMo3exDF6W3iz8plpuP+RQ4Me4Iljm7O5aPDp0SSrIoZxyX4zS668mVAoz5HHj1Ka0kQkft8mq26Q==", + "dependencies": { + "@lexical/code-core": "0.44.0", + "@lexical/link": "0.44.0", + "@lexical/list": "0.44.0", + "@lexical/rich-text": "0.44.0", + "@lexical/text": "0.44.0", + "@lexical/utils": "0.44.0", + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/overflow": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.44.0.tgz", + "integrity": "sha512-5GYaYjSxn27pqHRfU+tQ2STF10wgJvI+MUnwTnUFSzy3dko1b+oV94K/Yx0TuEewPbwDibfoFA8CwqUvOLHAyw==", + "dependencies": { + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/plain-text": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.44.0.tgz", + "integrity": "sha512-bIV4Lljk0x70zFhkZIwzSPK5q3m9FpDisjGm2/3Q/chb+5BW3Tv8QJmqnpCiSO6S2KXO7gfSy81ZfkQ1dcd4EQ==", + "dependencies": { + "@lexical/clipboard": "0.44.0", + "@lexical/dragon": "0.44.0", + "@lexical/selection": "0.44.0", + "@lexical/utils": "0.44.0", + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/react": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.44.0.tgz", + "integrity": "sha512-p/NQd/fMh3pXb1XqegE2ruvWDcUmfB12OidQ9nwtMtj5VfcUjQu2I+trUhgGRIADxSYxMWmw+8PPj5YSf4m5oA==", + "dependencies": { + "@floating-ui/react": "^0.27.16", + "@lexical/devtools-core": "0.44.0", + "@lexical/dragon": "0.44.0", + "@lexical/extension": "0.44.0", + "@lexical/hashtag": "0.44.0", + "@lexical/history": "0.44.0", + "@lexical/link": "0.44.0", + "@lexical/list": "0.44.0", + "@lexical/mark": "0.44.0", + "@lexical/markdown": "0.44.0", + "@lexical/overflow": "0.44.0", + "@lexical/plain-text": "0.44.0", + "@lexical/rich-text": "0.44.0", + "@lexical/table": "0.44.0", + "@lexical/text": "0.44.0", + "@lexical/utils": "0.44.0", + "@lexical/yjs": "0.44.0", + "lexical": "0.44.0", + "react-error-boundary": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.x", + "react-dom": ">=17.x", + "yjs": ">=13.5.22" + }, + "peerDependenciesMeta": { + "yjs": { + "optional": true + } + } + }, + "node_modules/@lexical/rich-text": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.44.0.tgz", + "integrity": "sha512-IIdrutK5GY47ITjPlZB7KzUi9dBDwygsyFOwolnrYSL7m6TtGhAqrYiFg/YNOTT/nBzK3KQeCJRbnxpjJAVZtQ==", + "dependencies": { + "@lexical/clipboard": "0.44.0", + "@lexical/dragon": "0.44.0", + "@lexical/selection": "0.44.0", + "@lexical/utils": "0.44.0", + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/selection": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.44.0.tgz", + "integrity": "sha512-AEyeZJFFr5YRLeqVR+X0QAW19c4Fk4MFAQu52z2gxAyDGTj9xwVJxjfepVpfUp4P9K+sPtJ/yaqfMXH506ksSQ==", + "dependencies": { + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/table": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.44.0.tgz", + "integrity": "sha512-5Uq0O/fBCxcZp9y17fXUONY7dU9lVo/mB5JHy23laIiKzBKP5IzzTLMU9ikZTppIXbMNxYXd+R2pmy7PYTLyvw==", + "dependencies": { + "@lexical/clipboard": "0.44.0", + "@lexical/extension": "0.44.0", + "@lexical/utils": "0.44.0", + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/text": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.44.0.tgz", + "integrity": "sha512-1XJD8ZbwaXljTl8k4+jjiopdhnYZm26IJw9Gv8+cIThVC0b6B3JZ/WxH97BMDcSloKvWHFkGiPztxRwNwA29Rw==", + "dependencies": { + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/utils": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.44.0.tgz", + "integrity": "sha512-/D2ptztNevfBJgtkj4uaiYBeRcvSy+1mQj6pNYaCFZIoPJIwl6H5fXwWAvpvr11vcQKP9DEEoXR+V4qkMOA+EA==", + "dependencies": { + "@lexical/selection": "0.44.0", + "lexical": "0.44.0" + } + }, + "node_modules/@lexical/yjs": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.44.0.tgz", + "integrity": "sha512-b3QTub9J/3LuwSSdooynb6GbMHBRyBT4xUbXzXqNPbDHgYe6CDrqf/uJIHRihIjAhOnPaHYqo9XUzitl++N1DQ==", + "dependencies": { + "@lexical/selection": "0.44.0", + "lexical": "0.44.0" + }, + "peerDependencies": { + "yjs": ">=13.5.22" + } + }, "node_modules/@nanostores/react": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@nanostores/react/-/react-1.0.0.tgz", @@ -727,6 +1033,15 @@ "react": ">=18.0.0" } }, + "node_modules/@preact/signals-core": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.1.tgz", + "integrity": "sha512-vxPpfXqrwUe9lpjqfYNjAF/0RF/eFGeLgdJzdmIIZjpOnTmGmAB4BjWone562mJGMRP4frU6iZ6ei3PDsu52Ng==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/@remix-run/router": { "version": "1.23.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", @@ -1107,6 +1422,11 @@ "meshoptimizer": "~0.22.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" + }, "node_modules/@types/webxr": { "version": "0.5.23", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.23.tgz", @@ -1299,6 +1619,16 @@ "node": ">=6.9.0" } }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "peer": true, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1328,6 +1658,32 @@ "node": ">=6" } }, + "node_modules/lexical": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.44.0.tgz", + "integrity": "sha512-ReDUjRlFgkGoPWzvdjr7s16PUVpHATN+2NH2NiZs+PLlISTaIFFgKil2P467oP3Vg+XgmpDsUgmWZsFJTztYjg==" + }, + "node_modules/lib0": { + "version": "0.2.117", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", + "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", + "peer": true, + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1454,6 +1810,14 @@ "react": "^18.3.1" } }, + "node_modules/react-error-boundary": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.1.1.tgz", + "integrity": "sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -1560,6 +1924,11 @@ "node": ">=0.10.0" } }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==" + }, "node_modules/three": { "version": "0.180.0", "resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz", @@ -1678,6 +2047,23 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true + }, + "node_modules/yjs": { + "version": "13.6.30", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.30.tgz", + "integrity": "sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==", + "peer": true, + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 197c3d8..230a982 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,8 +9,17 @@ "preview": "vite preview" }, "dependencies": { + "@lexical/html": "^0.44.0", + "@lexical/link": "^0.44.0", + "@lexical/list": "^0.44.0", + "@lexical/markdown": "^0.44.0", + "@lexical/react": "^0.44.0", + "@lexical/rich-text": "^0.44.0", + "@lexical/selection": "^0.44.0", + "@lexical/utils": "^0.44.0", "@nanostores/react": "^1.0.0", "@types/three": "^0.180.0", + "lexical": "^0.44.0", "nanostores": "^1.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/frontend/src/components/document/AutoSavePlugin.tsx b/frontend/src/components/document/AutoSavePlugin.tsx new file mode 100644 index 0000000..d3d0eb5 --- /dev/null +++ b/frontend/src/components/document/AutoSavePlugin.tsx @@ -0,0 +1,140 @@ +import { useEffect, useRef, useState, useCallback } from 'react'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { UNDO_COMMAND } from 'lexical'; + +const COUNTDOWN_DURATION_MS = 3000; +const TICK_INTERVAL_MS = 50; + +interface AutoSavePluginProps { + onAutoSave: (content: string) => void; + getContent: () => string; + enabled?: boolean; +} + +export default function AutoSavePlugin({ + onAutoSave, + getContent, + enabled = true, +}: AutoSavePluginProps) { + const [editor] = useLexicalComposerContext(); + const [countdown, setCountdown] = useState(null); + const timerRef = useRef | null>(null); + const startTimeRef = useRef(0); + const pendingContentRef = useRef(''); + const lastSavedContentRef = useRef(''); + + const clearTimer = useCallback(() => { + if (timerRef.current !== null) { + clearInterval(timerRef.current); + timerRef.current = null; + } + setCountdown(null); + }, []); + + const cancelCountdown = useCallback(() => { + clearTimer(); + }, [clearTimer]); + + const startCountdown = useCallback( + (content: string) => { + pendingContentRef.current = content; + clearTimer(); + + startTimeRef.current = Date.now(); + setCountdown(COUNTDOWN_DURATION_MS); + + timerRef.current = setInterval(() => { + const elapsed = Date.now() - startTimeRef.current; + const remaining = COUNTDOWN_DURATION_MS - elapsed; + + if (remaining <= 0) { + clearTimer(); + lastSavedContentRef.current = pendingContentRef.current; + onAutoSave(pendingContentRef.current); + } else { + setCountdown(remaining); + } + }, TICK_INTERVAL_MS); + }, + [clearTimer, onAutoSave] + ); + + // Listen for editor updates (content changes) + useEffect(() => { + if (!enabled) return; + + const unregister = editor.registerUpdateListener(({ editorState, dirtyElements, dirtyLeaves }) => { + // Only trigger on actual content changes + if (dirtyElements.size === 0 && dirtyLeaves.size === 0) return; + + const content = getContent(); + if (content !== lastSavedContentRef.current) { + startCountdown(content); + } + }); + + return unregister; + }, [editor, enabled, getContent, startCountdown]); + + // Listen for undo command to cancel countdown + useEffect(() => { + const unregister = editor.registerCommand( + UNDO_COMMAND, + () => { + cancelCountdown(); + return false; // Don't prevent the undo from executing + }, + 1 // COMMAND_PRIORITY_LOW + ); + + return unregister; + }, [editor, cancelCountdown]); + + // Listen for Escape key to cancel countdown + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && countdown !== null) { + e.preventDefault(); + cancelCountdown(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [countdown, cancelCountdown]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (timerRef.current !== null) { + clearInterval(timerRef.current); + } + }; + }, []); + + if (countdown === null) return null; + + const progressPercent = (countdown / COUNTDOWN_DURATION_MS) * 100; + const secondsLeft = Math.ceil(countdown / 1000); + + return ( +
+ + Saving in {secondsLeft}s... Esc to cancel + +
+
+
+ +
+ ); +} diff --git a/frontend/src/components/document/DocumentEditor.css b/frontend/src/components/document/DocumentEditor.css new file mode 100644 index 0000000..0be1151 --- /dev/null +++ b/frontend/src/components/document/DocumentEditor.css @@ -0,0 +1,246 @@ +/* ============================================ + Document Editor - Clean, modern document UI + ============================================ */ + +.document-editor-container { + max-width: 800px; + margin: 0 auto; + padding: 2rem 1rem; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + position: relative; +} + +/* ---- Lexical Root ---- */ +.doc-editor-root { + outline: none; + min-height: 400px; + padding: 1rem 0; + color: #1a1a2e; + line-height: 1.7; + font-size: 16px; +} + +/* ---- Headings ---- */ +.doc-editor-h1 { + font-size: 2.25rem; + font-weight: 700; + color: #0f0f23; + margin: 0 0 0.25rem 0; + padding: 0; + line-height: 1.3; + letter-spacing: -0.02em; + border: none; +} + +.doc-editor-h2 { + font-size: 1.5rem; + font-weight: 600; + color: #1a1a2e; + margin: 1.5rem 0 0.5rem 0; + line-height: 1.4; +} + +.doc-editor-h3 { + font-size: 1.2rem; + font-weight: 600; + color: #2a2a4a; + margin: 1.25rem 0 0.4rem 0; + line-height: 1.4; +} + +/* ---- Paragraphs ---- */ +.doc-editor-paragraph { + margin: 0.4rem 0; + padding: 0; + color: #374151; + line-height: 1.7; +} + +/* ---- Text Formatting ---- */ +.doc-editor-text-bold { + font-weight: 700; +} + +.doc-editor-text-italic { + font-style: italic; +} + +.doc-editor-text-underline { + text-decoration: underline; +} + +.doc-editor-text-strikethrough { + text-decoration: line-through; +} + +.doc-editor-text-code { + background: #f3f4f6; + border-radius: 3px; + padding: 0.15em 0.35em; + font-family: 'SF Mono', 'Fira Code', 'Fira Mono', Menlo, Consolas, monospace; + font-size: 0.9em; + color: #e11d48; +} + +/* ---- Lists ---- */ +.doc-editor-list-ul { + padding-left: 1.5rem; + margin: 0.5rem 0; + list-style-type: disc; +} + +.doc-editor-list-ol { + padding-left: 1.5rem; + margin: 0.5rem 0; + list-style-type: decimal; +} + +.doc-editor-listitem { + margin: 0.25rem 0; + color: #374151; +} + +.doc-editor-nested-listitem { + list-style-type: circle; +} + +/* ---- Links ---- */ +.doc-editor-link { + color: #2563eb; + text-decoration: underline; + cursor: pointer; +} + +.doc-editor-link:hover { + color: #1d4ed8; +} + +/* ---- Placeholder ---- */ +.doc-editor-placeholder { + color: #9ca3af; + position: absolute; + top: 1rem; + left: 0; + pointer-events: none; + font-size: 16px; + user-select: none; +} + +/* ---- Content Editable wrapper ---- */ +.doc-editor-input { + position: relative; +} + +.doc-editor-content-editable { + outline: none; + position: relative; +} + +/* ---- Divider between title and body ---- */ +.doc-editor-title-divider { + height: 1px; + background: #e5e7eb; + margin: 0.5rem 0 1rem 0; + border: none; +} + +/* ============================================ + Auto-Save Countdown Bar + ============================================ */ + +.autosave-bar { + position: sticky; + bottom: 0; + left: 0; + right: 0; + z-index: 50; + background: #fefce8; + border-top: 1px solid #fde68a; + padding: 0.5rem 1rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + font-size: 0.85rem; + color: #92400e; + transition: opacity 0.2s ease; +} + +.autosave-bar-hidden { + opacity: 0; + pointer-events: none; + height: 0; + padding: 0; + overflow: hidden; +} + +.autosave-bar-text { + display: flex; + align-items: center; + gap: 0.5rem; + white-space: nowrap; +} + +.autosave-bar-text kbd { + background: #fef3c7; + border: 1px solid #fde68a; + border-radius: 3px; + padding: 0.1em 0.4em; + font-size: 0.8em; + font-family: inherit; +} + +.autosave-bar-progress-track { + flex: 1; + height: 4px; + background: #fde68a; + border-radius: 2px; + overflow: hidden; + min-width: 80px; +} + +.autosave-bar-progress-fill { + height: 100%; + background: #f59e0b; + border-radius: 2px; + transition: width 0.1s linear; +} + +.autosave-bar-cancel { + background: none; + border: 1px solid #d97706; + border-radius: 4px; + color: #92400e; + padding: 0.2rem 0.6rem; + cursor: pointer; + font-size: 0.8rem; + white-space: nowrap; + transition: background 0.15s ease; +} + +.autosave-bar-cancel:hover { + background: #fef3c7; +} + +/* ============================================ + Responsive + ============================================ */ + +@media (max-width: 640px) { + .document-editor-container { + padding: 1rem 0.75rem; + } + + .doc-editor-h1 { + font-size: 1.75rem; + } + + .doc-editor-root { + font-size: 15px; + } + + .autosave-bar { + font-size: 0.78rem; + padding: 0.4rem 0.75rem; + } +} diff --git a/frontend/src/components/document/DocumentEditor.tsx b/frontend/src/components/document/DocumentEditor.tsx index ec7cd6b..d8cc27f 100644 --- a/frontend/src/components/document/DocumentEditor.tsx +++ b/frontend/src/components/document/DocumentEditor.tsx @@ -1,20 +1,137 @@ -import React from 'react' +import React, { useCallback, useEffect, useMemo } from 'react' +import { LexicalComposer } from '@lexical/react/LexicalComposer' +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin' +import { ContentEditable } from '@lexical/react/LexicalContentEditable' +import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin' +import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin' +import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { HeadingNode } from '@lexical/rich-text' +import { ListNode, ListItemNode } from '@lexical/list' +import { LinkNode } from '@lexical/link' +import { $getRoot, $createParagraphNode, $createTextNode, EditorState } from 'lexical' import { DirectiveWithSteps } from '../../services/directiveApi' +import { StepsDiagramNode, $createStepsDiagramNode, $isStepsDiagramNode } from './nodes/StepsDiagramNode' +import editorTheme from './EditorTheme' +import AutoSavePlugin from './AutoSavePlugin' +import './DocumentEditor.css' +import './nodes/StepsDiagram.css' -// Stub component - will be replaced by the parallel task that builds the full editor interface DocumentEditorProps { directive: DirectiveWithSteps onGoalChange: (goal: string) => void } -export function DocumentEditor({ directive }: DocumentEditorProps) { +// Plugin that initializes the editor content from the directive +function InitializePlugin({ directive }: { directive: DirectiveWithSteps }) { + const [editor] = useLexicalComposerContext() + const initializedRef = React.useRef(false) + + useEffect(() => { + if (initializedRef.current) return + initializedRef.current = true + + editor.update(() => { + const root = $getRoot() + root.clear() + + // Add goal text as paragraphs + const goalText = directive.goal || '' + const lines = goalText.split('\n') + for (const line of lines) { + const paragraph = $createParagraphNode() + if (line.trim()) { + paragraph.append($createTextNode(line)) + } + root.append(paragraph) + } + + // Insert steps diagram node after the goal content + const stepsNode = $createStepsDiagramNode(directive.id) + root.append(stepsNode) + + // Add a trailing paragraph so the user can type below the diagram + const trailingParagraph = $createParagraphNode() + root.append(trailingParagraph) + }) + }, [editor, directive]) + + return null +} + +export function DocumentEditor({ directive, onGoalChange }: DocumentEditorProps) { + const initialConfig = useMemo( + () => ({ + namespace: 'DirectiveEditor', + theme: editorTheme, + nodes: [ + HeadingNode, + ListNode, + ListItemNode, + LinkNode, + StepsDiagramNode, + ], + onError: (error: Error) => { + console.error('Lexical error:', error) + }, + }), + [] + ) + + const getContent = useCallback(() => { + // This will be called by AutoSavePlugin - return goal text only + // (exclude the steps diagram node content) + return directive.goal || '' + }, [directive.goal]) + + const handleChange = useCallback( + (editorState: EditorState) => { + editorState.read(() => { + const root = $getRoot() + const children = root.getChildren() + // Extract text content from non-diagram nodes + const textParts: string[] = [] + for (const child of children) { + if (!$isStepsDiagramNode(child)) { + const text = child.getTextContent() + textParts.push(text) + } + } + // Join but don't trigger save if content hasn't changed + const newGoal = textParts.join('\n') + if (newGoal !== directive.goal) { + // Goal change will be debounced by AutoSavePlugin + } + }) + }, + [directive.goal] + ) + return ( -
-

{directive.title}

-

Editor loading...

-
-        {directive.goal}
-      
+
+ +
+ + } + placeholder={ +
+ Describe your goal... +
+ } + ErrorBoundary={LexicalErrorBoundary} + /> +
+ + + + +
) } diff --git a/frontend/src/components/document/EditorTheme.ts b/frontend/src/components/document/EditorTheme.ts new file mode 100644 index 0000000..5b336ad --- /dev/null +++ b/frontend/src/components/document/EditorTheme.ts @@ -0,0 +1,30 @@ +import type { EditorThemeClasses } from 'lexical'; + +const editorTheme: EditorThemeClasses = { + root: 'doc-editor-root', + paragraph: 'doc-editor-paragraph', + heading: { + h1: 'doc-editor-h1', + h2: 'doc-editor-h2', + h3: 'doc-editor-h3', + }, + text: { + bold: 'doc-editor-text-bold', + italic: 'doc-editor-text-italic', + underline: 'doc-editor-text-underline', + strikethrough: 'doc-editor-text-strikethrough', + code: 'doc-editor-text-code', + }, + list: { + ul: 'doc-editor-list-ul', + ol: 'doc-editor-list-ol', + listitem: 'doc-editor-listitem', + nested: { + listitem: 'doc-editor-nested-listitem', + }, + }, + link: 'doc-editor-link', + placeholder: 'doc-editor-placeholder', +}; + +export default editorTheme; diff --git a/frontend/src/components/document/nodes/StepsDiagram.css b/frontend/src/components/document/nodes/StepsDiagram.css new file mode 100644 index 0000000..f3e9305 --- /dev/null +++ b/frontend/src/components/document/nodes/StepsDiagram.css @@ -0,0 +1,360 @@ +/* ============================================ + Steps Diagram Block + ============================================ */ + +.steps-diagram-block { + margin: 1.5rem 0; + user-select: none; +} + +.steps-diagram { + background: #f8f9fc; + border: 1px solid #e2e5ef; + border-radius: 10px; + padding: 1rem 1.25rem; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-size: 14px; + color: #374151; +} + +/* ---- Header ---- */ +.steps-diagram-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; + padding-bottom: 0.6rem; + border-bottom: 1px solid #e5e7eb; +} + +.steps-diagram-header-left { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.steps-diagram-header-title { + font-weight: 600; + font-size: 0.9rem; + color: #1f2937; + letter-spacing: 0.01em; +} + +.steps-diagram-header-count { + font-size: 0.78rem; + color: #6b7280; + background: #e5e7eb; + border-radius: 10px; + padding: 0.15rem 0.55rem; +} + +.steps-diagram-header-author { + font-size: 0.72rem; + color: #9ca3af; + font-style: italic; +} + +/* ---- DAG Layout ---- */ +.steps-diagram-dag { + display: flex; + flex-direction: column; + align-items: center; + gap: 0; +} + +.steps-diagram-group { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + justify-content: center; + width: 100%; +} + +/* ---- Arrow between groups ---- */ +.steps-diagram-arrow { + display: flex; + flex-direction: column; + align-items: center; + padding: 0.15rem 0; +} + +.steps-diagram-arrow-line { + width: 2px; + height: 16px; + background: #cbd5e1; +} + +.steps-diagram-arrow-head { + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 6px solid #cbd5e1; +} + +/* ---- Step Card ---- */ +.steps-diagram-card { + flex: 1 1 180px; + max-width: 280px; + background: #ffffff; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 0.65rem 0.8rem; + transition: box-shadow 0.2s ease, border-color 0.2s ease; + animation: stepCardAppear 0.35s ease-out both; +} + +@keyframes stepCardAppear { + from { + opacity: 0; + transform: translateY(8px) scale(0.97); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.steps-diagram-card:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +} + +.steps-diagram-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + margin-bottom: 0.3rem; +} + +.steps-diagram-card-name { + font-weight: 600; + font-size: 0.85rem; + color: #1f2937; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; +} + +.steps-diagram-card-desc { + font-size: 0.78rem; + color: #6b7280; + margin: 0.2rem 0 0.4rem 0; + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.steps-diagram-card-footer { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 0.72rem; + color: #9ca3af; +} + +.steps-diagram-card-index { + font-weight: 500; +} + +.steps-diagram-card-progress { + color: #d97706; + font-style: italic; +} + +.steps-diagram-card-time { + color: #6b7280; +} + +/* ---- Status Badge ---- */ +.steps-diagram-status-badge { + font-size: 0.68rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; + padding: 0.12rem 0.45rem; + border-radius: 9px; + white-space: nowrap; + flex-shrink: 0; +} + +.steps-diagram-status-badge--pending { + background: #f3f4f6; + color: #6b7280; +} + +.steps-diagram-status-badge--ready { + background: #dbeafe; + color: #2563eb; +} + +.steps-diagram-status-badge--running { + background: #fef3c7; + color: #d97706; + animation: statusPulse 2s ease-in-out infinite; +} + +.steps-diagram-status-badge--completed { + background: #d1fae5; + color: #059669; +} + +.steps-diagram-status-badge--failed { + background: #fee2e2; + color: #dc2626; +} + +.steps-diagram-status-badge--skipped { + background: repeating-linear-gradient( + 45deg, + #f3f4f6, + #f3f4f6 4px, + #e5e7eb 4px, + #e5e7eb 8px + ); + color: #9ca3af; +} + +/* ---- Status-specific Card Borders ---- */ +.steps-diagram-card--pending { + border-left: 3px solid #d1d5db; +} + +.steps-diagram-card--ready { + border-left: 3px solid #3b82f6; +} + +.steps-diagram-card--running { + border-left: 3px solid #f59e0b; + animation: cardGlow 2s ease-in-out infinite; +} + +.steps-diagram-card--completed { + border-left: 3px solid #10b981; +} + +.steps-diagram-card--failed { + border-left: 3px solid #ef4444; +} + +.steps-diagram-card--skipped { + border-left: 3px solid #d1d5db; + opacity: 0.7; +} + +/* ---- Animations ---- */ +@keyframes statusPulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.65; + } +} + +@keyframes cardGlow { + 0%, 100% { + box-shadow: 0 0 0 0 rgba(245, 158, 11, 0); + } + 50% { + box-shadow: 0 0 8px 2px rgba(245, 158, 11, 0.15); + } +} + +/* ---- Loading State ---- */ +.steps-diagram-loading { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 1rem 0; + color: #9ca3af; + font-size: 0.85rem; +} + +.steps-diagram-spinner { + width: 16px; + height: 16px; + border: 2px solid #e5e7eb; + border-top-color: #6b7280; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* ---- Planning State ---- */ +.steps-diagram-planning { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1.25rem 0; + color: #6b7280; + font-size: 0.85rem; + font-style: italic; +} + +.steps-diagram-planning-dots { + display: flex; + gap: 4px; +} + +.steps-diagram-planning-dots span { + width: 6px; + height: 6px; + background: #9ca3af; + border-radius: 50%; + animation: dotBounce 1.4s ease-in-out infinite; +} + +.steps-diagram-planning-dots span:nth-child(2) { + animation-delay: 0.2s; +} + +.steps-diagram-planning-dots span:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes dotBounce { + 0%, 80%, 100% { + transform: scale(0.6); + opacity: 0.4; + } + 40% { + transform: scale(1); + opacity: 1; + } +} + +/* ---- Empty / Error ---- */ +.steps-diagram-empty { + padding: 1rem 0; + color: #9ca3af; + font-size: 0.85rem; + text-align: center; +} + +.steps-diagram-error { + padding: 0.75rem; + background: #fef2f2; + border: 1px solid #fecaca; + border-radius: 6px; + color: #dc2626; + font-size: 0.82rem; +} + +/* ---- Responsive ---- */ +@media (max-width: 640px) { + .steps-diagram { + padding: 0.75rem; + } + + .steps-diagram-card { + flex: 1 1 100%; + max-width: 100%; + } +} diff --git a/frontend/src/components/document/nodes/StepsDiagramComponent.tsx b/frontend/src/components/document/nodes/StepsDiagramComponent.tsx new file mode 100644 index 0000000..606c0ab --- /dev/null +++ b/frontend/src/components/document/nodes/StepsDiagramComponent.tsx @@ -0,0 +1,180 @@ +import React, { useEffect, useState, useRef, useCallback } from 'react'; +import { getDirective, DirectiveStep, DirectiveWithSteps } from '../../../services/directiveApi'; +import './StepsDiagram.css'; + +interface StepsDiagramComponentProps { + directiveId: string; +} + +type StepStatus = 'pending' | 'ready' | 'running' | 'completed' | 'failed' | 'skipped'; + +const STATUS_LABELS: Record = { + pending: 'Pending', + ready: 'Ready', + running: 'Running', + completed: 'Done', + failed: 'Failed', + skipped: 'Skipped', +}; + +function formatTime(dateStr: string): string { + if (!dateStr) return ''; + const d = new Date(dateStr); + return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); +} + +function StepCard({ step }: { step: DirectiveStep }) { + const status = (step.status || 'pending').toLowerCase() as StepStatus; + + return ( +
+
+ {step.name} + + {STATUS_LABELS[status] || status} + +
+ {step.description && ( +

{step.description}

+ )} +
+ #{step.orderIndex} + {status === 'running' && ( + In progress... + )} + {status === 'completed' && step.completedAt && ( + + Completed {formatTime(step.completedAt)} + + )} +
+
+ ); +} + +export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProps) { + const [steps, setSteps] = useState([]); + const [directiveStatus, setDirectiveStatus] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const intervalRef = useRef | null>(null); + const prevStepCountRef = useRef(0); + + const fetchSteps = useCallback(async () => { + try { + const data: DirectiveWithSteps = await getDirective(directiveId); + setSteps(data.steps || []); + setDirectiveStatus(data.status || ''); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load steps'); + } finally { + setLoading(false); + } + }, [directiveId]); + + useEffect(() => { + fetchSteps(); + intervalRef.current = setInterval(fetchSteps, 5000); + return () => { + if (intervalRef.current) clearInterval(intervalRef.current); + }; + }, [fetchSteps]); + + // Track when new steps appear for animation + useEffect(() => { + prevStepCountRef.current = steps.length; + }, [steps.length]); + + const completedCount = steps.filter(s => s.status?.toLowerCase() === 'completed').length; + const totalCount = steps.length; + const isActive = ['active', 'running', 'planning'].includes(directiveStatus.toLowerCase()); + const isBuilding = isActive && steps.length === 0; + const isAddingSteps = isActive && steps.length > 0 && steps.length > prevStepCountRef.current; + + // Group steps by orderIndex + const groupedSteps: Map = new Map(); + const sortedSteps = [...steps].sort((a, b) => a.orderIndex - b.orderIndex); + for (const step of sortedSteps) { + const idx = step.orderIndex; + if (!groupedSteps.has(idx)) groupedSteps.set(idx, []); + groupedSteps.get(idx)!.push(step); + } + const orderGroups = Array.from(groupedSteps.entries()).sort((a, b) => a[0] - b[0]); + + if (loading) { + return ( +
+
+ Steps + Authored by Makima +
+
+
+ Loading steps... +
+
+ ); + } + + if (error) { + return ( +
+
+ Steps + Authored by Makima +
+
Failed to load steps: {error}
+
+ ); + } + + return ( +
+
+
+ Steps + {totalCount > 0 && ( + + {completedCount}/{totalCount} completed + + )} +
+ Authored by Makima +
+ + {isBuilding && ( +
+
+ +
+ Makima is building the plan... +
+ )} + + {totalCount === 0 && !isBuilding && ( +
No steps defined yet.
+ )} + + {totalCount > 0 && ( +
+ {orderGroups.map(([orderIndex, groupSteps], groupIdx) => ( + + {groupIdx > 0 && ( +
+
+
+
+ )} +
+ {groupSteps.map((step) => ( + + ))} +
+ + ))} +
+ )} +
+ ); +} diff --git a/frontend/src/components/document/nodes/StepsDiagramNode.tsx b/frontend/src/components/document/nodes/StepsDiagramNode.tsx new file mode 100644 index 0000000..8b37f52 --- /dev/null +++ b/frontend/src/components/document/nodes/StepsDiagramNode.tsx @@ -0,0 +1,91 @@ +import { + DecoratorNode, + DOMExportOutput, + LexicalNode, + NodeKey, + SerializedLexicalNode, + Spread, +} from 'lexical'; +import React from 'react'; +import { StepsDiagramComponent } from './StepsDiagramComponent'; + +export type SerializedStepsDiagramNode = Spread< + { + directiveId: string; + }, + SerializedLexicalNode +>; + +export class StepsDiagramNode extends DecoratorNode { + __directiveId: string; + + static getType(): string { + return 'steps-diagram'; + } + + static clone(node: StepsDiagramNode): StepsDiagramNode { + return new StepsDiagramNode(node.__directiveId, node.__key); + } + + constructor(directiveId: string, key?: NodeKey) { + super(key); + this.__directiveId = directiveId; + } + + createDOM(): HTMLElement { + const div = document.createElement('div'); + div.className = 'steps-diagram-block'; + return div; + } + + updateDOM(): boolean { + return false; + } + + decorate(): JSX.Element { + return ; + } + + exportJSON(): SerializedStepsDiagramNode { + return { + ...super.exportJSON(), + type: 'steps-diagram', + directiveId: this.__directiveId, + version: 1, + }; + } + + static importJSON(serializedNode: SerializedStepsDiagramNode): StepsDiagramNode { + return $createStepsDiagramNode(serializedNode.directiveId); + } + + isInline(): boolean { + return false; + } + + canInsertTextBefore(): boolean { + return false; + } + + canInsertTextAfter(): boolean { + return false; + } + + exportDOM(): DOMExportOutput { + const element = document.createElement('div'); + element.className = 'steps-diagram-block'; + element.setAttribute('data-directive-id', this.__directiveId); + element.textContent = '[Steps Diagram]'; + return { element }; + } +} + +export function $createStepsDiagramNode(directiveId: string): StepsDiagramNode { + return new StepsDiagramNode(directiveId); +} + +export function $isStepsDiagramNode( + node: LexicalNode | null | undefined, +): node is StepsDiagramNode { + return node instanceof StepsDiagramNode; +} diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index 79408dc..48c31c7 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/types.ts","./src/components/bottombar.tsx","./src/components/choicemenu.tsx","./src/components/cityscapebackground.tsx","./src/components/configmodal.tsx","./src/components/contractdetail.tsx","./src/components/contractlist.tsx","./src/components/dialoguebox.tsx","./src/components/filedetail.tsx","./src/components/heartlogo.tsx","./src/components/landingpage.tsx","./src/components/loadingscreen.tsx","./src/components/origamidragonlogo.tsx","./src/components/topbar.tsx","./src/components/vnapp.tsx","./src/components/vninterface.tsx","./src/components/vnviewport.tsx","./src/services/ws.ts","./src/stores/index.ts"],"version":"5.9.2"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/types.ts","./src/components/bottombar.tsx","./src/components/choicemenu.tsx","./src/components/cityscapebackground.tsx","./src/components/configmodal.tsx","./src/components/contractdetail.tsx","./src/components/contractlist.tsx","./src/components/daemondetail.tsx","./src/components/daemonlist.tsx","./src/components/dialoguebox.tsx","./src/components/filedetail.tsx","./src/components/heartlogo.tsx","./src/components/landingpage.tsx","./src/components/loadingscreen.tsx","./src/components/origamidragonlogo.tsx","./src/components/topbar.tsx","./src/components/vnapp.tsx","./src/components/vninterface.tsx","./src/components/vnviewport.tsx","./src/components/document/autosaveplugin.tsx","./src/components/document/directivefiletree.tsx","./src/components/document/documenteditor.tsx","./src/components/document/documentlayout.tsx","./src/components/document/editortheme.ts","./src/components/document/nodes/stepsdiagramcomponent.tsx","./src/components/document/nodes/stepsdiagramnode.tsx","./src/services/directiveapi.ts","./src/services/ws.ts","./src/stores/index.ts"],"version":"5.9.2"} \ No newline at end of file -- cgit v1.2.3