diff --git a/bun.lock b/bun.lock index c1f630d13..44688b3b1 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "mux", @@ -18,16 +19,18 @@ "@orpc/server": "^1.11.3", "@orpc/zod": "^1.11.3", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", + "@radix-ui/react-visually-hidden": "^1.2.4", "ai": "^5.0.101", "ai-tokenizer": "^1.0.4", "chalk": "^5.6.2", @@ -849,6 +852,8 @@ "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "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-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="], + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "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-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], @@ -909,7 +914,7 @@ "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], - "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "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-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "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-kaeiyGCe844dkb9AVF+rb4yTyb1LiLN/e3es3nLiRyN4dC8AduBYPMnnNlDjX2VDOcvDEiPnRNMJeWCfsX0txg=="], "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], @@ -3733,6 +3738,8 @@ "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-context-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -3759,6 +3766,8 @@ "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-select/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "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-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + "@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], "@radix-ui/react-toggle/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], @@ -3769,7 +3778,7 @@ "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-tooltip/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "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-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], @@ -4203,6 +4212,8 @@ "@radix-ui/react-checkbox/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-context-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -4223,8 +4234,6 @@ "@radix-ui/react-toggle/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@testing-library/dom/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "@testing-library/dom/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], diff --git a/components.json b/components.json index 26216a973..12486c390 100644 --- a/components.json +++ b/components.json @@ -5,13 +5,13 @@ "tsx": true, "tailwind": { "config": "", - "css": "src/styles/globals.css", + "css": "src/browser/styles/globals.css", "baseColor": "slate", "cssVariables": true, "prefix": "" }, "aliases": { - "components": "@/components", - "utils": "@/lib/utils" + "components": "@/browser/components", + "utils": "@/common/lib/utils" } } diff --git a/package.json b/package.json index 0ed6aa134..b12c4d1e2 100644 --- a/package.json +++ b/package.json @@ -59,16 +59,18 @@ "@orpc/server": "^1.11.3", "@orpc/zod": "^1.11.3", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", + "@radix-ui/react-visually-hidden": "^1.2.4", "ai": "^5.0.101", "ai-tokenizer": "^1.0.4", "chalk": "^5.6.2", diff --git a/src/browser/components/AIView.tsx b/src/browser/components/AIView.tsx index 1da709782..03dc7ea25 100644 --- a/src/browser/components/AIView.tsx +++ b/src/browser/components/AIView.tsx @@ -592,7 +592,7 @@ const AIViewInner: React.FC = ({ diff --git a/src/browser/components/AuthTokenModal.tsx b/src/browser/components/AuthTokenModal.tsx index 6110adec1..56095bc65 100644 --- a/src/browser/components/AuthTokenModal.tsx +++ b/src/browser/components/AuthTokenModal.tsx @@ -1,5 +1,13 @@ import { useState, useCallback } from "react"; -import { Modal } from "./Modal"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/browser/components/ui/dialog"; +import { Button } from "@/browser/components/ui/button"; interface AuthTokenModalProps { isOpen: boolean; @@ -48,64 +56,43 @@ export function AuthTokenModal(props: AuthTokenModalProps) { [token, onSubmit] ); + // This modal cannot be dismissed without providing a token + const handleOpenChange = useCallback(() => { + // Do nothing - modal cannot be closed without submitting + }, []); + return ( - undefined} title="Authentication Required"> -
-

- This server requires an authentication token. Enter the token provided when the server was - started. -

+ + + + Authentication Required + + This server requires an authentication token. Enter the token provided when the server + was started. + + - {props.error && ( -
- {props.error} -
- )} + + {props.error && ( +
{props.error}
+ )} - setToken(e.target.value)} - placeholder="Enter auth token" - autoFocus - style={{ - padding: "10px 12px", - borderRadius: 4, - border: "1px solid var(--color-border)", - backgroundColor: "var(--color-input-background)", - color: "var(--color-text)", - fontSize: 14, - outline: "none", - }} - /> + setToken(e.target.value)} + placeholder="Enter auth token" + autoFocus + className="bg-modal-bg border-border-medium focus:border-accent placeholder:text-muted text-foreground rounded border px-3 py-2.5 text-sm focus:outline-none" + /> - - -
+ + + + + + ); } diff --git a/src/browser/components/DirectoryPickerModal.tsx b/src/browser/components/DirectoryPickerModal.tsx index 4b3268e77..8530985e1 100644 --- a/src/browser/components/DirectoryPickerModal.tsx +++ b/src/browser/components/DirectoryPickerModal.tsx @@ -1,5 +1,13 @@ import React, { useCallback, useEffect, useState } from "react"; -import { Modal, ModalActions, CancelButton, PrimaryButton } from "./Modal"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/browser/components/ui/dialog"; +import { Button } from "@/browser/components/ui/button"; import type { FileTreeNode } from "@/common/utils/git/numstatParser"; import { DirectoryTree } from "./DirectoryTree"; import { useAPI } from "@/browser/contexts/API"; @@ -79,38 +87,48 @@ export const DirectoryPickerModal: React.FC = ({ onClose(); }, [onClose, onSelectPath, root]); - if (!isOpen) return null; + const handleOpenChange = useCallback( + (open: boolean) => { + if (!open && !isLoading) { + onClose(); + } + }, + [isLoading, onClose] + ); + const entries = root?.children .filter((child) => child.isDirectory) .map((child) => ({ name: child.name, path: child.path })) ?? []; return ( - - {error &&
{error}
} -
- -
- - - Cancel - - void handleConfirm()} disabled={isLoading || !root}> - Select - - -
+ + + + Select Project Directory + + {root ? root.path : "Select a directory to use as your project root"} + + + {error &&
{error}
} +
+ +
+ + + + +
+
); }; diff --git a/src/browser/components/ForceDeleteModal.tsx b/src/browser/components/ForceDeleteModal.tsx index c6d2f803b..b470d468b 100644 --- a/src/browser/components/ForceDeleteModal.tsx +++ b/src/browser/components/ForceDeleteModal.tsx @@ -1,16 +1,19 @@ -import React, { useState } from "react"; +import React, { useState, useCallback } from "react"; import { - Modal, - ModalActions, - CancelButton, - DangerButton, + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, ErrorSection, ErrorLabel, ErrorCodeBlock, WarningBox, WarningTitle, WarningText, -} from "./Modal"; +} from "@/browser/components/ui/dialog"; +import { Button } from "@/browser/components/ui/button"; interface ForceDeleteModalProps { isOpen: boolean; @@ -43,40 +46,47 @@ export const ForceDeleteModal: React.FC = ({ })(); }; + const handleOpenChange = useCallback( + (open: boolean) => { + if (!open && !isDeleting) { + onClose(); + } + }, + [isDeleting, onClose] + ); + return ( - - - Git Error - {error} - + + + + Force Delete Workspace? + The workspace could not be removed normally + + + Git Error + {error} + - - This action cannot be undone - - Force deleting will permanently remove the workspace and{" "} - {error.includes("unpushed commits:") - ? "discard the unpushed commits shown above" - : "may discard uncommitted work or lose data"} - . This action cannot be undone. - - + + This action cannot be undone + + Force deleting will permanently remove the workspace and{" "} + {error.includes("unpushed commits:") + ? "discard the unpushed commits shown above" + : "may discard uncommitted work or lose data"} + . This action cannot be undone. + + - - - Cancel - - - {isDeleting ? "Deleting..." : "Force Delete"} - - - + + + + + + ); }; diff --git a/src/browser/components/LeftSidebar.tsx b/src/browser/components/LeftSidebar.tsx index 7f2c1d24d..7c34da73a 100644 --- a/src/browser/components/LeftSidebar.tsx +++ b/src/browser/components/LeftSidebar.tsx @@ -25,7 +25,7 @@ export function LeftSidebar(props: LeftSidebarProps) { title="Open sidebar" aria-label="Open sidebar menu" className={cn( - "hidden mobile-menu-btn fixed top-3 left-3 z-[998]", + "hidden mobile-menu-btn fixed top-3 left-3 z-30", "w-10 h-10 bg-sidebar border border-border-light rounded-md cursor-pointer", "items-center justify-center text-foreground text-xl transition-all duration-200", "shadow-[0_2px_4px_rgba(0,0,0,0.3)]", @@ -40,7 +40,7 @@ export function LeftSidebar(props: LeftSidebarProps) { {/* Overlay backdrop - only visible on mobile when sidebar is open */}
void; - role?: string; - className?: string; -}> = ({ children, onClick, role, className }) => ( -
- {children} -
-); - -export const ModalContent: React.FC< - { - children: React.ReactNode; - maxWidth?: string; - maxHeight?: string; - className?: string; - } & React.HTMLAttributes -> = ({ children, maxWidth = "500px", maxHeight, className, ...props }) => ( -
- {children} -
-); - -export const ModalSubtitle: React.FC<{ - children: React.ReactNode; - id?: string; - className?: string; -}> = ({ children, id, className }) => ( -

- {children} -

-); - -export const ModalInfo: React.FC<{ - children: React.ReactNode; - className?: string; - id?: string; -}> = ({ children, className, id }) => ( -
- {children} -
-); - -export const ModalActions: React.FC<{ children: React.ReactNode; className?: string }> = ({ - children, - className, -}) =>
{children}
; - -// Reusable error/warning display components for modals -export const ErrorSection: React.FC<{ children: React.ReactNode; className?: string }> = ({ - children, - className, -}) =>
{children}
; - -export const ErrorLabel: React.FC<{ children: React.ReactNode; className?: string }> = ({ - children, - className, -}) => ( -
- {children} -
-); - -export const ErrorCodeBlock: React.FC<{ children: React.ReactNode; className?: string }> = ({ - children, - className, -}) => ( -
-    {children}
-  
-); - -export const WarningBox: React.FC<{ children: React.ReactNode; className?: string }> = ({ - children, - className, -}) => ( -
- {children} -
-); - -export const WarningTitle: React.FC<{ children: React.ReactNode; className?: string }> = ({ - children, - className, -}) =>
{children}
; - -export const WarningText: React.FC<{ children: React.ReactNode; className?: string }> = ({ - children, - className, -}) =>
{children}
; - -// Button components -interface ButtonProps extends React.ButtonHTMLAttributes { - children: React.ReactNode; -} - -export const Button: React.FC = ({ children, className, ...props }) => ( - -); - -export const CancelButton: React.FC = ({ children, className, ...props }) => ( - -); - -export const PrimaryButton: React.FC = ({ children, className, ...props }) => ( - -); - -export const DangerButton: React.FC = ({ children, className, ...props }) => ( - -); - -// Modal wrapper component -interface ModalProps { - isOpen: boolean; - title: string; - subtitle?: string; - onClose: () => void; - children: React.ReactNode; - maxWidth?: string; - maxHeight?: string; - isLoading?: boolean; - describedById?: string; -} - -export const Modal: React.FC = ({ - isOpen, - title, - subtitle, - onClose, - children, - maxWidth, - maxHeight, - isLoading = false, - describedById, -}) => { - const headingId = useId(); - const subtitleId = subtitle ? `${headingId}-subtitle` : undefined; - const ariaDescribedBy = [subtitleId, describedById].filter(Boolean).join(" ") || undefined; - - const handleCancel = useCallback(() => { - if (!isLoading) { - onClose(); - } - }, [isLoading, onClose]); - - // Handle cancel keybind to close modal - useEffect(() => { - if (!isOpen) return; - - const handleKeyDown = (e: KeyboardEvent) => { - if (matchesKeybind(e, KEYBINDS.CANCEL) && !isLoading) { - handleCancel(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [isOpen, isLoading, handleCancel]); - - if (!isOpen) return null; - - return ( - - event.stopPropagation()} - > -

{title}

- {subtitle && {subtitle}} - {children} -
-
- ); -}; diff --git a/src/browser/components/ProjectCreateModal.tsx b/src/browser/components/ProjectCreateModal.tsx index dd0bf57b4..a08e8453e 100644 --- a/src/browser/components/ProjectCreateModal.tsx +++ b/src/browser/components/ProjectCreateModal.tsx @@ -1,6 +1,14 @@ import React, { useState, useCallback } from "react"; -import { Modal, ModalActions, CancelButton, PrimaryButton } from "./Modal"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/browser/components/ui/dialog"; import { DirectoryPickerModal } from "./DirectoryPickerModal"; +import { Button } from "@/browser/components/ui/button"; import type { ProjectConfig } from "@/node/config"; import { useAPI } from "@/browser/contexts/API"; @@ -122,50 +130,59 @@ export const ProjectCreateModal: React.FC = ({ [handleSelect] ); + const handleOpenChange = useCallback( + (open: boolean) => { + if (!open && !isCreating) { + handleCancel(); + } + }, + [isCreating, handleCancel] + ); + return ( <> - -
- { - setPath(e.target.value); - setError(""); - }} - onKeyDown={handleKeyDown} - placeholder="/home/user/projects/my-project" - autoFocus - disabled={isCreating} - className="bg-modal-bg border-border-medium focus:border-accent placeholder:text-muted text-foreground min-w-0 flex-1 rounded border px-3 py-2 font-mono text-sm focus:outline-none disabled:opacity-50" - /> - {(isDesktop || hasWebFsPicker) && ( - - )} -
- {error &&
{error}
} - - - Cancel - - void handleSelect()} disabled={isCreating}> - {isCreating ? "Adding..." : "Add Project"} - - -
+ className="bg-modal-bg border-border-medium focus:border-accent placeholder:text-muted text-foreground min-w-0 flex-1 rounded border px-3 py-2 font-mono text-sm focus:outline-none disabled:opacity-50" + /> + {(isDesktop || hasWebFsPicker) && ( + + )} +
+ {error &&
{error}
} + + + + + + = ({ } }, [isOpen, initialSecrets]); - const handleCancel = () => { + const handleCancel = useCallback(() => { setSecrets(initialSecrets); setVisibleSecrets(new Set()); onClose(); - }; + }, [initialSecrets, onClose]); const handleSave = async () => { setIsLoading(true); @@ -120,90 +129,99 @@ const SecretsModal: React.FC = ({ setVisibleSecrets(newVisible); }; + const handleOpenChange = useCallback( + (open: boolean) => { + if (!open && !isLoading) { + handleCancel(); + } + }, + [isLoading, handleCancel] + ); + return ( - - -

- Secrets are stored in ~/.mux/secrets.json (kept away from source code) but - namespaced per project. -

-

Secrets are injected as environment variables to compute commands (e.g. Bash)

-
- -
- {secrets.length === 0 ? ( -
No secrets configured
- ) : ( -
- - -
{/* Empty cell for eye icon column */} -
{/* Empty cell for delete button column */} - {secrets.map((secret, index) => ( - - updateSecret(index, "key", e.target.value)} - placeholder="SECRET_NAME" - disabled={isLoading} - className="bg-modal-bg border-border-medium focus:border-accent placeholder:text-dim w-full rounded border px-2.5 py-1.5 font-mono text-[13px] text-white focus:outline-none" - /> - updateSecret(index, "value", e.target.value)} - placeholder="secret value" - disabled={isLoading} - className="bg-modal-bg border-border-medium focus:border-accent placeholder:text-dim w-full rounded border px-2.5 py-1.5 font-mono text-[13px] text-white focus:outline-none" - /> - - - - ))} -
- )} -
- - - - - - Cancel - - void handleSave()} disabled={isLoading}> - {isLoading ? "Saving..." : "Save"} - - - + + + + Manage Secrets + Project: {projectName} + + +

+ Secrets are stored in ~/.mux/secrets.json (kept away from source code) but + namespaced per project. +

+

Secrets are injected as environment variables to compute commands (e.g. Bash)

+
+ +
+ {secrets.length === 0 ? ( +
+ No secrets configured +
+ ) : ( +
+ + +
{/* Empty cell for eye icon column */} +
{/* Empty cell for delete button column */} + {secrets.map((secret, index) => ( + + updateSecret(index, "key", e.target.value)} + placeholder="SECRET_NAME" + disabled={isLoading} + className="bg-modal-bg border-border-medium focus:border-accent placeholder:text-dim w-full rounded border px-2.5 py-1.5 font-mono text-[13px] text-white focus:outline-none" + /> + updateSecret(index, "value", e.target.value)} + placeholder="secret value" + disabled={isLoading} + className="bg-modal-bg border-border-medium focus:border-accent placeholder:text-dim w-full rounded border px-2.5 py-1.5 font-mono text-[13px] text-white focus:outline-none" + /> + + + + ))} +
+ )} +
+ + + + + + + + +
); }; diff --git a/src/browser/components/Select.tsx b/src/browser/components/Select.tsx index 2f529e55c..799797392 100644 --- a/src/browser/components/Select.tsx +++ b/src/browser/components/Select.tsx @@ -1,4 +1,11 @@ import React from "react"; +import { + Select as ShadcnSelect, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/browser/components/ui/select"; interface SelectOption { value: string; @@ -17,7 +24,7 @@ interface SelectProps { /** * Reusable select component with consistent styling - * Centralizes select styling to avoid duplication and ensure consistent UX + * Wraps shadcn Select with a simpler API for common use cases */ export function Select({ value, @@ -34,19 +41,17 @@ export function Select({ ); return ( - + + + + + + {normalizedOptions.map((opt) => ( + + {opt.label} + + ))} + + ); } diff --git a/src/browser/components/Settings/SettingsModal.tsx b/src/browser/components/Settings/SettingsModal.tsx index 3a5a5eeea..41202f637 100644 --- a/src/browser/components/Settings/SettingsModal.tsx +++ b/src/browser/components/Settings/SettingsModal.tsx @@ -1,11 +1,11 @@ -import React, { useEffect, useCallback } from "react"; +import React from "react"; import { Settings, Key, Cpu, X } from "lucide-react"; import { useSettings } from "@/browser/contexts/SettingsContext"; -import { ModalOverlay } from "@/browser/components/Modal"; -import { matchesKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds"; +import { Dialog, DialogContent, DialogTitle, VisuallyHidden } from "@/browser/components/ui/dialog"; import { GeneralSection } from "./sections/GeneralSection"; import { ProvidersSection } from "./sections/ProvidersSection"; import { ModelsSection } from "./sections/ModelsSection"; +import { Button } from "@/browser/components/ui/button"; import type { SettingsSection } from "./types"; const SECTIONS: SettingsSection[] = [ @@ -32,70 +32,51 @@ const SECTIONS: SettingsSection[] = [ export function SettingsModal() { const { isOpen, close, activeSection, setActiveSection } = useSettings(); - const handleClose = useCallback(() => { - close(); - }, [close]); - - // Handle escape key - useEffect(() => { - if (!isOpen) return; - - const handleKeyDown = (e: KeyboardEvent) => { - if (matchesKeybind(e, KEYBINDS.CANCEL)) { - e.preventDefault(); - handleClose(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [isOpen, handleClose]); - - if (!isOpen) return null; - const currentSection = SECTIONS.find((s) => s.id === activeSection) ?? SECTIONS[0]; const SectionComponent = currentSection.component; return ( - -
e.stopPropagation()} - className="bg-dark border-border flex h-[80vh] max-h-[600px] w-[95%] max-w-[800px] flex-col overflow-hidden rounded-lg border shadow-lg md:h-[70vh] md:flex-row" + !open && close()}> + + {/* Visually hidden title for accessibility */} + + Settings + {/* Sidebar - horizontal tabs on mobile, vertical on desktop */}
- - Settings - + Settings {/* Close button in header on mobile only */} - +
@@ -104,20 +85,21 @@ export function SettingsModal() {
{currentSection.label} - +
-
-
+ + ); } diff --git a/src/browser/components/Settings/sections/GeneralSection.tsx b/src/browser/components/Settings/sections/GeneralSection.tsx index 5699e76c2..fcb2fff95 100644 --- a/src/browser/components/Settings/sections/GeneralSection.tsx +++ b/src/browser/components/Settings/sections/GeneralSection.tsx @@ -1,5 +1,12 @@ import React from "react"; import { useTheme, THEME_OPTIONS, type ThemeMode } from "@/browser/contexts/ThemeContext"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/browser/components/ui/select"; export function GeneralSection() { const { theme, setTheme } = useTheme(); @@ -13,17 +20,18 @@ export function GeneralSection() {
Theme
Choose your preferred theme
- +
diff --git a/src/browser/components/Settings/sections/ModelRow.tsx b/src/browser/components/Settings/sections/ModelRow.tsx index bcf82bbf5..2a4c463ec 100644 --- a/src/browser/components/Settings/sections/ModelRow.tsx +++ b/src/browser/components/Settings/sections/ModelRow.tsx @@ -4,6 +4,7 @@ import { GatewayIcon } from "@/browser/components/icons/GatewayIcon"; import { cn } from "@/common/lib/utils"; import { TooltipWrapper, Tooltip } from "@/browser/components/Tooltip"; import { ProviderWithIcon } from "@/browser/components/ProviderIcon"; +import { Button } from "@/browser/components/ui/button"; export interface ModelRowProps { provider: string; @@ -74,24 +75,26 @@ export function ModelRow(props: ModelRowProps) {
{props.isEditing ? ( <> - - + ) : ( <> @@ -116,22 +119,23 @@ export function ModelRow(props: ModelRowProps) { )} {/* Favorite/default button */} - + {props.isDefault ? "Default model" : "Set as default"} @@ -139,24 +143,26 @@ export function ModelRow(props: ModelRowProps) { {/* Edit/delete buttons only for custom models */} {props.isCustom && ( <> - - + )} diff --git a/src/browser/components/Settings/sections/ModelsSection.tsx b/src/browser/components/Settings/sections/ModelsSection.tsx index b255e60ef..1fc8de4fa 100644 --- a/src/browser/components/Settings/sections/ModelsSection.tsx +++ b/src/browser/components/Settings/sections/ModelsSection.tsx @@ -7,6 +7,14 @@ import { useGateway } from "@/browser/hooks/useGatewayModels"; import { ModelRow } from "./ModelRow"; import { useAPI } from "@/browser/contexts/API"; import { useProvidersConfig } from "@/browser/hooks/useProvidersConfig"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/browser/components/ui/select"; +import { Button } from "@/browser/components/ui/button"; // Providers to exclude from the custom models UI (handled specially or internal) const HIDDEN_PROVIDERS = new Set(["mux-gateway"]); @@ -167,19 +175,22 @@ export function ModelsSection() { {/* Add new model form */}
-
- setNewModel((prev) => ({ ...prev, provider: e.target.value }))} - className="bg-modal-bg border-border-medium focus:border-accent shrink-0 rounded border px-2 py-1 text-xs focus:outline-none" + onValueChange={(value) => setNewModel((prev) => ({ ...prev, provider: value }))} > - - {SUPPORTED_PROVIDERS.map((p) => ( - - ))} - + + + + + {SUPPORTED_PROVIDERS.map((p) => ( + + {PROVIDER_DISPLAY_NAMES[p]} + + ))} + + - +
{error && !editing &&
{error}
}
diff --git a/src/browser/components/Settings/sections/ProvidersSection.tsx b/src/browser/components/Settings/sections/ProvidersSection.tsx index 834f80abc..f507c4199 100644 --- a/src/browser/components/Settings/sections/ProvidersSection.tsx +++ b/src/browser/components/Settings/sections/ProvidersSection.tsx @@ -6,6 +6,7 @@ import { ProviderWithIcon } from "@/browser/components/ProviderIcon"; import { useAPI } from "@/browser/contexts/API"; import { useProvidersConfig } from "@/browser/hooks/useProvidersConfig"; import { useGateway } from "@/browser/hooks/useGatewayModels"; +import { Button } from "@/browser/components/ui/button"; interface FieldConfig { key: string; @@ -214,10 +215,10 @@ export function ProvidersSection() { className="border-border-medium bg-background-secondary overflow-hidden rounded-md border" > {/* Provider header */} - + {/* Provider settings */} {isExpanded && ( @@ -266,20 +267,22 @@ export function ProvidersSection() { if (e.key === "Escape") handleCancelEdit(); }} /> - - +
) : (
@@ -294,23 +297,25 @@ export function ProvidersSection() { {(fieldConfig.type === "text" ? !!fieldValue : fieldConfig.type === "secret" && fieldIsSet) && ( - + )} - +
)} diff --git a/src/browser/components/SettingsButton.tsx b/src/browser/components/SettingsButton.tsx index 01382ed72..f6b7e099c 100644 --- a/src/browser/components/SettingsButton.tsx +++ b/src/browser/components/SettingsButton.tsx @@ -2,22 +2,24 @@ import { Settings } from "lucide-react"; import { useSettings } from "@/browser/contexts/SettingsContext"; import { TooltipWrapper, Tooltip } from "./Tooltip"; import { formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds"; +import { Button } from "@/browser/components/ui/button"; export function SettingsButton() { const { open } = useSettings(); return ( - + Settings ({formatKeybind(KEYBINDS.OPEN_SETTINGS)}) ); diff --git a/src/browser/components/StartHereModal.tsx b/src/browser/components/StartHereModal.tsx index d05d2d6f1..a3c2e8eb5 100644 --- a/src/browser/components/StartHereModal.tsx +++ b/src/browser/components/StartHereModal.tsx @@ -1,5 +1,13 @@ import React, { useState, useCallback } from "react"; -import { Modal, ModalActions, CancelButton, PrimaryButton } from "./Modal"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/browser/components/ui/dialog"; +import { Button } from "@/browser/components/ui/button"; interface StartHereModalProps { isOpen: boolean; @@ -28,22 +36,33 @@ export const StartHereModal: React.FC = ({ isOpen, onClose, } }, [isExecuting, onConfirm, onClose]); + const handleOpenChange = useCallback( + (open: boolean) => { + if (!open && !isExecuting) { + handleCancel(); + } + }, + [isExecuting, handleCancel] + ); + return ( - - - - Cancel - - void handleConfirm()} disabled={isExecuting}> - {isExecuting ? "Starting..." : "OK"} - - - + + + + Start Here + + This will replace all chat history with this message + + + + + + + + ); }; diff --git a/src/browser/components/ThemeSelector.tsx b/src/browser/components/ThemeSelector.tsx index e3a6f723f..79d0ca5e5 100644 --- a/src/browser/components/ThemeSelector.tsx +++ b/src/browser/components/ThemeSelector.tsx @@ -1,5 +1,12 @@ import { useTheme, THEME_OPTIONS, type ThemeMode } from "@/browser/contexts/ThemeContext"; import { TooltipWrapper, Tooltip } from "./Tooltip"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/browser/components/ui/select"; export function ThemeSelector() { const { theme, setTheme } = useTheme(); @@ -7,19 +14,22 @@ export function ThemeSelector() { return ( - + Theme: {currentLabel} ); diff --git a/src/browser/components/WorkspaceHeader.tsx b/src/browser/components/WorkspaceHeader.tsx index adfaf64df..794c876d1 100644 --- a/src/browser/components/WorkspaceHeader.tsx +++ b/src/browser/components/WorkspaceHeader.tsx @@ -5,6 +5,7 @@ import { TooltipWrapper, Tooltip } from "./Tooltip"; import { formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds"; import { useGitStatus } from "@/browser/stores/GitStatusStore"; import { useWorkspaceSidebarState } from "@/browser/stores/WorkspaceStore"; +import { Button } from "@/browser/components/ui/button"; import type { RuntimeConfig } from "@/common/types/runtime"; import { useTutorial } from "@/browser/contexts/TutorialContext"; import { useOpenTerminal } from "@/browser/hooks/useOpenTerminal"; @@ -64,15 +65,17 @@ export const WorkspaceHeader: React.FC = ({ - + Open terminal window ({formatKeybind(KEYBINDS.OPEN_TERMINAL)}) diff --git a/src/browser/components/ui/button.tsx b/src/browser/components/ui/button.tsx index 902ea85b8..e2c0a39f2 100644 --- a/src/browser/components/ui/button.tsx +++ b/src/browser/components/ui/button.tsx @@ -4,18 +4,17 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/common/lib/utils"; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { - default: "bg-button-bg text-button-text shadow hover:bg-button-hover", + default: "bg-accent text-white shadow hover:bg-accent-dark", destructive: "bg-error text-white shadow-sm hover:bg-error/90", outline: - "border border-input-border bg-transparent shadow-sm hover:bg-button-bg hover:text-button-text", - secondary: - "bg-background-secondary text-foreground shadow-sm hover:bg-background-secondary/80", - ghost: "hover:bg-button-bg hover:text-button-text", - link: "text-plan-mode underline-offset-4 hover:underline", + "border border-border-medium bg-transparent shadow-sm hover:bg-hover hover:text-foreground", + secondary: "bg-border-medium text-foreground shadow-sm hover:bg-border-darker", + ghost: "hover:bg-hover hover:text-foreground", + link: "text-accent underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2", diff --git a/src/browser/components/ui/context-menu.tsx b/src/browser/components/ui/context-menu.tsx new file mode 100644 index 000000000..4237ac741 --- /dev/null +++ b/src/browser/components/ui/context-menu.tsx @@ -0,0 +1,184 @@ +import * as React from "react"; +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; +import { Check, ChevronRight, Circle } from "lucide-react"; + +import { cn } from "@/common/lib/utils"; + +const ContextMenu = ContextMenuPrimitive.Root; + +const ContextMenuTrigger = ContextMenuPrimitive.Trigger; + +const ContextMenuGroup = ContextMenuPrimitive.Group; + +const ContextMenuPortal = ContextMenuPrimitive.Portal; + +const ContextMenuSub = ContextMenuPrimitive.Sub; + +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; + +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; + +const ContextMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; + +const ContextMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName; + +const ContextMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName; + +const ContextMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName; + +const ContextMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName; + +const ContextMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName; + +const ContextMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName; + +const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { + return ( + + ); +}; +ContextMenuShortcut.displayName = "ContextMenuShortcut"; + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +}; diff --git a/src/browser/components/ui/dialog.tsx b/src/browser/components/ui/dialog.tsx new file mode 100644 index 000000000..ae292463e --- /dev/null +++ b/src/browser/components/ui/dialog.tsx @@ -0,0 +1,214 @@ +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import * as VisuallyHiddenPrimitive from "@radix-ui/react-visually-hidden"; +import { X } from "lucide-react"; + +import { cn } from "@/common/lib/utils"; + +/** + * VisuallyHidden component for accessibility - hides content visually but keeps it available to screen readers. + * Use this to wrap DialogTitle when you want a custom visible title but still need accessibility. + */ +const VisuallyHidden = VisuallyHiddenPrimitive.Root; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + /** Whether to show the close button (default: true) */ + showCloseButton?: boolean; + /** Maximum width of the dialog (default: max-w-lg) */ + maxWidth?: string; + /** Maximum height of the dialog */ + maxHeight?: string; + } +>(({ className, children, showCloseButton = true, maxWidth, maxHeight, style, ...props }, ref) => ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +// Utility components for modal content +const DialogInfo = ({ + children, + className, + id, +}: { + children: React.ReactNode; + className?: string; + id?: string; +}) => ( +
+ {children} +
+); +DialogInfo.displayName = "DialogInfo"; + +// Error/Warning display components +const ErrorSection = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) =>
{children}
; +ErrorSection.displayName = "ErrorSection"; + +const ErrorLabel = ({ children, className }: { children: React.ReactNode; className?: string }) => ( +
+ {children} +
+); +ErrorLabel.displayName = "ErrorLabel"; + +const ErrorCodeBlock = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => ( +
+    {children}
+  
+); +ErrorCodeBlock.displayName = "ErrorCodeBlock"; + +const WarningBox = ({ children, className }: { children: React.ReactNode; className?: string }) => ( +
+ {children} +
+); +WarningBox.displayName = "WarningBox"; + +const WarningTitle = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) =>
{children}
; +WarningTitle.displayName = "WarningTitle"; + +const WarningText = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) =>
{children}
; +WarningText.displayName = "WarningText"; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, + DialogInfo, + ErrorSection, + ErrorLabel, + ErrorCodeBlock, + WarningBox, + WarningTitle, + WarningText, + VisuallyHidden, +}; diff --git a/src/browser/components/ui/select.tsx b/src/browser/components/ui/select.tsx new file mode 100644 index 000000000..1c8633f86 --- /dev/null +++ b/src/browser/components/ui/select.tsx @@ -0,0 +1,151 @@ +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown, ChevronUp } from "lucide-react"; + +import { cn } from "@/common/lib/utils"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; diff --git a/src/browser/stories/App.settings.stories.tsx b/src/browser/stories/App.settings.stories.tsx index 6b20345be..70a4c28c0 100644 --- a/src/browser/stories/App.settings.stories.tsx +++ b/src/browser/stories/App.settings.stories.tsx @@ -51,10 +51,11 @@ async function openSettingsToSection(canvasElement: HTMLElement, section?: strin const settingsButton = await canvas.findByTestId("settings-button", {}, { timeout: 10000 }); await userEvent.click(settingsButton); - // Wait for modal to appear + // Wait for modal to appear - Radix Dialog uses a portal so we need to search the entire document + const body = within(document.body); await waitFor( () => { - const modal = canvas.getByRole("dialog"); + const modal = body.getByRole("dialog"); if (!modal) throw new Error("Settings modal not found"); }, { timeout: 5000 } @@ -63,7 +64,7 @@ async function openSettingsToSection(canvasElement: HTMLElement, section?: strin // Navigate to specific section if requested // The sidebar nav has buttons with exact section names if (section && section !== "general") { - const modal = canvas.getByRole("dialog"); + const modal = body.getByRole("dialog"); const modalCanvas = within(modal); // Find the nav section button (exact text match) const navButtons = await modalCanvas.findAllByRole("button"); diff --git a/tests/e2e/scenarios/settings.spec.ts b/tests/e2e/scenarios/settings.spec.ts index 7a4a83702..7aaefed7b 100644 --- a/tests/e2e/scenarios/settings.spec.ts +++ b/tests/e2e/scenarios/settings.spec.ts @@ -69,9 +69,9 @@ test.describe("Settings Modal", () => { await ui.projects.openFirstWorkspace(); await ui.settings.open(); - // Click overlay (outside modal content) - const overlay = page.locator('[role="presentation"]'); - await overlay.click({ position: { x: 10, y: 10 } }); + // Click overlay (outside modal content) - Radix Dialog uses data-state attribute + const overlay = page.locator('[data-state="open"].fixed.inset-0'); + await overlay.click({ position: { x: 10, y: 10 }, force: true }); // Verify closed await ui.settings.expectClosed();