From d26067603f1fe55a540b334f0cd8a23655b64745 Mon Sep 17 00:00:00 2001 From: Evan Date: Wed, 22 Apr 2026 16:38:10 +0100 Subject: [PATCH 01/15] feat: change icon --- .../_components/browser-action-list.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx b/src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx index 44df18a8..41c38892 100644 --- a/src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx +++ b/src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx @@ -5,7 +5,7 @@ import { PortalPopover } from "@/components/portal/popover"; import { PopoverTrigger } from "@/components/ui/popover"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; -import { CogIcon, LayersIcon, PackageXIcon, PinIcon, PinOffIcon, PuzzleIcon } from "lucide-react"; +import { CogIcon, LayersIcon, PackageXIcon, PinIcon, PinOffIcon, PuzzleIcon, Settings2Icon } from "lucide-react"; import { MouseEvent, useCallback, useMemo, useRef, useState } from "react"; import { useFocusedTab } from "@/components/providers/tabs-provider"; @@ -181,7 +181,16 @@ export function BrowserActionList() { event.stopPropagation(); }} > - +
+ +
From 3a86a1d4fc6e6e6e6f2a87f4dca2435acb854f91 Mon Sep 17 00:00:00 2001 From: Evan Date: Wed, 22 Apr 2026 19:05:02 +0100 Subject: [PATCH 02/15] chore: add @base-ui/react --- bun.lock | 35 +++++++++++++++++++++++++++---- docs/contributing/dependencies.md | 1 + package.json | 1 + 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index b582bdbe..0c999c2a 100644 --- a/bun.lock +++ b/bun.lock @@ -26,6 +26,7 @@ "devDependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.7.10", "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": "^3.2.15", + "@base-ui/react": "^1.4.1", "@chenglou/pretext": "^0.0.5", "@electron-toolkit/eslint-config-prettier": "^3.0.0", "@electron-toolkit/eslint-config-ts": "^3.1.0", @@ -159,6 +160,10 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@base-ui/react": ["@base-ui/react@1.4.1", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.2.8", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-Ab5/LIhcmL8BQcsBUYiOfkSDRdLpvgUBzMK30cu684JPcLclYlztharvCZyNNgzJtbAiREzI9q0pI5erHCMgCw=="], + + "@base-ui/utils": ["@base-ui/utils@0.2.8", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-jvOi+c+ftGlGotNcKnzPVg2IhCaDTB6/6R3JeqdjdXktuAJi3wKH9T7+svuaKh1mmfVU11UWzUZVH74JDfi/wQ=="], + "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], "@canvas/image-data": ["@canvas/image-data@1.1.0", "", {}, "sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA=="], @@ -279,15 +284,15 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - "@floating-ui/core": ["@floating-ui/core@1.7.1", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw=="], + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], - "@floating-ui/dom": ["@floating-ui/dom@1.7.1", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/utils": "^0.2.9" } }, "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ=="], + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], "@floating-ui/react": ["@floating-ui/react@0.26.28", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/utils": "^0.2.8", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw=="], - "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.3", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA=="], + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], - "@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], "@ghostery/adblocker": ["@ghostery/adblocker@2.14.1", "", { "dependencies": { "@ghostery/adblocker-content": "^2.14.1", "@ghostery/adblocker-extended-selectors": "^2.14.1", "@ghostery/url-parser": "^1.3.1", "@remusao/guess-url-type": "^2.1.0", "@remusao/small": "^2.1.0", "@remusao/smaz": "^2.2.0", "tldts-experimental": "^7.0.23" } }, "sha512-/cQTMJRd4/7zpgFHWqe+9wqiRPGTy+BhqfkxplvejPgq8d6spBjC6bSJV61rVHJZw5F4KOO5ReKOvuguaKG28A=="], @@ -1673,6 +1678,8 @@ "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + "resize-observer-polyfill": ["resize-observer-polyfill@1.5.1", "", {}, "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="], "resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], @@ -2021,6 +2028,10 @@ "@babel/traverse/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "@base-ui/react/@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + + "@base-ui/utils/@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], "@electron/fuses/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=="], @@ -2049,6 +2060,10 @@ "@eslint/eslintrc/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "@floating-ui/react/@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.3", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA=="], + + "@floating-ui/react/@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], + "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -2079,6 +2094,8 @@ "@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA=="], + "@radix-ui/react-popper/@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.3", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA=="], + "@radix-ui/react-use-is-hydrated/use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], "@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], @@ -2313,6 +2330,8 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@floating-ui/react/@floating-ui/react-dom/@floating-ui/dom": ["@floating-ui/dom@1.7.1", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/utils": "^0.2.9" } }, "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], @@ -2323,6 +2342,8 @@ "@jimp/core/file-type/token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], + "@radix-ui/react-popper/@floating-ui/react-dom/@floating-ui/dom": ["@floating-ui/dom@1.7.1", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/utils": "^0.2.9" } }, "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], @@ -2435,6 +2456,12 @@ "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + "@floating-ui/react/@floating-ui/react-dom/@floating-ui/dom/@floating-ui/core": ["@floating-ui/core@1.7.1", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw=="], + + "@radix-ui/react-popper/@floating-ui/react-dom/@floating-ui/dom/@floating-ui/core": ["@floating-ui/core@1.7.1", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw=="], + + "@radix-ui/react-popper/@floating-ui/react-dom/@floating-ui/dom/@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], + "app-builder-lib/@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], diff --git a/docs/contributing/dependencies.md b/docs/contributing/dependencies.md index 39d41476..56903530 100644 --- a/docs/contributing/dependencies.md +++ b/docs/contributing/dependencies.md @@ -81,6 +81,7 @@ These dependencies are either used in the build process, or they are only used i - nuqs - Use for managing query parameters in the URL. - @tanstack/react-query - React Query for the frontend. - @chenglou/pretext - Calculate text width for the frontend. +- @base-ui/react - UI Components for the frontend. ## Tailwind CSS Dependencies for the Frontend diff --git a/package.json b/package.json index d51cd2ba..c0eb8c4e 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "devDependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.7.10", "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": "^3.2.15", + "@base-ui/react": "^1.4.1", "@chenglou/pretext": "^0.0.5", "@electron-toolkit/eslint-config-prettier": "^3.0.0", "@electron-toolkit/eslint-config-ts": "^3.1.0", From 4952f9cea1e0cf5911fb83ad7232653ea027ec0c Mon Sep 17 00:00:00 2001 From: Evan Date: Wed, 22 Apr 2026 19:52:08 +0100 Subject: [PATCH 03/15] feat: new BaseUI Popover Component --- .../src/components/ui/baseui-popover.tsx | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/renderer/src/components/ui/baseui-popover.tsx diff --git a/src/renderer/src/components/ui/baseui-popover.tsx b/src/renderer/src/components/ui/baseui-popover.tsx new file mode 100644 index 00000000..ce19d7bc --- /dev/null +++ b/src/renderer/src/components/ui/baseui-popover.tsx @@ -0,0 +1,109 @@ +"use client"; + +import * as React from "react"; +import { Popover as PopoverPrimitive } from "@base-ui/react/popover"; + +import { cn } from "@/lib/utils"; + +export function ArrowSvg(props: React.ComponentProps<"svg">) { + return ( + + + + + + ); +} + +function Popover({ ...props }: PopoverPrimitive.Root.Props) { + return ; +} + +function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) { + return ; +} + +function PopoverContent({ + className, + align = "center", + alignOffset = 0, + side = "bottom", + sideOffset = 4, + children, + positionerClassName, + portalContainer, + arrow = true, + ...props +}: PopoverPrimitive.Popup.Props & + Pick & { + positionerClassName?: string; + portalContainer?: React.ComponentProps["container"]; + arrow?: boolean; + }) { + return ( + + + + {arrow && ( + + + + )} + {children} + + + + ); +} + +function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) { + return
; +} + +function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) { + return ; +} + +function PopoverDescription({ className, ...props }: PopoverPrimitive.Description.Props) { + return ( + + ); +} + +export { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger }; From 1c7516cda623d719c5d608c05ff68a6c4d4234ee Mon Sep 17 00:00:00 2001 From: Evan Date: Wed, 22 Apr 2026 20:07:10 +0100 Subject: [PATCH 04/15] feat: portal baseui popover --- .../src/components/portal/baseui-popover.tsx | 97 +++++++++++++++++++ src/renderer/src/components/portal/portal.tsx | 11 ++- 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/components/portal/baseui-popover.tsx diff --git a/src/renderer/src/components/portal/baseui-popover.tsx b/src/renderer/src/components/portal/baseui-popover.tsx new file mode 100644 index 00000000..5a03e5c7 --- /dev/null +++ b/src/renderer/src/components/portal/baseui-popover.tsx @@ -0,0 +1,97 @@ +import { useOptionalBrowserSidebar } from "@/components/browser-ui/browser-sidebar/provider"; +import { PortalComponent } from "@/components/portal/portal"; +import { Popover as BasePopover, PopoverContent as BasePopoverContent } from "@/components/ui/baseui-popover"; +import { type PopoverRootChangeEventDetails } from "@base-ui/react"; +import { createContext, useContext, useEffect, useId, useRef, useState } from "react"; +import { ViewLayer } from "~/layers"; + +export { PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from "@/components/ui/baseui-popover"; + +interface PopoverContextType { + open: boolean; + setOpen: ((open: boolean, eventDetails: PopoverRootChangeEventDetails) => void) | undefined; +} +const PopoverContext = createContext(undefined); +export function Popover({ + open: userOpen, + onOpenChange: userSetOpen, + ...props +}: React.ComponentProps) { + const [internalOpen, internalSetOpen] = useState(false); + + const useUser = userOpen !== undefined; + + const open = useUser ? userOpen : internalOpen; + const setOpen = useUser ? userSetOpen : internalSetOpen; + + const id = useId(); + const sidebar = useOptionalBrowserSidebar(); + + useEffect(() => { + if (open) sidebar?.addActivePopover(id); + else sidebar?.removeActivePopover(id); + return () => sidebar?.removeActivePopover(id); + }, [open, sidebar, id]); + + return ( + + + + ); +} + +/** + * Set open to true instantly, but wait the given delay before setting open to false to delay removing of the popover. + * @param value - The value to delay. + * @param delay - The delay in milliseconds. + * @returns The delayed value. + */ +function useDelayedOpenValue(value: boolean, delay: number): boolean { + const [delayedValue, setDelayedValue] = useState(value); + + useEffect(() => { + if (value === true) { + setDelayedValue(value); + return () => {}; + } else { + const timer = setTimeout(() => { + setDelayedValue(value); + }, delay); + + return () => { + clearTimeout(timer); + }; + } + }, [value, delay]); + + return delayedValue; +} + +export function PopoverContent({ ...props }: Omit, "portalContainer">) { + const { open } = usePopover(); + const delayedOpen = useDelayedOpenValue(open, 200); + const portalContainerRef = useRef(null); + if (!delayedOpen) return null; + + return ( + <> + + + + ); +} + +// Hook to use the popover context +export const usePopover = () => { + const context = useContext(PopoverContext); + if (!context) { + throw new Error("usePopover must be used within a PortalPopover.Root"); + } + return context; +}; diff --git a/src/renderer/src/components/portal/portal.tsx b/src/renderer/src/components/portal/portal.tsx index 616306b3..6251e66c 100644 --- a/src/renderer/src/components/portal/portal.tsx +++ b/src/renderer/src/components/portal/portal.tsx @@ -5,10 +5,11 @@ import { useCopyStyles } from "@/hooks/use-copy-styles"; import { mergeRefs } from "@/lib/merge-refs"; import { cn } from "@/lib/utils"; import { ViewLayer } from "~/layers"; -import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef } from "react"; +import { createContext, RefObject, useContext, useEffect, useLayoutEffect, useMemo, useRef } from "react"; import { createPortal } from "react-dom"; interface PortalComponentProps extends React.ComponentProps<"div"> { + portalBodyRef?: RefObject; visible?: boolean; zIndex?: number; autoFocus?: boolean; @@ -40,6 +41,7 @@ export function PortalComponent({ className, children, ref, + portalBodyRef, ...args }: PortalComponentProps) { const { usePortal } = usePortalsProvider(); @@ -61,6 +63,13 @@ export function PortalComponent({ // Copy styles from parent window to portal window useCopyStyles(portal?.window ?? null); + // Keep portalBodyRef in sync with the portal window's document body + useEffect(() => { + if (!portal?.window) return; + if (!portalBodyRef) return; + portalBodyRef.current = portal.window.document.body; + }, [portal, portalBodyRef]); + const portalChildren = useMemo(() => { const contextValue: PortalContextValue = { x: bounds.x, From 5e2e96bc3db7df362b394e8bcd16ffe6a84d98ff Mon Sep 17 00:00:00 2001 From: Evan Date: Wed, 22 Apr 2026 20:37:46 +0100 Subject: [PATCH 05/15] fix: arrow style --- src/renderer/src/components/ui/baseui-popover.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/ui/baseui-popover.tsx b/src/renderer/src/components/ui/baseui-popover.tsx index ce19d7bc..9fb94e44 100644 --- a/src/renderer/src/components/ui/baseui-popover.tsx +++ b/src/renderer/src/components/ui/baseui-popover.tsx @@ -12,13 +12,15 @@ export function ArrowSvg(props: React.ComponentProps<"svg">) { d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z" className="fill-popover" /> + {/* Light mode border */} + {/* Dark mode border */} ); From 4df2f18e4603914c6f4f6d064de01f5c7f816b22 Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 24 Apr 2026 17:45:43 +0100 Subject: [PATCH 06/15] refactor: migrate bottom extras menu --- .../_components/bottom/bottom-extras-menu.tsx | 72 +++++++++---------- .../src/components/logic/bubble-event.tsx | 48 +++++++++++++ 2 files changed, 80 insertions(+), 40 deletions(-) create mode 100644 src/renderer/src/components/logic/bubble-event.tsx diff --git a/src/renderer/src/components/browser-ui/browser-sidebar/_components/bottom/bottom-extras-menu.tsx b/src/renderer/src/components/browser-ui/browser-sidebar/_components/bottom/bottom-extras-menu.tsx index cec387f8..fc8f2d1d 100644 --- a/src/renderer/src/components/browser-ui/browser-sidebar/_components/bottom/bottom-extras-menu.tsx +++ b/src/renderer/src/components/browser-ui/browser-sidebar/_components/bottom/bottom-extras-menu.tsx @@ -1,57 +1,49 @@ -import { PortalPopover } from "@/components/portal/popover"; +import { BubbleEvent } from "@/components/logic/bubble-event"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/portal/baseui-popover"; import { useSpaces } from "@/components/providers/spaces-provider"; import { Button } from "@/components/ui/button"; -import { PopoverListboxItem, PopoverListboxList, usePopoverListbox } from "@/components/ui/popover-listbox"; -import { PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandItem, CommandList } from "@/components/ui/command"; import { cn } from "@/lib/utils"; import { ArchiveIcon, HistoryIcon, SettingsIcon } from "lucide-react"; -import { useCallback, useState } from "react"; - -const EXTRA_ITEM_COUNT = 2; +import { useCallback, useRef, useState } from "react"; export function BottomExtrasMenu() { const [open, setOpen] = useState(false); + const commandRef = useRef(null); - const { isCurrentSpaceLight } = useSpaces(); - const spaceInjectedClasses = cn(isCurrentSpaceLight ? "" : "dark"); - - const onActivate = useCallback((index: number) => { - if (index === 0) { - flow.tabs.newTab("flow://history", true); - } else if (index === 1) { + const onItemSelected = useCallback((url: string) => { + if (url === "internal://settings") { flow.windows.openSettingsWindow(); + } else { + flow.tabs.newTab(url, true); } setOpen(false); }, []); - const listbox = usePopoverListbox({ - open, - itemCount: EXTRA_ITEM_COUNT, - ariaLabel: "Sidebar extras", - getOptionId: (i) => `bottom-extra-${i}`, - onActivate, - initialHighlightedIndex: EXTRA_ITEM_COUNT - 1 - }); - + const { isCurrentSpaceLight } = useSpaces(); + const spaceInjectedClasses = cn(isCurrentSpaceLight ? "" : "dark"); return ( - - - - - - - - - History - - - - Settings - - - - + + + + + + + onItemSelected("flow://history")}> + + History + + onItemSelected("internal://settings")}> + + Settings + + + + + ); } diff --git a/src/renderer/src/components/logic/bubble-event.tsx b/src/renderer/src/components/logic/bubble-event.tsx new file mode 100644 index 00000000..bd6f8f9e --- /dev/null +++ b/src/renderer/src/components/logic/bubble-event.tsx @@ -0,0 +1,48 @@ +import { useEffect, type RefObject } from "react"; + +/** + * Forwards document-level events into a target element so that components + * scoped to that subtree receive events that would otherwise only fire on the + * document root. + * + * A listener is attached to the `ownerDocument` of `documentRef` (defaults to + * `targetRef`). When an event of `eventType` fires and its `target` is **not** + * inside `targetRef`, a cloned `KeyboardEvent` is dispatched on `targetRef` so + * the event bubbles through that subtree as if it originated there. + * + * Renders nothing — use it as a logic-only child inside any component tree. + * + * Mostly used for Portal + cmdk (command component) so it can receive up and down arrow keys. + * + * @example + * // Forward keyboard events from the document into a portalled overlay + * + */ +export function BubbleEvent({ + targetRef, + eventType, + documentRef = targetRef +}: { + targetRef: RefObject; + eventType: EventType; + documentRef?: RefObject; +}) { + useEffect(() => { + const document = documentRef.current?.ownerDocument; + if (!document) return; + + const bubbleTarget = targetRef.current; + if (!bubbleTarget) return; + + const handler = (event: DocumentEventMap[EventType]) => { + if (bubbleTarget.contains(event.target as Node)) return; + const cloned = new KeyboardEvent(event.type, event); + bubbleTarget.dispatchEvent(cloned); + }; + + document.addEventListener(eventType, handler); + return () => document.removeEventListener(eventType, handler); + }, [targetRef, eventType, documentRef]); + + return null; +} From f7644b5475d7cd9cc2b73460b2695a050098a45b Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 24 Apr 2026 17:46:00 +0100 Subject: [PATCH 07/15] refactor: migrate navigation controls --- .../_components/navigation-controls.tsx | 53 +++-- .../src/components/ui/popover-listbox.tsx | 204 ------------------ 2 files changed, 24 insertions(+), 233 deletions(-) delete mode 100644 src/renderer/src/components/ui/popover-listbox.tsx diff --git a/src/renderer/src/components/browser-ui/browser-sidebar/_components/navigation-controls.tsx b/src/renderer/src/components/browser-ui/browser-sidebar/_components/navigation-controls.tsx index a340cc9b..0f7839d0 100644 --- a/src/renderer/src/components/browser-ui/browser-sidebar/_components/navigation-controls.tsx +++ b/src/renderer/src/components/browser-ui/browser-sidebar/_components/navigation-controls.tsx @@ -3,9 +3,9 @@ import { ArrowLeftIcon, ArrowLeftIconHandle } from "@/components/icons/arrow-lef import { ArrowRightIcon, ArrowRightIconHandle } from "@/components/icons/arrow-right"; import { useAddressUrl, useFocusedTabId, useFocusedTabLoading } from "@/components/providers/tabs-provider"; import { useSpaces } from "@/components/providers/spaces-provider"; -import { PortalPopover } from "@/components/portal/popover"; -import { PopoverTrigger } from "@/components/ui/popover"; -import { PopoverListboxItem, PopoverListboxList, usePopoverListbox } from "@/components/ui/popover-listbox"; +import { BubbleEvent } from "@/components/logic/bubble-event"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/portal/baseui-popover"; +import { Command, CommandItem, CommandList } from "@/components/ui/command"; import { cn } from "@/lib/utils"; import { XIcon } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; @@ -99,28 +99,19 @@ function NavigationButton({ }) { const { isCurrentSpaceLight } = useSpaces(); const iconRef = useRef(null); + const commandRef = useRef(null); const [open, setOpen] = useState(false); const { handleMouseDown, handleMouseUp } = usePressAnimation(iconRef); const onActivateHistory = useCallback( - (index: number) => { + (entry: NavigationEntryWithIndex) => { if (!focusedTabId) return; - const entry = entries[index]; - if (!entry) return; flow.navigation.goToNavigationEntry(focusedTabId, entry.index); setOpen(false); }, - [entries, focusedTabId] + [focusedTabId] ); - const listbox = usePopoverListbox({ - open, - itemCount: entries.length, - ariaLabel: direction === "back" ? "Back history" : "Forward history", - getOptionId: (i) => `nav-history-${direction}-${entries[i]!.index}`, - onActivate: onActivateHistory - }); - const navigate = useCallback(() => { if (!focusedTabId || entries.length === 0) return; flow.navigation.goToNavigationEntry(focusedTabId, entries[0].index); @@ -158,21 +149,25 @@ function NavigationButton({ /> {entries.length > 0 && ( - + - - - {entries.map((entry, index) => ( - - {entry.title || entry.url} - - ))} - - - + + + + + {entries.map((entry) => ( + onActivateHistory(entry)} + > + {entry.title || entry.url} + + ))} + + + + )}
); diff --git a/src/renderer/src/components/ui/popover-listbox.tsx b/src/renderer/src/components/ui/popover-listbox.tsx deleted file mode 100644 index f1de0b19..00000000 --- a/src/renderer/src/components/ui/popover-listbox.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import { cn } from "@/lib/utils"; -import { createContext, useCallback, useContext, useEffect, useRef, useState, type HTMLAttributes } from "react"; - -export type UsePopoverListboxOptions = { - open: boolean; - itemCount: number; - ariaLabel: string; - getOptionId: (index: number) => string; - onActivate: (index: number) => void; - /** When true, ArrowDown from last item wraps to first (and vice versa). Default true. */ - wrap?: boolean; - /** Highlighted row when the list opens. Clamped to `itemCount - 1`. Default 0. */ - initialHighlightedIndex?: number; -}; - -export type PopoverListboxOptionProps = Pick< - HTMLAttributes, - "id" | "role" | "aria-selected" | "onMouseEnter" ->; - -export type PopoverListbox = ReturnType; - -const defaultListClassName = - "max-h-64 custom-scrollbar overflow-y-auto rounded-sm outline-none focus:outline-none focus-visible:outline-none"; - -type PopoverListboxContextType = { - highlightedIndex: number; - getOptionProps: (index: number) => PopoverListboxOptionProps; - onActivate: (index: number) => void; -}; - -const PopoverListboxContext = createContext(undefined); - -function usePopoverListboxContext() { - const ctx = useContext(PopoverListboxContext); - if (!ctx) throw new Error("PopoverListboxItem must be used within a PopoverListboxList"); - return ctx; -} - -export function usePopoverListbox({ - open, - itemCount, - ariaLabel, - getOptionId, - onActivate, - wrap = true, - initialHighlightedIndex = 0 -}: UsePopoverListboxOptions) { - const listRef = useRef(null); - const [highlightedIndex, setHighlightedIndex] = useState(0); - - const itemCountRef = useRef(itemCount); - itemCountRef.current = itemCount; - const highlightedIndexRef = useRef(highlightedIndex); - highlightedIndexRef.current = highlightedIndex; - const getOptionIdRef = useRef(getOptionId); - getOptionIdRef.current = getOptionId; - const onActivateRef = useRef(onActivate); - onActivateRef.current = onActivate; - - const wasOpenRef = useRef(false); - useEffect(() => { - if (open && !wasOpenRef.current) { - const n = itemCount; - const i = n <= 0 ? 0 : Math.min(Math.max(0, initialHighlightedIndex), n - 1); - setHighlightedIndex(i); - } - wasOpenRef.current = open; - }, [open, itemCount, initialHighlightedIndex]); - - useEffect(() => { - if (!open) return; - setHighlightedIndex((i) => Math.min(i, Math.max(0, itemCount - 1))); - }, [itemCount, open]); - - useEffect(() => { - if (!open || itemCount === 0 || highlightedIndex < 0 || highlightedIndex >= itemCount) return; - const id = getOptionIdRef.current(highlightedIndex); - const root = listRef.current?.ownerDocument ?? document; - root.getElementById(id)?.scrollIntoView({ block: "nearest" }); - }, [highlightedIndex, itemCount, open]); - - useEffect(() => { - if (!open) return; - const el = listRef.current; - if (!el) return; - const doc = el.ownerDocument; - - el.focus(); - - const onKeyDown = (e: KeyboardEvent) => { - const n = itemCountRef.current; - if (n === 0) return; - - if (e.key === "ArrowDown") { - e.preventDefault(); - if (wrap) { - setHighlightedIndex((i) => (i + 1) % n); - } else { - setHighlightedIndex((i) => Math.min(n - 1, i + 1)); - } - return; - } - if (e.key === "ArrowUp") { - e.preventDefault(); - if (wrap) { - setHighlightedIndex((i) => (i - 1 + n) % n); - } else { - setHighlightedIndex((i) => Math.max(0, i - 1)); - } - return; - } - if (e.key === "Enter") { - e.preventDefault(); - const i = highlightedIndexRef.current; - if (i >= 0 && i < itemCountRef.current) { - onActivateRef.current(i); - } - } - }; - - doc.addEventListener("keydown", onKeyDown); - return () => doc.removeEventListener("keydown", onKeyDown); - }, [open, wrap]); - - const activeDescendant = - itemCount > 0 && highlightedIndex >= 0 && highlightedIndex < itemCount ? getOptionId(highlightedIndex) : undefined; - - const listProps: HTMLAttributes = { - role: "listbox", - tabIndex: -1, - "aria-label": ariaLabel, - "aria-activedescendant": activeDescendant, - className: defaultListClassName - }; - - const getOptionProps = useCallback( - (index: number): PopoverListboxOptionProps => ({ - id: getOptionId(index), - role: "option", - "aria-selected": index === highlightedIndex, - onMouseEnter: () => setHighlightedIndex(index) - }), - [getOptionId, highlightedIndex] - ); - - const contentProps = { - onOpenAutoFocus: (event: Event) => event.preventDefault() - }; - - return { - listRef, - listProps, - highlightedIndex, - getOptionProps, - onActivate, - contentProps - }; -} - -export function PopoverListboxList({ - listbox, - className, - children -}: { - listbox: PopoverListbox; - className?: string; - children: React.ReactNode; -}) { - const { listRef, listProps, highlightedIndex, getOptionProps, onActivate } = listbox; - const { className: listClassName, ...rest } = listProps; - return ( - -
- {children} -
-
- ); -} - -export function PopoverListboxItem({ - index, - className, - children -}: { - index: number; - className?: string; - children: React.ReactNode; -}) { - const { highlightedIndex, getOptionProps, onActivate } = usePopoverListboxContext(); - return ( -
onActivate(index)} - className={cn( - "flex min-w-0 w-full items-center gap-2 truncate px-2 py-1.5 text-sm rounded-sm", - index === highlightedIndex ? "bg-accent" : "hover:bg-accent", - className - )} - > - {children} -
- ); -} From 1abf8174362b51b4edafb553f289176bd1ccfbe6 Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 24 Apr 2026 17:48:44 +0100 Subject: [PATCH 08/15] refactor: migrate extensions list --- .../_components/browser-action-list.tsx | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx b/src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx index 41c38892..793e7e9f 100644 --- a/src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx +++ b/src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx @@ -1,8 +1,7 @@ import { type ActivateEventType, useBrowserAction } from "@/components/providers/browser-action-provider"; import { useExtensions } from "@/components/providers/extensions-provider"; import { useSpaces } from "@/components/providers/spaces-provider"; -import { PortalPopover } from "@/components/portal/popover"; -import { PopoverTrigger } from "@/components/ui/popover"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/portal/baseui-popover"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; import { CogIcon, LayersIcon, PackageXIcon, PinIcon, PinOffIcon, PuzzleIcon, Settings2Icon } from "lucide-react"; @@ -168,32 +167,30 @@ export function BrowserActionList() { if (!focusedTab) return null; return ( - - - + + - + {!noActiveTab && !noActions && actions.map((action) => ( @@ -225,7 +222,7 @@ export function BrowserActionList() { Manage Extensions - - + + ); } From e3dbee6a504e0496a30f35966a0ca95b4cb9020d Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 24 Apr 2026 17:50:02 +0100 Subject: [PATCH 09/15] chore: remove unused popover component --- .../src/components/portal/popover.tsx | 75 ------------------- src/renderer/src/components/ui/popover.tsx | 44 ----------- 2 files changed, 119 deletions(-) delete mode 100644 src/renderer/src/components/portal/popover.tsx delete mode 100644 src/renderer/src/components/ui/popover.tsx diff --git a/src/renderer/src/components/portal/popover.tsx b/src/renderer/src/components/portal/popover.tsx deleted file mode 100644 index bbb57c56..00000000 --- a/src/renderer/src/components/portal/popover.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { PortalComponent } from "@/components/portal/portal"; -import { Popover, PopoverContent } from "@/components/ui/popover"; -import { useOptionalBrowserSidebar } from "@/components/browser-ui/browser-sidebar/provider"; -import { ViewLayer } from "~/layers"; -import { createContext, useContext, useEffect, useId, useState } from "react"; -import { AnimatePresence, motion } from "motion/react"; -import { PopoverArrow } from "@radix-ui/react-popover"; - -type PopoverContextType = { - open: boolean; - setOpen: ((open: boolean) => void) | undefined; -}; - -const PopoverContext = createContext(undefined); - -function PortalPopoverRoot({ - open: userOpen, - onOpenChange: userSetOpen, - ...props -}: React.ComponentProps) { - const [internalOpen, internalSetOpen] = useState(false); - - const useUser = userOpen !== undefined; - - const open = useUser ? userOpen : internalOpen; - const setOpen = useUser ? userSetOpen : internalSetOpen; - - const id = useId(); - const sidebar = useOptionalBrowserSidebar(); - - useEffect(() => { - if (open) sidebar?.addActivePopover(id); - else sidebar?.removeActivePopover(id); - return () => sidebar?.removeActivePopover(id); - }, [open, sidebar, id]); - - return ( - - - - ); -} - -function PortalPopoverContent({ children, ...props }: React.ComponentProps) { - const { open } = usePopover(); - - return ( - - {open && ( - - - - - {children} - - - - )} - - ); -} - -export const PortalPopover = { - Root: PortalPopoverRoot, - Content: PortalPopoverContent -}; - -// Hook to use the popover context -export const usePopover = () => { - const context = useContext(PopoverContext); - if (!context) { - throw new Error("usePopover must be used within a PortalPopover.Root"); - } - return context; -}; diff --git a/src/renderer/src/components/ui/popover.tsx b/src/renderer/src/components/ui/popover.tsx deleted file mode 100644 index 11d6de11..00000000 --- a/src/renderer/src/components/ui/popover.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import * as React from "react"; -import { Popover as PopoverPrimitive } from "radix-ui"; - -import { cn } from "@/lib/utils"; -import { Fragment } from "react"; - -function Popover({ ...props }: React.ComponentProps) { - return ; -} - -function PopoverTrigger({ ...props }: React.ComponentProps) { - return ; -} - -function PopoverContent({ - className, - align = "center", - sideOffset = 4, - portal = true, - ...props -}: React.ComponentProps & { portal?: boolean }) { - const Portal = portal ? PopoverPrimitive.Portal : Fragment; - - return ( - - - - ); -} - -function PopoverAnchor({ ...props }: React.ComponentProps) { - return ; -} - -export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; From a5326d30dc6739710b71c101c139f3972fe59ae6 Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 24 Apr 2026 17:50:55 +0100 Subject: [PATCH 10/15] chore: rename baseui popover to just popover --- .../browser-sidebar/_components/bottom/bottom-extras-menu.tsx | 2 +- .../browser-sidebar/_components/browser-action-list.tsx | 2 +- .../browser-sidebar/_components/navigation-controls.tsx | 2 +- .../src/components/portal/{baseui-popover.tsx => popover.tsx} | 4 ++-- .../src/components/ui/{baseui-popover.tsx => popover.tsx} | 0 5 files changed, 5 insertions(+), 5 deletions(-) rename src/renderer/src/components/portal/{baseui-popover.tsx => popover.tsx} (96%) rename src/renderer/src/components/ui/{baseui-popover.tsx => popover.tsx} (100%) diff --git a/src/renderer/src/components/browser-ui/browser-sidebar/_components/bottom/bottom-extras-menu.tsx b/src/renderer/src/components/browser-ui/browser-sidebar/_components/bottom/bottom-extras-menu.tsx index fc8f2d1d..1c118254 100644 --- a/src/renderer/src/components/browser-ui/browser-sidebar/_components/bottom/bottom-extras-menu.tsx +++ b/src/renderer/src/components/browser-ui/browser-sidebar/_components/bottom/bottom-extras-menu.tsx @@ -1,5 +1,5 @@ import { BubbleEvent } from "@/components/logic/bubble-event"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/portal/baseui-popover"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/portal/popover"; import { useSpaces } from "@/components/providers/spaces-provider"; import { Button } from "@/components/ui/button"; import { Command, CommandItem, CommandList } from "@/components/ui/command"; diff --git a/src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx b/src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx index 793e7e9f..3164e2a4 100644 --- a/src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx +++ b/src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx @@ -1,7 +1,7 @@ import { type ActivateEventType, useBrowserAction } from "@/components/providers/browser-action-provider"; import { useExtensions } from "@/components/providers/extensions-provider"; import { useSpaces } from "@/components/providers/spaces-provider"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/portal/baseui-popover"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/portal/popover"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; import { CogIcon, LayersIcon, PackageXIcon, PinIcon, PinOffIcon, PuzzleIcon, Settings2Icon } from "lucide-react"; diff --git a/src/renderer/src/components/browser-ui/browser-sidebar/_components/navigation-controls.tsx b/src/renderer/src/components/browser-ui/browser-sidebar/_components/navigation-controls.tsx index 0f7839d0..828291a8 100644 --- a/src/renderer/src/components/browser-ui/browser-sidebar/_components/navigation-controls.tsx +++ b/src/renderer/src/components/browser-ui/browser-sidebar/_components/navigation-controls.tsx @@ -4,7 +4,7 @@ import { ArrowRightIcon, ArrowRightIconHandle } from "@/components/icons/arrow-r import { useAddressUrl, useFocusedTabId, useFocusedTabLoading } from "@/components/providers/tabs-provider"; import { useSpaces } from "@/components/providers/spaces-provider"; import { BubbleEvent } from "@/components/logic/bubble-event"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/portal/baseui-popover"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/portal/popover"; import { Command, CommandItem, CommandList } from "@/components/ui/command"; import { cn } from "@/lib/utils"; import { XIcon } from "lucide-react"; diff --git a/src/renderer/src/components/portal/baseui-popover.tsx b/src/renderer/src/components/portal/popover.tsx similarity index 96% rename from src/renderer/src/components/portal/baseui-popover.tsx rename to src/renderer/src/components/portal/popover.tsx index 5a03e5c7..f9d18a88 100644 --- a/src/renderer/src/components/portal/baseui-popover.tsx +++ b/src/renderer/src/components/portal/popover.tsx @@ -1,11 +1,11 @@ import { useOptionalBrowserSidebar } from "@/components/browser-ui/browser-sidebar/provider"; import { PortalComponent } from "@/components/portal/portal"; -import { Popover as BasePopover, PopoverContent as BasePopoverContent } from "@/components/ui/baseui-popover"; +import { Popover as BasePopover, PopoverContent as BasePopoverContent } from "@/components/ui/popover"; import { type PopoverRootChangeEventDetails } from "@base-ui/react"; import { createContext, useContext, useEffect, useId, useRef, useState } from "react"; import { ViewLayer } from "~/layers"; -export { PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from "@/components/ui/baseui-popover"; +export { PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from "@/components/ui/popover"; interface PopoverContextType { open: boolean; diff --git a/src/renderer/src/components/ui/baseui-popover.tsx b/src/renderer/src/components/ui/popover.tsx similarity index 100% rename from src/renderer/src/components/ui/baseui-popover.tsx rename to src/renderer/src/components/ui/popover.tsx From dc3cf483038bc8b5ce54665d4911fcbde4aadcdb Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 24 Apr 2026 17:57:42 +0100 Subject: [PATCH 11/15] fix: colors --- .../_components/bottom/bottom-extras-menu.tsx | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/renderer/src/components/browser-ui/browser-sidebar/_components/bottom/bottom-extras-menu.tsx b/src/renderer/src/components/browser-ui/browser-sidebar/_components/bottom/bottom-extras-menu.tsx index 1c118254..0341db60 100644 --- a/src/renderer/src/components/browser-ui/browser-sidebar/_components/bottom/bottom-extras-menu.tsx +++ b/src/renderer/src/components/browser-ui/browser-sidebar/_components/bottom/bottom-extras-menu.tsx @@ -4,9 +4,30 @@ import { useSpaces } from "@/components/providers/spaces-provider"; import { Button } from "@/components/ui/button"; import { Command, CommandItem, CommandList } from "@/components/ui/command"; import { cn } from "@/lib/utils"; -import { ArchiveIcon, HistoryIcon, SettingsIcon } from "lucide-react"; +import { ArchiveIcon, HistoryIcon, LucideIcon, SettingsIcon } from "lucide-react"; import { useCallback, useRef, useState } from "react"; +function BottomExtrasMenuItem({ + id, + Icon, + label, + url, + onItemSelected +}: { + id: string; + Icon: LucideIcon; + label: string; + url: string; + onItemSelected: (url: string) => void; +}) { + return ( + onItemSelected(url)} className="text-black dark:text-white"> + + {label} + + ); +} + export function BottomExtrasMenu() { const [open, setOpen] = useState(false); const commandRef = useRef(null); @@ -33,14 +54,20 @@ export function BottomExtrasMenu() { - onItemSelected("flow://history")}> - - History - - onItemSelected("internal://settings")}> - - Settings - + + From ba74bea67ce4d3c297acd3aa1b02575f2d2a0d57 Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 24 Apr 2026 22:48:38 +0100 Subject: [PATCH 12/15] feat: site controls component instaed of browser actions (extensions) --- .../_components/address-bar.tsx | 4 +- .../_components/site-controls/extensions.tsx | 247 ++++++++++++++++++ .../_components/site-controls/index.tsx | 65 +++++ src/renderer/src/components/ui/popover.tsx | 33 ++- src/renderer/src/routes/extensions/page.tsx | 2 +- 5 files changed, 341 insertions(+), 10 deletions(-) create mode 100644 src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/extensions.tsx create mode 100644 src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/index.tsx diff --git a/src/renderer/src/components/browser-ui/browser-sidebar/_components/address-bar.tsx b/src/renderer/src/components/browser-ui/browser-sidebar/_components/address-bar.tsx index 0df03bf0..0542fd82 100644 --- a/src/renderer/src/components/browser-ui/browser-sidebar/_components/address-bar.tsx +++ b/src/renderer/src/components/browser-ui/browser-sidebar/_components/address-bar.tsx @@ -4,8 +4,8 @@ import { memo, useCallback, useRef, type MouseEvent } from "react"; import { useAddressUrl, useFocusedTabId } from "@/components/providers/tabs-provider"; import { simplifyUrl } from "@/lib/url"; import { PinnedBrowserActions } from "./pinned-browser-actions"; -import { BrowserActionList } from "@/components/browser-ui/browser-sidebar/_components/browser-action-list"; import { useBrowserSidebar } from "@/components/browser-ui/browser-sidebar/provider"; +import { SiteControls } from "@/components/browser-ui/browser-sidebar/_components/site-controls"; export const AddressBar = memo(function AddressBar() { const containerRef = useRef(null); @@ -70,7 +70,7 @@ export const AddressBar = memo(function AddressBar() {
- +
diff --git a/src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/extensions.tsx b/src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/extensions.tsx new file mode 100644 index 00000000..082301bf --- /dev/null +++ b/src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/extensions.tsx @@ -0,0 +1,247 @@ +import { type ActivateEventType, useBrowserAction } from "@/components/providers/browser-action-provider"; +import { useExtensions } from "@/components/providers/extensions-provider"; +import { cn } from "@/lib/utils"; +import { CHROME_WEB_STORE_URL } from "@/routes/extensions/page"; +import { PlusIcon, PuzzleIcon } from "lucide-react"; +import { MouseEvent, useCallback, useRef, useState } from "react"; + +interface ExtensionAction { + color?: string; + text?: string; + title?: string; + icon?: chrome.browserAction.TabIconDetails; + popup?: string; + iconModified?: number; +} + +interface Action { + id: string; + title: string; + popup: string; + tabs: Record; +} + +function RotatedPinInCircle({ className }: { className?: string }) { + return ( + + + + + + {/* Pin cutout */} + + + + + + + + + + ); +} + +// Extension icon via crx:// protocol +function BrowserActionIcon({ + action, + activeTabId, + tabInfo, + partitionId +}: { + action: Action; + activeTabId: number; + tabInfo: ExtensionAction | null; + partitionId: string; +}) { + const { iconModified } = { ...action, ...tabInfo }; + const [isError, setIsError] = useState(false); + const iconSize = 32; + const resizeType = 2; + const timeParam = iconModified ? `&t=${iconModified}` : ""; + const iconUrl = `crx://extension-icon/${action.id}/${iconSize}/${resizeType}?tabId=${activeTabId}${timeParam}&partition=${encodeURIComponent(partitionId)}`; + + if (isError) { + return ; + } + + return ( + + {/* eslint-disable-next-line react/no-unknown-property */} + setIsError(true)} /> + + ); +} + +// Badge overlay on extension icon +function Badge({ color, text }: { color?: string; text?: string }) { + if (!text) return null; + + return ( +
+ {text} +
+ ); +} + +function ExtensionButtonContainer({ + children, + className, + ...props +}: { children: React.ReactNode } & React.ComponentProps<"button">) { + return ( + + ); +} + +// Grid tile for a single extension +function ExtensionGridTile({ + action, + alignment, + partition, + activeTabId +}: { + action: Action; + alignment: string; + partition: string; + activeTabId: number; +}) { + const { activate } = useBrowserAction(); + const buttonRef = useRef(null); + + const tabInfo = activeTabId > -1 ? action.tabs[activeTabId] : null; + + const { extensions } = useExtensions(); + const extension = extensions.find((e) => e.id === action.id); + const isPinned = extension?.pinned; + + const onActivated = useCallback( + (eventType: ActivateEventType) => { + if (!buttonRef.current) return; + activate(action.id, activeTabId, buttonRef.current, alignment, eventType); + }, + [action.id, activeTabId, alignment, activate] + ); + + const onClick = useCallback(() => onActivated("click"), [onActivated]); + + const onContextMenu = useCallback( + (event: MouseEvent) => { + event.stopPropagation(); + event.nativeEvent.stopImmediatePropagation(); + return onActivated("contextmenu"); + }, + [onActivated] + ); + + return ( + + + + + + ); +} + +export function ExtensionsList({ setOpen }: { setOpen: (open: boolean) => void }) { + const { actions, activeTabId, partition } = useBrowserAction(); + const alignment = "right bottom" as const; + + const noActions = actions.length === 0; + const noActiveTab = typeof activeTabId !== "number"; + + if (noActiveTab || noActions) return null; + + return ( +
+ {actions.map((action) => ( + + ))} + { + event.stopPropagation(); + flow.tabs.newTab(CHROME_WEB_STORE_URL, true); + setOpen(false); + }} + > + + +
+ ); +} + +export function SiteControlExtensions({ setOpen }: { setOpen: (open: boolean) => void }) { + return ( +
+ {/* Title and Manage button */} +
+ Extensions + +
+ {/* Extensions list */} +
+ +
+
+ ); +} diff --git a/src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/index.tsx b/src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/index.tsx new file mode 100644 index 00000000..922a4ee0 --- /dev/null +++ b/src/renderer/src/components/browser-ui/browser-sidebar/_components/site-controls/index.tsx @@ -0,0 +1,65 @@ +import { useSpaces } from "@/components/providers/spaces-provider"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/portal/popover"; +import { cn } from "@/lib/utils"; +import { EllipsisIcon, LockIcon, Settings2Icon } from "lucide-react"; +import { useState } from "react"; +import { useFocusedTab } from "@/components/providers/tabs-provider"; +import { SiteControlExtensions } from "@/components/browser-ui/browser-sidebar/_components/site-controls/extensions"; +import { Separator } from "@/components/ui/separator"; +import { Button } from "@/components/ui/button"; + +// Main extensions popover for the new browser UI sidebar +export function SiteControls() { + const { isCurrentSpaceLight } = useSpaces(); + const focusedTab = useFocusedTab(); + const [open, setOpen] = useState(false); + + const spaceInjectedClasses = cn(isCurrentSpaceLight ? "" : "dark"); + + if (!focusedTab) return null; + return ( + + { + event.stopPropagation(); + }} + > +
+ +
+
+ + {/* Extensions Section */} + + + {/* Utilities Section */} +
+ + +
+
+
+ ); +} diff --git a/src/renderer/src/components/ui/popover.tsx b/src/renderer/src/components/ui/popover.tsx index 9fb94e44..7583ca04 100644 --- a/src/renderer/src/components/ui/popover.tsx +++ b/src/renderer/src/components/ui/popover.tsx @@ -5,12 +5,14 @@ import { Popover as PopoverPrimitive } from "@base-ui/react/popover"; import { cn } from "@/lib/utils"; -export function ArrowSvg(props: React.ComponentProps<"svg">) { +export type PopoverVariants = "default" | "translucent"; + +export function ArrowSvg({ variant, ...props }: React.ComponentProps<"svg"> & { variant: PopoverVariants }) { return ( {/* Light mode border */} & { + variant?: PopoverVariants; positionerClassName?: string; portalContainer?: React.ComponentProps["container"]; arrow?: boolean; @@ -58,29 +62,44 @@ function PopoverContent({ alignOffset={alignOffset} side={side} sideOffset={sideOffset} - className={cn("isolate z-50", positionerClassName)} + className={cn("isolate z-50", variant === "translucent" && "dark", positionerClassName)} > {arrow && ( - - + + )} {children} diff --git a/src/renderer/src/routes/extensions/page.tsx b/src/renderer/src/routes/extensions/page.tsx index c50eae9e..e365efe5 100644 --- a/src/renderer/src/routes/extensions/page.tsx +++ b/src/renderer/src/routes/extensions/page.tsx @@ -11,7 +11,7 @@ import ExtensionDetails from "./components/extension-details"; import { ExtensionsProvider, useExtensions } from "@/components/providers/extensions-provider"; import { useQueryState } from "nuqs"; -const CHROME_WEB_STORE_URL = "https://chromewebstore.google.com/category/extensions?utm_source=ext_sidebar"; +export const CHROME_WEB_STORE_URL = "https://chromewebstore.google.com/category/extensions?utm_source=ext_sidebar"; function ExtensionsPage() { const [isDeveloperMode, setIsDeveloperMode] = useState(false); From 4de07d01998892147f6a5a0b371883b81af6e835 Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 30 Apr 2026 18:54:41 +0100 Subject: [PATCH 13/15] improve --- src/renderer/src/components/ui/popover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/components/ui/popover.tsx b/src/renderer/src/components/ui/popover.tsx index 7583ca04..6b627906 100644 --- a/src/renderer/src/components/ui/popover.tsx +++ b/src/renderer/src/components/ui/popover.tsx @@ -76,7 +76,7 @@ function PopoverContent({ "dark:-outline-offset-1", // Rounded corners variant === "default" && "rounded-lg", - variant === "translucent" && "rounded-3xl", + variant === "translucent" && "rounded-2xl", // Background variant === "default" && "bg-popover", variant === "translucent" && "bg-popover/65 backdrop-blur-sm", From bf19f4711fa6f52699d0e725b6b96e4606d05c6a Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 1 May 2026 20:12:15 +0100 Subject: [PATCH 14/15] chore: revert to old browser action list --- .../browser-ui/browser-sidebar/_components/address-bar.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/browser-ui/browser-sidebar/_components/address-bar.tsx b/src/renderer/src/components/browser-ui/browser-sidebar/_components/address-bar.tsx index 0542fd82..92592db0 100644 --- a/src/renderer/src/components/browser-ui/browser-sidebar/_components/address-bar.tsx +++ b/src/renderer/src/components/browser-ui/browser-sidebar/_components/address-bar.tsx @@ -5,7 +5,8 @@ import { useAddressUrl, useFocusedTabId } from "@/components/providers/tabs-prov import { simplifyUrl } from "@/lib/url"; import { PinnedBrowserActions } from "./pinned-browser-actions"; import { useBrowserSidebar } from "@/components/browser-ui/browser-sidebar/provider"; -import { SiteControls } from "@/components/browser-ui/browser-sidebar/_components/site-controls"; +import { BrowserActionList } from "@/components/browser-ui/browser-sidebar/_components/browser-action-list"; +// import { SiteControls } from "@/components/browser-ui/browser-sidebar/_components/site-controls"; export const AddressBar = memo(function AddressBar() { const containerRef = useRef(null); @@ -70,7 +71,9 @@ export const AddressBar = memo(function AddressBar() {
- + + {/* TODO: Add site controls */} + {/* */}
From d401b91b7fb6d4ad49ecfc8c292747ddb36d7bd3 Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 1 May 2026 20:15:58 +0100 Subject: [PATCH 15/15] chore: revert 2 --- .../_components/browser-action-list.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx b/src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx index 3164e2a4..628506ee 100644 --- a/src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx +++ b/src/renderer/src/components/browser-ui/browser-sidebar/_components/browser-action-list.tsx @@ -4,7 +4,7 @@ import { useSpaces } from "@/components/providers/spaces-provider"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/portal/popover"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; -import { CogIcon, LayersIcon, PackageXIcon, PinIcon, PinOffIcon, PuzzleIcon, Settings2Icon } from "lucide-react"; +import { CogIcon, LayersIcon, PackageXIcon, PinIcon, PinOffIcon, PuzzleIcon } from "lucide-react"; import { MouseEvent, useCallback, useMemo, useRef, useState } from "react"; import { useFocusedTab } from "@/components/providers/tabs-provider"; @@ -179,16 +179,7 @@ export function BrowserActionList() { event.stopPropagation(); }} > -
- -
+ {!noActiveTab &&