From 9403230ef0916136c4452036fbb729c1f160e4cc Mon Sep 17 00:00:00 2001 From: AndyMik90 Date: Tue, 16 Dec 2025 17:49:35 +0100 Subject: [PATCH] Implement roadmap feature management kanban with drag-and-drop support --- auto-claude-ui/package-lock.json | 1215 ++++++++++++++++- .../src/main/ipc-handlers/roadmap-handlers.ts | 64 + auto-claude-ui/src/preload/api/agent-api.ts | 4 + .../renderer/__tests__/roadmap-store.test.ts | 634 +++++++++ .../renderer/components/AddFeatureDialog.tsx | 367 +++++ .../src/renderer/components/Roadmap.tsx | 42 +- .../renderer/components/RoadmapKanbanView.tsx | 345 +++++ .../components/SortableFeatureCard.tsx | 145 ++ .../src/renderer/lib/browser-mock.ts | 39 +- .../src/renderer/stores/roadmap-store.ts | 96 +- auto-claude-ui/src/shared/constants.ts | 1 + auto-claude-ui/src/shared/types/ipc.ts | 1 + 12 files changed, 2928 insertions(+), 25 deletions(-) create mode 100644 auto-claude-ui/src/renderer/__tests__/roadmap-store.test.ts create mode 100644 auto-claude-ui/src/renderer/components/AddFeatureDialog.tsx create mode 100644 auto-claude-ui/src/renderer/components/RoadmapKanbanView.tsx create mode 100644 auto-claude-ui/src/renderer/components/SortableFeatureCard.tsx diff --git a/auto-claude-ui/package-lock.json b/auto-claude-ui/package-lock.json index 563afb6b..f17568aa 100644 --- a/auto-claude-ui/package-lock.json +++ b/auto-claude-ui/package-lock.json @@ -38,8 +38,8 @@ "lucide-react": "^0.560.0", "motion": "^12.23.26", "node-pty": "^1.0.0", - "react": "^19.2.1", - "react-dom": "^19.2.1", + "react": "^19.2.3", + "react-dom": "^19.2.3", "react-resizable-panels": "^3.0.6", "tailwind-merge": "^3.4.0", "uuid": "^13.0.0", @@ -52,6 +52,7 @@ "@eslint/js": "^9.39.1", "@playwright/test": "^1.52.0", "@tailwindcss/postcss": "^4.1.17", + "@testing-library/react": "^16.1.0", "@types/node": "^25.0.0", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", @@ -65,6 +66,9 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "globals": "^16.5.0", + "husky": "^9.1.7", + "jsdom": "^26.0.0", + "lint-staged": "^16.2.7", "postcss": "^8.5.6", "tailwindcss": "^4.1.17", "typescript": "^5.9.3", @@ -86,6 +90,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -356,6 +381,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -404,6 +439,121 @@ "node": ">=6.9.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -3688,6 +3838,55 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.1.tgz", + "integrity": "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -3698,6 +3897,14 @@ "node": ">= 10" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4433,6 +4640,22 @@ "ajv": "^6.9.1" } }, + "node_modules/ansi-escapes": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -4563,6 +4786,17 @@ "node": ">=10" } }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -4900,6 +5134,19 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -5384,6 +5631,13 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5542,6 +5796,20 @@ "node": ">= 8" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -5549,6 +5817,20 @@ "devOptional": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -5621,6 +5903,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -5726,6 +6015,17 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -5832,6 +6132,14 @@ "node": ">=0.10.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -6130,6 +6438,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -6140,6 +6461,19 @@ "node": ">=6" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -6657,6 +6991,13 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -6801,6 +7142,19 @@ "node": ">=10" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -7064,6 +7418,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -7442,6 +7809,19 @@ "dev": true, "license": "ISC" }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -7501,6 +7881,22 @@ "ms": "^2.0.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-corefoundation": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", @@ -7911,6 +8307,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-number-object": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", @@ -7928,6 +8334,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -8195,6 +8608,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -8571,6 +9024,211 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lint-staged": { + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz", + "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.2", + "listr2": "^9.0.5", + "micromatch": "^4.0.8", + "nano-spawn": "^2.0.0", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/cli-truncate": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8618,6 +9276,206 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -8660,6 +9518,17 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -8774,6 +9643,33 @@ "node": ">= 0.4" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -8820,6 +9716,19 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -9034,6 +9943,19 @@ "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==", "license": "MIT" }, + "node_modules/nano-spawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", + "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -9147,6 +10069,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9430,6 +10359,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9550,6 +10492,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/playwright": { "version": "1.57.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", @@ -9683,6 +10638,44 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/proc-log": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", @@ -10072,6 +11065,13 @@ "node": ">= 4" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -10185,6 +11185,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -10285,6 +11292,19 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -10674,6 +11694,16 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -10882,6 +11912,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", @@ -11104,6 +12141,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -11124,6 +12181,45 @@ "tmp": "^0.2.0" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -12137,6 +13233,19 @@ } } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -12147,6 +13256,53 @@ "defaults": "^1.0.3" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -12323,6 +13479,38 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", @@ -12333,6 +13521,13 @@ "node": ">=8.0" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -12350,6 +13545,22 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/auto-claude-ui/src/main/ipc-handlers/roadmap-handlers.ts b/auto-claude-ui/src/main/ipc-handlers/roadmap-handlers.ts index 316a6e19..2829fdc7 100644 --- a/auto-claude-ui/src/main/ipc-handlers/roadmap-handlers.ts +++ b/auto-claude-ui/src/main/ipc-handlers/roadmap-handlers.ts @@ -224,6 +224,70 @@ export function registerRoadmapHandlers( } ); + // ============================================ + // Roadmap Save (full state persistence for drag-and-drop) + // ============================================ + + ipcMain.handle( + IPC_CHANNELS.ROADMAP_SAVE, + async ( + _, + projectId: string, + features: RoadmapFeature[] + ): Promise => { + const project = projectStore.getProject(projectId); + if (!project) { + return { success: false, error: 'Project not found' }; + } + + const roadmapPath = path.join( + project.path, + AUTO_BUILD_PATHS.ROADMAP_DIR, + AUTO_BUILD_PATHS.ROADMAP_FILE + ); + + if (!existsSync(roadmapPath)) { + return { success: false, error: 'Roadmap not found' }; + } + + try { + const content = readFileSync(roadmapPath, 'utf-8'); + const roadmap = JSON.parse(content); + + // Transform camelCase features back to snake_case for JSON file + roadmap.features = features.map((feature) => ({ + id: feature.id, + title: feature.title, + description: feature.description, + rationale: feature.rationale || '', + priority: feature.priority, + complexity: feature.complexity, + impact: feature.impact, + phase_id: feature.phaseId, + dependencies: feature.dependencies || [], + status: feature.status, + acceptance_criteria: feature.acceptanceCriteria || [], + user_stories: feature.userStories || [], + linked_spec_id: feature.linkedSpecId, + competitor_insight_ids: feature.competitorInsightIds + })); + + // Update metadata timestamp + roadmap.metadata = roadmap.metadata || {}; + roadmap.metadata.updated_at = new Date().toISOString(); + + writeFileSync(roadmapPath, JSON.stringify(roadmap, null, 2)); + + return { success: true }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to save roadmap' + }; + } + } + ); + ipcMain.handle( IPC_CHANNELS.ROADMAP_UPDATE_FEATURE, async ( diff --git a/auto-claude-ui/src/preload/api/agent-api.ts b/auto-claude-ui/src/preload/api/agent-api.ts index c8cfc9af..844bec95 100644 --- a/auto-claude-ui/src/preload/api/agent-api.ts +++ b/auto-claude-ui/src/preload/api/agent-api.ts @@ -47,6 +47,7 @@ import type { export interface AgentAPI { // Roadmap Operations getRoadmap: (projectId: string) => Promise>; + saveRoadmap: (projectId: string, roadmap: Roadmap) => Promise; generateRoadmap: (projectId: string) => void; refreshRoadmap: (projectId: string) => void; updateFeatureStatus: ( @@ -173,6 +174,9 @@ export const createAgentAPI = (): AgentAPI => ({ getRoadmap: (projectId: string): Promise> => ipcRenderer.invoke(IPC_CHANNELS.ROADMAP_GET, projectId), + saveRoadmap: (projectId: string, roadmap: Roadmap): Promise => + ipcRenderer.invoke(IPC_CHANNELS.ROADMAP_SAVE, projectId, roadmap), + generateRoadmap: (projectId: string): void => ipcRenderer.send(IPC_CHANNELS.ROADMAP_GENERATE, projectId), diff --git a/auto-claude-ui/src/renderer/__tests__/roadmap-store.test.ts b/auto-claude-ui/src/renderer/__tests__/roadmap-store.test.ts new file mode 100644 index 00000000..0dc9ada7 --- /dev/null +++ b/auto-claude-ui/src/renderer/__tests__/roadmap-store.test.ts @@ -0,0 +1,634 @@ +/** + * Unit tests for Roadmap Store + * Tests Zustand store for roadmap state management including drag-and-drop actions + */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { useRoadmapStore, getFeaturesByPhase, getFeaturesByPriority, getFeatureStats } from '../stores/roadmap-store'; +import type { + Roadmap, + RoadmapFeature, + RoadmapPhase, + RoadmapFeaturePriority, + RoadmapFeatureStatus +} from '../../shared/types'; + +// Helper to create test features +function createTestFeature(overrides: Partial = {}): RoadmapFeature { + return { + id: `feature-${Date.now()}-${Math.random().toString(36).substring(7)}`, + title: 'Test Feature', + description: 'Test description', + rationale: 'Test rationale', + priority: 'should' as RoadmapFeaturePriority, + complexity: 'medium', + impact: 'medium', + phaseId: 'phase-1', + dependencies: [], + status: 'idea' as RoadmapFeatureStatus, + acceptanceCriteria: ['Test criteria'], + userStories: ['As a user, I want to test'], + ...overrides + }; +} + +// Helper to create test phases +function createTestPhase(overrides: Partial = {}): RoadmapPhase { + return { + id: `phase-${Date.now()}-${Math.random().toString(36).substring(7)}`, + name: 'Test Phase', + description: 'Test phase description', + order: 1, + status: 'planned', + features: [], + milestones: [], + ...overrides + }; +} + +// Helper to create test roadmap +function createTestRoadmap(overrides: Partial = {}): Roadmap { + return { + id: 'roadmap-1', + projectId: 'project-1', + projectName: 'Test Project', + version: '1.0.0', + vision: 'Test vision', + targetAudience: { + primary: 'Developers', + secondary: ['DevOps'] + }, + phases: [ + createTestPhase({ id: 'phase-1', name: 'Phase 1', order: 1 }), + createTestPhase({ id: 'phase-2', name: 'Phase 2', order: 2 }), + createTestPhase({ id: 'phase-3', name: 'Phase 3', order: 3 }) + ], + features: [], + status: 'draft', + createdAt: new Date(), + updatedAt: new Date(), + ...overrides + }; +} + +describe('Roadmap Store', () => { + beforeEach(() => { + // Reset store to initial state before each test + useRoadmapStore.setState({ + roadmap: null, + competitorAnalysis: null, + generationStatus: { + phase: 'idle', + progress: 0, + message: '' + } + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('setRoadmap', () => { + it('should set roadmap', () => { + const roadmap = createTestRoadmap(); + + useRoadmapStore.getState().setRoadmap(roadmap); + + expect(useRoadmapStore.getState().roadmap).toBeDefined(); + expect(useRoadmapStore.getState().roadmap?.id).toBe('roadmap-1'); + }); + + it('should clear roadmap with null', () => { + useRoadmapStore.setState({ roadmap: createTestRoadmap() }); + + useRoadmapStore.getState().setRoadmap(null); + + expect(useRoadmapStore.getState().roadmap).toBeNull(); + }); + }); + + describe('reorderFeatures', () => { + it('should reorder features within a phase', () => { + const features = [ + createTestFeature({ id: 'feature-1', phaseId: 'phase-1', title: 'Feature 1' }), + createTestFeature({ id: 'feature-2', phaseId: 'phase-1', title: 'Feature 2' }), + createTestFeature({ id: 'feature-3', phaseId: 'phase-1', title: 'Feature 3' }) + ]; + const roadmap = createTestRoadmap({ features }); + + useRoadmapStore.setState({ roadmap }); + + // Reorder: move feature-3 to the top + useRoadmapStore.getState().reorderFeatures('phase-1', ['feature-3', 'feature-1', 'feature-2']); + + const state = useRoadmapStore.getState(); + const phase1Features = state.roadmap?.features.filter((f) => f.phaseId === 'phase-1') || []; + + expect(phase1Features).toHaveLength(3); + expect(phase1Features[0].id).toBe('feature-3'); + expect(phase1Features[1].id).toBe('feature-1'); + expect(phase1Features[2].id).toBe('feature-2'); + }); + + it('should not affect features in other phases', () => { + const features = [ + createTestFeature({ id: 'feature-1', phaseId: 'phase-1' }), + createTestFeature({ id: 'feature-2', phaseId: 'phase-1' }), + createTestFeature({ id: 'feature-3', phaseId: 'phase-2' }), + createTestFeature({ id: 'feature-4', phaseId: 'phase-2' }) + ]; + const roadmap = createTestRoadmap({ features }); + + useRoadmapStore.setState({ roadmap }); + + // Reorder phase-1 features only + useRoadmapStore.getState().reorderFeatures('phase-1', ['feature-2', 'feature-1']); + + const state = useRoadmapStore.getState(); + const phase2Features = state.roadmap?.features.filter((f) => f.phaseId === 'phase-2') || []; + + // Phase 2 features should be unchanged + expect(phase2Features).toHaveLength(2); + expect(phase2Features.map((f) => f.id)).toContain('feature-3'); + expect(phase2Features.map((f) => f.id)).toContain('feature-4'); + }); + + it('should update updatedAt timestamp', () => { + const originalDate = new Date('2024-01-01'); + const roadmap = createTestRoadmap({ + features: [ + createTestFeature({ id: 'feature-1', phaseId: 'phase-1' }), + createTestFeature({ id: 'feature-2', phaseId: 'phase-1' }) + ], + updatedAt: originalDate + }); + + useRoadmapStore.setState({ roadmap }); + + useRoadmapStore.getState().reorderFeatures('phase-1', ['feature-2', 'feature-1']); + + const state = useRoadmapStore.getState(); + expect(state.roadmap?.updatedAt.getTime()).toBeGreaterThan(originalDate.getTime()); + }); + + it('should handle empty feature array', () => { + const roadmap = createTestRoadmap({ features: [] }); + + useRoadmapStore.setState({ roadmap }); + + useRoadmapStore.getState().reorderFeatures('phase-1', []); + + expect(useRoadmapStore.getState().roadmap?.features).toHaveLength(0); + }); + + it('should handle non-existent feature IDs gracefully', () => { + const features = [ + createTestFeature({ id: 'feature-1', phaseId: 'phase-1' }), + createTestFeature({ id: 'feature-2', phaseId: 'phase-1' }) + ]; + const roadmap = createTestRoadmap({ features }); + + useRoadmapStore.setState({ roadmap }); + + // Try to reorder with a non-existent ID - it should be filtered out + useRoadmapStore.getState().reorderFeatures('phase-1', ['feature-2', 'nonexistent', 'feature-1']); + + const state = useRoadmapStore.getState(); + const phase1Features = state.roadmap?.features.filter((f) => f.phaseId === 'phase-1') || []; + + expect(phase1Features).toHaveLength(2); + }); + + it('should do nothing if roadmap is null', () => { + useRoadmapStore.setState({ roadmap: null }); + + useRoadmapStore.getState().reorderFeatures('phase-1', ['feature-1', 'feature-2']); + + expect(useRoadmapStore.getState().roadmap).toBeNull(); + }); + }); + + describe('updateFeaturePhase', () => { + it('should move feature to a different phase', () => { + const features = [ + createTestFeature({ id: 'feature-1', phaseId: 'phase-1' }), + createTestFeature({ id: 'feature-2', phaseId: 'phase-1' }) + ]; + const roadmap = createTestRoadmap({ features }); + + useRoadmapStore.setState({ roadmap }); + + useRoadmapStore.getState().updateFeaturePhase('feature-1', 'phase-2'); + + const state = useRoadmapStore.getState(); + const movedFeature = state.roadmap?.features.find((f) => f.id === 'feature-1'); + + expect(movedFeature?.phaseId).toBe('phase-2'); + }); + + it('should not affect other features', () => { + const features = [ + createTestFeature({ id: 'feature-1', phaseId: 'phase-1' }), + createTestFeature({ id: 'feature-2', phaseId: 'phase-1' }), + createTestFeature({ id: 'feature-3', phaseId: 'phase-2' }) + ]; + const roadmap = createTestRoadmap({ features }); + + useRoadmapStore.setState({ roadmap }); + + useRoadmapStore.getState().updateFeaturePhase('feature-1', 'phase-3'); + + const state = useRoadmapStore.getState(); + + // Other features should remain in their original phases + expect(state.roadmap?.features.find((f) => f.id === 'feature-2')?.phaseId).toBe('phase-1'); + expect(state.roadmap?.features.find((f) => f.id === 'feature-3')?.phaseId).toBe('phase-2'); + }); + + it('should update updatedAt timestamp', () => { + const originalDate = new Date('2024-01-01'); + const roadmap = createTestRoadmap({ + features: [createTestFeature({ id: 'feature-1', phaseId: 'phase-1' })], + updatedAt: originalDate + }); + + useRoadmapStore.setState({ roadmap }); + + useRoadmapStore.getState().updateFeaturePhase('feature-1', 'phase-2'); + + const state = useRoadmapStore.getState(); + expect(state.roadmap?.updatedAt.getTime()).toBeGreaterThan(originalDate.getTime()); + }); + + it('should do nothing for non-existent feature', () => { + const features = [createTestFeature({ id: 'feature-1', phaseId: 'phase-1' })]; + const roadmap = createTestRoadmap({ features }); + + useRoadmapStore.setState({ roadmap }); + + useRoadmapStore.getState().updateFeaturePhase('nonexistent', 'phase-2'); + + const state = useRoadmapStore.getState(); + expect(state.roadmap?.features).toHaveLength(1); + expect(state.roadmap?.features[0].phaseId).toBe('phase-1'); + }); + + it('should do nothing if roadmap is null', () => { + useRoadmapStore.setState({ roadmap: null }); + + useRoadmapStore.getState().updateFeaturePhase('feature-1', 'phase-2'); + + expect(useRoadmapStore.getState().roadmap).toBeNull(); + }); + + it('should handle moving feature to same phase (no change needed)', () => { + const features = [createTestFeature({ id: 'feature-1', phaseId: 'phase-1' })]; + const roadmap = createTestRoadmap({ features }); + + useRoadmapStore.setState({ roadmap }); + + useRoadmapStore.getState().updateFeaturePhase('feature-1', 'phase-1'); + + const state = useRoadmapStore.getState(); + expect(state.roadmap?.features.find((f) => f.id === 'feature-1')?.phaseId).toBe('phase-1'); + }); + }); + + describe('addFeature', () => { + it('should add a new feature to the roadmap', () => { + const roadmap = createTestRoadmap({ features: [] }); + + useRoadmapStore.setState({ roadmap }); + + const newFeature = { + title: 'New Feature', + description: 'New feature description', + rationale: 'New feature rationale', + priority: 'must' as RoadmapFeaturePriority, + complexity: 'high' as const, + impact: 'high' as const, + phaseId: 'phase-1', + dependencies: [], + status: 'idea' as RoadmapFeatureStatus, + acceptanceCriteria: ['Criteria 1'], + userStories: ['User story 1'] + }; + + const newId = useRoadmapStore.getState().addFeature(newFeature); + + const state = useRoadmapStore.getState(); + expect(state.roadmap?.features).toHaveLength(1); + expect(state.roadmap?.features[0].id).toBe(newId); + expect(state.roadmap?.features[0].title).toBe('New Feature'); + }); + + it('should generate unique ID for new feature', () => { + const roadmap = createTestRoadmap({ features: [] }); + + useRoadmapStore.setState({ roadmap }); + + const featureData = { + title: 'Feature', + description: 'Description', + rationale: 'Rationale', + priority: 'should' as RoadmapFeaturePriority, + complexity: 'medium' as const, + impact: 'medium' as const, + phaseId: 'phase-1', + dependencies: [], + status: 'idea' as RoadmapFeatureStatus, + acceptanceCriteria: [], + userStories: [] + }; + + const id1 = useRoadmapStore.getState().addFeature(featureData); + const id2 = useRoadmapStore.getState().addFeature(featureData); + + expect(id1).toBeDefined(); + expect(id2).toBeDefined(); + expect(id1).not.toBe(id2); + expect(id1).toMatch(/^feature-\d+-[a-z0-9]+$/); + }); + + it('should append feature to existing features', () => { + const features = [ + createTestFeature({ id: 'existing-1' }), + createTestFeature({ id: 'existing-2' }) + ]; + const roadmap = createTestRoadmap({ features }); + + useRoadmapStore.setState({ roadmap }); + + useRoadmapStore.getState().addFeature({ + title: 'New Feature', + description: 'Description', + rationale: 'Rationale', + priority: 'could' as RoadmapFeaturePriority, + complexity: 'low' as const, + impact: 'low' as const, + phaseId: 'phase-2', + dependencies: [], + status: 'planned' as RoadmapFeatureStatus, + acceptanceCriteria: [], + userStories: [] + }); + + const state = useRoadmapStore.getState(); + expect(state.roadmap?.features).toHaveLength(3); + expect(state.roadmap?.features[2].title).toBe('New Feature'); + }); + + it('should update updatedAt timestamp', () => { + const originalDate = new Date('2024-01-01'); + const roadmap = createTestRoadmap({ features: [], updatedAt: originalDate }); + + useRoadmapStore.setState({ roadmap }); + + useRoadmapStore.getState().addFeature({ + title: 'New Feature', + description: 'Description', + rationale: 'Rationale', + priority: 'must' as RoadmapFeaturePriority, + complexity: 'medium' as const, + impact: 'high' as const, + phaseId: 'phase-1', + dependencies: [], + status: 'idea' as RoadmapFeatureStatus, + acceptanceCriteria: [], + userStories: [] + }); + + const state = useRoadmapStore.getState(); + expect(state.roadmap?.updatedAt.getTime()).toBeGreaterThan(originalDate.getTime()); + }); + + it('should return empty string if roadmap is null', () => { + useRoadmapStore.setState({ roadmap: null }); + + const newId = useRoadmapStore.getState().addFeature({ + title: 'New Feature', + description: 'Description', + rationale: 'Rationale', + priority: 'must' as RoadmapFeaturePriority, + complexity: 'medium' as const, + impact: 'high' as const, + phaseId: 'phase-1', + dependencies: [], + status: 'idea' as RoadmapFeatureStatus, + acceptanceCriteria: [], + userStories: [] + }); + + // The function still generates an ID, but the roadmap remains null + expect(newId).toMatch(/^feature-\d+-[a-z0-9]+$/); + expect(useRoadmapStore.getState().roadmap).toBeNull(); + }); + + it('should correctly assign phaseId from input', () => { + const roadmap = createTestRoadmap({ features: [] }); + + useRoadmapStore.setState({ roadmap }); + + useRoadmapStore.getState().addFeature({ + title: 'Phase 3 Feature', + description: 'Description', + rationale: 'Rationale', + priority: 'should' as RoadmapFeaturePriority, + complexity: 'medium' as const, + impact: 'medium' as const, + phaseId: 'phase-3', + dependencies: [], + status: 'idea' as RoadmapFeatureStatus, + acceptanceCriteria: [], + userStories: [] + }); + + const state = useRoadmapStore.getState(); + expect(state.roadmap?.features[0].phaseId).toBe('phase-3'); + }); + + it('should preserve all feature properties', () => { + const roadmap = createTestRoadmap({ features: [] }); + + useRoadmapStore.setState({ roadmap }); + + const featureData = { + title: 'Complete Feature', + description: 'Full description', + rationale: 'Solid rationale', + priority: 'must' as RoadmapFeaturePriority, + complexity: 'high' as const, + impact: 'high' as const, + phaseId: 'phase-1', + dependencies: ['dep-1', 'dep-2'], + status: 'planned' as RoadmapFeatureStatus, + acceptanceCriteria: ['AC1', 'AC2'], + userStories: ['Story 1', 'Story 2'], + linkedSpecId: 'spec-123', + competitorInsightIds: ['insight-1'] + }; + + useRoadmapStore.getState().addFeature(featureData); + + const state = useRoadmapStore.getState(); + const addedFeature = state.roadmap?.features[0]; + + expect(addedFeature?.title).toBe('Complete Feature'); + expect(addedFeature?.description).toBe('Full description'); + expect(addedFeature?.rationale).toBe('Solid rationale'); + expect(addedFeature?.priority).toBe('must'); + expect(addedFeature?.complexity).toBe('high'); + expect(addedFeature?.impact).toBe('high'); + expect(addedFeature?.dependencies).toEqual(['dep-1', 'dep-2']); + expect(addedFeature?.acceptanceCriteria).toEqual(['AC1', 'AC2']); + expect(addedFeature?.userStories).toEqual(['Story 1', 'Story 2']); + expect(addedFeature?.linkedSpecId).toBe('spec-123'); + expect(addedFeature?.competitorInsightIds).toEqual(['insight-1']); + }); + }); + + describe('updateFeatureStatus', () => { + it('should update feature status by id', () => { + const features = [createTestFeature({ id: 'feature-1', status: 'idea' })]; + const roadmap = createTestRoadmap({ features }); + + useRoadmapStore.setState({ roadmap }); + + useRoadmapStore.getState().updateFeatureStatus('feature-1', 'in_progress'); + + const state = useRoadmapStore.getState(); + expect(state.roadmap?.features[0].status).toBe('in_progress'); + }); + }); + + describe('updateFeatureLinkedSpec', () => { + it('should update linked spec and set status to planned', () => { + const features = [createTestFeature({ id: 'feature-1', status: 'idea' })]; + const roadmap = createTestRoadmap({ features }); + + useRoadmapStore.setState({ roadmap }); + + useRoadmapStore.getState().updateFeatureLinkedSpec('feature-1', 'spec-abc'); + + const state = useRoadmapStore.getState(); + expect(state.roadmap?.features[0].linkedSpecId).toBe('spec-abc'); + expect(state.roadmap?.features[0].status).toBe('planned'); + }); + }); + + describe('clearRoadmap', () => { + it('should clear roadmap and reset status', () => { + useRoadmapStore.setState({ + roadmap: createTestRoadmap(), + generationStatus: { + phase: 'complete', + progress: 100, + message: 'Done' + } + }); + + useRoadmapStore.getState().clearRoadmap(); + + const state = useRoadmapStore.getState(); + expect(state.roadmap).toBeNull(); + expect(state.generationStatus.phase).toBe('idle'); + expect(state.generationStatus.progress).toBe(0); + }); + }); + + describe('Helper Functions', () => { + describe('getFeaturesByPhase', () => { + it('should return features for specific phase', () => { + const roadmap = createTestRoadmap({ + features: [ + createTestFeature({ id: 'f1', phaseId: 'phase-1' }), + createTestFeature({ id: 'f2', phaseId: 'phase-1' }), + createTestFeature({ id: 'f3', phaseId: 'phase-2' }) + ] + }); + + const phase1Features = getFeaturesByPhase(roadmap, 'phase-1'); + + expect(phase1Features).toHaveLength(2); + expect(phase1Features.map((f) => f.id)).toContain('f1'); + expect(phase1Features.map((f) => f.id)).toContain('f2'); + }); + + it('should return empty array for null roadmap', () => { + const features = getFeaturesByPhase(null, 'phase-1'); + expect(features).toHaveLength(0); + }); + + it('should return empty array for non-existent phase', () => { + const roadmap = createTestRoadmap({ + features: [createTestFeature({ id: 'f1', phaseId: 'phase-1' })] + }); + + const features = getFeaturesByPhase(roadmap, 'non-existent'); + expect(features).toHaveLength(0); + }); + }); + + describe('getFeaturesByPriority', () => { + it('should return features for specific priority', () => { + const roadmap = createTestRoadmap({ + features: [ + createTestFeature({ id: 'f1', priority: 'must' }), + createTestFeature({ id: 'f2', priority: 'should' }), + createTestFeature({ id: 'f3', priority: 'must' }) + ] + }); + + const mustFeatures = getFeaturesByPriority(roadmap, 'must'); + + expect(mustFeatures).toHaveLength(2); + expect(mustFeatures.map((f) => f.id)).toContain('f1'); + expect(mustFeatures.map((f) => f.id)).toContain('f3'); + }); + + it('should return empty array for null roadmap', () => { + const features = getFeaturesByPriority(null, 'must'); + expect(features).toHaveLength(0); + }); + }); + + describe('getFeatureStats', () => { + it('should return correct stats', () => { + const roadmap = createTestRoadmap({ + features: [ + createTestFeature({ priority: 'must', status: 'idea', complexity: 'high' }), + createTestFeature({ priority: 'must', status: 'planned', complexity: 'medium' }), + createTestFeature({ priority: 'should', status: 'idea', complexity: 'low' }) + ] + }); + + const stats = getFeatureStats(roadmap); + + expect(stats.total).toBe(3); + expect(stats.byPriority['must']).toBe(2); + expect(stats.byPriority['should']).toBe(1); + expect(stats.byStatus['idea']).toBe(2); + expect(stats.byStatus['planned']).toBe(1); + expect(stats.byComplexity['high']).toBe(1); + expect(stats.byComplexity['medium']).toBe(1); + expect(stats.byComplexity['low']).toBe(1); + }); + + it('should return zero stats for null roadmap', () => { + const stats = getFeatureStats(null); + + expect(stats.total).toBe(0); + expect(stats.byPriority).toEqual({}); + expect(stats.byStatus).toEqual({}); + expect(stats.byComplexity).toEqual({}); + }); + + it('should return zero stats for empty features', () => { + const roadmap = createTestRoadmap({ features: [] }); + const stats = getFeatureStats(roadmap); + + expect(stats.total).toBe(0); + }); + }); + }); +}); diff --git a/auto-claude-ui/src/renderer/components/AddFeatureDialog.tsx b/auto-claude-ui/src/renderer/components/AddFeatureDialog.tsx new file mode 100644 index 00000000..08208f3d --- /dev/null +++ b/auto-claude-ui/src/renderer/components/AddFeatureDialog.tsx @@ -0,0 +1,367 @@ +/** + * AddFeatureDialog - Dialog for adding new features to the roadmap + * + * Allows users to create new roadmap features with title, description, + * priority, phase, complexity, and impact fields. + * Follows the same dialog pattern as TaskEditDialog for consistency. + * + * Features: + * - Form validation (title and description required) + * - Selectable classification fields (priority, phase, complexity, impact) + * - Adds feature to roadmap store and persists to file + * + * @example + * ```tsx + * console.log('Feature added:', featureId)} + * /> + * ``` + */ +import { useState, useEffect } from 'react'; +import { Loader2, X } from 'lucide-react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from './ui/dialog'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; +import { Textarea } from './ui/textarea'; +import { Label } from './ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from './ui/select'; +import { useRoadmapStore } from '../stores/roadmap-store'; +import { + ROADMAP_PRIORITY_LABELS, + ROADMAP_COMPLEXITY_COLORS, + ROADMAP_IMPACT_COLORS +} from '../../shared/constants'; +import type { + RoadmapPhase, + RoadmapFeaturePriority, + RoadmapFeatureStatus +} from '../../shared/types'; + +/** + * Props for the AddFeatureDialog component + */ +interface AddFeatureDialogProps { + /** Available phases to select from */ + phases: RoadmapPhase[]; + /** Whether the dialog is open */ + open: boolean; + /** Callback when the dialog open state changes */ + onOpenChange: (open: boolean) => void; + /** Optional callback when feature is successfully added, receives the new feature ID */ + onFeatureAdded?: (featureId: string) => void; + /** Optional default phase ID to pre-select */ + defaultPhaseId?: string; +} + +// Complexity options +const COMPLEXITY_OPTIONS = [ + { value: 'low', label: 'Low' }, + { value: 'medium', label: 'Medium' }, + { value: 'high', label: 'High' } +] as const; + +// Impact options +const IMPACT_OPTIONS = [ + { value: 'low', label: 'Low Impact' }, + { value: 'medium', label: 'Medium Impact' }, + { value: 'high', label: 'High Impact' } +] as const; + +export function AddFeatureDialog({ + phases, + open, + onOpenChange, + onFeatureAdded, + defaultPhaseId +}: AddFeatureDialogProps) { + // Form state + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [rationale, setRationale] = useState(''); + const [priority, setPriority] = useState('should'); + const [phaseId, setPhaseId] = useState(''); + const [complexity, setComplexity] = useState<'low' | 'medium' | 'high'>('medium'); + const [impact, setImpact] = useState<'low' | 'medium' | 'high'>('medium'); + + // UI state + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + + // Store actions + const addFeature = useRoadmapStore((state) => state.addFeature); + + // Reset form when dialog opens/closes + useEffect(() => { + if (open) { + setTitle(''); + setDescription(''); + setRationale(''); + setPriority('should'); + setPhaseId(defaultPhaseId || (phases.length > 0 ? phases[0].id : '')); + setComplexity('medium'); + setImpact('medium'); + setError(null); + } + }, [open, defaultPhaseId, phases]); + + const handleSave = async () => { + // Validate required fields + if (!title.trim()) { + setError('Title is required'); + return; + } + if (!description.trim()) { + setError('Description is required'); + return; + } + if (!phaseId) { + setError('Please select a phase'); + return; + } + + setIsSaving(true); + setError(null); + + try { + // Add feature to store + const newFeatureId = addFeature({ + title: title.trim(), + description: description.trim(), + rationale: rationale.trim() || `User-created feature for ${title.trim()}`, + priority, + complexity, + impact, + phaseId, + dependencies: [], + status: 'idea' as RoadmapFeatureStatus, + acceptanceCriteria: [], + userStories: [] + }); + + // Persist to file via IPC + const roadmap = useRoadmapStore.getState().roadmap; + if (roadmap) { + // Get the project ID from the roadmap + const result = await window.electronAPI.saveRoadmap(roadmap.projectId, roadmap); + if (!result.success) { + throw new Error(result.error || 'Failed to save roadmap'); + } + } + + // Success - close dialog and notify parent + onOpenChange(false); + onFeatureAdded?.(newFeatureId); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to add feature. Please try again.'); + } finally { + setIsSaving(false); + } + }; + + const handleClose = () => { + if (!isSaving) { + onOpenChange(false); + } + }; + + // Form validation + const isValid = title.trim().length > 0 && description.trim().length > 0 && phaseId !== ''; + + return ( + + + + Add Feature + + Add a new feature to your roadmap. Provide details about what you want to build + and how it fits into your product strategy. + + + +
+ {/* Title (Required) */} +
+ + setTitle(e.target.value)} + disabled={isSaving} + /> +
+ + {/* Description (Required) */} +
+ +