From 7b630e9527c26bc4f9c565fda0d816698e07a403 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 2 Apr 2026 11:42:46 +0000 Subject: [PATCH 1/8] refactor: migrate SectionHeader actions menu to PositionedMenu --- bun.lock | 64 +++--- .../SectionHeader/SectionHeader.test.tsx | 75 +++++- .../SectionHeader/SectionHeader.tsx | 213 ++++++++---------- 3 files changed, 202 insertions(+), 150 deletions(-) diff --git a/bun.lock b/bun.lock index 4ceb423c53..b53f6694fa 100644 --- a/bun.lock +++ b/bun.lock @@ -1468,7 +1468,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], @@ -3922,14 +3922,26 @@ "@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + "@jest/console/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "@jest/console/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "@jest/core/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "@jest/core/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "@jest/core/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@jest/core/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], + "@jest/environment/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + + "@jest/fake-timers/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + + "@jest/pattern/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + + "@jest/reporters/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "@jest/reporters/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@jest/reporters/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], @@ -3946,6 +3958,8 @@ "@jest/transform/write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], + "@jest/types/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], @@ -4038,33 +4052,17 @@ "@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], - "@types/asn1/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - - "@types/body-parser/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - - "@types/cacheable-request/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - - "@types/connect/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - - "@types/express-serve-static-core/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - - "@types/keyv/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - - "@types/plist/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], + "@types/cors/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "@types/responselike/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], + "@types/fs-extra/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "@types/send/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - - "@types/serve-static/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], + "@types/jsdom/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], "@types/ssh2/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], - "@types/sshpk/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - - "@types/wait-on/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], + "@types/write-file-atomic/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "@types/yauzl/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], + "@types/ws/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -4094,6 +4092,8 @@ "builder-util/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "bun-types/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "cacache/fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="], "cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], @@ -4140,8 +4140,6 @@ "dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], - "electron/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - "electron-builder/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "electron-publish/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -4202,6 +4200,8 @@ "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "happy-dom/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "hasha/type-fest": ["type-fest@0.8.1", "", {}, "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="], "hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], @@ -4222,8 +4222,6 @@ "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-circus/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - "jest-circus/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "jest-cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -4238,7 +4236,7 @@ "jest-each/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "jest-environment-node/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], + "jest-haste-map/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], "jest-haste-map/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -4250,18 +4248,20 @@ "jest-message-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "jest-mock/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "jest-process-manager/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "jest-process-manager/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "jest-resolve/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "jest-runner/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - "jest-runner/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "jest-runner/source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="], + "jest-runtime/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "jest-runtime/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "jest-runtime/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], @@ -4270,6 +4270,8 @@ "jest-snapshot/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "jest-util/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "jest-util/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], @@ -4278,12 +4280,16 @@ "jest-watch-typeahead/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], + "jest-watcher/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "jest-watcher/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "jest-watcher/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "jest-watcher/string-length": ["string-length@4.0.2", "", { "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" } }, "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="], + "jest-worker/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "jsdom/parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], "jsdom/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], diff --git a/src/browser/components/SectionHeader/SectionHeader.test.tsx b/src/browser/components/SectionHeader/SectionHeader.test.tsx index 7cdd215a72..9f4baa4379 100644 --- a/src/browser/components/SectionHeader/SectionHeader.test.tsx +++ b/src/browser/components/SectionHeader/SectionHeader.test.tsx @@ -1,10 +1,11 @@ import "../../../../tests/ui/dom"; import type { ComponentProps } from "react"; -import { describe, expect, mock, test } from "bun:test"; -import { fireEvent, render, waitFor } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"; +import { cleanup, fireEvent, render, waitFor } from "@testing-library/react"; import type { SectionConfig } from "@/common/types/project"; import { TooltipProvider } from "../Tooltip/Tooltip"; +import * as PositionedMenuModule from "../PositionedMenu/PositionedMenu"; import { SectionHeader } from "./SectionHeader"; @@ -47,11 +48,37 @@ function renderSectionHeader(overrides: Partial { + spyOn(PositionedMenuModule, "PositionedMenu").mockImplementation(((props: { + open: boolean; + children: React.ReactNode; + }) => + props.open ? ( +
{props.children}
+ ) : null) as unknown as typeof PositionedMenuModule.PositionedMenu); + + spyOn(PositionedMenuModule, "PositionedMenuItem").mockImplementation(((props: { + label: string; + disabled?: boolean; + onClick: (event: React.MouseEvent) => void; + }) => ( + + )) as unknown as typeof PositionedMenuModule.PositionedMenuItem); +}); + +afterEach(() => { + cleanup(); + mock.restore(); +}); + describe("SectionHeader auto-created section editing", () => { test("starts in edit mode when autoStartEditing is true", async () => { const view = renderSectionHeader(); @@ -101,3 +128,47 @@ describe("SectionHeader auto-created section editing", () => { expect(view.onRename).not.toHaveBeenCalled(); }); }); + +describe("SectionHeader actions menu", () => { + test("opens the menu when clicking section actions", () => { + const view = renderSectionHeader({ autoStartEditing: false }); + + expect(view.queryByTestId("section-actions-menu")).toBeNull(); + + fireEvent.click(view.getByLabelText("Section actions")); + + expect(view.getByTestId("section-actions-menu")).toBeTruthy(); + }); + + test("shows the color picker when selecting Change color", () => { + const view = renderSectionHeader({ autoStartEditing: false }); + + fireEvent.click(view.getByLabelText("Section actions")); + fireEvent.click(view.getByRole("button", { name: "Change color" })); + + expect(view.container.querySelector(".section-color-picker")).not.toBeNull(); + }); + + test("enters edit mode when selecting Rename", async () => { + const view = renderSectionHeader({ autoStartEditing: false }); + + fireEvent.click(view.getByLabelText("Section actions")); + fireEvent.click(view.getByRole("button", { name: "Rename" })); + + await waitFor(() => { + expect(view.getByTestId("section-rename-input")).toBeTruthy(); + }); + }); + + test("calls onDelete when selecting Delete section", () => { + const view = renderSectionHeader({ autoStartEditing: false }); + + fireEvent.click(view.getByLabelText("Section actions")); + + const deleteButton = view.getByRole("button", { name: "Delete section" }); + fireEvent.click(deleteButton); + + expect(view.onDelete).toHaveBeenCalledTimes(1); + expect(view.onDelete.mock.calls[0]?.[0]).toBe(deleteButton); + }); +}); diff --git a/src/browser/components/SectionHeader/SectionHeader.tsx b/src/browser/components/SectionHeader/SectionHeader.tsx index 14582dce87..cc6cc96b14 100644 --- a/src/browser/components/SectionHeader/SectionHeader.tsx +++ b/src/browser/components/SectionHeader/SectionHeader.tsx @@ -13,7 +13,8 @@ import type { SectionConfig } from "@/common/types/project"; import { Tooltip, TooltipTrigger, TooltipContent, TooltipIfPresent } from "../Tooltip/Tooltip"; import { resolveSectionColor, SECTION_COLOR_PALETTE } from "@/common/constants/ui"; import { HexColorPicker } from "react-colorful"; -import { PositionedMenuItem } from "../PositionedMenu/PositionedMenu"; +import { useContextMenuPosition } from "../../hooks/useContextMenuPosition"; +import { PositionedMenu, PositionedMenuItem } from "../PositionedMenu/PositionedMenu"; interface SectionHeaderProps { section: SectionConfig; @@ -48,11 +49,10 @@ export const SectionHeader: React.FC = ({ const [editValue, setEditValue] = useState(section.name); const [hasEditedName, setHasEditedName] = useState(false); const [showColorPicker, setShowColorPicker] = useState(false); - const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false); const [hexInputValue, setHexInputValue] = useState(section.color ?? ""); const inputRef = useRef(null); - const actionsMenuRef = useRef(null); const autoStartHandledRef = useRef(false); + const sectionMenu = useContextMenuPosition(); const startEditing = () => { setEditValue(section.name); @@ -77,28 +77,6 @@ export const SectionHeader: React.FC = ({ } }, [isEditing]); - useEffect(() => { - if (!isActionsMenuOpen) { - return; - } - - const handlePointerDown = (event: MouseEvent) => { - if ( - actionsMenuRef.current && - event.target instanceof Node && - !actionsMenuRef.current.contains(event.target) - ) { - setIsActionsMenuOpen(false); - setShowColorPicker(false); - } - }; - - document.addEventListener("mousedown", handlePointerDown); - return () => { - document.removeEventListener("mousedown", handlePointerDown); - }; - }, [isActionsMenuOpen]); - const handleSubmitRename = () => { const trimmed = editValue.trim(); if (trimmed && trimmed !== section.name) { @@ -217,105 +195,102 @@ export const SectionHeader: React.FC = ({ {/* Section actions kebab sits immediately to the right of New chat */} -
- - - - - Section actions - - - {isActionsMenuOpen && ( -
) => event.stopPropagation()} + + +
-
- { - setHexInputValue(newColor); - onChangeColor(newColor); - }} - /> -
-
- { - const value = e.target.value; - setHexInputValue(value); - if (/^#[0-9a-fA-F]{6}$/.test(value)) { - onChangeColor(value); - } + + + + Section actions + + + { + if (!open) { + setShowColorPicker(false); + } + sectionMenu.onOpenChange(open); + }} + position={sectionMenu.position} + > + } + label="Change color" + onClick={() => { + setShowColorPicker((open) => !open); + }} + /> + {showColorPicker && ( +
+
+ {SECTION_COLOR_PALETTE.map(([name, color]) => ( + +
-
- )} - } - label="Rename" - onClick={() => { - startEditing(); - setShowColorPicker(false); - setIsActionsMenuOpen(false); - }} - /> - } - label="Delete section" - variant="destructive" - onClick={(event) => { - onDelete(event.currentTarget); - setShowColorPicker(false); - setIsActionsMenuOpen(false); - }} - /> + + ))} +
+
+ { + setHexInputValue(newColor); + onChangeColor(newColor); + }} + /> +
+
+ { + const value = e.target.value; + setHexInputValue(value); + if (/^#[0-9a-fA-F]{6}$/.test(value)) { + onChangeColor(value); + } + }} + className="bg-background/50 text-foreground w-full rounded border border-white/20 px-1.5 py-0.5 text-xs outline-none select-text" + /> +
)} - + } + label="Rename" + onClick={() => { + startEditing(); + setShowColorPicker(false); + sectionMenu.close(); + }} + /> + } + label="Delete section" + variant="destructive" + onClick={(event) => { + onDelete(event.currentTarget); + setShowColorPicker(false); + sectionMenu.close(); + }} + /> + ); From 361f3d45dcbf7080b44517f7d7d24ab645dc18f4 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 2 Apr 2026 13:38:28 +0000 Subject: [PATCH 2/8] fix: toggle section kebab menu on repeated click --- src/browser/components/SectionHeader/SectionHeader.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/browser/components/SectionHeader/SectionHeader.tsx b/src/browser/components/SectionHeader/SectionHeader.tsx index cc6cc96b14..d6d15cbcb8 100644 --- a/src/browser/components/SectionHeader/SectionHeader.tsx +++ b/src/browser/components/SectionHeader/SectionHeader.tsx @@ -198,7 +198,13 @@ export const SectionHeader: React.FC = ({ -
+ + {multiProjectWorkspaces.length > 0 && (
@@ -2970,7 +2972,8 @@ const ProjectSidebarInner: React.FC = ({ ); }) )} -
+ + )} , + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)); +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; + +const ScrollAreaViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ScrollAreaViewport.displayName = ScrollAreaPrimitive.Viewport.displayName; + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)); +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; + +export { ScrollArea, ScrollAreaViewport, ScrollBar }; diff --git a/src/browser/styles/globals.css b/src/browser/styles/globals.css index e7f6ded47b..350ab712a9 100644 --- a/src/browser/styles/globals.css +++ b/src/browser/styles/globals.css @@ -124,6 +124,7 @@ --color-surface-destructive: hsla(0, 75%, 15%, 1); --color-surface-invert-secondary: hsla(240, 5%, 65%, 1); --color-surface-tertiary: hsla(240, 4%, 16%, 1); + --color-surface-quaternary: hsla(240, 5%, 26%, 1); --color-content-success: hsla(142, 76%, 36%, 1); --color-surface-green: hsla(145, 80%, 10%, 1); --color-border-pending: hsla(188, 75%, 80%, 1); @@ -427,6 +428,7 @@ --color-surface-destructive: hsla(0, 93%, 94%, 1); --color-surface-invert-secondary: hsla(240, 5%, 26%, 1); --color-surface-tertiary: hsla(240, 6%, 90%, 1); + --color-surface-quaternary: hsla(240, 5%, 84%, 1); --color-content-success: hsla(142, 72%, 29%, 1); --color-surface-green: hsla(141, 79%, 85%, 1); --color-border-pending: hsla(194, 49%, 45%, 1); @@ -1106,37 +1108,19 @@ body { font-size: 14px; } -/* Custom scrollbar styles */ -::-webkit-scrollbar { - width: 10px; - height: 10px; -} - -::-webkit-scrollbar-track { - background: var(--color-scrollbar-track); +/* Global scrollbar styles */ +* { + scrollbar-width: thin; + scrollbar-color: var(--color-surface-quaternary) transparent; } ::-webkit-scrollbar-thumb { - background: var(--color-scrollbar-thumb); - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: var(--color-scrollbar-thumb-hover); -} - -/* Firefox scrollbar */ -* { - scrollbar-width: thin; - scrollbar-color: var(--color-scrollbar-thumb) var(--color-scrollbar-track); + background: var(--color-surface-quaternary); } /* * Override for Tailwind's scrollbar-none utility. - * The global ::-webkit-scrollbar rules above opt Chromium/Electron into - * WebKit custom scrollbar rendering, which ignores the standard - * scrollbar-width: none that scrollbar-none generates. This explicit - * pseudo-element override restores the hidden behavior. + * Keep this as a defensive Chromium/Electron fallback for full hiding. */ .scrollbar-none::-webkit-scrollbar { display: none; From 621a975eb0f82300b0b4ccf612db47b861f20f33 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Fri, 3 Apr 2026 13:47:30 +0700 Subject: [PATCH 5/8] fix: fix scrollarea --- .../ProjectSidebar/ProjectSidebar.tsx | 2185 +++++++++-------- .../components/ScrollArea/ScrollArea.tsx | 26 +- src/browser/styles/globals.css | 2 + 3 files changed, 1123 insertions(+), 1090 deletions(-) diff --git a/src/browser/components/ProjectSidebar/ProjectSidebar.tsx b/src/browser/components/ProjectSidebar/ProjectSidebar.tsx index 4bd898d803..274ad5bdf6 100644 --- a/src/browser/components/ProjectSidebar/ProjectSidebar.tsx +++ b/src/browser/components/ProjectSidebar/ProjectSidebar.tsx @@ -107,7 +107,7 @@ import { WorkspaceDragLayer } from "../WorkspaceDragLayer/WorkspaceDragLayer"; import { SectionDragLayer } from "../SectionDragLayer/SectionDragLayer"; import { DraggableSection } from "../DraggableSection/DraggableSection"; import { Separator } from "../Separator/Separator"; -import { ScrollArea, ScrollAreaViewport } from "../ScrollArea/ScrollArea"; +import { ScrollArea } from "../ScrollArea/ScrollArea"; import type { SectionConfig } from "@/common/types/project"; import { getErrorMessage } from "@/common/utils/errors"; import { isMultiProject } from "@/common/utils/multiProject"; @@ -1783,1196 +1783,1211 @@ const ProjectSidebarInner: React.FC = ({ Add Project
- - - {multiProjectWorkspaces.length > 0 && ( -
-
+ + {multiProjectWorkspaces.length > 0 && ( +
+
+ +
+ + Multi-Project + + + ({multiProjectWorkspaces.length}) + +
+
+ {isMultiProjectSectionExpanded && ( +
+ {visibleMultiProjectWorkspaces.map((metadata) => { + const rowRenderMeta = multiProjectRowMetaByWorkspaceId.get(metadata.id); + + return ( + + ); + })} +
+ )} +
+ )} + + {sortedProjectPaths.length === 0 && multiProjectWorkspaces.length === 0 ? ( +
+

No projects

-
- - Multi-Project - - - ({multiProjectWorkspaces.length}) - -
- {isMultiProjectSectionExpanded && ( -
- {visibleMultiProjectWorkspaces.map((metadata) => { - const rowRenderMeta = multiProjectRowMetaByWorkspaceId.get(metadata.id); - - return ( - { + const config = userProjects.get(projectPath); + if (!config) return null; + const projectFolderColor = config.color + ? resolveSectionColor(config.color) + : undefined; + const projectName = getProjectNameFromPath(projectPath); + const sanitizedProjectId = + projectPath.replace(/[^a-zA-Z0-9_-]/g, "-") || "root"; + const workspaceListId = `workspace-list-${sanitizedProjectId}`; + const isExpanded = expandedProjectsList.includes(projectPath); + const displayProjectName = + config.displayName ?? getProjectFallbackLabel(projectPath); + const isEditingProjectDisplayName = editingProjectPath === projectPath; + const projectWorkspaces = + singleProjectWorkspacesByProject.get(projectPath) ?? []; + const projectAgentCount = projectWorkspaces.length; + const projectHasAttention = projectWorkspaces.some( + (workspace) => workspaceAttentionById.get(workspace.id) === true + ); + + return ( +
+ { + if (projectContextMenu.suppressClickIfLongPress()) { + return; } - onSelectWorkspace={handleSelectWorkspace} - onForkWorkspace={handleForkWorkspace} - onArchiveWorkspace={handleArchiveWorkspace} - onCancelCreation={handleCancelWorkspaceCreation} - depth={ - rowRenderMeta?.depth ?? - multiProjectDepthByWorkspaceId[metadata.id] ?? - 0 + if (isEditingProjectDisplayName) { + return; } - rowRenderMeta={rowRenderMeta} - completedChildrenExpanded={ - expandedCompletedSubAgents[metadata.id] ?? false - } - onToggleCompletedChildren={toggleCompletedChildrenExpansion} - /> - ); - })} -
- )} -
- )} - - {sortedProjectPaths.length === 0 && multiProjectWorkspaces.length === 0 ? ( -
-

No projects

- -
- ) : ( - sortedProjectPaths.map((projectPath) => { - const config = userProjects.get(projectPath); - if (!config) return null; - const projectFolderColor = config.color - ? resolveSectionColor(config.color) - : undefined; - const projectName = getProjectNameFromPath(projectPath); - const sanitizedProjectId = - projectPath.replace(/[^a-zA-Z0-9_-]/g, "-") || "root"; - const workspaceListId = `workspace-list-${sanitizedProjectId}`; - const isExpanded = expandedProjectsList.includes(projectPath); - const displayProjectName = - config.displayName ?? getProjectFallbackLabel(projectPath); - const isEditingProjectDisplayName = editingProjectPath === projectPath; - const projectWorkspaces = - singleProjectWorkspacesByProject.get(projectPath) ?? []; - const projectAgentCount = projectWorkspaces.length; - const projectHasAttention = projectWorkspaces.some( - (workspace) => workspaceAttentionById.get(workspace.id) === true - ); - - return ( -
- { - if (projectContextMenu.suppressClickIfLongPress()) { - return; - } - if (isEditingProjectDisplayName) { - return; - } - handleAddWorkspace(projectPath); - }} - onContextMenu={(event) => handleOpenProjectMenu(event, projectPath)} - onTouchStart={(event) => - handleProjectContextMenuTouchStart(event, projectPath) - } - onTouchEnd={projectContextMenu.touchHandlers.onTouchEnd} - onTouchMove={projectContextMenu.touchHandlers.onTouchMove} - onKeyDown={(e: React.KeyboardEvent) => { - // Ignore key events from child buttons - if (e.target instanceof HTMLElement && e.target !== e.currentTarget) { - return; - } - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); handleAddWorkspace(projectPath); + }} + onContextMenu={(event) => handleOpenProjectMenu(event, projectPath)} + onTouchStart={(event) => + handleProjectContextMenuTouchStart(event, projectPath) } - }} - role="button" - tabIndex={0} - aria-expanded={isExpanded} - aria-controls={workspaceListId} - aria-label={`Create workspace in ${projectName}`} - data-project-path={projectPath} - > - -
handleOpenProjectMenu(event, projectPath)} - > - - - {isEditingProjectDisplayName ? ( - event.stopPropagation()} - onMouseDown={(event) => event.stopPropagation()} - onContextMenu={(event) => event.stopPropagation()} - onChange={(event) => { - setEditingProjectDisplayName(event.target.value); - }} - onKeyDown={(event) => { - stopKeyboardPropagation(event); - if (event.key === "Escape") { - event.preventDefault(); - skipNextProjectNameBlurCommitRef.current = true; - cancelProjectDisplayNameEditing(); - return; - } - - if (event.key === "Enter") { - event.preventDefault(); - event.currentTarget.blur(); - } - }} - onBlur={(event) => { - event.stopPropagation(); - if (skipNextProjectNameBlurCommitRef.current) { - skipNextProjectNameBlurCommitRef.current = false; - return; - } - void commitProjectDisplayNameEdit( - projectPath, - event.currentTarget.value - ); - }} + {isExpanded ? ( + ) : ( -
- - {displayProjectName} - - - ({projectAgentCount}) - -
+ )} + + +
handleOpenProjectMenu(event, projectPath)} + > + + + {isEditingProjectDisplayName ? ( + event.stopPropagation()} + onMouseDown={(event) => event.stopPropagation()} + onContextMenu={(event) => event.stopPropagation()} + onChange={(event) => { + setEditingProjectDisplayName(event.target.value); + }} + onKeyDown={(event) => { + stopKeyboardPropagation(event); + if (event.key === "Escape") { + event.preventDefault(); + skipNextProjectNameBlurCommitRef.current = true; + cancelProjectDisplayNameEditing(); + return; + } + + if (event.key === "Enter") { + event.preventDefault(); + event.currentTarget.blur(); + } + }} + onBlur={(event) => { + event.stopPropagation(); + if (skipNextProjectNameBlurCommitRef.current) { + skipNextProjectNameBlurCommitRef.current = false; + return; + } + void commitProjectDisplayNameEdit( + projectPath, + event.currentTarget.value + ); + }} + /> + ) : ( +
+ + {displayProjectName} + + + ({projectAgentCount}) + +
+ )} +
+ {projectPath} +
+
+ + + - {projectPath} + + New chat ({formatKeybind(KEYBINDS.NEW_WORKSPACE)}) + -
- - - - - - New chat ({formatKeybind(KEYBINDS.NEW_WORKSPACE)}) - - - - - - - Project options - -
- - {isExpanded && ( -
- + )} +
+ ); + }) + )} )} diff --git a/src/browser/components/ScrollArea/ScrollArea.tsx b/src/browser/components/ScrollArea/ScrollArea.tsx index 22bf87a336..bb3d2684ef 100644 --- a/src/browser/components/ScrollArea/ScrollArea.tsx +++ b/src/browser/components/ScrollArea/ScrollArea.tsx @@ -4,10 +4,22 @@ import { cn } from "@/common/lib/utils"; const ScrollArea = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - + React.ComponentPropsWithoutRef & { + viewportRef?: React.Ref>; + viewportClassName?: string; + onViewportScroll?: React.UIEventHandler; + } +>(({ className, children, viewportRef, viewportClassName, onViewportScroll, ...props }, ref) => ( + + {children} @@ -20,7 +32,11 @@ const ScrollAreaViewport = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); ScrollAreaViewport.displayName = ScrollAreaPrimitive.Viewport.displayName; diff --git a/src/browser/styles/globals.css b/src/browser/styles/globals.css index 350ab712a9..6a0a69d319 100644 --- a/src/browser/styles/globals.css +++ b/src/browser/styles/globals.css @@ -689,6 +689,7 @@ --color-surface-destructive: color-mix(in srgb, var(--color-danger) 12%, var(--color-background) 88%); --color-surface-invert-secondary: #403e3c; --color-surface-tertiary: #f2f0e5; + --color-surface-quaternary: #dad8ce; --color-content-success: #66800b; --color-surface-green: color-mix(in srgb, var(--color-success) 20%, var(--color-background) 80%); --color-border-pending: #24837b; @@ -921,6 +922,7 @@ --color-surface-destructive: color-mix(in srgb, var(--color-danger) 16%, var(--color-background) 84%); --color-surface-invert-secondary: #b7b5ac; --color-surface-tertiary: #282726; + --color-surface-quaternary: #343331; --color-content-success: #879a39; --color-surface-green: color-mix(in srgb, var(--color-success) 18%, var(--color-background) 82%); --color-border-pending: #3aa99f; From 1425eb06a124c27c274cc16d29b0b0b6da45269b Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Fri, 3 Apr 2026 22:13:37 +0700 Subject: [PATCH 6/8] fix: Keep kebab click from reopening an already-open menu --- src/browser/components/SectionHeader/SectionHeader.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/browser/components/SectionHeader/SectionHeader.tsx b/src/browser/components/SectionHeader/SectionHeader.tsx index ddef3c8f54..ea8d47b3dc 100644 --- a/src/browser/components/SectionHeader/SectionHeader.tsx +++ b/src/browser/components/SectionHeader/SectionHeader.tsx @@ -52,6 +52,7 @@ export const SectionHeader: React.FC = ({ const [hexInputValue, setHexInputValue] = useState(section.color ?? ""); const inputRef = useRef(null); const autoStartHandledRef = useRef(false); + const wasMenuOpenOnPointerDownRef = useRef(false); const sectionMenu = useContextMenuPosition(); const startEditing = () => { @@ -198,8 +199,15 @@ export const SectionHeader: React.FC = ({ -
- - Multi-Project - - - ({multiProjectWorkspaces.length}) - -
-
- {isMultiProjectSectionExpanded && ( -
- {visibleMultiProjectWorkspaces.map((metadata) => { - const rowRenderMeta = multiProjectRowMetaByWorkspaceId.get(metadata.id); - - return ( - - ); - })} -
- )} -
- )} - - {sortedProjectPaths.length === 0 && multiProjectWorkspaces.length === 0 ? ( -
-

No projects

+ {multiProjectWorkspaces.length > 0 && ( +
+
+
+ + Multi-Project + + + ({multiProjectWorkspaces.length}) + +
- ) : ( - sortedProjectPaths.map((projectPath) => { - const config = userProjects.get(projectPath); - if (!config) return null; - const projectFolderColor = config.color - ? resolveSectionColor(config.color) - : undefined; - const projectName = getProjectNameFromPath(projectPath); - const sanitizedProjectId = - projectPath.replace(/[^a-zA-Z0-9_-]/g, "-") || "root"; - const workspaceListId = `workspace-list-${sanitizedProjectId}`; - const isExpanded = expandedProjectsList.includes(projectPath); - const displayProjectName = - config.displayName ?? getProjectFallbackLabel(projectPath); - const isEditingProjectDisplayName = editingProjectPath === projectPath; - const projectWorkspaces = - singleProjectWorkspacesByProject.get(projectPath) ?? []; - const projectAgentCount = projectWorkspaces.length; - const projectHasAttention = projectWorkspaces.some( - (workspace) => workspaceAttentionById.get(workspace.id) === true - ); - - return ( -
- { - if (projectContextMenu.suppressClickIfLongPress()) { - return; + {isMultiProjectSectionExpanded && ( +
+ {visibleMultiProjectWorkspaces.map((metadata) => { + const rowRenderMeta = multiProjectRowMetaByWorkspaceId.get(metadata.id); + + return ( + + ); + })} +
+ )} +
+ )} + + {sortedProjectPaths.length === 0 && multiProjectWorkspaces.length === 0 ? ( +
+

No projects

+ +
+ ) : ( + sortedProjectPaths.map((projectPath) => { + const config = userProjects.get(projectPath); + if (!config) return null; + const projectFolderColor = config.color + ? resolveSectionColor(config.color) + : undefined; + const projectName = getProjectNameFromPath(projectPath); + const sanitizedProjectId = + projectPath.replace(/[^a-zA-Z0-9_-]/g, "-") || "root"; + const workspaceListId = `workspace-list-${sanitizedProjectId}`; + const isExpanded = expandedProjectsList.includes(projectPath); + const displayProjectName = + config.displayName ?? getProjectFallbackLabel(projectPath); + const isEditingProjectDisplayName = editingProjectPath === projectPath; + const projectWorkspaces = + singleProjectWorkspacesByProject.get(projectPath) ?? []; + const projectAgentCount = projectWorkspaces.length; + const projectHasAttention = projectWorkspaces.some( + (workspace) => workspaceAttentionById.get(workspace.id) === true + ); + + return ( +
+ { + if (projectContextMenu.suppressClickIfLongPress()) { + return; + } + if (isEditingProjectDisplayName) { + return; + } + handleAddWorkspace(projectPath); + }} + onContextMenu={(event) => handleOpenProjectMenu(event, projectPath)} + onTouchStart={(event) => + handleProjectContextMenuTouchStart(event, projectPath) + } + onTouchEnd={projectContextMenu.touchHandlers.onTouchEnd} + onTouchMove={projectContextMenu.touchHandlers.onTouchMove} + onKeyDown={(e: React.KeyboardEvent) => { + // Ignore key events from child buttons + if (e.target instanceof HTMLElement && e.target !== e.currentTarget) { + return; + } + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); handleAddWorkspace(projectPath); - }} - onContextMenu={(event) => handleOpenProjectMenu(event, projectPath)} - onTouchStart={(event) => - handleProjectContextMenuTouchStart(event, projectPath) } - onTouchEnd={projectContextMenu.touchHandlers.onTouchEnd} - onTouchMove={projectContextMenu.touchHandlers.onTouchMove} - onKeyDown={(e: React.KeyboardEvent) => { - // Ignore key events from child buttons - if (e.target instanceof HTMLElement && e.target !== e.currentTarget) { - return; - } - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - handleAddWorkspace(projectPath); - } + }} + role="button" + tabIndex={0} + aria-expanded={isExpanded} + aria-controls={workspaceListId} + aria-label={`Create workspace in ${projectName}`} + data-project-path={projectPath} + > + -
handleOpenProjectMenu(event, projectPath)} - > - - - {isEditingProjectDisplayName ? ( - event.stopPropagation()} - onMouseDown={(event) => event.stopPropagation()} - onContextMenu={(event) => event.stopPropagation()} - onChange={(event) => { - setEditingProjectDisplayName(event.target.value); - }} - onKeyDown={(event) => { - stopKeyboardPropagation(event); - if (event.key === "Escape") { - event.preventDefault(); - skipNextProjectNameBlurCommitRef.current = true; - cancelProjectDisplayNameEditing(); - return; - } - - if (event.key === "Enter") { - event.preventDefault(); - event.currentTarget.blur(); - } - }} - onBlur={(event) => { - event.stopPropagation(); - if (skipNextProjectNameBlurCommitRef.current) { - skipNextProjectNameBlurCommitRef.current = false; - return; - } - void commitProjectDisplayNameEdit( - projectPath, - event.currentTarget.value - ); - }} - /> - ) : ( -
- - {displayProjectName} - - - ({projectAgentCount}) - -
- )} -
- {projectPath} -
-
- - - - - - New chat ({formatKeybind(KEYBINDS.NEW_WORKSPACE)}) - - + ) : ( + + )} + + +
handleOpenProjectMenu(event, projectPath)} + > - + ) : ( +
+ + {displayProjectName} + + + ({projectAgentCount}) + +
+ )}
- Project options + {projectPath}
- - - {isExpanded && ( +
+ + + + + + New chat ({formatKeybind(KEYBINDS.NEW_WORKSPACE)}) + + + + + + + Project options + +
+ + {isExpanded && ( +
- + + {isTierExpanded && ( + <> + {renderWorkspaceRowsWithTaskGroupCoalescing({ + rows: bucket, + allRows: allRowsForTaskGroupCoalescing, + sectionId, + rowMetaByWorkspaceId: rowMetaByVisibleWorkspaceId, + })} + {(() => { + const nextTier = findNextNonEmptyTier( + buckets, + tierIndex + 1 + ); + return nextTier !== -1 ? renderTier(nextTier) : null; + })()} + + )} + ); }; - // Partition both the full section membership and the filtered visible rows. - // Best-of grouping stays leaf-only by consulting the unfiltered section data, - // while actual rendering still follows the visible hierarchy. - const { - unsectioned: allUnsectionedForNormalRendering, - bySectionId: allBySectionIdForNormalRendering, - } = partitionWorkspacesBySection( - workspacesForNormalRendering, - sections - ); - const { unsectioned, bySectionId } = partitionWorkspacesBySection( - visibleWorkspacesForNormalRendering, - sections + return ( + <> + {renderWorkspaceRowsWithTaskGroupCoalescing({ + rows: topVisibleRows, + allRows: allRowsForTaskGroupCoalescing, + sectionId, + rowMetaByWorkspaceId: rowMetaByVisibleWorkspaceId, + })} + {firstTier !== -1 && renderTier(firstTier)} + ); - - // Handle workspace drop into section - const handleWorkspaceSectionDrop = ( - workspaceId: string, - targetSectionId: string | null - ) => { - void (async () => { - const result = await assignWorkspaceToSection( - projectPath, - workspaceId, - targetSectionId - ); - if (result.success) { - // Refresh workspace metadata so UI shows updated sectionId - await refreshWorkspaceMetadata(); - } - })(); - }; - - // Handle section reorder (drag section onto another section) - const handleSectionReorder = ( - draggedSectionId: string, - targetSectionId: string - ) => { - void (async () => { - // Compute new order: move dragged section to position of target - const currentOrder = sections.map((s) => s.id); - const draggedIndex = currentOrder.indexOf(draggedSectionId); - const targetIndex = currentOrder.indexOf(targetSectionId); - - if (draggedIndex === -1 || targetIndex === -1) return; - - // Remove dragged from current position - const newOrder = [...currentOrder]; - newOrder.splice(draggedIndex, 1); - // Insert at target position - newOrder.splice(targetIndex, 0, draggedSectionId); - - await reorderSections(projectPath, newOrder); - })(); - }; - - // Render section with its workspaces - const renderSection = (section: SectionConfig) => { - const sectionWorkspaces = bySectionId.get(section.id) ?? []; - const sectionAllWorkspaces = - allBySectionIdForNormalRendering.get(section.id) ?? []; - const sectionDrafts = draftsBySectionId.get(section.id) ?? []; - const sectionHasPromotedAttention = sectionDrafts.some( - (draft) => { - const promotedMetadata = activeDraftPromotions[draft.draftId]; - return promotedMetadata - ? workspaceAttentionById.get(promotedMetadata.id) === true - : false; - } - ); - const sectionHasAttention = - sectionAllWorkspaces.some( - (workspace) => - workspaceAttentionById.get(workspace.id) === true - ) || sectionHasPromotedAttention; - - const sectionExpandedKey = getSectionExpandedKey( + }; + + // Partition both the full section membership and the filtered visible rows. + // Best-of grouping stays leaf-only by consulting the unfiltered section data, + // while actual rendering still follows the visible hierarchy. + const { + unsectioned: allUnsectionedForNormalRendering, + bySectionId: allBySectionIdForNormalRendering, + } = partitionWorkspacesBySection( + workspacesForNormalRendering, + sections + ); + const { unsectioned, bySectionId } = partitionWorkspacesBySection( + visibleWorkspacesForNormalRendering, + sections + ); + + // Handle workspace drop into section + const handleWorkspaceSectionDrop = ( + workspaceId: string, + targetSectionId: string | null + ) => { + void (async () => { + const result = await assignWorkspaceToSection( projectPath, - section.id + workspaceId, + targetSectionId ); - const isSectionExpanded = - expandedSections[sectionExpandedKey] ?? true; - const shouldAutoEditSection = - autoEditingSection?.projectPath === projectPath && - autoEditingSection?.sectionId === section.id; + if (result.success) { + // Refresh workspace metadata so UI shows updated sectionId + await refreshWorkspaceMetadata(); + } + })(); + }; + + // Handle section reorder (drag section onto another section) + const handleSectionReorder = ( + draggedSectionId: string, + targetSectionId: string + ) => { + void (async () => { + // Compute new order: move dragged section to position of target + const currentOrder = sections.map((s) => s.id); + const draggedIndex = currentOrder.indexOf(draggedSectionId); + const targetIndex = currentOrder.indexOf(targetSectionId); + + if (draggedIndex === -1 || targetIndex === -1) return; + + // Remove dragged from current position + const newOrder = [...currentOrder]; + newOrder.splice(draggedIndex, 1); + // Insert at target position + newOrder.splice(targetIndex, 0, draggedSectionId); + + await reorderSections(projectPath, newOrder); + })(); + }; + + // Render section with its workspaces + const renderSection = (section: SectionConfig) => { + const sectionWorkspaces = bySectionId.get(section.id) ?? []; + const sectionAllWorkspaces = + allBySectionIdForNormalRendering.get(section.id) ?? []; + const sectionDrafts = draftsBySectionId.get(section.id) ?? []; + const sectionHasPromotedAttention = sectionDrafts.some((draft) => { + const promotedMetadata = activeDraftPromotions[draft.draftId]; + return promotedMetadata + ? workspaceAttentionById.get(promotedMetadata.id) === true + : false; + }); + const sectionHasAttention = + sectionAllWorkspaces.some( + (workspace) => workspaceAttentionById.get(workspace.id) === true + ) || sectionHasPromotedAttention; + + const sectionExpandedKey = getSectionExpandedKey( + projectPath, + section.id + ); + const isSectionExpanded = + expandedSections[sectionExpandedKey] ?? true; + const shouldAutoEditSection = + autoEditingSection?.projectPath === projectPath && + autoEditingSection?.sectionId === section.id; - return ( - + - - - toggleSection(projectPath, section.id) - } - onAddWorkspace={() => { - // Create workspace in this section - handleAddWorkspace(projectPath, section.id); - }} - onRename={(name) => { - if (shouldAutoEditSection) { - setAutoEditingSection(null); - } - void updateSection(projectPath, section.id, { name }); - }} - onChangeColor={(color) => { - void updateSection(projectPath, section.id, { color }); - }} - autoStartEditing={shouldAutoEditSection} - onAutoCreateAbandon={ - shouldAutoEditSection - ? () => { - void (async () => { - setAutoEditingSection(null); - await handleRemoveSection( - projectPath, - section.id - ); - })(); - } - : undefined + + toggleSection(projectPath, section.id) + } + onAddWorkspace={() => { + // Create workspace in this section + handleAddWorkspace(projectPath, section.id); + }} + onRename={(name) => { + if (shouldAutoEditSection) { + setAutoEditingSection(null); } - onAutoCreateRenameCancel={ - shouldAutoEditSection - ? () => { + void updateSection(projectPath, section.id, { name }); + }} + onChangeColor={(color) => { + void updateSection(projectPath, section.id, { color }); + }} + autoStartEditing={shouldAutoEditSection} + onAutoCreateAbandon={ + shouldAutoEditSection + ? () => { + void (async () => { setAutoEditingSection(null); - } - : undefined - } - onDelete={(anchorEl) => { - void handleRemoveSection( - projectPath, + await handleRemoveSection( + projectPath, + section.id + ); + })(); + } + : undefined + } + onAutoCreateRenameCancel={ + shouldAutoEditSection + ? () => { + setAutoEditingSection(null); + } + : undefined + } + onDelete={(anchorEl) => { + void handleRemoveSection( + projectPath, + section.id, + anchorEl + ); + }} + /> + {isSectionExpanded && ( +
+ {sectionDrafts.map((draft) => renderDraft(draft))} + {sectionWorkspaces.length > 0 ? ( + renderAgeTiers( + sectionWorkspaces, + getSectionTierKey(projectPath, section.id, 0).replace( + ":tier:0", + ":tier" + ), section.id, - anchorEl - ); - }} - /> - {isSectionExpanded && ( -
- {sectionDrafts.map((draft) => renderDraft(draft))} - {sectionWorkspaces.length > 0 ? ( - renderAgeTiers( - sectionWorkspaces, - getSectionTierKey( - projectPath, - section.id, - 0 - ).replace(":tier:0", ":tier"), - section.id, - sectionAllWorkspaces - ) - ) : sectionDrafts.length === 0 ? ( -
- No chats in this sub-folder -
- ) : null} -
+ sectionAllWorkspaces + ) + ) : sectionDrafts.length === 0 ? ( +
+ No chats in this sub-folder +
+ ) : null} +
+ )} +
+
+ ); + }; + + return ( + <> + {projectHasNoAgentsOrDrafts && ( +
+ Empty +
+ )} + {/* Unsectioned workspaces first - always show drop zone when sections exist */} + {sections.length > 0 ? ( + + {unsectionedDrafts.map((draft) => renderDraft(draft))} + {unsectioned.length > 0 ? ( + renderAgeTiers( + unsectioned, + getTierKey(projectPath, 0).replace(":0", ""), + undefined, + allUnsectionedForNormalRendering + ) + ) : unsectionedDrafts.length === 0 ? ( +
+ No unsectioned chats +
+ ) : null} +
+ ) : ( + <> + {unsectionedDrafts.map((draft) => renderDraft(draft))} + {unsectioned.length > 0 && + renderAgeTiers( + unsectioned, + getTierKey(projectPath, 0).replace(":0", ""), + undefined, + allUnsectionedForNormalRendering )} - - - ); - }; + + )} - return ( - <> - {projectHasNoAgentsOrDrafts && ( -
- Empty -
- )} - {/* Unsectioned workspaces first - always show drop zone when sections exist */} - {sections.length > 0 ? ( - - {unsectionedDrafts.map((draft) => renderDraft(draft))} - {unsectioned.length > 0 ? ( - renderAgeTiers( - unsectioned, - getTierKey(projectPath, 0).replace(":0", ""), - undefined, - allUnsectionedForNormalRendering - ) - ) : unsectionedDrafts.length === 0 ? ( -
- No unsectioned chats -
- ) : null} -
- ) : ( - <> - {unsectionedDrafts.map((draft) => renderDraft(draft))} - {unsectioned.length > 0 && - renderAgeTiers( - unsectioned, - getTierKey(projectPath, 0).replace(":0", ""), - undefined, - allUnsectionedForNormalRendering - )} - - )} - - {/* Sections */} - {sections.map(renderSection)} - - ); - })()} -
- )} -
- ); - }) - )} + {/* Sections */} + {sections.map(renderSection)} + + ); + })()} +
+ )} +
+ ); + }) + )} )}