diff --git a/CLAUDE.md b/CLAUDE.md index 63ec5b0e5..bc4846b12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,4 +89,13 @@ Design inspiration from Linear, Vercel, Stripe, Airbnb, or Perplexity. ### Spring animations - Default to spring animations when using Framer Motion. -- Avoid using bouncy spring animations unless you are working with drag gestures. \ No newline at end of file +- Avoid using bouncy spring animations unless you are working with drag gestures. + + +## Testing + +Test if the backend or frontend works using the browser tool or running tests. + +## AVOID AT ALL COST +- Never edit or even modify outside of your worktree directory — it's STRICTLY prohibited. +- Never start this app outside of your worktree directory — it's STRICTLY prohibited. diff --git a/package-lock.json b/package-lock.json index 3170799ae..a70f3bf32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,13 +36,12 @@ "clsx": "^2.1.1", "cors": "^2.8.5", "express": "^5.1.0", + "framer-motion": "^12.23.24", "lucide-react": "^0.546.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-resizable-panels": "^3.0.6", "react-router-dom": "^6.26.0", - "socket.io": "^4.8.1", - "socket.io-client": "^4.8.1", "tailwind-merge": "^3.3.1", "xterm": "^5.3.0", "xterm-addon-fit": "^0.8.0", @@ -2382,12 +2381,6 @@ "win32" ] }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, "node_modules/@tauri-apps/api": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.8.0.tgz", @@ -2723,15 +2716,6 @@ "@babel/types": "^7.28.2" } }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2743,7 +2727,10 @@ "version": "24.8.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.0.tgz", "integrity": "sha512-5x08bUtU8hfboMTrJ7mEO4CpepS9yBwAqcL52y86SWNmbPX8LVbNs3EP4cNrIZgdjk2NAlP2ahNihozpoZIxSg==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "undici-types": "~7.14.0" } @@ -2941,15 +2928,6 @@ ], "license": "MIT" }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "license": "MIT", - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, "node_modules/baseline-browser-mapping": { "version": "2.8.16", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", @@ -3510,125 +3488,6 @@ "once": "^1.4.0" } }, - "node_modules/engine.io": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", - "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", - "license": "MIT", - "dependencies": { - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.7.2", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3890,6 +3749,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", + "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -4461,6 +4347,21 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5564,173 +5465,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/socket.io": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", - "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.6.0", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", - "license": "MIT", - "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" - } - }, - "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-client": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", - "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6102,7 +5836,10 @@ "version": "7.14.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", - "license": "MIT" + "dev": true, + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/unpipe": { "version": "1.0.0", @@ -6388,35 +6125,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "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/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/xterm": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", diff --git a/package.json b/package.json index 8068b4bd7..22b9371b6 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "clsx": "^2.1.1", "cors": "^2.8.5", "express": "^5.1.0", + "framer-motion": "^12.23.24", "lucide-react": "^0.546.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/sidebar-design-spec.json b/sidebar-design-spec.json new file mode 100644 index 000000000..39cb8847a --- /dev/null +++ b/sidebar-design-spec.json @@ -0,0 +1,431 @@ +{ + "sidebarDesignSpec": { + "version": "1.0.0", + "overview": { + "description": "Collapsible sidebar with GitHub profile and repository workspaces", + "implementation": "shadcn sidebar component with collapsible sections", + "layout": "Fixed left sidebar with main content area" + }, + + "structure": { + "container": { + "component": "Sidebar", + "states": ["expanded", "collapsed"], + "defaultState": "expanded", + "width": { + "expanded": "280px", + "collapsed": "60px" + }, + "background": "bg-sidebar", + "border": "border-r border-sidebar-border", + "transition": { + "property": "width", + "duration": "200ms", + "easing": "cubic-bezier(.165, .84, .44, 1)" + } + }, + + "sections": [ + { + "id": "header", + "component": "SidebarHeader", + "order": 1, + "content": { + "collapseToggle": { + "component": "Button", + "variant": "ghost", + "size": "icon", + "position": "top-left", + "icon": { + "expanded": "PanelLeftClose", + "collapsed": "PanelLeftOpen" + }, + "aria-label": "Toggle sidebar", + "padding": "p-4" + } + } + }, + + { + "id": "profile", + "component": "SidebarProfile", + "order": 2, + "padding": "px-4 pb-4", + "content": { + "avatar": { + "component": "Avatar", + "size": "h-8 w-8", + "fallback": "initials", + "source": "github.avatar_url" + }, + "info": { + "username": { + "component": "Text", + "source": "github.username", + "fontSize": "text-sm", + "fontWeight": "font-medium", + "color": "text-sidebar-foreground", + "showWhen": "expanded" + }, + "role": { + "component": "Text", + "source": "github.email", + "fontSize": "text-xs", + "color": "text-muted-foreground", + "showWhen": "expanded" + } + }, + "layout": { + "direction": "flex-row", + "align": "items-center", + "gap": "gap-3", + "collapsedLayout": "justify-center" + } + } + }, + + { + "id": "repositories", + "component": "SidebarContent", + "order": 3, + "scrollable": true, + "padding": "px-2", + "content": { + "repositoryList": { + "component": "SidebarGroup", + "items": "repositories[]", + "itemComponent": "RepositoryItem" + } + } + } + ] + }, + + "components": { + "RepositoryItem": { + "type": "CollapsibleSection", + "defaultState": "collapsed", + "persistState": true, + "stateKey": "sidebar.repo.{repoId}.expanded", + + "trigger": { + "component": "SidebarMenuButton", + "layout": "flex items-center justify-between w-full", + "padding": "px-3 py-2", + "hover": "hover:bg-sidebar-accent", + "transition": { + "properties": ["background-color"], + "duration": "200ms", + "easing": "ease" + }, + + "content": { + "leftSection": { + "layout": "flex items-center gap-3", + "icon": { + "component": "Icon", + "name": "FolderGit2", + "size": "h-4 w-4", + "color": "text-sidebar-foreground/70" + }, + "label": { + "component": "Text", + "source": "repository.name", + "fontSize": "text-sm", + "fontWeight": "font-medium", + "color": "text-sidebar-foreground", + "truncate": true, + "showWhen": "expanded" + } + }, + + "rightSection": { + "layout": "flex items-center gap-2", + "badge": { + "component": "Badge", + "variant": "secondary", + "size": "sm", + "content": "workspaces.length", + "showWhen": "expanded && workspaces.length > 0" + }, + "chevron": { + "component": "Icon", + "name": "ChevronDown", + "size": "h-4 w-4", + "color": "text-sidebar-foreground/50", + "rotate": { + "expanded": "0deg", + "collapsed": "-90deg" + }, + "transition": { + "property": "transform", + "duration": "200ms", + "easing": "cubic-bezier(.165, .84, .44, 1)" + }, + "showWhen": "expanded" + } + } + } + }, + + "content": { + "component": "CollapsibleContent", + "animation": { + "enter": { + "duration": "200ms", + "easing": "cubic-bezier(.165, .84, .44, 1)", + "from": "opacity-0 scale-y-95", + "to": "opacity-100 scale-y-100" + }, + "exit": { + "duration": "150ms", + "easing": "cubic-bezier(.165, .84, .44, 1)", + "from": "opacity-100 scale-y-100", + "to": "opacity-0 scale-y-95" + } + }, + + "workspaceList": { + "component": "SidebarMenuSub", + "padding": "pl-7 pt-1 pb-2", + "items": "repository.workspaces[]", + "itemComponent": "WorkspaceItem", + "emptyState": { + "component": "Text", + "text": "No workspaces", + "fontSize": "text-xs", + "color": "text-muted-foreground", + "padding": "py-2" + } + } + } + }, + + "WorkspaceItem": { + "component": "SidebarMenuSubButton", + "padding": "px-3 py-2", + "layout": "flex items-center gap-3", + "hover": "hover:bg-sidebar-accent", + "active": "bg-sidebar-accent", + "activeIndicator": { + "position": "left", + "width": "w-0.5", + "color": "bg-primary", + "height": "full" + }, + "transition": { + "properties": ["background-color"], + "duration": "200ms", + "easing": "ease" + }, + + "content": { + "icon": { + "component": "Icon", + "name": "GitBranch", + "size": "h-3.5 w-3.5", + "color": "text-sidebar-foreground/60" + }, + "info": { + "layout": "flex flex-col flex-1 min-w-0", + "name": { + "component": "Text", + "source": "workspace.name", + "fontSize": "text-sm", + "color": "text-sidebar-foreground", + "truncate": true + }, + "branch": { + "component": "Text", + "source": "workspace.branch", + "fontSize": "text-xs", + "color": "text-muted-foreground", + "truncate": true + } + }, + "status": { + "component": "StatusDot", + "size": "h-2 w-2", + "colors": { + "idle": "bg-muted", + "running": "bg-blue-500 animate-pulse", + "success": "bg-green-500", + "error": "bg-red-500" + }, + "source": "workspace.status" + } + } + } + }, + + "dataModel": { + "profile": { + "username": "string", + "email": "string", + "avatar_url": "string" + }, + + "repositories": [ + { + "id": "string (uuid)", + "name": "string", + "path": "string", + "expanded": "boolean", + "workspaces": [ + { + "id": "string (uuid)", + "name": "string", + "branch": "string", + "status": "enum['idle', 'running', 'success', 'error']", + "active": "boolean", + "created_at": "timestamp", + "updated_at": "timestamp" + } + ] + } + ] + }, + + "interactions": { + "collapseToggle": { + "action": "toggleSidebar", + "effect": "Toggle sidebar between expanded and collapsed states", + "persistState": true, + "stateKey": "sidebar.collapsed" + }, + + "repositoryClick": { + "action": "toggleRepository", + "effect": "Expand/collapse repository to show/hide workspaces", + "persistState": true, + "stateKey": "sidebar.repo.{repoId}.expanded" + }, + + "workspaceClick": { + "action": "selectWorkspace", + "effect": "Set workspace as active and navigate to workspace view", + "navigation": "/workspace/{workspaceId}" + }, + + "workspaceContextMenu": { + "trigger": "right-click", + "actions": [ + { + "label": "Open in Editor", + "icon": "ExternalLink", + "action": "openInEditor" + }, + { + "label": "Rename", + "icon": "Edit", + "action": "renameWorkspace" + }, + { + "type": "separator" + }, + { + "label": "Delete", + "icon": "Trash", + "variant": "destructive", + "action": "deleteWorkspace" + } + ] + } + }, + + "styling": { + "colors": { + "sidebar": "var(--sidebar-background)", + "sidebarForeground": "var(--sidebar-foreground)", + "sidebarBorder": "var(--sidebar-border)", + "sidebarAccent": "var(--sidebar-accent)", + "sidebarAccentForeground": "var(--sidebar-accent-foreground)" + }, + + "spacing": { + "padding": "16px", + "gap": "8px", + "itemPadding": "12px 16px" + }, + + "typography": { + "username": { + "size": "14px", + "weight": "500", + "lineHeight": "1.4" + }, + "email": { + "size": "12px", + "weight": "400", + "lineHeight": "1.4" + }, + "repository": { + "size": "14px", + "weight": "500", + "lineHeight": "1.4" + }, + "workspace": { + "size": "13px", + "weight": "400", + "lineHeight": "1.4" + } + }, + + "animations": { + "reducedMotion": { + "media": "prefers-reduced-motion", + "disableTransforms": true + } + } + }, + + "accessibility": { + "keyboardNavigation": { + "ArrowUp": "Navigate to previous item", + "ArrowDown": "Navigate to next item", + "ArrowLeft": "Collapse current repository", + "ArrowRight": "Expand current repository", + "Enter": "Select workspace", + "Space": "Toggle repository expansion" + }, + + "ariaLabels": { + "sidebar": "Main navigation sidebar", + "collapseButton": "Toggle sidebar", + "repositorySection": "Repository {name}", + "workspaceItem": "Workspace {name} on branch {branch}" + }, + + "focusManagement": { + "visible": "outline-2 outline-primary", + "order": "sequential" + } + }, + + "responsiveness": { + "breakpoints": { + "mobile": { + "maxWidth": "768px", + "behavior": "overlay", + "defaultState": "collapsed", + "backdrop": true + }, + "tablet": { + "minWidth": "769px", + "maxWidth": "1024px", + "width": { + "expanded": "240px", + "collapsed": "60px" + } + }, + "desktop": { + "minWidth": "1025px", + "width": { + "expanded": "280px", + "collapsed": "60px" + } + } + } + } + } +} diff --git a/src/Dashboard.tsx b/src/Dashboard.tsx index e1a9e659a..92f454766 100644 --- a/src/Dashboard.tsx +++ b/src/Dashboard.tsx @@ -1,5 +1,4 @@ import { useState, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; import { WorkspaceDetail } from "./WorkspaceDetail"; import { TerminalPanel } from "./TerminalPanel"; @@ -9,7 +8,6 @@ import { NewWorkspaceModal, DiffModal, SystemPromptModal, - RepoGroup as RepoGroupComponent, } from "./features/dashboard/components"; import { useDashboardData, @@ -21,14 +19,10 @@ import { Badge, EmptyState, Skeleton, - Sidebar, SidebarProvider, - SidebarContent, - SidebarHeader, - SidebarMenu, - SidebarMenuItem, - ScrollArea, + SidebarInset, } from "./components/ui"; +import { AppSidebar } from "./components/app-sidebar"; import { Card, CardHeader, CardTitle, CardContent } from "./components/ui/card"; import { Separator } from "./components/ui/separator"; import { FileText, Package, GitPullRequest, Archive, Square } from "lucide-react"; @@ -46,7 +40,6 @@ import type { // BASE_URL is now async - use getBaseURL() export function Dashboard() { - const navigate = useNavigate(); // Zustand stores - Global state const selectedWorkspace = useWorkspaceStore((state) => state.selectedWorkspace); @@ -58,14 +51,12 @@ export function Dashboard() { showNewWorkspaceModal, showSystemPromptModal, diffModal, - collapsedRepos, openNewWorkspaceModal, closeNewWorkspaceModal, openSystemPromptModal, closeSystemPromptModal, openDiffModal, closeDiffModal, - toggleRepoCollapse, } = useUIStore(); // Dashboard data hook - manages workspaces, stats @@ -235,6 +226,16 @@ export function Dashboard() { selectWorkspace(workspace); } + /** + * Handle creating a new workspace with optional repo pre-selection + */ + function handleNewWorkspace(repoId?: string) { + if (repoId) { + setSelectedRepoId(repoId); + } + openNewWorkspaceModal(); + } + /** * Load and display diff for a specific file */ @@ -308,96 +309,52 @@ export function Dashboard() { return ( + {/* Floating Sidebar */} + {loading ? ( +
+ + + + +
+ ) : repoGroups.length === 0 ? ( +
+ handleNewWorkspace()} + size="sm" + > + + Create Workspace + + } + /> +
+ ) : ( + + )} + + {/* Main Content with SidebarInset */} + - {/* LEFT SIDEBAR - shadcn Sidebar */} - - - - - -
-

Conductor

-
- -
- -
-
-
-
-
-
- -
- -
- - - - {loading ? ( -
- - - - -
- ) : repoGroups.length === 0 ? ( -
- openNewWorkspaceModal()} - size="sm" - > - + Create Workspace - - } - /> -
- ) : ( - repoGroups.map((group) => ( - toggleRepoCollapse(group.repo_id)} - onWorkspaceClick={handleWorkspaceClick} - /> - )) - )} -
-
-
-
- - - {/* MAIN CONTENT */}
@@ -671,6 +628,7 @@ export function Dashboard() {
+
{/* Modals */} = 2) { + return (parts[0][0] + parts[1][0]).toUpperCase(); + } + return repoName.slice(0, 2).toUpperCase(); +} + +// Helper function to generate consistent color from string +function getRepoColor(repoName: string): { bg: string; text: string } { + // Using neutral gray tones for a more subtle, professional look + return { + bg: 'bg-sidebar-accent', + text: 'text-sidebar-foreground/60' + }; +} + +interface Repository { + repo_id: string; + repo_name: string; + workspaces: Workspace[]; +} + +interface AppSidebarProps { + repositories: Repository[]; + selectedWorkspaceId: string | null; + diffStats: Record; + onWorkspaceClick: (workspace: Workspace) => void; + onNewWorkspace: (repoId?: string) => void; + profile?: { + username: string; + email?: string; + }; +} + +export function AppSidebar({ + repositories, + selectedWorkspaceId, + diffStats, + onWorkspaceClick, + onNewWorkspace, + profile = { username: "User" }, +}: AppSidebarProps) { + const navigate = useNavigate(); + const { state, toggleSidebar } = useSidebar(); + const { collapsedRepos, toggleRepoCollapse } = useUIStore(); + + const isExpanded = state === "expanded"; + + return ( + + {/* Header with Profile and Collapse Button */} + +
+ {isExpanded ? ( + <> +
navigate("/settings")} + > + + + {profile.username.slice(0, 2).toUpperCase()} + + +
+

{profile.username}

+
+
+ + + ) : ( + + )} +
+
+ + {/* Repositories List */} + + + + {repositories.map((repo) => ( + toggleRepoCollapse(repo.repo_id)} + onWorkspaceClick={onWorkspaceClick} + onNewWorkspace={onNewWorkspace} + sidebarExpanded={isExpanded} + /> + ))} + + + + + {/* Footer with Add Repository */} + + + + onNewWorkspace()} + tooltip={!isExpanded ? "New Workspace" : undefined} + > + + {isExpanded && New Workspace} + + + + +
+ ); +} + +interface RepositoryItemProps { + repository: Repository; + isCollapsed: boolean; + selectedWorkspaceId: string | null; + diffStats: Record; + onToggleCollapse: () => void; + onWorkspaceClick: (workspace: Workspace) => void; + onNewWorkspace: (repoId?: string) => void; + sidebarExpanded: boolean; +} + +function RepositoryItem({ + repository, + isCollapsed, + selectedWorkspaceId, + diffStats, + onToggleCollapse, + onWorkspaceClick, + onNewWorkspace, + sidebarExpanded, +}: RepositoryItemProps) { + const { toggleSidebar } = useSidebar(); + const hasRunningWorkspace = repository.workspaces.some( + (ws) => ws.session_status === "working" + ); + + const handleClick = (e: React.MouseEvent) => { + if (!sidebarExpanded) { + // When sidebar is collapsed, expand it and open the repository + e.preventDefault(); // Prevent default collapsible behavior + toggleSidebar(); + // Use setTimeout to ensure sidebar expands before toggling repository + setTimeout(() => { + if (isCollapsed) { + onToggleCollapse(); + } + }, 100); + } + // When expanded, let CollapsibleTrigger handle it naturally + }; + + return ( + + + + +
+ {!sidebarExpanded && ( +
+ {(() => { + const repoColor = getRepoColor(repository.repo_name); + return ( +
+ {getRepoInitials(repository.repo_name)} +
+ ); + })()} + {hasRunningWorkspace && ( + + + + + )} +
+ )} + {sidebarExpanded && ( + + {repository.repo_name} + + )} + {sidebarExpanded && ( + + )} +
+
+
+ + + {/* New Workspace Button - At Top, Compact Height */} + {sidebarExpanded && ( + + + + )} + + {repository.workspaces.map((workspace) => ( + onWorkspaceClick(workspace)} + /> + ))} + + +
+
+ ); +} + +interface WorkspaceItemProps { + workspace: Workspace; + isActive: boolean; + diffStats?: DiffStats; + onClick: () => void; +} + +function WorkspaceItem({ workspace, isActive, diffStats, onClick }: WorkspaceItemProps) { + const formatTime = (timestamp: string) => { + const date = new Date(timestamp); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffMins < 1) return "now"; + if (diffMins < 60) return `${diffMins}m`; + if (diffHours < 24) return `${diffHours}h`; + return `${diffDays}d`; + }; + + const getStatusText = (status: string | null | undefined) => { + if (!status) return "Archived"; + if (status === "idle") return formatTime(workspace.updated_at); + const capitalized = status.charAt(0).toUpperCase() + status.slice(1); + return shouldShimmer(status) ? `${capitalized}...` : capitalized; + }; + + const getStatusTextColor = (status: string | null | undefined) => { + switch (status) { + case "working": + return "text-blue-500"; + case "idle": + return "text-muted-foreground/70"; + case "compacting": + return "text-yellow-500"; + default: + return "text-rose-400"; + } + }; + + const shouldShimmer = (status: string | null | undefined) => { + return status === "working" || status === "compacting"; + }; + + const hasChanges = diffStats && (diffStats.additions > 0 || diffStats.deletions > 0); + + return ( + +
+
+ {workspace.session_status === "working" ? ( + + ) : ( + + )} +
+ {/* Branch name on top */} + + {workspace.branch} + + {/* Directory name and status on bottom */} +
+ + {workspace.directory_name} + + + {shouldShimmer(workspace.session_status) ? ( + + {getStatusText(workspace.session_status)} + + ) : ( + + {getStatusText(workspace.session_status)} + + )} +
+
+
+ {hasChanges && ( +
+ {diffStats.additions > 0 && ( + + +{diffStats.additions} + + )} + {diffStats.deletions > 0 && ( + + -{diffStats.deletions} + + )} +
+ )} +
+
+ ); +} diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 000000000..f3a1372a8 --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Avatar.displayName = "Avatar" + +const AvatarImage = React.forwardRef< + HTMLImageElement, + React.ImgHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = "AvatarImage" + +const AvatarFallback = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AvatarFallback.displayName = "AvatarFallback" + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index cba6ad719..7a824b9ca 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -48,10 +48,14 @@ export { SidebarRail, SidebarSeparator, SidebarTrigger, - useSidebar + useSidebar, } from './sidebar'; export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './tooltip'; +export { Avatar, AvatarImage, AvatarFallback } from './avatar'; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } from './collapsible'; + // Custom components (not shadcn) export { EmptyState } from './EmptyState'; diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index ed6f66dbd..98ae8851d 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -25,8 +25,8 @@ import { const SIDEBAR_COOKIE_NAME = "sidebar_state" const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 -const SIDEBAR_WIDTH = "16rem" -const SIDEBAR_WIDTH_MOBILE = "18rem" +const SIDEBAR_WIDTH = "22rem" +const SIDEBAR_WIDTH_MOBILE = "20rem" const SIDEBAR_WIDTH_ICON = "3rem" const SIDEBAR_KEYBOARD_SHORTCUT = "b" diff --git a/src/components/ui/text-shimmer.tsx b/src/components/ui/text-shimmer.tsx new file mode 100644 index 000000000..3f02924ee --- /dev/null +++ b/src/components/ui/text-shimmer.tsx @@ -0,0 +1,54 @@ +import React, { useMemo, type JSX } from 'react'; +import { motion } from 'framer-motion'; +import { cn } from '@/lib/utils'; + +interface TextShimmerProps { + children: string; + as?: React.ElementType; + className?: string; + duration?: number; + spread?: number; +} + +export function TextShimmer({ + children, + as: Component = 'p', + className, + duration = 2, + spread = 2, +}: TextShimmerProps) { + const MotionComponent = motion(Component as keyof JSX.IntrinsicElements); + + const dynamicSpread = useMemo(() => { + return children.length * spread; + }, [children, spread]); + + return ( + + {children} + + ); +} diff --git a/tailwind.config.js b/tailwind.config.js index fb0c7133a..b3aff80a2 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -139,13 +139,22 @@ module.exports = { opacity: '1', transform: 'translateY(0)' } + }, + shimmer: { + '0%, 100%': { + opacity: '1' + }, + '50%': { + opacity: '0.5' + } } }, animation: { 'accordion-down': 'accordion-down 0.2s ease-out', 'accordion-up': 'accordion-up 0.2s ease-out', 'fade-in': 'fadeIn 0.2s ease-out', - 'fade-in-up': 'fadeInUp 0.25s ease-out' + 'fade-in-up': 'fadeInUp 0.25s ease-out', + 'shimmer': 'shimmer 2s ease-in-out infinite' } } },