diff --git a/package-lock.json b/package-lock.json
index af3d37a4..3e8928b7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,6 +21,7 @@
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
@@ -35,6 +36,7 @@
"@uiw/codemirror-themes": "^4.21.24",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
+ "cmdk": "^1.0.0",
"codemirror": "^6.0.1",
"dayjs": "^1.11.10",
"lucide-react": "^0.372.0",
@@ -2180,6 +2182,418 @@
}
}
},
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.0.tgz",
+ "integrity": "sha512-2wdgj6eKNVoFNFtYv2xwkzhIJPlJ5L2aV0eKTZHi5dUVrGy+MhgoV8IyyeFpkZQrwwFzbFlnWl1bwyjVBCNapQ==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.0",
+ "@radix-ui/react-dismissable-layer": "1.1.0",
+ "@radix-ui/react-focus-guards": "1.1.0",
+ "@radix-ui/react-focus-scope": "1.1.0",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-popper": "1.2.0",
+ "@radix-ui/react-portal": "1.1.0",
+ "@radix-ui/react-presence": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-slot": "1.1.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "aria-hidden": "^1.1.1",
+ "react-remove-scroll": "2.5.7"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
+ "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
+ "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.0"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
+ "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
+ "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz",
+ "integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-escape-keydown": "1.1.0"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz",
+ "integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz",
+ "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-id": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
+ "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
+ "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0",
+ "@radix-ui/react-use-rect": "1.1.0",
+ "@radix-ui/react-use-size": "1.1.0",
+ "@radix-ui/rect": "1.1.0"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.0.tgz",
+ "integrity": "sha512-0tXZ5O6qAVvuN9SWP0X+zadHf9hzHiMf/vxOU+kXO+fbtS8lS57MXa6EmikDxk9s/Bmkk80+dcxgbvisIyeqxg==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.0"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz",
+ "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
+ "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.1.0"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
+ "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
+ "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
+ "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
+ "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
+ "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
+ "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
+ "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/rect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
+ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/react-remove-scroll": {
+ "version": "2.5.7",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz",
+ "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.4",
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.0",
+ "use-sidecar": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-popper": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz",
@@ -3814,6 +4228,19 @@
"node": ">=6"
}
},
+ "node_modules/cmdk": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz",
+ "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==",
+ "dependencies": {
+ "@radix-ui/react-dialog": "1.0.5",
+ "@radix-ui/react-primitive": "1.0.3"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
"node_modules/codemirror": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
diff --git a/package.json b/package.json
index 6af8e697..55d5e433 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
@@ -37,6 +38,7 @@
"@uiw/codemirror-themes": "^4.21.24",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
+ "cmdk": "^1.0.0",
"codemirror": "^6.0.1",
"dayjs": "^1.11.10",
"lucide-react": "^0.372.0",
diff --git a/src-tauri/src/db/settings.rs b/src-tauri/src/db/settings.rs
index 63b5b298..d93bb40e 100644
--- a/src-tauri/src/db/settings.rs
+++ b/src-tauri/src/db/settings.rs
@@ -7,13 +7,26 @@ pub fn insert_initial_settings(conn: &Connection) -> Result<()> {
// theme
("theme", "dark"),
// editor
- ("vim", "true"),
+ ("vim", "false"),
("line_numbers", "false"),
("highlight_active_line", "false"),
- ("line_wrapping", "false"),
- // nostr
- ("public_key", ""),
- ("private_key", ""),
+ ("line_wrapping", "true"),
+ ("unordered_list_bullet", "*"),
+ ("indent_unit", "4"),
+ ("tab_size", "4"),
+ ("font_size", "16"),
+ (
+ "font_family",
+ r#"SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace"#,
+ ),
+ ("font_weight", "normal"),
+ ("line_height", "1.5"),
+ // profile
+ ("npub", ""),
+ ("nsec", ""),
+ // relays
+ ("relays", "[\"relay.damus.io\", \"nos.lol\"]"),
+
];
for (key, value) in initial_settings {
diff --git a/src/components/context/Login.tsx b/src/components/context/Login.tsx
index 04a78316..c6a775f2 100644
--- a/src/components/context/Login.tsx
+++ b/src/components/context/Login.tsx
@@ -2,7 +2,7 @@ import { GearIcon } from "@radix-ui/react-icons";
import { useAppContext } from "~/store";
export default function Login() {
- const { setActivePage } = useAppContext();
+ const { setActivePage, settings } = useAppContext();
const handleOpenSettings = () => {
setActivePage("settings");
@@ -12,9 +12,15 @@ export default function Login() {
-
- Chris Chiarulli
-
+ {settings.npub ? (
+
+ {settings.npub.slice(0, 8)}...
+
+ ) : (
+
+ Login
+
+ )}
logged in
{
+ setEditorSettings({
+ ...editorSettings,
+ selectedFontFamily: initialUserSelectedFontFamily(settings.font_family),
+ });
+ }, []);
+
+ async function updateSetting(key: string, value: string) {
+ if (key in settings) {
+ await setSetting(key, value);
+ setSettings({ ...settings, [key]: value });
+ }
+ }
async function handleSwitchOnClick(
event: React.MouseEvent,
- settingKey: SettingsSwitchKeys,
+ key: string,
) {
if (event.target instanceof HTMLButtonElement) {
setLoading(true);
- const updatedSettings = { ...settings };
try {
if (event.target.dataset.state === "unchecked") {
- await setSetting(settingKey, "true");
- const getSettingResponse = await getSetting(settingKey);
- if (getSettingResponse.data === "true") {
- updatedSettings[settingKey] = getSettingResponse.data;
- setSettings(updatedSettings);
- }
+ await updateSetting(key, "true");
} else if (event.target.dataset.state === "checked") {
- await setSetting(settingKey, "false");
- const getSettingResponse = await getSetting(settingKey);
- if (getSettingResponse.data === "false") {
- updatedSettings[settingKey] = getSettingResponse.data;
- setSettings(updatedSettings);
- }
+ await updateSetting(key, "false");
}
} catch (error) {
- console.error("Settings error: ", error);
+ console.error("Editor settings error: ", error);
} finally {
setLoading(false);
}
}
}
- async function handleInputOnChange(
- event: React.ChangeEvent,
+ async function handleSelectOnValueChange(
+ key: string,
+ value: UnorderedListBullet | FontWeight,
) {
- console.log("handleInputOnChange event: ", event);
+ setLoading(true);
+ try {
+ await updateSetting(key, value);
+ if (value === "-" || value === "*" || value === "+") {
+ setEditorSettings({ ...editorSettings, unorderedListBullet: value });
+ } else if (
+ value === "lighter" ||
+ value === "normal" ||
+ value === "bold" ||
+ value === "bolder"
+ ) {
+ setEditorSettings({ ...editorSettings, fontWeight: value });
+ }
+ } catch (error) {
+ console.error("Editor settings error: ", error);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ async function handleInputOnBlur(key: string, value: string) {
+ setLoading(true);
+ try {
+ const validationResult = partialEditorSettingsSchema.safeParse({
+ [key]: value,
+ });
+
+ if (validationResult.success) {
+ await updateSetting(key, value);
+ setErrorMessages({ ...errorMessages, [key]: "" });
+ } else {
+ setErrorMessages({
+ ...errorMessages,
+ [key]: validationResult.error.issues[0].message,
+ });
+ }
+ } catch (error) {
+ console.error("Editor settings error: ", error);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ async function handleComboxOnSelect(key: string, value: string) {
+ setLoading(true);
+ try {
+ await updateSetting(key, prioritizeUserFontFamilies(value));
+ setEditorSettings({
+ ...editorSettings,
+ selectedFontFamily:
+ value === editorSettings.selectedFontFamily ? "" : value,
+ });
+ setOpenFontFamilyCombobox(false);
+ } catch (error) {
+ console.error("Editor settings error: ", error);
+ } finally {
+ setLoading(false);
+ }
}
return (
@@ -72,10 +175,10 @@ export default function EditorSettings() {
-
+
handleSwitchOnClick(event, "vim")}
className="ml-2 disabled:cursor-pointer disabled:opacity-100"
disabled={loading}
@@ -86,10 +189,10 @@ export default function EditorSettings() {
-
+
handleSwitchOnClick(event, "line_numbers")}
className="ml-2 disabled:cursor-pointer disabled:opacity-100"
disabled={loading}
@@ -100,10 +203,10 @@ export default function EditorSettings() {
-
+
handleSwitchOnClick(event, "highlight_active_line")
}
@@ -116,10 +219,10 @@ export default function EditorSettings() {
-
+
handleSwitchOnClick(event, "line_wrapping")}
className="ml-2 disabled:cursor-pointer disabled:opacity-100"
disabled={loading}
@@ -131,9 +234,16 @@ export default function EditorSettings() {
-
-
+
handleInputOnChange(event)}
+ placeholder="4"
+ className="disabled:cursor-text disabled:opacity-100"
+ disabled={loading}
+ min="0"
+ max="100"
+ value={editorSettings.indentUnit}
+ onChange={(event) =>
+ setEditorSettings({
+ ...editorSettings,
+ indentUnit: event.currentTarget.value,
+ })
+ }
+ onBlur={() =>
+ handleInputOnBlur("indent_unit", editorSettings.indentUnit)
+ }
/>
How many spaces a block should be indented
+ {errorMessages.indent_unit !== "" && (
+
+ {errorMessages.indent_unit}
+
+ )}
-
+
handleInputOnChange(event)}
+ className="disabled:cursor-text disabled:opacity-100"
+ disabled={loading}
+ min="0"
+ max="100"
+ value={editorSettings.tabSize}
+ onChange={(event) =>
+ setEditorSettings({
+ ...editorSettings,
+ tabSize: event.currentTarget.value,
+ })
+ }
+ onBlur={() =>
+ handleInputOnBlur("tab_size", editorSettings.tabSize)
+ }
/>
The width of the tab character
+ {errorMessages.tab_size !== "" && (
+
+ {errorMessages.tab_size}
+
+ )}
-
+
handleInputOnChange(event)}
+ placeholder="16"
+ className="disabled:cursor-text disabled:opacity-100"
+ disabled={loading}
+ min="1"
+ max="100"
+ value={editorSettings.fontSize}
+ onChange={(event) =>
+ setEditorSettings({
+ ...editorSettings,
+ fontSize: event.currentTarget.value,
+ })
+ }
+ onBlur={() =>
+ handleInputOnBlur("font_size", editorSettings.fontSize)
+ }
/>
Height in pixels of editor text
+ {errorMessages.font_size !== "" && (
+
+ {errorMessages.font_size}
+
+ )}
The name of the font family used for editor text
@@ -213,35 +442,70 @@ export default function EditorSettings() {
-
- handleInputOnChange(event)}
- />
-
+
+ handleSelectOnValueChange("font_weight", value)
+ }
+ >
+
+
+
+
+
+
+ lighter
+ normal
+ bold
+ bolder
+
+
The weight of the font used for editor text
-
+
handleInputOnChange(event)}
+ className="disabled:cursor-text disabled:opacity-100"
+ disabled={loading}
+ min="1"
+ max="10"
+ step="0.5"
+ value={editorSettings.lineHeight}
+ onChange={(event) =>
+ setEditorSettings({
+ ...editorSettings,
+ lineHeight: event.currentTarget.value,
+ })
+ }
+ onBlur={() =>
+ handleInputOnBlur("line_height", editorSettings.lineHeight)
+ }
/>
Height of editor lines, as a multiplier of font size
+ {errorMessages.line_height !== "" && (
+
+ {errorMessages.line_height}
+
+ )}
diff --git a/src/components/settings/NostrSettings.tsx b/src/components/settings/NostrSettings.tsx
deleted file mode 100644
index 927b0055..00000000
--- a/src/components/settings/NostrSettings.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { zodResolver } from "@hookform/resolvers/zod";
-import { Button } from "~/components/ui/button";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "~/components/ui/card";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "~/components/ui/form";
-import { Input } from "~/components/ui/input";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-
-const FormSchema = z.object({
- nsec: z.string(),
-});
-
-export default function NostrSettings() {
- const form = useForm
>({
- resolver: zodResolver(FormSchema),
- defaultValues: {
- nsec: "",
- },
- });
-
- function onSubmit(data: z.infer) {
- console.log(data);
- }
-
- return (
-
-
- Nostr
-
- Enter your Nostr private key to enable Nostr features
-
-
-
-
-
-
-
- );
-}
diff --git a/src/components/settings/ProfileSettings.tsx b/src/components/settings/ProfileSettings.tsx
new file mode 100644
index 00000000..82fc6194
--- /dev/null
+++ b/src/components/settings/ProfileSettings.tsx
@@ -0,0 +1,106 @@
+import { useEffect, useState } from "react";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { setSetting } from "~/api";
+import { Button } from "~/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "~/components/ui/card";
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "~/components/ui/form";
+import { Input } from "~/components/ui/input";
+import { useAppContext } from "~/store";
+import { getPublicKey, nip19 } from "nostr-tools";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+const isValidNsec = (nsec: string) => {
+ try {
+ return nip19.decode(nsec).type === "nsec";
+ } catch (e) {
+ return false;
+ }
+};
+
+const formSchema = z.object({
+ nsec: z.string().refine(isValidNsec, {
+ message: "Invalid nsec.",
+ }),
+});
+
+export default function ProfileSettings() {
+ const [loading, setLoading] = useState(false);
+
+ const { settings, setSettings } = useAppContext();
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ nsec: settings.nsec || "", // Set the default value from settings
+ },
+ });
+
+ // If settings.nsec is not available at the initial render, update the form state once settings are fetched
+ useEffect(() => {
+ if (settings.nsec) {
+ form.reset({ nsec: settings.nsec });
+ }
+ }, [settings.nsec, form]);
+
+ async function onSubmit(data: z.infer) {
+ setLoading(true);
+ const privateKey = nip19.decode(data.nsec).data as Uint8Array;
+ const publicKey = getPublicKey(privateKey);
+ const npub = nip19.npubEncode(publicKey);
+ const { nsec } = data;
+ await setSetting("nsec", nsec);
+ await setSetting("npub", npub);
+ setSettings({ ...settings, nsec: nsec, npub: npub });
+ setLoading(false);
+ }
+
+ return (
+
+
+ Profile
+
+ Enter your profile Details
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/settings/RelaySettings.tsx b/src/components/settings/RelaySettings.tsx
new file mode 100644
index 00000000..dfd5e5da
--- /dev/null
+++ b/src/components/settings/RelaySettings.tsx
@@ -0,0 +1,99 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Button } from "~/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormMessage,
+} from "~/components/ui/form";
+import { Input } from "~/components/ui/input";
+import { useFieldArray, useForm } from "react-hook-form";
+import { z } from "zod";
+
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "../ui/card";
+
+const profileFormSchema = z.object({
+ urls: z
+ .array(
+ z.object({
+ value: z.string().url({ message: "Please enter a valid URL." }),
+ }),
+ )
+ .optional(),
+});
+
+type ProfileFormValues = z.infer;
+
+// This can come from your database or API.
+const defaultValues: Partial = {
+ urls: [
+ { value: "wss://relay.damus.io" },
+ { value: "wss://nos.lol" },
+ ],
+};
+
+export default function RelaySettings() {
+ const form = useForm({
+ resolver: zodResolver(profileFormSchema),
+ defaultValues,
+ mode: "onChange",
+ });
+
+ const { fields, append } = useFieldArray({
+ name: "urls",
+ control: form.control,
+ });
+
+ function onSubmit(data: ProfileFormValues) {
+ console.log(data);
+ }
+
+ return (
+
+
+ Relays
+ Configure your relays
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/tags/DeleteTagDialog.tsx b/src/components/tags/DeleteTagDialog.tsx
index 697875ae..07c55424 100644
--- a/src/components/tags/DeleteTagDialog.tsx
+++ b/src/components/tags/DeleteTagDialog.tsx
@@ -49,12 +49,10 @@ export default function DeleteTagDialog() {