ui: theme system, routing fixes, major overhaul, still need to fix dropdowns
This commit is contained in:
@@ -5,21 +5,29 @@
|
|||||||
"name": "concord-client",
|
"name": "concord-client",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@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-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
"@tanstack/react-query": "^5.90.2",
|
"@tanstack/react-query": "^5.90.2",
|
||||||
"@tanstack/react-query-devtools": "^5.90.2",
|
"@tanstack/react-query-devtools": "^5.90.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"react-router": "^7.9.3",
|
"react-router": "^7.9.3",
|
||||||
|
"react-syntax-highlighter": "^15.6.6",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
@@ -28,6 +36,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.64",
|
"@types/react": "^18.2.64",
|
||||||
"@types/react-dom": "^18.2.21",
|
"@types/react-dom": "^18.2.21",
|
||||||
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
||||||
"@typescript-eslint/parser": "^7.1.1",
|
"@typescript-eslint/parser": "^7.1.1",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
@@ -79,6 +88,8 @@
|
|||||||
|
|
||||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||||
|
|
||||||
|
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
||||||
|
|
||||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
"@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
|
"@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
|
||||||
@@ -205,6 +216,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": ["@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-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=="],
|
"@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=="],
|
||||||
|
|
||||||
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "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-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
|
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "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-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
|
||||||
@@ -233,8 +246,16 @@
|
|||||||
|
|
||||||
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@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-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
|
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@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-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@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-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "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-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "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-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "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-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="],
|
||||||
|
|
||||||
"@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-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-switch": ["@radix-ui/react-switch@1.2.6", "", { "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-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "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-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "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-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@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", "@radix-ui/react-visually-hidden": "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-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
|
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "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-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@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", "@radix-ui/react-visually-hidden": "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-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
|
||||||
|
|
||||||
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
||||||
@@ -249,6 +270,8 @@
|
|||||||
|
|
||||||
"@radix-ui/react-use-layout-effect": ["@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-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
"@radix-ui/react-use-layout-effect": ["@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-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
|
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
|
||||||
|
|
||||||
"@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-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=="],
|
||||||
@@ -361,12 +384,18 @@
|
|||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||||
|
|
||||||
"@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="],
|
"@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="],
|
||||||
|
|
||||||
|
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||||
|
|
||||||
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="],
|
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="],
|
||||||
|
|
||||||
"@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],
|
"@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],
|
||||||
|
|
||||||
|
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||||
|
|
||||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@20.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ=="],
|
"@types/node": ["@types/node@20.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ=="],
|
||||||
@@ -379,8 +408,12 @@
|
|||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
|
"@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
|
||||||
|
|
||||||
|
"@types/react-syntax-highlighter": ["@types/react-syntax-highlighter@15.5.13", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="],
|
||||||
|
|
||||||
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
|
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
|
||||||
|
|
||||||
|
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||||
|
|
||||||
"@types/verror": ["@types/verror@1.10.11", "", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="],
|
"@types/verror": ["@types/verror@1.10.11", "", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="],
|
||||||
|
|
||||||
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
|
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
|
||||||
@@ -447,6 +480,8 @@
|
|||||||
|
|
||||||
"at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="],
|
"at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="],
|
||||||
|
|
||||||
|
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||||
|
|
||||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
@@ -489,8 +524,18 @@
|
|||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001745", "", {}, "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001745", "", {}, "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ=="],
|
||||||
|
|
||||||
|
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||||
|
|
||||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
|
"character-entities": ["character-entities@1.2.4", "", {}, "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw=="],
|
||||||
|
|
||||||
|
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||||
|
|
||||||
|
"character-entities-legacy": ["character-entities-legacy@1.1.4", "", {}, "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA=="],
|
||||||
|
|
||||||
|
"character-reference-invalid": ["character-reference-invalid@1.1.4", "", {}, "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="],
|
||||||
|
|
||||||
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
|
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
|
||||||
|
|
||||||
"chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="],
|
"chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="],
|
||||||
@@ -513,6 +558,8 @@
|
|||||||
|
|
||||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||||
|
|
||||||
|
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||||
|
|
||||||
"commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
|
"commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
|
||||||
|
|
||||||
"compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="],
|
"compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="],
|
||||||
@@ -539,8 +586,12 @@
|
|||||||
|
|
||||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
|
||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="],
|
||||||
|
|
||||||
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||||
|
|
||||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||||
@@ -553,12 +604,16 @@
|
|||||||
|
|
||||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
|
|
||||||
|
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||||
|
|
||||||
"detect-libc": ["detect-libc@2.1.1", "", {}, "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw=="],
|
"detect-libc": ["detect-libc@2.1.1", "", {}, "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw=="],
|
||||||
|
|
||||||
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
|
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
|
||||||
|
|
||||||
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||||
|
|
||||||
|
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||||
|
|
||||||
"dir-compare": ["dir-compare@3.3.0", "", { "dependencies": { "buffer-equal": "^1.0.0", "minimatch": "^3.0.4" } }, "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg=="],
|
"dir-compare": ["dir-compare@3.3.0", "", { "dependencies": { "buffer-equal": "^1.0.0", "minimatch": "^3.0.4" } }, "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg=="],
|
||||||
|
|
||||||
"dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
|
"dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
|
||||||
@@ -633,8 +688,12 @@
|
|||||||
|
|
||||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||||
|
|
||||||
|
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
|
||||||
|
|
||||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||||
|
|
||||||
|
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||||
|
|
||||||
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
|
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
|
||||||
|
|
||||||
"extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="],
|
"extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="],
|
||||||
@@ -649,6 +708,8 @@
|
|||||||
|
|
||||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||||
|
|
||||||
|
"fault": ["fault@1.0.4", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA=="],
|
||||||
|
|
||||||
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
|
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
|
||||||
|
|
||||||
"file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="],
|
"file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="],
|
||||||
@@ -667,6 +728,8 @@
|
|||||||
|
|
||||||
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
|
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
|
||||||
|
|
||||||
|
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
|
||||||
|
|
||||||
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
|
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
|
||||||
|
|
||||||
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
|
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
|
||||||
@@ -721,8 +784,22 @@
|
|||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"hast-util-parse-selector": ["hast-util-parse-selector@2.2.5", "", {}, "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ=="],
|
||||||
|
|
||||||
|
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
|
||||||
|
|
||||||
|
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||||
|
|
||||||
|
"hastscript": ["hastscript@6.0.0", "", { "dependencies": { "@types/hast": "^2.0.0", "comma-separated-tokens": "^1.0.0", "hast-util-parse-selector": "^2.0.0", "property-information": "^5.0.0", "space-separated-tokens": "^1.0.0" } }, "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w=="],
|
||||||
|
|
||||||
|
"highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="],
|
||||||
|
|
||||||
|
"highlightjs-vue": ["highlightjs-vue@1.0.0", "", {}, "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA=="],
|
||||||
|
|
||||||
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
|
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
|
||||||
|
|
||||||
|
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
||||||
|
|
||||||
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
||||||
|
|
||||||
"http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="],
|
"http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="],
|
||||||
@@ -747,18 +824,30 @@
|
|||||||
|
|
||||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
|
"inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],
|
||||||
|
|
||||||
|
"is-alphabetical": ["is-alphabetical@1.0.4", "", {}, "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="],
|
||||||
|
|
||||||
|
"is-alphanumerical": ["is-alphanumerical@1.0.4", "", { "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" } }, "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A=="],
|
||||||
|
|
||||||
"is-ci": ["is-ci@3.0.1", "", { "dependencies": { "ci-info": "^3.2.0" }, "bin": { "is-ci": "bin.js" } }, "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ=="],
|
"is-ci": ["is-ci@3.0.1", "", { "dependencies": { "ci-info": "^3.2.0" }, "bin": { "is-ci": "bin.js" } }, "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ=="],
|
||||||
|
|
||||||
|
"is-decimal": ["is-decimal@1.0.4", "", {}, "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw=="],
|
||||||
|
|
||||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||||
|
|
||||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||||
|
|
||||||
|
"is-hexadecimal": ["is-hexadecimal@1.0.4", "", {}, "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="],
|
||||||
|
|
||||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||||
|
|
||||||
"is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
|
"is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
|
||||||
|
|
||||||
|
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||||
|
|
||||||
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
|
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
|
||||||
|
|
||||||
"isbinaryfile": ["isbinaryfile@5.0.6", "", {}, "sha512-I+NmIfBHUl+r2wcDd6JwE9yWje/PIVY/R5/CmV8dXLZd5K+L9X2klAOwfAHNnondLXkbHyTAleQAWonpTJBTtw=="],
|
"isbinaryfile": ["isbinaryfile@5.0.6", "", {}, "sha512-I+NmIfBHUl+r2wcDd6JwE9yWje/PIVY/R5/CmV8dXLZd5K+L9X2klAOwfAHNnondLXkbHyTAleQAWonpTJBTtw=="],
|
||||||
@@ -835,10 +924,14 @@
|
|||||||
|
|
||||||
"lodash.union": ["lodash.union@4.6.0", "", {}, "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="],
|
"lodash.union": ["lodash.union@4.6.0", "", {}, "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="],
|
||||||
|
|
||||||
|
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||||
|
|
||||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
|
|
||||||
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
|
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
|
||||||
|
|
||||||
|
"lowlight": ["lowlight@1.20.0", "", { "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" } }, "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw=="],
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
"lucide-react": ["lucide-react@0.544.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw=="],
|
"lucide-react": ["lucide-react@0.544.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw=="],
|
||||||
@@ -849,8 +942,66 @@
|
|||||||
|
|
||||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
|
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
|
||||||
|
|
||||||
|
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
|
||||||
|
|
||||||
|
"mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
|
||||||
|
|
||||||
|
"mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
|
||||||
|
|
||||||
|
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
|
||||||
|
|
||||||
|
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="],
|
||||||
|
|
||||||
|
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
|
||||||
|
|
||||||
|
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
||||||
|
|
||||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||||
|
|
||||||
|
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||||
|
|
||||||
|
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||||
|
|
||||||
|
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
|
||||||
|
|
||||||
|
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
|
||||||
|
|
||||||
|
"micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
|
||||||
|
|
||||||
|
"micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
|
||||||
|
|
||||||
|
"micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
|
||||||
|
|
||||||
|
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||||
|
|
||||||
|
"micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
|
||||||
|
|
||||||
|
"micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
|
||||||
|
|
||||||
|
"micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
|
||||||
|
|
||||||
|
"micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
|
||||||
|
|
||||||
|
"micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
|
||||||
|
|
||||||
|
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
||||||
|
|
||||||
|
"micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
|
||||||
|
|
||||||
|
"micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
|
||||||
|
|
||||||
|
"micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
|
||||||
|
|
||||||
|
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
||||||
|
|
||||||
|
"micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
|
||||||
|
|
||||||
|
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
||||||
|
|
||||||
|
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||||
|
|
||||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||||
|
|
||||||
"mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
|
"mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
|
||||||
@@ -903,6 +1054,8 @@
|
|||||||
|
|
||||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||||
|
|
||||||
|
"parse-entities": ["parse-entities@2.0.0", "", { "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", "character-reference-invalid": "^1.0.0", "is-alphanumerical": "^1.0.0", "is-decimal": "^1.0.0", "is-hexadecimal": "^1.0.0" } }, "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ=="],
|
||||||
|
|
||||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||||
|
|
||||||
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||||
@@ -925,12 +1078,16 @@
|
|||||||
|
|
||||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
|
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
||||||
|
|
||||||
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
||||||
|
|
||||||
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
|
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
|
||||||
|
|
||||||
"promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
|
"promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
|
||||||
|
|
||||||
|
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||||
|
|
||||||
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
@@ -943,6 +1100,8 @@
|
|||||||
|
|
||||||
"react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
|
"react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
|
||||||
|
|
||||||
|
"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
|
||||||
|
|
||||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||||
|
|
||||||
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
|
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
|
||||||
@@ -953,12 +1112,20 @@
|
|||||||
|
|
||||||
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||||
|
|
||||||
|
"react-syntax-highlighter": ["react-syntax-highlighter@15.6.6", "", { "dependencies": { "@babel/runtime": "^7.3.1", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", "refractor": "^3.6.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw=="],
|
||||||
|
|
||||||
"read-config-file": ["read-config-file@6.3.2", "", { "dependencies": { "config-file-ts": "^0.2.4", "dotenv": "^9.0.2", "dotenv-expand": "^5.1.0", "js-yaml": "^4.1.0", "json5": "^2.2.0", "lazy-val": "^1.0.4" } }, "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q=="],
|
"read-config-file": ["read-config-file@6.3.2", "", { "dependencies": { "config-file-ts": "^0.2.4", "dotenv": "^9.0.2", "dotenv-expand": "^5.1.0", "js-yaml": "^4.1.0", "json5": "^2.2.0", "lazy-val": "^1.0.4" } }, "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q=="],
|
||||||
|
|
||||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||||
|
|
||||||
"readdir-glob": ["readdir-glob@1.1.3", "", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="],
|
"readdir-glob": ["readdir-glob@1.1.3", "", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="],
|
||||||
|
|
||||||
|
"refractor": ["refractor@3.6.0", "", { "dependencies": { "hastscript": "^6.0.0", "parse-entities": "^2.0.0", "prismjs": "~1.27.0" } }, "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA=="],
|
||||||
|
|
||||||
|
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||||
|
|
||||||
|
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
|
||||||
|
|
||||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
"resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
|
"resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
|
||||||
@@ -1019,6 +1186,8 @@
|
|||||||
|
|
||||||
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||||
|
|
||||||
|
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||||
|
|
||||||
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
||||||
|
|
||||||
"stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
|
"stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
|
||||||
@@ -1029,12 +1198,18 @@
|
|||||||
|
|
||||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||||
|
|
||||||
|
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||||
|
|
||||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||||
|
|
||||||
|
"style-to-js": ["style-to-js@1.1.17", "", { "dependencies": { "style-to-object": "1.0.9" } }, "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA=="],
|
||||||
|
|
||||||
|
"style-to-object": ["style-to-object@1.0.9", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw=="],
|
||||||
|
|
||||||
"sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="],
|
"sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="],
|
||||||
|
|
||||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
@@ -1059,6 +1234,10 @@
|
|||||||
|
|
||||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
|
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||||
|
|
||||||
|
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||||
|
|
||||||
"truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="],
|
"truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="],
|
||||||
|
|
||||||
"ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="],
|
"ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="],
|
||||||
@@ -1075,6 +1254,18 @@
|
|||||||
|
|
||||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
|
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||||
|
|
||||||
|
"unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="],
|
||||||
|
|
||||||
|
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||||
|
|
||||||
|
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||||
|
|
||||||
|
"unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
|
||||||
|
|
||||||
|
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="],
|
||||||
|
|
||||||
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||||
|
|
||||||
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
||||||
@@ -1093,6 +1284,10 @@
|
|||||||
|
|
||||||
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
|
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
|
||||||
|
|
||||||
|
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||||
|
|
||||||
|
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||||
|
|
||||||
"vite": ["vite@5.4.20", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g=="],
|
"vite": ["vite@5.4.20", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g=="],
|
||||||
|
|
||||||
"vite-plugin-electron": ["vite-plugin-electron@0.28.8", "", { "peerDependencies": { "vite-plugin-electron-renderer": "*" }, "optionalPeers": ["vite-plugin-electron-renderer"] }, "sha512-ir+B21oSGK9j23OEvt4EXyco9xDCaF6OGFe0V/8Zc0yL2+HMyQ6mmNQEIhXsEsZCSfIowBpwQBeHH4wVsfraeg=="],
|
"vite-plugin-electron": ["vite-plugin-electron@0.28.8", "", { "peerDependencies": { "vite-plugin-electron-renderer": "*" }, "optionalPeers": ["vite-plugin-electron-renderer"] }, "sha512-ir+B21oSGK9j23OEvt4EXyco9xDCaF6OGFe0V/8Zc0yL2+HMyQ6mmNQEIhXsEsZCSfIowBpwQBeHH4wVsfraeg=="],
|
||||||
@@ -1111,6 +1306,8 @@
|
|||||||
|
|
||||||
"xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
|
"xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
|
||||||
|
|
||||||
|
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||||
|
|
||||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||||
|
|
||||||
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||||
@@ -1127,6 +1324,8 @@
|
|||||||
|
|
||||||
"zustand": ["zustand@5.0.8", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw=="],
|
"zustand": ["zustand@5.0.8", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw=="],
|
||||||
|
|
||||||
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
@@ -1177,6 +1376,8 @@
|
|||||||
|
|
||||||
"clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
|
"clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
|
||||||
|
|
||||||
|
"decode-named-character-reference/character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||||
|
|
||||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
"filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
|
"filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
|
||||||
@@ -1187,12 +1388,22 @@
|
|||||||
|
|
||||||
"glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
"glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||||
|
|
||||||
|
"hastscript/@types/hast": ["@types/hast@2.3.10", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="],
|
||||||
|
|
||||||
|
"hastscript/comma-separated-tokens": ["comma-separated-tokens@1.0.8", "", {}, "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw=="],
|
||||||
|
|
||||||
|
"hastscript/property-information": ["property-information@5.6.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA=="],
|
||||||
|
|
||||||
|
"hastscript/space-separated-tokens": ["space-separated-tokens@1.1.5", "", {}, "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA=="],
|
||||||
|
|
||||||
"hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
"hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||||
|
|
||||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||||
|
|
||||||
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
|
"mdast-util-mdx-jsx/parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||||
|
|
||||||
"minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
"minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
|
|
||||||
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
@@ -1201,12 +1412,16 @@
|
|||||||
|
|
||||||
"readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
|
"readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
|
||||||
|
|
||||||
|
"refractor/prismjs": ["prismjs@1.27.0", "", {}, "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA=="],
|
||||||
|
|
||||||
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
|
|
||||||
"serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
"serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
||||||
|
|
||||||
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
|
"stringify-entities/character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||||
|
|
||||||
"zip-stream/archiver-utils": ["archiver-utils@3.0.4", "", { "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw=="],
|
"zip-stream/archiver-utils": ["archiver-utils@3.0.4", "", { "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw=="],
|
||||||
|
|
||||||
"@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
"@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
||||||
@@ -1237,10 +1452,26 @@
|
|||||||
|
|
||||||
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
|
||||||
|
"hastscript/@types/hast/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||||
|
|
||||||
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||||
|
|
||||||
|
"mdast-util-mdx-jsx/parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||||
|
|
||||||
|
"mdast-util-mdx-jsx/parse-entities/character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||||
|
|
||||||
|
"mdast-util-mdx-jsx/parse-entities/character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
||||||
|
|
||||||
|
"mdast-util-mdx-jsx/parse-entities/is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
|
||||||
|
|
||||||
|
"mdast-util-mdx-jsx/parse-entities/is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
|
||||||
|
|
||||||
|
"mdast-util-mdx-jsx/parse-entities/is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
||||||
|
|
||||||
"readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
"readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
|
||||||
"zip-stream/archiver-utils/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
"zip-stream/archiver-utils/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
|
|
||||||
|
"mdast-util-mdx-jsx/parse-entities/is-alphanumerical/is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,21 +11,29 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@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-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
"@tanstack/react-query": "^5.90.2",
|
"@tanstack/react-query": "^5.90.2",
|
||||||
"@tanstack/react-query-devtools": "^5.90.2",
|
"@tanstack/react-query-devtools": "^5.90.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"react-router": "^7.9.3",
|
"react-router": "^7.9.3",
|
||||||
|
"react-syntax-highlighter": "^15.6.6",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
@@ -34,6 +42,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.64",
|
"@types/react": "^18.2.64",
|
||||||
"@types/react-dom": "^18.2.21",
|
"@types/react-dom": "^18.2.21",
|
||||||
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
||||||
"@typescript-eslint/parser": "^7.1.1",
|
"@typescript-eslint/parser": "^7.1.1",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import React, { useEffect } from "react";
|
import React from "react";
|
||||||
import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router";
|
import { BrowserRouter as Router, Routes, Route } from "react-router";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
|
||||||
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import AppLayout from "@/components/layout/AppLayout";
|
import AppLayout from "@/components/layout/AppLayout";
|
||||||
import LoginPage from "@/pages/LoginPage";
|
import LoginPage from "@/pages/LoginPage";
|
||||||
import ChatPage from "@/pages/ChatPage";
|
import ChatPage from "@/pages/ChatPage";
|
||||||
import SettingsPage from "@/pages/SettingsPage";
|
import SettingsPage from "@/pages/SettingsPage";
|
||||||
import NotFoundPage from "@/pages/NotFoundPage";
|
import NotFoundPage from "@/pages/NotFoundPage";
|
||||||
|
|
||||||
import { useAuthStore } from "@/stores/authStore";
|
// import { useAuthStore } from "@/stores/authStore";
|
||||||
import { useUiStore } from "@/stores/uiStore";
|
// import { useUiStore } from "@/stores/uiStore";
|
||||||
import ErrorBoundary from "@/components/common/ErrorBoundary";
|
import ErrorBoundary from "@/components/common/ErrorBoundary";
|
||||||
|
import { Home } from "lucide-react";
|
||||||
|
|
||||||
// Create a client
|
// Create a client
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
@@ -33,29 +35,39 @@ const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
// const { isAuthenticated } = useAuthStore();
|
// const { isAuthenticated } = useAuthStore();
|
||||||
|
|
||||||
// if (!isAuthenticated) {
|
// if (!isAuthenticated) {
|
||||||
// return <Navigate to="/login" replace />;
|
// return <Navigate to="/login" replace />;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Home page component - shows server selection
|
||||||
|
const HomePage: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex items-center justify-center bg-concord-primary">
|
||||||
|
<div className="text-center text-concord-secondary max-w-md">
|
||||||
|
<div className="w-16 h-16 mx-auto mb-4 bg-concord-secondary rounded-full flex items-center justify-center">
|
||||||
|
<Home />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-semibold mb-2 text-concord-primary">
|
||||||
|
Welcome to Concord
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm mb-4">
|
||||||
|
Select a server from the sidebar to start chatting, or create a new
|
||||||
|
server
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { theme } = useUiStore();
|
|
||||||
|
|
||||||
// Apply theme to document
|
|
||||||
useEffect(() => {
|
|
||||||
document.documentElement.classList.toggle("dark", theme === "dark");
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<ThemeProvider defaultTheme="system" storageKey="discord-theme">
|
||||||
<Router>
|
<Router>
|
||||||
<div
|
<div className="h-screen w-screen overflow-hidden bg-background text-foreground">
|
||||||
className={`h-screen w-screen overflow-hidden ${theme === "dark" ? "bg-gray-900 text-white" : "bg-white text-gray-900"}`}
|
|
||||||
>
|
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Auth routes */}
|
{/* Auth routes */}
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
@@ -64,23 +76,15 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
// <ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<AppLayout />
|
<AppLayout />
|
||||||
// </ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* Default redirect to channels */}
|
{/* Default redirect to home */}
|
||||||
<Route
|
<Route index element={<HomePage />} />
|
||||||
index
|
|
||||||
element={<Navigate to="/channels/@me" replace />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Chat routes */}
|
{/* Server and channel routes */}
|
||||||
<Route
|
|
||||||
path="channels"
|
|
||||||
element={<Navigate to="/channels/@me" replace />}
|
|
||||||
/>
|
|
||||||
<Route path="channels/@me" element={<ChatPage />} />
|
|
||||||
<Route path="channels/:instanceId" element={<ChatPage />} />
|
<Route path="channels/:instanceId" element={<ChatPage />} />
|
||||||
<Route
|
<Route
|
||||||
path="channels/:instanceId/:channelId"
|
path="channels/:instanceId/:channelId"
|
||||||
@@ -98,11 +102,10 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
|
|
||||||
{/* Dev tools - only in development */}
|
{import.meta.env.DEV === true && <ReactQueryDevtools />}
|
||||||
{/*process.env.NODE_ENV === "development" && <ReactQueryDevtools />*/}
|
|
||||||
|
|
||||||
{/* Toast notifications */}
|
{/* Toast notifications */}
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
</ThemeProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ const ChannelItem: React.FC<ChannelItemProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={`w-full justify-start px-2 py-1 h-8 text-left font-medium ${
|
className={`w-full justify-start px-2 py-2 h-8 text-left font-medium p-3 rounded-lg transition-all ${
|
||||||
isActive
|
isActive
|
||||||
? "bg-gray-600 text-white"
|
? "border-primary bg-primary/10 border-2 "
|
||||||
: "text-gray-300 hover:bg-gray-700 hover:text-gray-100"
|
: "hover:border-primary/50"
|
||||||
}`}
|
}`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -49,14 +49,14 @@ const Avatar: React.FC<AvatarProps> = ({
|
|||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "online":
|
case "online":
|
||||||
return "bg-green-500";
|
return "bg-status-online";
|
||||||
case "away":
|
case "away":
|
||||||
return "bg-yellow-500";
|
return "bg-status-away";
|
||||||
case "busy":
|
case "busy":
|
||||||
return "bg-red-500";
|
return "bg-status-busy";
|
||||||
case "offline":
|
case "offline":
|
||||||
default:
|
default:
|
||||||
return "bg-gray-500";
|
return "bg-status-offline";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ const Avatar: React.FC<AvatarProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getFallbackColor = (userId: string) => {
|
const getFallbackColor = (userId: string) => {
|
||||||
// Generate a consistent color based on user ID
|
// Generate a consistent color based on user ID using theme colors
|
||||||
const colors = [
|
const colors = [
|
||||||
"bg-red-500",
|
"bg-red-500",
|
||||||
"bg-blue-500",
|
"bg-blue-500",
|
||||||
@@ -123,7 +123,7 @@ const Avatar: React.FC<AvatarProps> = ({
|
|||||||
{showStatus && (
|
{showStatus && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute rounded-full border-2 border-gray-800",
|
"absolute rounded-full border-2 border-sidebar",
|
||||||
statusSizes[size],
|
statusSizes[size],
|
||||||
statusPositions[size],
|
statusPositions[size],
|
||||||
getStatusColor(user.status),
|
getStatusColor(user.status),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import { Outlet } from "react-router";
|
import { Outlet, useLocation } from "react-router";
|
||||||
import { useAuthStore } from "@/stores/authStore";
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
import { useUiStore } from "@/stores/uiStore";
|
import { useUiStore } from "@/stores/uiStore";
|
||||||
|
|
||||||
@@ -10,79 +10,76 @@ import MemberList from "@/components/layout/MemberList";
|
|||||||
import LoadingSpinner from "@/components/common/LoadingSpinner";
|
import LoadingSpinner from "@/components/common/LoadingSpinner";
|
||||||
|
|
||||||
const AppLayout: React.FC = () => {
|
const AppLayout: React.FC = () => {
|
||||||
const { user, isLoading } = useAuthStore();
|
const { isLoading } = useAuthStore();
|
||||||
const { showMemberList, sidebarCollapsed, isMobile } = useUiStore();
|
const {
|
||||||
|
showMemberList,
|
||||||
|
sidebarCollapsed,
|
||||||
|
shouldShowChannelSidebar,
|
||||||
|
updateSidebarVisibility,
|
||||||
|
} = useUiStore();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
// Update sidebar visibility when route changes
|
||||||
|
useEffect(() => {
|
||||||
|
updateSidebarVisibility(location.pathname);
|
||||||
|
}, [location.pathname, updateSidebarVisibility]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-screen flex items-center justify-center bg-gray-900">
|
<div className="h-screen w-screen flex items-center justify-center bg-concord-primary">
|
||||||
<LoadingSpinner size="lg" />
|
<LoadingSpinner size="lg" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uncomment if auth is required
|
||||||
// if (!user) {
|
// if (!user) {
|
||||||
// return (
|
// return (
|
||||||
// <div className="h-screen w-screen flex items-center justify-center bg-gray-900">
|
// <div className="h-screen w-screen flex items-center justify-center bg-concord-primary">
|
||||||
// <div className="text-red-400">Authentication required</div>
|
// <div className="text-red-400">Authentication required</div>
|
||||||
// </div>
|
// </div>
|
||||||
// );
|
// );
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-gray-900 text-white">
|
<div className="flex h-screen overflow-hidden bg-concord-primary text-concord-primary">
|
||||||
{/* Server List Sidebar - Always visible on desktop, overlay on mobile */}
|
{/* Server List Sidebar - Always visible on desktop, overlay on mobile */}
|
||||||
<div
|
<div className="relative w-[72px] sidebar-primary flex-shrink-0">
|
||||||
className={`${
|
|
||||||
isMobile
|
|
||||||
? "fixed left-0 top-0 z-50 h-full w-[72px] transform transition-transform duration-200 ease-in-out"
|
|
||||||
: "relative w-[72px]"
|
|
||||||
} bg-gray-900 flex-shrink-0`}
|
|
||||||
>
|
|
||||||
<ServerSidebar />
|
<ServerSidebar />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Channel Sidebar - Collapsible */}
|
{/* Channel Sidebar - Only shown when in a server context and not collapsed */}
|
||||||
|
{shouldShowChannelSidebar && (
|
||||||
<div
|
<div
|
||||||
// className={`${
|
className={`${
|
||||||
// sidebarCollapsed
|
sidebarCollapsed
|
||||||
// ? isMobile
|
? "w-0" // Collapse by setting width to 0
|
||||||
// ? "hidden"
|
: "w-60" // Default width
|
||||||
// : "w-0 overflow-hidden"
|
}
|
||||||
// : isMobile
|
flex-col flex-shrink-0 sidebar-secondary transition-all duration-200 ease-in-out overflow-hidden`}
|
||||||
// ? "fixed left-[72px] top-0 z-40 h-full w-60"
|
|
||||||
// : "w-60"
|
|
||||||
// } bg-gray-800 flex flex-col flex-shrink-0 transition-all duration-200 ease-in-out`}
|
|
||||||
>
|
>
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex flex-col h-full">
|
||||||
<ChannelSidebar />
|
<ChannelSidebar />
|
||||||
<UserPanel />
|
<UserPanel />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div
|
<div
|
||||||
className={`flex-1 flex flex-col min-w-0 ${
|
className={`flex-1 flex flex-col min-w-0 ${
|
||||||
isMobile && !sidebarCollapsed ? "ml-60" : ""
|
!sidebarCollapsed ? "" : ""
|
||||||
} transition-all duration-200 ease-in-out`}
|
} transition-all duration-200 ease-in-out bg-concord-secondary`}
|
||||||
>
|
>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Member List - Conditionally shown */}
|
{/* Member List - Only shown when in a channel and member list is enabled */}
|
||||||
{showMemberList && !isMobile && (
|
{showMemberList && shouldShowChannelSidebar && (
|
||||||
<div className="w-60 bg-gray-800 flex-shrink-0 border-l border-gray-700">
|
<div className="flex-0 sidebar-secondary order-l border-sidebar">
|
||||||
<MemberList />
|
<MemberList />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Mobile overlay for sidebars */}
|
|
||||||
{isMobile && !sidebarCollapsed && (
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 bg-black bg-opacity-50 z-30"
|
|
||||||
onClick={() => useUiStore.getState().toggleSidebar()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,103 +1,58 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import { ChevronDown, Plus, Users, X } from "lucide-react";
|
import { ChevronDown, Plus, Users } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { useInstanceDetails } from "@/hooks/useServers";
|
import { useInstanceDetails } from "@/hooks/useServers";
|
||||||
import { useChannels } from "@/hooks/useChannel";
|
|
||||||
import { useUiStore } from "@/stores/uiStore";
|
import { useUiStore } from "@/stores/uiStore";
|
||||||
import { useResponsive } from "@/hooks/useResponsive";
|
|
||||||
import ChannelList from "@/components/channel/ChannelList";
|
import ChannelList from "@/components/channel/ChannelList";
|
||||||
|
|
||||||
const ChannelSidebar: React.FC = () => {
|
const ChannelSidebar: React.FC = () => {
|
||||||
const { instanceId } = useParams();
|
const { instanceId } = useParams();
|
||||||
const { data: instance, isLoading: instanceLoading } =
|
const { data: instance, isLoading: instanceLoading } =
|
||||||
useInstanceDetails(instanceId);
|
useInstanceDetails(instanceId);
|
||||||
const { data: categories, isLoading: channelsLoading } =
|
const categories = instance?.categories;
|
||||||
useChannels(instanceId);
|
|
||||||
const {
|
const {
|
||||||
toggleMemberList,
|
toggleMemberList,
|
||||||
showMemberList,
|
showMemberList,
|
||||||
toggleSidebar,
|
|
||||||
openCreateChannel,
|
openCreateChannel,
|
||||||
openServerSettings,
|
openServerSettings,
|
||||||
} = useUiStore();
|
} = useUiStore();
|
||||||
const { isMobile, isDesktop } = useResponsive();
|
|
||||||
|
|
||||||
// Handle Direct Messages view
|
// Only show for valid instance IDs
|
||||||
if (!instanceId || instanceId === "@me") {
|
if (!instanceId) {
|
||||||
return (
|
return null;
|
||||||
<div className="flex flex-col h-full">
|
|
||||||
{/* DM Header */}
|
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
{isMobile && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-6 w-6"
|
|
||||||
onClick={toggleSidebar}
|
|
||||||
>
|
|
||||||
<X size={16} />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<h2 className="font-semibold text-white">Direct Messages</h2>
|
|
||||||
</div>
|
|
||||||
<Button variant="ghost" size="icon" className="h-6 w-6">
|
|
||||||
<Plus size={16} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* DM List */}
|
|
||||||
<ScrollArea className="flex-1 px-2">
|
|
||||||
<div className="py-2 space-y-1">
|
|
||||||
<div className="text-sm text-gray-400 px-2 py-1">
|
|
||||||
No direct messages yet
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instanceLoading || channelsLoading) {
|
if (instanceLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="sidebar-secondary flex items-center justify-center h-full">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="sidebar-secondary flex items-center justify-center h-full">
|
||||||
<div className="text-gray-400">Server not found</div>
|
<div className="text-concord-secondary">Server not found</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="sidebar-secondary flex-1">
|
||||||
|
<ScrollArea className="">
|
||||||
{/* Server Header */}
|
{/* Server Header */}
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700 shadow-sm">
|
<div className="flex items-center justify-between border-b border-concord-primary shadow-sm px-4 py-3">
|
||||||
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
||||||
{isMobile && (
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
className="flex items-center justify-between w-full h-8 font-semibold text-concord-primary hover:bg-concord-tertiary"
|
||||||
className="h-6 w-6 flex-shrink-0"
|
|
||||||
onClick={toggleSidebar}
|
|
||||||
>
|
|
||||||
<X size={16} />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="flex items-center justify-between w-full px-2 py-1 h-auto font-semibold text-white hover:bg-gray-700"
|
|
||||||
onClick={openServerSettings}
|
onClick={openServerSettings}
|
||||||
>
|
>
|
||||||
<span className="truncate">{instance.name}</span>
|
<span className="truncate">{instance.name}</span>
|
||||||
<ChevronDown size={18} className="flex-shrink-0 ml-1" />
|
<ChevronDown size={20} className="flex-shrink-0 ml-1" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,7 +63,7 @@ const ChannelSidebar: React.FC = () => {
|
|||||||
{categories && categories.length > 0 ? (
|
{categories && categories.length > 0 ? (
|
||||||
<ChannelList categories={categories} />
|
<ChannelList categories={categories} />
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm text-gray-400 px-2 py-4 text-center">
|
<div className="text-sm text-concord-secondary text-center px-2 py-4">
|
||||||
No channels yet
|
No channels yet
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -116,30 +71,29 @@ const ChannelSidebar: React.FC = () => {
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
{/* Bottom Actions */}
|
{/* Bottom Actions */}
|
||||||
<div className="px-2 py-2 border-t border-gray-700">
|
<div className="border-t border-sidebar px-2 py-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-gray-400 hover:text-white"
|
className="interactive-hover"
|
||||||
onClick={openCreateChannel}
|
onClick={openCreateChannel}
|
||||||
>
|
>
|
||||||
<Plus size={16} className="mr-1" />
|
<Plus size={16} className="mr-1" />
|
||||||
Add Channel
|
Add Channel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{isDesktop && (
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={`h-8 w-8 ${showMemberList ? "text-white" : "text-gray-400 hover:text-white"}`}
|
className={`h-8 w-8 ${showMemberList ? "text-interactive-active" : "interactive-hover"}`}
|
||||||
onClick={toggleMemberList}
|
onClick={toggleMemberList}
|
||||||
>
|
>
|
||||||
<Users size={16} />
|
<Users size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,50 +1,68 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import { Crown, Shield } from "lucide-react";
|
import { Crown, Shield, UserIcon } from "lucide-react";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import { Role } from "@/types/database";
|
||||||
import { useInstanceMembers } from "@/hooks/useServers";
|
import { useInstanceMembers } from "@/hooks/useServers";
|
||||||
import { UserWithRoles } from "@/types/api";
|
import { User } from "@/types/database";
|
||||||
|
|
||||||
interface MemberItemProps {
|
// Status color utility
|
||||||
member: UserWithRoles;
|
|
||||||
isOwner?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MemberItem: React.FC<MemberItemProps> = ({ member, isOwner = false }) => {
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "online":
|
case "online":
|
||||||
return "bg-green-500";
|
return "bg-status-online";
|
||||||
case "away":
|
case "away":
|
||||||
return "bg-yellow-500";
|
return "bg-status-away";
|
||||||
case "busy":
|
case "busy":
|
||||||
return "bg-red-500";
|
return "bg-status-busy";
|
||||||
default:
|
default:
|
||||||
return "bg-gray-500";
|
return "bg-status-offline";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getHighestRole = (roles: any[]) => {
|
interface MemberItemProps {
|
||||||
if (!roles || roles.length === 0) return null;
|
member: User;
|
||||||
// Sort by position (higher position = higher role)
|
isOwner?: boolean;
|
||||||
return roles.sort((a, b) => b.position - a.position)[0];
|
}
|
||||||
|
|
||||||
|
// Get the user's role for this specific instance
|
||||||
|
const getUserRoleForInstance = (roles: Role[], instanceId: string) => {
|
||||||
|
return roles.find((r) => r.instanceId === instanceId)?.role || "member";
|
||||||
};
|
};
|
||||||
|
|
||||||
const highestRole = getHighestRole(member.roles);
|
// Define role colors and priorities
|
||||||
|
const getRoleInfo = (role: string) => {
|
||||||
|
switch (role) {
|
||||||
|
case "admin":
|
||||||
|
return { color: "#ff6b6b", priority: 3, name: "Admin" };
|
||||||
|
case "mod":
|
||||||
|
return { color: "#4ecdc4", priority: 2, name: "Moderator" };
|
||||||
|
default:
|
||||||
|
return { color: null, priority: 1, name: "Member" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const MemberItem: React.FC<MemberItemProps> = ({ member, isOwner = false }) => {
|
||||||
|
const { instanceId } = useParams();
|
||||||
|
const userRole = getUserRoleForInstance(member.roles, instanceId || "");
|
||||||
|
const roleInfo = getRoleInfo(userRole);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center px-2 py-1 mx-2 rounded hover:bg-gray-700/50 cursor-pointer group">
|
<div className="panel-button">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar className="h-8 w-8">
|
<Avatar className="h-8 w-8">
|
||||||
<AvatarImage src={member.picture} alt={member.username} />
|
<AvatarImage
|
||||||
<AvatarFallback className="text-xs">
|
src={member.picture || undefined}
|
||||||
|
alt={member.username}
|
||||||
|
/>
|
||||||
|
<AvatarFallback className="text-xs bg-primary text-primary-foreground">
|
||||||
{member.username.slice(0, 2).toUpperCase()}
|
{member.username.slice(0, 2).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
{/* Status indicator */}
|
{/* Status indicator */}
|
||||||
<div
|
<div
|
||||||
className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-gray-800 ${getStatusColor(member.status)}`}
|
className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-sidebar ${getStatusColor(member.status)}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -53,22 +71,24 @@ const MemberItem: React.FC<MemberItemProps> = ({ member, isOwner = false }) => {
|
|||||||
{isOwner && (
|
{isOwner && (
|
||||||
<Crown size={12} className="text-yellow-500 flex-shrink-0" />
|
<Crown size={12} className="text-yellow-500 flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
{!isOwner && highestRole && (
|
{!isOwner && userRole !== "member" && (
|
||||||
<Shield
|
<Shield
|
||||||
size={12}
|
size={12}
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
style={{ color: highestRole.color || "#ffffff" }}
|
style={{ color: roleInfo.color || "var(--background)" }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
className="text-sm font-medium truncate"
|
className="text-sm font-medium truncate"
|
||||||
style={{ color: highestRole?.color || "#ffffff" }}
|
style={{ color: roleInfo.color || "var(--color-text-primary)" }}
|
||||||
>
|
>
|
||||||
{member.nickname || member.username}
|
{member.nickname || member.username}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{member.bio && (
|
{member.bio && (
|
||||||
<div className="text-xs text-gray-400 truncate">{member.bio}</div>
|
<div className="text-xs text-concord-secondary truncate">
|
||||||
|
{member.bio}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,16 +97,16 @@ const MemberItem: React.FC<MemberItemProps> = ({ member, isOwner = false }) => {
|
|||||||
|
|
||||||
const MemberList: React.FC = () => {
|
const MemberList: React.FC = () => {
|
||||||
const { instanceId } = useParams();
|
const { instanceId } = useParams();
|
||||||
const { members, isLoading } = useInstanceMembers(instanceId);
|
const { data: members, isLoading } = useInstanceMembers(instanceId);
|
||||||
|
|
||||||
if (!instanceId || instanceId === "@me") {
|
if (!instanceId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-full">
|
||||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-white"></div>
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -94,7 +114,7 @@ const MemberList: React.FC = () => {
|
|||||||
if (!members || members.length === 0) {
|
if (!members || members.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-full">
|
||||||
<div className="text-gray-400 text-sm">No members</div>
|
<div className="text-concord-secondary text-sm">No members</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -102,44 +122,40 @@ const MemberList: React.FC = () => {
|
|||||||
// Group members by role
|
// Group members by role
|
||||||
const groupedMembers = members.reduce(
|
const groupedMembers = members.reduce(
|
||||||
(acc, member) => {
|
(acc, member) => {
|
||||||
const highestRole =
|
const userRole =
|
||||||
member.roles?.length > 0
|
member.roles.find((r) => r.instanceId === instanceId)?.role || "member";
|
||||||
? member.roles.sort((a, b) => b.position - a.position)[0]
|
const roleInfo = getRoleInfo(userRole);
|
||||||
: null;
|
|
||||||
|
|
||||||
const roleName = highestRole?.name || "Members";
|
if (!acc[roleInfo.name]) {
|
||||||
|
acc[roleInfo.name] = [];
|
||||||
if (!acc[roleName]) {
|
|
||||||
acc[roleName] = [];
|
|
||||||
}
|
}
|
||||||
acc[roleName].push(member);
|
acc[roleInfo.name].push(member);
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<string, UserWithRoles[]>,
|
{} as Record<string, User[]>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sort role groups by highest role position
|
// Sort role groups by priority (admin > mod > member)
|
||||||
const sortedRoleGroups = Object.entries(groupedMembers).sort(
|
const sortedRoleGroups = Object.entries(groupedMembers).sort(
|
||||||
([roleNameA, membersA], [roleNameB, membersB]) => {
|
([roleNameA], [roleNameB]) => {
|
||||||
const roleA = membersA[0]?.roles?.find((r) => r.name === roleNameA);
|
const priorityA = getRoleInfo(roleNameA.toLowerCase())?.priority || 1;
|
||||||
const roleB = membersB[0]?.roles?.find((r) => r.name === roleNameB);
|
const priorityB = getRoleInfo(roleNameB.toLowerCase())?.priority || 1;
|
||||||
|
return priorityB - priorityA;
|
||||||
if (!roleA && !roleB) return 0;
|
|
||||||
if (!roleA) return 1;
|
|
||||||
if (!roleB) return -1;
|
|
||||||
|
|
||||||
return roleB.position - roleA.position;
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col flex-1 border-l border-concord-primary h-full bg-concord-secondary">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="px-4 py-3 border-b border-gray-700">
|
<div className="px-4 py-3 border-b border-concord-primary flex items-center justify-between">
|
||||||
<h3 className="text-sm font-semibold text-gray-300 uppercase tracking-wide">
|
<UserIcon size={20} className="text-concord-primary h-8" />
|
||||||
Members — {members.length}
|
<div className="h-8 flex flex-col justify-center">
|
||||||
|
<h3 className="text-sm font-semibold text-concord-secondary tracking-wide">
|
||||||
|
Members: {members.length} Online:{" "}
|
||||||
|
{members.filter((m) => m.status === "online").length}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Member List */}
|
{/* Member List */}
|
||||||
<ScrollArea className="flex-1">
|
<ScrollArea className="flex-1">
|
||||||
@@ -148,7 +164,7 @@ const MemberList: React.FC = () => {
|
|||||||
<div key={roleName} className="mb-4">
|
<div key={roleName} className="mb-4">
|
||||||
{/* Role Header */}
|
{/* Role Header */}
|
||||||
<div className="px-4 py-1">
|
<div className="px-4 py-1">
|
||||||
<h4 className="text-xs font-semibold text-gray-400 uppercase tracking-wide">
|
<h4 className="text-xs font-semibold text-concord-secondary uppercase tracking-wide">
|
||||||
{roleName} — {roleMembers.length}
|
{roleName} — {roleMembers.length}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useNavigate, useParams } from "react-router";
|
import { useNavigate, useParams } from "react-router";
|
||||||
import { Plus, Home, Menu } from "lucide-react";
|
import { Plus, Home } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -10,61 +10,46 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { useServers } from "@/hooks/useServers";
|
import { useServers } from "@/hooks/useServers";
|
||||||
import { useUiStore } from "@/stores/uiStore";
|
import { useUiStore } from "@/stores/uiStore";
|
||||||
import { useResponsive } from "@/hooks/useResponsive";
|
|
||||||
import ServerIcon from "@/components/server/ServerIcon";
|
import ServerIcon from "@/components/server/ServerIcon";
|
||||||
|
|
||||||
const ServerSidebar: React.FC = () => {
|
const ServerSidebar: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { instanceId } = useParams();
|
const { instanceId } = useParams();
|
||||||
const { data: servers, isLoading } = useServers();
|
const { data: servers, isLoading } = useServers();
|
||||||
const { openCreateServer, toggleSidebar, setActiveInstance } = useUiStore();
|
const { openCreateServer, setActiveInstance, getSelectedChannelForInstance } =
|
||||||
const { isMobile } = useResponsive();
|
useUiStore();
|
||||||
|
|
||||||
const handleServerClick = (serverId: string) => {
|
const handleServerClick = (serverId: string) => {
|
||||||
setActiveInstance(serverId);
|
setActiveInstance(serverId);
|
||||||
navigate(`/channels/${serverId}`);
|
const lastChannelId = getSelectedChannelForInstance(serverId);
|
||||||
};
|
|
||||||
|
|
||||||
|
console.log(servers);
|
||||||
|
|
||||||
|
if (lastChannelId) {
|
||||||
|
navigate(`/channels/${serverId}/${lastChannelId}`);
|
||||||
|
} else {
|
||||||
|
// Fallback: navigate to the server, let the page component handle finding a channel
|
||||||
|
// A better UX would be to find and navigate to the first channel here.
|
||||||
|
navigate(`/channels/${serverId}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
const handleHomeClick = () => {
|
const handleHomeClick = () => {
|
||||||
setActiveInstance(null);
|
setActiveInstance(null);
|
||||||
navigate("/channels/@me");
|
navigate("/channels/@me");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMenuToggle = () => {
|
|
||||||
toggleSidebar();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="flex flex-col items-center py-3 space-y-2 h-full bg-gray-900 border-r border-gray-800">
|
<div className="sidebar-primary flex flex-col items-center h-full py-2 space-y-2">
|
||||||
{/* Mobile menu toggle */}
|
{/* Home/DM Button */}
|
||||||
{isMobile && (
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="w-12 h-12 rounded-2xl hover:rounded-xl bg-gray-700 hover:bg-gray-600 text-white transition-all duration-200"
|
className={` w-12 h-12 ml-0 ${
|
||||||
onClick={handleMenuToggle}
|
!instanceId || instanceId === "@me" ? "active" : ""
|
||||||
>
|
}`}
|
||||||
<Menu size={24} />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right">
|
|
||||||
<p>Toggle Menu</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Home/DM Button */}
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant={
|
|
||||||
!instanceId || instanceId === "@me" ? "default" : "ghost"
|
|
||||||
}
|
|
||||||
size="icon"
|
|
||||||
className="w-12 h-12 rounded-2xl hover:rounded-xl transition-all duration-200"
|
|
||||||
onClick={handleHomeClick}
|
onClick={handleHomeClick}
|
||||||
>
|
>
|
||||||
<Home size={24} />
|
<Home size={24} />
|
||||||
@@ -76,13 +61,13 @@ const ServerSidebar: React.FC = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* Separator */}
|
{/* Separator */}
|
||||||
<div className="w-8 h-0.5 bg-gray-600 rounded-full" />
|
<div className="w-8 h-0.5 bg-border rounded-full" />
|
||||||
|
|
||||||
{/* Server List */}
|
{/* Server List */}
|
||||||
<div className="flex-1 flex flex-col space-y-2 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-600">
|
<div className="flex-1 flex flex-col overflow-y-auto scrollbar-thin scrollbar-thumb-border space-y-2">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center py-4">
|
<div className="flex items-center justify-center py-4">
|
||||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-white"></div>
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
servers?.map((server) => (
|
servers?.map((server) => (
|
||||||
@@ -110,7 +95,7 @@ const ServerSidebar: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="w-12 h-12 rounded-2xl hover:rounded-xl bg-gray-700 hover:bg-green-600 text-green-500 hover:text-white transition-all duration-200"
|
className="w-12 h-12 ml-3 rounded-2xl hover:rounded-xl bg-concord-secondary hover:bg-green-600 text-green-500 hover:text-white transition-all duration-200"
|
||||||
onClick={openCreateServer}
|
onClick={openCreateServer}
|
||||||
>
|
>
|
||||||
<Plus size={24} />
|
<Plus size={24} />
|
||||||
|
|||||||
@@ -18,125 +18,94 @@ import {
|
|||||||
|
|
||||||
import { useAuthStore } from "@/stores/authStore";
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
import { useUiStore } from "@/stores/uiStore";
|
import { useUiStore } from "@/stores/uiStore";
|
||||||
|
import { SAMPLE_USERS } from "@/hooks/useServers";
|
||||||
|
|
||||||
const UserPanel: React.FC = () => {
|
// Status color utility
|
||||||
const { user, logout } = useAuthStore();
|
|
||||||
const { openUserSettings } = useUiStore();
|
|
||||||
|
|
||||||
// Voice/Audio states (for future implementation)
|
|
||||||
const [isMuted, setIsMuted] = useState(false);
|
|
||||||
const [isDeafened, setIsDeafened] = useState(false);
|
|
||||||
|
|
||||||
if (!user) return null;
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "online":
|
case "online":
|
||||||
return "bg-green-500";
|
return "bg-status-online";
|
||||||
case "away":
|
case "away":
|
||||||
return "bg-yellow-500";
|
return "bg-status-away";
|
||||||
case "busy":
|
case "busy":
|
||||||
return "bg-red-500";
|
return "bg-status-busy";
|
||||||
default:
|
default:
|
||||||
return "bg-gray-500";
|
return "bg-status-offline";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStatusChange = (newStatus: string) => {
|
// User Status Dropdown Component
|
||||||
// TODO: Implement status change
|
interface UserStatusDropdownProps {
|
||||||
console.log("Status change to:", newStatus);
|
currentStatus: string;
|
||||||
};
|
onStatusChange: (status: string) => void;
|
||||||
|
children: React.ReactNode;
|
||||||
const handleMuteToggle = () => {
|
|
||||||
setIsMuted(!isMuted);
|
|
||||||
// TODO: Implement actual mute functionality
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeafenToggle = () => {
|
|
||||||
setIsDeafened(!isDeafened);
|
|
||||||
if (!isDeafened) {
|
|
||||||
setIsMuted(true); // Deafening also mutes
|
|
||||||
}
|
}
|
||||||
// TODO: Implement actual deafen functionality
|
|
||||||
};
|
const UserStatusDropdown: React.FC<UserStatusDropdownProps> = ({
|
||||||
|
// currentStatus,
|
||||||
|
onStatusChange,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const statusOptions = [
|
||||||
|
{ value: "online", label: "Online", color: "bg-status-online" },
|
||||||
|
{ value: "away", label: "Away", color: "bg-status-away" },
|
||||||
|
{ value: "busy", label: "Do Not Disturb", color: "bg-status-busy" },
|
||||||
|
{ value: "offline", label: "Invisible", color: "bg-status-offline" },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between px-2 py-2 bg-gray-900 border-t border-gray-700">
|
|
||||||
{/* User Info */}
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger className="checkchekchek" asChild>
|
||||||
<Button
|
{children}
|
||||||
variant="ghost"
|
|
||||||
className="flex items-center space-x-2 p-1 h-auto hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<Avatar className="h-8 w-8">
|
|
||||||
<AvatarImage src={user.picture} alt={user.username} />
|
|
||||||
<AvatarFallback className="text-xs bg-blue-600">
|
|
||||||
{user.username.slice(0, 2).toUpperCase()}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
{/* Status indicator */}
|
|
||||||
<div
|
|
||||||
className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-gray-900 ${getStatusColor(user.status)}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0 text-left">
|
|
||||||
<div className="text-sm font-medium text-white truncate">
|
|
||||||
{user.nickname || user.username}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400 truncate capitalize">
|
|
||||||
{user.status}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="center" className="w-48">
|
||||||
<DropdownMenuContent align="start" className="w-48">
|
{statusOptions.map((status) => (
|
||||||
<DropdownMenuItem onClick={() => handleStatusChange("online")}>
|
<DropdownMenuItem
|
||||||
|
key={status.value}
|
||||||
|
onClick={() => onStatusChange(status.value)}
|
||||||
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="w-3 h-3 rounded-full bg-green-500" />
|
<div className={`w-3 h-3 rounded-full ${status.color}`} />
|
||||||
<span>Online</span>
|
<span>{status.label}</span>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => handleStatusChange("away")}>
|
))}
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div className="w-3 h-3 rounded-full bg-yellow-500" />
|
|
||||||
<span>Away</span>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => handleStatusChange("busy")}>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div className="w-3 h-3 rounded-full bg-red-500" />
|
|
||||||
<span>Do Not Disturb</span>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => handleStatusChange("offline")}>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div className="w-3 h-3 rounded-full bg-gray-500" />
|
|
||||||
<span>Invisible</span>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
<DropdownMenuItem onClick={openUserSettings}>
|
onClick={() => useUiStore.getState().openUserSettings()}
|
||||||
|
>
|
||||||
<Settings size={16} className="mr-2" />
|
<Settings size={16} className="mr-2" />
|
||||||
User Settings
|
User Settings
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={logout}
|
onClick={() => useAuthStore.getState().logout()}
|
||||||
className="text-red-400 focus:text-red-400"
|
className="text-destructive focus:text-destructive"
|
||||||
>
|
>
|
||||||
Log Out
|
Log Out
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
{/* Voice Controls */}
|
// Voice Controls Component
|
||||||
|
interface VoiceControlsProps {
|
||||||
|
isMuted: boolean;
|
||||||
|
isDeafened: boolean;
|
||||||
|
onMuteToggle: () => void;
|
||||||
|
onDeafenToggle: () => void;
|
||||||
|
onSettingsClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VoiceControls: React.FC<VoiceControlsProps> = ({
|
||||||
|
isMuted,
|
||||||
|
isDeafened,
|
||||||
|
onMuteToggle,
|
||||||
|
onDeafenToggle,
|
||||||
|
onSettingsClick,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
{/* Mute/Unmute */}
|
{/* Mute/Unmute */}
|
||||||
@@ -145,8 +114,8 @@ const UserPanel: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={`h-8 w-8 ${isMuted ? "text-red-400 hover:text-red-300" : "text-gray-400 hover:text-white"}`}
|
className={`h-8 w-8 ${isMuted ? "text-destructive hover:text-destructive/80" : "interactive-hover"}`}
|
||||||
onClick={handleMuteToggle}
|
onClick={onMuteToggle}
|
||||||
>
|
>
|
||||||
{isMuted ? <MicOff size={18} /> : <Mic size={18} />}
|
{isMuted ? <MicOff size={18} /> : <Mic size={18} />}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -162,8 +131,8 @@ const UserPanel: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={`h-8 w-8 ${isDeafened ? "text-red-400 hover:text-red-300" : "text-gray-400 hover:text-white"}`}
|
className={`h-8 w-8 ${isDeafened ? "text-destructive hover:text-destructive/80" : "interactive-hover"}`}
|
||||||
onClick={handleDeafenToggle}
|
onClick={onDeafenToggle}
|
||||||
>
|
>
|
||||||
<Headphones size={18} />
|
<Headphones size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -179,8 +148,8 @@ const UserPanel: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 text-gray-400 hover:text-white"
|
className="h-8 w-8 interactive-hover"
|
||||||
onClick={openUserSettings}
|
onClick={onSettingsClick}
|
||||||
>
|
>
|
||||||
<Settings size={18} />
|
<Settings size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -191,6 +160,113 @@ const UserPanel: React.FC = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// User Avatar Component
|
||||||
|
interface UserAvatarProps {
|
||||||
|
user: any;
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
showStatus?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserAvatar: React.FC<UserAvatarProps> = ({
|
||||||
|
user,
|
||||||
|
size = "md",
|
||||||
|
showStatus = true,
|
||||||
|
}) => {
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: "h-6 w-6",
|
||||||
|
md: "h-8 w-8",
|
||||||
|
lg: "h-10 w-10",
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusSizeClasses = {
|
||||||
|
sm: "w-2 h-2",
|
||||||
|
md: "w-3 h-3",
|
||||||
|
lg: "w-4 h-4",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<Avatar className={sizeClasses[size]}>
|
||||||
|
<AvatarImage src={user.picture || undefined} alt={user.username} />
|
||||||
|
<AvatarFallback className="text-xs text-primary-foreground bg-primary">
|
||||||
|
{user.username.slice(0, 2).toUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
{showStatus && (
|
||||||
|
<div
|
||||||
|
className={`absolute -bottom-0.5 -right-0.5 ${statusSizeClasses[size]} rounded-full border-2 border-sidebar ${getStatusColor(user.status)}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main UserPanel Component
|
||||||
|
const UserPanel: React.FC = () => {
|
||||||
|
const { user } = useAuthStore();
|
||||||
|
const { openUserSettings } = useUiStore();
|
||||||
|
|
||||||
|
const [isMuted, setIsMuted] = useState(false);
|
||||||
|
const [isDeafened, setIsDeafened] = useState(false);
|
||||||
|
|
||||||
|
const displayUser = user || SAMPLE_USERS.find((u) => u.id === "current");
|
||||||
|
|
||||||
|
if (!displayUser) {
|
||||||
|
return (
|
||||||
|
<div className="flex-shrink-0 p-2 bg-concord-tertiary">
|
||||||
|
<div className="text-concord-secondary text-sm">No user data</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStatusChange = (newStatus: string) => {
|
||||||
|
console.log("Status change to:", newStatus);
|
||||||
|
// TODO: Implement API call to update user status
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMuteToggle = () => setIsMuted(!isMuted);
|
||||||
|
const handleDeafenToggle = () => {
|
||||||
|
const newDeafenState = !isDeafened;
|
||||||
|
setIsDeafened(newDeafenState);
|
||||||
|
if (newDeafenState) {
|
||||||
|
setIsMuted(true); // Deafening also mutes
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="user-panel flex items-center p-2 bg-concord-tertiary border-t border-sidebar">
|
||||||
|
{/* User Info with Dropdown */}
|
||||||
|
<UserStatusDropdown
|
||||||
|
currentStatus={displayUser.status}
|
||||||
|
onStatusChange={handleStatusChange}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="flex-1 flex items-center h-auto p-1 rounded-md hover:bg-concord-secondary"
|
||||||
|
>
|
||||||
|
<UserAvatar user={displayUser} size="md" />
|
||||||
|
<div className="ml-2 flex-1 min-w-0 text-left">
|
||||||
|
<div className="text-sm font-medium text-concord-primary truncate">
|
||||||
|
{displayUser.nickname || displayUser.username}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-concord-secondary truncate capitalize">
|
||||||
|
{displayUser.status}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</UserStatusDropdown>
|
||||||
|
|
||||||
|
{/* Voice Controls */}
|
||||||
|
<VoiceControls
|
||||||
|
isMuted={isMuted}
|
||||||
|
isDeafened={isDeafened}
|
||||||
|
onMuteToggle={handleMuteToggle}
|
||||||
|
onDeafenToggle={handleDeafenToggle}
|
||||||
|
onSettingsClick={openUserSettings}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
254
concord-client/src/components/message/Message.tsx
Normal file
254
concord-client/src/components/message/Message.tsx
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { formatDistanceToNow } from "date-fns";
|
||||||
|
import Avatar from "@/components/common/Avatar";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Copy, Edit, Trash2, Reply, MoreHorizontal } from "lucide-react";
|
||||||
|
import { Message, User } from "@/types/database";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
|
interface MessageProps {
|
||||||
|
message: Message;
|
||||||
|
user: any;
|
||||||
|
currentUser?: any;
|
||||||
|
isGrouped?: boolean | null;
|
||||||
|
onEdit?: (messageId: string) => void;
|
||||||
|
onDelete?: (messageId: string) => void;
|
||||||
|
onReply?: (messageId: string) => void;
|
||||||
|
replyTo?: Message | null;
|
||||||
|
replyToUser?: User | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageComponent: React.FC<MessageProps> = ({
|
||||||
|
message,
|
||||||
|
user,
|
||||||
|
currentUser,
|
||||||
|
isGrouped = false,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onReply,
|
||||||
|
}) => {
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
|
const formatTimestamp = (timestamp: string) => {
|
||||||
|
return formatDistanceToNow(new Date(timestamp), { addSuffix: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderContent = (content: string) => {
|
||||||
|
// Simple code block detection
|
||||||
|
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
|
||||||
|
const parts = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = codeBlockRegex.exec(content)) !== null) {
|
||||||
|
// Add text before code block
|
||||||
|
if (match.index > lastIndex) {
|
||||||
|
parts.push(
|
||||||
|
<span key={lastIndex}>{content.slice(lastIndex, match.index)}</span>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add code block
|
||||||
|
const language = match[1] || "text";
|
||||||
|
const code = match[2];
|
||||||
|
parts.push(
|
||||||
|
<div key={match.index} className="my-2">
|
||||||
|
<div className="bg-concord-tertiary rounded-md p-3 border border-border">
|
||||||
|
<div className="text-xs text-concord-secondary mb-2 font-mono">
|
||||||
|
{language}
|
||||||
|
</div>
|
||||||
|
<pre className="text-sm font-mono text-concord-primary overflow-x-auto">
|
||||||
|
<code>{code}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
|
||||||
|
lastIndex = codeBlockRegex.lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add remaining text
|
||||||
|
if (lastIndex < content.length) {
|
||||||
|
parts.push(<span key={lastIndex}>{content.slice(lastIndex)}</span>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.length > 0 ? parts : content;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOwnMessage = currentUser?.id === message.userId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`group relative px-4 py-2 hover:bg-concord-secondary/50 transition-colors ${
|
||||||
|
isGrouped ? "mt-0.5" : "mt-4"
|
||||||
|
}`}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{/* Avatar - only show if not grouped */}
|
||||||
|
<div className="w-10 flex-shrink-0">
|
||||||
|
{!isGrouped && <Avatar user={user} size="md" showStatus={true} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
{/* Header - only show if not grouped */}
|
||||||
|
{!isGrouped && (
|
||||||
|
<div className="flex items-baseline gap-2 mb-1">
|
||||||
|
<span className="font-semibold text-concord-primary">
|
||||||
|
{user.nickname || user.username}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-concord-secondary">
|
||||||
|
{formatTimestamp(message.createdAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Message content */}
|
||||||
|
<div className="text-concord-primary leading-relaxed">
|
||||||
|
{renderContent(message.content)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reactions */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message actions */}
|
||||||
|
{isHovered && (
|
||||||
|
<div className="absolute top-0 right-4 bg-concord-secondary border border-border rounded-md shadow-md flex">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0 interactive-hover"
|
||||||
|
onClick={() => onReply?.(message.id)}
|
||||||
|
>
|
||||||
|
<Reply className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0 interactive-hover"
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => navigator.clipboard.writeText(message.content)}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4 mr-2" />
|
||||||
|
Copy Text
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{isOwnMessage && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem onClick={() => onEdit?.(message.id)}>
|
||||||
|
<Edit className="h-4 w-4 mr-2" />
|
||||||
|
Edit Message
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onDelete?.(message.id)}
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Delete Message
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Message Input Component
|
||||||
|
interface MessageInputProps {
|
||||||
|
channelName?: string;
|
||||||
|
onSendMessage: (content: string) => void;
|
||||||
|
replyingTo?: Message;
|
||||||
|
onCancelReply?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageInput: React.FC<MessageInputProps> = ({
|
||||||
|
channelName,
|
||||||
|
onSendMessage,
|
||||||
|
replyingTo,
|
||||||
|
onCancelReply,
|
||||||
|
}) => {
|
||||||
|
const [content, setContent] = useState("");
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (content.trim()) {
|
||||||
|
onSendMessage(content.trim());
|
||||||
|
setContent("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-4 pb-4">
|
||||||
|
{replyingTo && (
|
||||||
|
<div className="mb-2 p-2 bg-concord-tertiary rounded-t-lg border border-b-0 border-border">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-concord-secondary">
|
||||||
|
Replying to {replyingTo.userId}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-auto p-1"
|
||||||
|
onClick={onCancelReply}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-concord-primary truncate">
|
||||||
|
{replyingTo.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="relative">
|
||||||
|
<textarea
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={`Message #${channelName || "channel"}`}
|
||||||
|
className="w-full bg-concord-tertiary border border-border rounded-lg px-4 py-3 text-concord-primary placeholder-concord-muted resize-none focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||||
|
rows={1}
|
||||||
|
style={{
|
||||||
|
minHeight: "44px",
|
||||||
|
maxHeight: "200px",
|
||||||
|
resize: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="absolute right-3 bottom-3 text-xs text-concord-secondary">
|
||||||
|
Press Enter to send
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { type MessageProps, MessageComponent, MessageInput };
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { Moon, Sun } from "lucide-react";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { useTheme } from "@/components/theme-provider";
|
|
||||||
|
|
||||||
export function ModeToggle() {
|
|
||||||
const { setTheme } = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="outline" size="icon">
|
|
||||||
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
|
||||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
|
||||||
<span className="sr-only">Toggle theme</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
|
||||||
Light
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
|
||||||
Dark
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
|
||||||
System
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -26,18 +26,17 @@ const ServerIcon: React.FC<ServerIconProps> = ({
|
|||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
{/* Active indicator */}
|
{/* Active indicator */}
|
||||||
<div
|
<div
|
||||||
className={`absolute left-0 top-1/2 transform -translate-y-1/2 w-1 bg-white rounded-r transition-all duration-200 ${
|
className={`absolute left-0 top-1/2 transform -translate-y-1/2 w-1 bg-accent-foreground rounded transition-all duration-200 ${
|
||||||
isActive ? "h-10" : "h-2 group-hover:h-5"
|
isActive ? "h-10 rounded-xl" : "rounded-r h-2 group-hover:h-5"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={`w-12 h-12 ml-3 transition-all duration-200 ${
|
className={`w-12 h-12 ml-3 transition-all duration-200 ${
|
||||||
isActive
|
isActive
|
||||||
? "rounded-xl bg-blue-600 hover:bg-blue-500 text-white"
|
? "rounded-xl border-primary bg-primary/10 border-2"
|
||||||
: "rounded-2xl hover:rounded-xl bg-gray-700 hover:bg-blue-600 text-gray-300 hover:text-white"
|
: "rounded-2xl hover:rounded-xl border hover:border-primary/50"
|
||||||
}`}
|
}`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,21 +1,279 @@
|
|||||||
import { createContext, useContext, useEffect, useState } from "react";
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
type Theme = "dark" | "light" | "system";
|
export interface ThemeColors {
|
||||||
|
background: string;
|
||||||
|
foreground: string;
|
||||||
|
card: string;
|
||||||
|
cardForeground: string;
|
||||||
|
popover: string;
|
||||||
|
popoverForeground: string;
|
||||||
|
primary: string;
|
||||||
|
primaryForeground: string;
|
||||||
|
secondary: string;
|
||||||
|
secondaryForeground: string;
|
||||||
|
muted: string;
|
||||||
|
mutedForeground: string;
|
||||||
|
accent: string;
|
||||||
|
accentForeground: string;
|
||||||
|
destructive: string;
|
||||||
|
border: string;
|
||||||
|
input: string;
|
||||||
|
ring: string;
|
||||||
|
// Chart colors
|
||||||
|
chart1: string;
|
||||||
|
chart2: string;
|
||||||
|
chart3: string;
|
||||||
|
chart4: string;
|
||||||
|
chart5: string;
|
||||||
|
// Sidebar colors
|
||||||
|
sidebar: string;
|
||||||
|
sidebarForeground: string;
|
||||||
|
sidebarPrimary: string;
|
||||||
|
sidebarPrimaryForeground: string;
|
||||||
|
sidebarAccent: string;
|
||||||
|
sidebarAccentForeground: string;
|
||||||
|
sidebarBorder: string;
|
||||||
|
sidebarRing: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemeDefinition {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
mode: "light" | "dark";
|
||||||
|
colors: ThemeColors;
|
||||||
|
isCustom?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixed themes using proper OKLCH format
|
||||||
|
const DEFAULT_THEMES: ThemeDefinition[] = [
|
||||||
|
{
|
||||||
|
id: "default-light",
|
||||||
|
name: "Default Light",
|
||||||
|
mode: "light",
|
||||||
|
colors: {
|
||||||
|
background: "oklch(1 0 0)",
|
||||||
|
foreground: "oklch(0.145 0 0)",
|
||||||
|
card: "oklch(1 0 0)",
|
||||||
|
cardForeground: "oklch(0.145 0 0)",
|
||||||
|
popover: "oklch(1 0 0)",
|
||||||
|
popoverForeground: "oklch(0.145 0 0)",
|
||||||
|
primary: "oklch(0.205 0 0)",
|
||||||
|
primaryForeground: "oklch(0.985 0 0)",
|
||||||
|
secondary: "oklch(0.97 0 0)",
|
||||||
|
secondaryForeground: "oklch(0.205 0 0)",
|
||||||
|
muted: "oklch(0.97 0 0)",
|
||||||
|
mutedForeground: "oklch(0.556 0 0)",
|
||||||
|
accent: "oklch(0.97 0 0)",
|
||||||
|
accentForeground: "oklch(0.205 0 0)",
|
||||||
|
destructive: "oklch(0.577 0.245 27.325)",
|
||||||
|
border: "oklch(0.922 0 0)",
|
||||||
|
input: "oklch(0.922 0 0)",
|
||||||
|
ring: "oklch(0.708 0 0)",
|
||||||
|
chart1: "oklch(0.646 0.222 41.116)",
|
||||||
|
chart2: "oklch(0.6 0.118 184.704)",
|
||||||
|
chart3: "oklch(0.398 0.07 227.392)",
|
||||||
|
chart4: "oklch(0.828 0.189 84.429)",
|
||||||
|
chart5: "oklch(0.769 0.188 70.08)",
|
||||||
|
sidebar: "oklch(0.985 0 0)",
|
||||||
|
sidebarForeground: "oklch(0.145 0 0)",
|
||||||
|
sidebarPrimary: "oklch(0.205 0 0)",
|
||||||
|
sidebarPrimaryForeground: "oklch(0.985 0 0)",
|
||||||
|
sidebarAccent: "oklch(0.97 0 0)",
|
||||||
|
sidebarAccentForeground: "oklch(0.205 0 0)",
|
||||||
|
sidebarBorder: "oklch(0.922 0 0)",
|
||||||
|
sidebarRing: "oklch(0.708 0 0)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "default-dark",
|
||||||
|
name: "Default Dark",
|
||||||
|
mode: "dark",
|
||||||
|
colors: {
|
||||||
|
background: "oklch(0.145 0 0)",
|
||||||
|
foreground: "oklch(0.985 0 0)",
|
||||||
|
card: "oklch(0.205 0 0)",
|
||||||
|
cardForeground: "oklch(0.985 0 0)",
|
||||||
|
popover: "oklch(0.205 0 0)",
|
||||||
|
popoverForeground: "oklch(0.985 0 0)",
|
||||||
|
primary: "oklch(0.922 0 0)",
|
||||||
|
primaryForeground: "oklch(0.205 0 0)",
|
||||||
|
secondary: "oklch(0.269 0 0)",
|
||||||
|
secondaryForeground: "oklch(0.985 0 0)",
|
||||||
|
muted: "oklch(0.269 0 0)",
|
||||||
|
mutedForeground: "oklch(0.708 0 0)",
|
||||||
|
accent: "oklch(0.269 0 0)",
|
||||||
|
accentForeground: "oklch(0.985 0 0)",
|
||||||
|
destructive: "oklch(0.704 0.191 22.216)",
|
||||||
|
border: "oklch(1 0 0 / 10%)",
|
||||||
|
input: "oklch(1 0 0 / 15%)",
|
||||||
|
ring: "oklch(0.556 0 0)",
|
||||||
|
chart1: "oklch(0.488 0.243 264.376)",
|
||||||
|
chart2: "oklch(0.696 0.17 162.48)",
|
||||||
|
chart3: "oklch(0.769 0.188 70.08)",
|
||||||
|
chart4: "oklch(0.627 0.265 303.9)",
|
||||||
|
chart5: "oklch(0.645 0.246 16.439)",
|
||||||
|
sidebar: "oklch(0.205 0 0)",
|
||||||
|
sidebarForeground: "oklch(0.985 0 0)",
|
||||||
|
sidebarPrimary: "oklch(0.488 0.243 264.376)",
|
||||||
|
sidebarPrimaryForeground: "oklch(0.985 0 0)",
|
||||||
|
sidebarAccent: "oklch(0.269 0 0)",
|
||||||
|
sidebarAccentForeground: "oklch(0.985 0 0)",
|
||||||
|
sidebarBorder: "oklch(1 0 0 / 10%)",
|
||||||
|
sidebarRing: "oklch(0.556 0 0)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "paper-light",
|
||||||
|
name: "Paper",
|
||||||
|
description: "Clean paper-like theme",
|
||||||
|
mode: "light",
|
||||||
|
colors: {
|
||||||
|
background: "oklch(0.99 0.01 85)",
|
||||||
|
foreground: "oklch(0.15 0.02 65)",
|
||||||
|
card: "oklch(0.99 0.01 85)",
|
||||||
|
cardForeground: "oklch(0.15 0.02 65)",
|
||||||
|
popover: "oklch(1 0 0)",
|
||||||
|
popoverForeground: "oklch(0.15 0.02 65)",
|
||||||
|
primary: "oklch(0.25 0.03 45)",
|
||||||
|
primaryForeground: "oklch(0.98 0.01 85)",
|
||||||
|
secondary: "oklch(0.96 0.01 75)",
|
||||||
|
secondaryForeground: "oklch(0.25 0.03 45)",
|
||||||
|
muted: "oklch(0.96 0.01 75)",
|
||||||
|
mutedForeground: "oklch(0.45 0.02 55)",
|
||||||
|
accent: "oklch(0.96 0.01 75)",
|
||||||
|
accentForeground: "oklch(0.25 0.03 45)",
|
||||||
|
destructive: "oklch(0.577 0.245 27.325)",
|
||||||
|
border: "oklch(0.90 0.01 65)",
|
||||||
|
input: "oklch(0.90 0.01 65)",
|
||||||
|
ring: "oklch(0.25 0.03 45)",
|
||||||
|
chart1: "oklch(0.646 0.222 41.116)",
|
||||||
|
chart2: "oklch(0.6 0.118 184.704)",
|
||||||
|
chart3: "oklch(0.398 0.07 227.392)",
|
||||||
|
chart4: "oklch(0.828 0.189 84.429)",
|
||||||
|
chart5: "oklch(0.769 0.188 70.08)",
|
||||||
|
sidebar: "oklch(0.97 0.01 80)",
|
||||||
|
sidebarForeground: "oklch(0.15 0.02 65)",
|
||||||
|
sidebarPrimary: "oklch(0.25 0.03 45)",
|
||||||
|
sidebarPrimaryForeground: "oklch(0.98 0.01 85)",
|
||||||
|
sidebarAccent: "oklch(0.94 0.01 75)",
|
||||||
|
sidebarAccentForeground: "oklch(0.25 0.03 45)",
|
||||||
|
sidebarBorder: "oklch(0.88 0.01 65)",
|
||||||
|
sidebarRing: "oklch(0.25 0.03 45)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "comfy-brown-dark",
|
||||||
|
name: "Comfy Brown",
|
||||||
|
description: "Warm brown theme for dark mode",
|
||||||
|
mode: "dark",
|
||||||
|
colors: {
|
||||||
|
background: "oklch(0.15 0.03 65)",
|
||||||
|
foreground: "oklch(0.95 0.01 85)",
|
||||||
|
card: "oklch(0.20 0.03 55)",
|
||||||
|
cardForeground: "oklch(0.95 0.01 85)",
|
||||||
|
popover: "oklch(0.20 0.03 55)",
|
||||||
|
popoverForeground: "oklch(0.95 0.01 85)",
|
||||||
|
primary: "oklch(0.65 0.15 45)",
|
||||||
|
primaryForeground: "oklch(0.95 0.01 85)",
|
||||||
|
secondary: "oklch(0.25 0.04 50)",
|
||||||
|
secondaryForeground: "oklch(0.95 0.01 85)",
|
||||||
|
muted: "oklch(0.25 0.04 50)",
|
||||||
|
mutedForeground: "oklch(0.70 0.02 65)",
|
||||||
|
accent: "oklch(0.25 0.04 50)",
|
||||||
|
accentForeground: "oklch(0.95 0.01 85)",
|
||||||
|
destructive: "oklch(0.704 0.191 22.216)",
|
||||||
|
border: "oklch(0.30 0.04 55)",
|
||||||
|
input: "oklch(0.30 0.04 55)",
|
||||||
|
ring: "oklch(0.65 0.15 45)",
|
||||||
|
chart1: "oklch(0.65 0.15 45)",
|
||||||
|
chart2: "oklch(0.55 0.12 85)",
|
||||||
|
chart3: "oklch(0.75 0.18 25)",
|
||||||
|
chart4: "oklch(0.60 0.14 105)",
|
||||||
|
chart5: "oklch(0.70 0.16 65)",
|
||||||
|
sidebar: "oklch(0.18 0.03 60)",
|
||||||
|
sidebarForeground: "oklch(0.95 0.01 85)",
|
||||||
|
sidebarPrimary: "oklch(0.65 0.15 45)",
|
||||||
|
sidebarPrimaryForeground: "oklch(0.95 0.01 85)",
|
||||||
|
sidebarAccent: "oklch(0.22 0.04 50)",
|
||||||
|
sidebarAccentForeground: "oklch(0.95 0.01 85)",
|
||||||
|
sidebarBorder: "oklch(0.28 0.04 55)",
|
||||||
|
sidebarRing: "oklch(0.65 0.15 45)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "midnight-dark",
|
||||||
|
name: "Midnight",
|
||||||
|
description: "Deep blue midnight theme",
|
||||||
|
mode: "dark",
|
||||||
|
colors: {
|
||||||
|
background: "oklch(0.12 0.08 250)",
|
||||||
|
foreground: "oklch(0.95 0.01 230)",
|
||||||
|
card: "oklch(0.18 0.06 240)",
|
||||||
|
cardForeground: "oklch(0.95 0.01 230)",
|
||||||
|
popover: "oklch(0.18 0.06 240)",
|
||||||
|
popoverForeground: "oklch(0.95 0.01 230)",
|
||||||
|
primary: "oklch(0.60 0.20 240)",
|
||||||
|
primaryForeground: "oklch(0.95 0.01 230)",
|
||||||
|
secondary: "oklch(0.22 0.05 235)",
|
||||||
|
secondaryForeground: "oklch(0.95 0.01 230)",
|
||||||
|
muted: "oklch(0.22 0.05 235)",
|
||||||
|
mutedForeground: "oklch(0.70 0.02 230)",
|
||||||
|
accent: "oklch(0.22 0.05 235)",
|
||||||
|
accentForeground: "oklch(0.95 0.01 230)",
|
||||||
|
destructive: "oklch(0.704 0.191 22.216)",
|
||||||
|
border: "oklch(0.25 0.05 235)",
|
||||||
|
input: "oklch(0.25 0.05 235)",
|
||||||
|
ring: "oklch(0.60 0.20 240)",
|
||||||
|
chart1: "oklch(0.60 0.20 240)",
|
||||||
|
chart2: "oklch(0.50 0.15 200)",
|
||||||
|
chart3: "oklch(0.65 0.18 280)",
|
||||||
|
chart4: "oklch(0.55 0.16 160)",
|
||||||
|
chart5: "oklch(0.70 0.22 300)",
|
||||||
|
sidebar: "oklch(0.15 0.07 245)",
|
||||||
|
sidebarForeground: "oklch(0.95 0.01 230)",
|
||||||
|
sidebarPrimary: "oklch(0.60 0.20 240)",
|
||||||
|
sidebarPrimaryForeground: "oklch(0.95 0.01 230)",
|
||||||
|
sidebarAccent: "oklch(0.20 0.05 235)",
|
||||||
|
sidebarAccentForeground: "oklch(0.95 0.01 230)",
|
||||||
|
sidebarBorder: "oklch(0.22 0.05 235)",
|
||||||
|
sidebarRing: "oklch(0.60 0.20 240)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type ThemeMode = "light" | "dark" | "system";
|
||||||
|
|
||||||
type ThemeProviderProps = {
|
type ThemeProviderProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
defaultTheme?: Theme;
|
defaultTheme?: ThemeMode;
|
||||||
storageKey?: string;
|
storageKey?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ThemeProviderState = {
|
type ThemeProviderState = {
|
||||||
theme: Theme;
|
mode: ThemeMode;
|
||||||
setTheme: (theme: Theme) => void;
|
currentTheme: ThemeDefinition;
|
||||||
|
currentLightTheme: ThemeDefinition;
|
||||||
|
currentDarkTheme: ThemeDefinition;
|
||||||
|
themes: ThemeDefinition[];
|
||||||
|
setMode: (mode: ThemeMode) => void;
|
||||||
|
setTheme: (themeId: string) => void;
|
||||||
|
addCustomTheme: (theme: Omit<ThemeDefinition, "id" | "isCustom">) => void;
|
||||||
|
removeCustomTheme: (themeId: string) => void;
|
||||||
|
getThemesForMode: (mode: "light" | "dark") => ThemeDefinition[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: ThemeProviderState = {
|
const initialState: ThemeProviderState = {
|
||||||
theme: "system",
|
mode: "system",
|
||||||
|
currentTheme: DEFAULT_THEMES[1], // Default to dark theme
|
||||||
|
currentLightTheme: DEFAULT_THEMES[0], // Default light
|
||||||
|
currentDarkTheme: DEFAULT_THEMES[1], // Default dark
|
||||||
|
themes: DEFAULT_THEMES,
|
||||||
|
setMode: () => null,
|
||||||
setTheme: () => null,
|
setTheme: () => null,
|
||||||
|
addCustomTheme: () => null,
|
||||||
|
removeCustomTheme: () => null,
|
||||||
|
getThemesForMode: () => [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||||
@@ -23,37 +281,206 @@ const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
|||||||
export function ThemeProvider({
|
export function ThemeProvider({
|
||||||
children,
|
children,
|
||||||
defaultTheme = "system",
|
defaultTheme = "system",
|
||||||
storageKey = "vite-ui-theme",
|
storageKey = "concord-theme",
|
||||||
...props
|
...props
|
||||||
}: ThemeProviderProps) {
|
}: ThemeProviderProps) {
|
||||||
const [theme, setTheme] = useState<Theme>(
|
const [mode, setMode] = useState<ThemeMode>(
|
||||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
|
() =>
|
||||||
|
(localStorage.getItem(storageKey + "-mode") as ThemeMode) || defaultTheme,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const [themes, setThemes] = useState<ThemeDefinition[]>(() => {
|
||||||
|
const saved = localStorage.getItem(storageKey + "-themes");
|
||||||
|
const customThemes = saved ? JSON.parse(saved) : [];
|
||||||
|
return [...DEFAULT_THEMES, ...customThemes];
|
||||||
|
});
|
||||||
|
|
||||||
|
const [currentLightThemeId, setCurrentLightThemeId] = useState<string>(() => {
|
||||||
|
const saved = localStorage.getItem(storageKey + "-light");
|
||||||
|
return saved || "default-light";
|
||||||
|
});
|
||||||
|
|
||||||
|
const [currentDarkThemeId, setCurrentDarkThemeId] = useState<string>(() => {
|
||||||
|
const saved = localStorage.getItem(storageKey + "-dark");
|
||||||
|
return saved || "default-dark";
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentLightTheme =
|
||||||
|
themes.find((t) => t.id === currentLightThemeId) || DEFAULT_THEMES[0];
|
||||||
|
const currentDarkTheme =
|
||||||
|
themes.find((t) => t.id === currentDarkThemeId) || DEFAULT_THEMES[2];
|
||||||
|
|
||||||
|
// Determine the current theme based on mode and system preference
|
||||||
|
const getCurrentTheme = (): ThemeDefinition => {
|
||||||
|
switch (mode) {
|
||||||
|
case "light":
|
||||||
|
return currentLightTheme;
|
||||||
|
case "dark":
|
||||||
|
return currentDarkTheme;
|
||||||
|
case "system":
|
||||||
|
const systemPrefersDark = window.matchMedia(
|
||||||
|
"(prefers-color-scheme: dark)",
|
||||||
|
).matches;
|
||||||
|
return systemPrefersDark ? currentDarkTheme : currentLightTheme;
|
||||||
|
default:
|
||||||
|
return currentDarkTheme;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentTheme = getCurrentTheme();
|
||||||
|
|
||||||
|
const applyTheme = (theme: ThemeDefinition) => {
|
||||||
const root = window.document.documentElement;
|
const root = window.document.documentElement;
|
||||||
|
|
||||||
|
// Remove existing theme classes
|
||||||
root.classList.remove("light", "dark");
|
root.classList.remove("light", "dark");
|
||||||
|
|
||||||
if (theme === "system") {
|
// Apply mode class
|
||||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
root.classList.add(theme.mode);
|
||||||
.matches
|
|
||||||
? "dark"
|
|
||||||
: "light";
|
|
||||||
|
|
||||||
root.classList.add(systemTheme);
|
// Apply CSS custom properties with proper mapping
|
||||||
return;
|
Object.entries(theme.colors).forEach(([key, value]) => {
|
||||||
|
// Convert camelCase to kebab-case and map to CSS variables
|
||||||
|
const cssVarMap: Record<string, string> = {
|
||||||
|
background: "--background",
|
||||||
|
foreground: "--foreground",
|
||||||
|
card: "--card",
|
||||||
|
cardForeground: "--card-foreground",
|
||||||
|
popover: "--popover",
|
||||||
|
popoverForeground: "--popover-foreground",
|
||||||
|
primary: "--primary",
|
||||||
|
primaryForeground: "--primary-foreground",
|
||||||
|
secondary: "--secondary",
|
||||||
|
secondaryForeground: "--secondary-foreground",
|
||||||
|
muted: "--muted",
|
||||||
|
mutedForeground: "--muted-foreground",
|
||||||
|
accent: "--accent",
|
||||||
|
accentForeground: "--accent-foreground",
|
||||||
|
destructive: "--destructive",
|
||||||
|
border: "--border",
|
||||||
|
input: "--input",
|
||||||
|
ring: "--ring",
|
||||||
|
chart1: "--chart-1",
|
||||||
|
chart2: "--chart-2",
|
||||||
|
chart3: "--chart-3",
|
||||||
|
chart4: "--chart-4",
|
||||||
|
chart5: "--chart-5",
|
||||||
|
sidebar: "--sidebar",
|
||||||
|
sidebarForeground: "--sidebar-foreground",
|
||||||
|
sidebarPrimary: "--sidebar-primary",
|
||||||
|
sidebarPrimaryForeground: "--sidebar-primary-foreground",
|
||||||
|
sidebarAccent: "--sidebar-accent",
|
||||||
|
sidebarAccentForeground: "--sidebar-accent-foreground",
|
||||||
|
sidebarBorder: "--sidebar-border",
|
||||||
|
sidebarRing: "--sidebar-ring",
|
||||||
|
};
|
||||||
|
|
||||||
|
const cssVar = cssVarMap[key];
|
||||||
|
if (cssVar) {
|
||||||
|
root.style.setProperty(cssVar, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
applyTheme(currentTheme);
|
||||||
|
}, [currentTheme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
const handleChange = () => {
|
||||||
|
if (mode === "system") {
|
||||||
|
// Theme will be recalculated due to getCurrentTheme dependency
|
||||||
|
const newTheme = getCurrentTheme();
|
||||||
|
applyTheme(newTheme);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaQuery.addEventListener("change", handleChange);
|
||||||
|
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||||
|
}, [mode, currentLightTheme, currentDarkTheme]);
|
||||||
|
|
||||||
|
const setTheme = (themeId: string) => {
|
||||||
|
const theme = themes.find((t) => t.id === themeId);
|
||||||
|
if (!theme) return;
|
||||||
|
|
||||||
|
// Update the appropriate theme based on the theme's mode
|
||||||
|
if (theme.mode === "light") {
|
||||||
|
setCurrentLightThemeId(themeId);
|
||||||
|
localStorage.setItem(storageKey + "-light", themeId);
|
||||||
|
} else {
|
||||||
|
setCurrentDarkThemeId(themeId);
|
||||||
|
localStorage.setItem(storageKey + "-dark", themeId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetMode = (newMode: ThemeMode) => {
|
||||||
|
setMode(newMode);
|
||||||
|
localStorage.setItem(storageKey + "-mode", newMode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addCustomTheme = (
|
||||||
|
themeData: Omit<ThemeDefinition, "id" | "isCustom">,
|
||||||
|
) => {
|
||||||
|
const newTheme: ThemeDefinition = {
|
||||||
|
...themeData,
|
||||||
|
id: `custom-${Date.now()}`,
|
||||||
|
isCustom: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedThemes = [...themes, newTheme];
|
||||||
|
setThemes(updatedThemes);
|
||||||
|
|
||||||
|
// Save only custom themes to localStorage
|
||||||
|
const customThemes = updatedThemes.filter((t) => t.isCustom);
|
||||||
|
localStorage.setItem(storageKey + "-themes", JSON.stringify(customThemes));
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCustomTheme = (themeId: string) => {
|
||||||
|
const updatedThemes = themes.filter((t) => t.id !== themeId);
|
||||||
|
setThemes(updatedThemes);
|
||||||
|
|
||||||
|
// If removing current theme, switch to default
|
||||||
|
if (currentLightThemeId === themeId) {
|
||||||
|
const defaultLight = updatedThemes.find(
|
||||||
|
(t) => t.mode === "light" && !t.isCustom,
|
||||||
|
);
|
||||||
|
if (defaultLight) {
|
||||||
|
setCurrentLightThemeId(defaultLight.id);
|
||||||
|
localStorage.setItem(storageKey + "-light", defaultLight.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
root.classList.add(theme);
|
if (currentDarkThemeId === themeId) {
|
||||||
}, [theme]);
|
const defaultDark = updatedThemes.find(
|
||||||
|
(t) => t.mode === "dark" && !t.isCustom,
|
||||||
|
);
|
||||||
|
if (defaultDark) {
|
||||||
|
setCurrentDarkThemeId(defaultDark.id);
|
||||||
|
localStorage.setItem(storageKey + "-dark", defaultDark.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save only custom themes to localStorage
|
||||||
|
const customThemes = updatedThemes.filter((t) => t.isCustom);
|
||||||
|
localStorage.setItem(storageKey + "-themes", JSON.stringify(customThemes));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getThemesForMode = (targetMode: "light" | "dark") => {
|
||||||
|
return themes.filter((t) => t.mode === targetMode);
|
||||||
|
};
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
theme,
|
mode,
|
||||||
setTheme: (theme: Theme) => {
|
currentTheme,
|
||||||
localStorage.setItem(storageKey, theme);
|
currentLightTheme,
|
||||||
setTheme(theme);
|
currentDarkTheme,
|
||||||
},
|
themes,
|
||||||
|
setMode: handleSetMode,
|
||||||
|
setTheme,
|
||||||
|
addCustomTheme,
|
||||||
|
removeCustomTheme,
|
||||||
|
getThemesForMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
419
concord-client/src/components/theme-selector.tsx
Normal file
419
concord-client/src/components/theme-selector.tsx
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Moon, Sun, Monitor, Palette, Plus, Trash2 } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
useTheme,
|
||||||
|
ThemeDefinition,
|
||||||
|
ThemeColors,
|
||||||
|
} from "@/components/theme-provider";
|
||||||
|
|
||||||
|
// Theme color input component for OKLCH values
|
||||||
|
const ColorInput: React.FC<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}> = ({ label, value, onChange }) => {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor={label} className="text-right text-sm">
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={label}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
className="col-span-3 font-mono text-sm"
|
||||||
|
placeholder="oklch(0.5 0.1 180)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom theme creation modal
|
||||||
|
const CreateThemeModal: React.FC<{
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (theme: Omit<ThemeDefinition, "id" | "isCustom">) => void;
|
||||||
|
}> = ({ isOpen, onClose, onSave }) => {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [mode, setMode] = useState<"light" | "dark">("dark");
|
||||||
|
const [colors, setColors] = useState<ThemeColors>({
|
||||||
|
background: "oklch(0.145 0 0)",
|
||||||
|
foreground: "oklch(0.985 0 0)",
|
||||||
|
card: "oklch(0.205 0 0)",
|
||||||
|
cardForeground: "oklch(0.985 0 0)",
|
||||||
|
popover: "oklch(0.205 0 0)",
|
||||||
|
popoverForeground: "oklch(0.985 0 0)",
|
||||||
|
primary: "oklch(0.922 0 0)",
|
||||||
|
primaryForeground: "oklch(0.205 0 0)",
|
||||||
|
secondary: "oklch(0.269 0 0)",
|
||||||
|
secondaryForeground: "oklch(0.985 0 0)",
|
||||||
|
muted: "oklch(0.269 0 0)",
|
||||||
|
mutedForeground: "oklch(0.708 0 0)",
|
||||||
|
accent: "oklch(0.269 0 0)",
|
||||||
|
accentForeground: "oklch(0.985 0 0)",
|
||||||
|
destructive: "oklch(0.704 0.191 22.216)",
|
||||||
|
border: "oklch(1 0 0 / 10%)",
|
||||||
|
input: "oklch(1 0 0 / 15%)",
|
||||||
|
ring: "oklch(0.556 0 0)",
|
||||||
|
chart1: "oklch(0.488 0.243 264.376)",
|
||||||
|
chart2: "oklch(0.696 0.17 162.48)",
|
||||||
|
chart3: "oklch(0.769 0.188 70.08)",
|
||||||
|
chart4: "oklch(0.627 0.265 303.9)",
|
||||||
|
chart5: "oklch(0.645 0.246 16.439)",
|
||||||
|
sidebar: "oklch(0.205 0 0)",
|
||||||
|
sidebarForeground: "oklch(0.985 0 0)",
|
||||||
|
sidebarPrimary: "oklch(0.488 0.243 264.376)",
|
||||||
|
sidebarPrimaryForeground: "oklch(0.985 0 0)",
|
||||||
|
sidebarAccent: "oklch(0.269 0 0)",
|
||||||
|
sidebarAccentForeground: "oklch(0.985 0 0)",
|
||||||
|
sidebarBorder: "oklch(1 0 0 / 10%)",
|
||||||
|
sidebarRing: "oklch(0.556 0 0)",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!name.trim()) return;
|
||||||
|
|
||||||
|
onSave({
|
||||||
|
name: name.trim(),
|
||||||
|
description: description.trim() || undefined,
|
||||||
|
mode,
|
||||||
|
colors,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setName("");
|
||||||
|
setDescription("");
|
||||||
|
setMode("dark");
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateColor = (key: keyof ThemeColors, value: string) => {
|
||||||
|
setColors((prev) => ({ ...prev, [key]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create Custom Theme</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create a custom theme by defining colors in OKLCH format (e.g.,
|
||||||
|
"oklch(0.5 0.1 180)")
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Basic Info */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="theme-name" className="text-right">
|
||||||
|
Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="theme-name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="col-span-3"
|
||||||
|
placeholder="My Custom Theme"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="theme-description" className="text-right">
|
||||||
|
Description
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="theme-description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
className="col-span-3"
|
||||||
|
placeholder="Optional description"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="theme-mode" className="text-right">
|
||||||
|
Mode
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={mode}
|
||||||
|
onValueChange={(v: "light" | "dark") => setMode(v)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="col-span-3">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="light">Light</SelectItem>
|
||||||
|
<SelectItem value="dark">Dark</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Color sections */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="font-medium">Basic Colors</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<ColorInput
|
||||||
|
label="Background"
|
||||||
|
value={colors.background}
|
||||||
|
onChange={(v) => updateColor("background", v)}
|
||||||
|
/>
|
||||||
|
<ColorInput
|
||||||
|
label="Foreground"
|
||||||
|
value={colors.foreground}
|
||||||
|
onChange={(v) => updateColor("foreground", v)}
|
||||||
|
/>
|
||||||
|
<ColorInput
|
||||||
|
label="Primary"
|
||||||
|
value={colors.primary}
|
||||||
|
onChange={(v) => updateColor("primary", v)}
|
||||||
|
/>
|
||||||
|
<ColorInput
|
||||||
|
label="Secondary"
|
||||||
|
value={colors.secondary}
|
||||||
|
onChange={(v) => updateColor("secondary", v)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 className="font-medium">Sidebar Colors</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<ColorInput
|
||||||
|
label="Sidebar"
|
||||||
|
value={colors.sidebar}
|
||||||
|
onChange={(v) => updateColor("sidebar", v)}
|
||||||
|
/>
|
||||||
|
<ColorInput
|
||||||
|
label="Sidebar Primary"
|
||||||
|
value={colors.sidebarPrimary}
|
||||||
|
onChange={(v) => updateColor("sidebarPrimary", v)}
|
||||||
|
/>
|
||||||
|
<ColorInput
|
||||||
|
label="Sidebar Accent"
|
||||||
|
value={colors.sidebarAccent}
|
||||||
|
onChange={(v) => updateColor("sidebarAccent", v)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave} disabled={!name.trim()}>
|
||||||
|
Create Theme
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main theme selector component
|
||||||
|
export function ThemeSelector() {
|
||||||
|
const {
|
||||||
|
mode,
|
||||||
|
currentTheme,
|
||||||
|
// themes,
|
||||||
|
setMode,
|
||||||
|
setTheme,
|
||||||
|
addCustomTheme,
|
||||||
|
removeCustomTheme,
|
||||||
|
getThemesForMode,
|
||||||
|
} = useTheme();
|
||||||
|
|
||||||
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const lightThemes = getThemesForMode("light");
|
||||||
|
const darkThemes = getThemesForMode("dark");
|
||||||
|
|
||||||
|
const getCurrentModeIcon = () => {
|
||||||
|
switch (mode) {
|
||||||
|
case "light":
|
||||||
|
return <Sun className="h-4 w-4" />;
|
||||||
|
case "dark":
|
||||||
|
return <Moon className="h-4 w-4" />;
|
||||||
|
default:
|
||||||
|
return <Monitor className="h-4 w-4" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
{getCurrentModeIcon()}
|
||||||
|
<span className="ml-2">{currentTheme.name}</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-56">
|
||||||
|
<DropdownMenuLabel>Appearance</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
{/* Mode Selection */}
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuLabel className="text-xs">Mode</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem onClick={() => setMode("light")}>
|
||||||
|
<Sun className="mr-2 h-4 w-4" />
|
||||||
|
<span>Light</span>
|
||||||
|
{mode === "light" && (
|
||||||
|
<Badge variant="secondary" className="ml-auto">
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setMode("dark")}>
|
||||||
|
<Moon className="mr-2 h-4 w-4" />
|
||||||
|
<span>Dark</span>
|
||||||
|
{mode === "dark" && (
|
||||||
|
<Badge variant="secondary" className="ml-auto">
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setMode("system")}>
|
||||||
|
<Monitor className="mr-2 h-4 w-4" />
|
||||||
|
<span>System</span>
|
||||||
|
{mode === "system" && (
|
||||||
|
<Badge variant="secondary" className="ml-auto">
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
{/* Light Themes */}
|
||||||
|
{lightThemes.length > 0 && (
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuLabel className="text-xs">
|
||||||
|
Light Themes
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{lightThemes.map((theme) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={theme.id}
|
||||||
|
onClick={() => setTheme(theme.id)}
|
||||||
|
className="justify-between"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Palette className="mr-2 h-4 w-4" />
|
||||||
|
<span>{theme.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{currentTheme.id === theme.id && (
|
||||||
|
<Badge variant="secondary">Active</Badge>
|
||||||
|
)}
|
||||||
|
{theme.isCustom && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-auto p-1 hover:text-destructive"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
removeCustomTheme(theme.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{lightThemes.length > 0 && darkThemes.length > 0 && (
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dark Themes */}
|
||||||
|
{darkThemes.length > 0 && (
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuLabel className="text-xs">
|
||||||
|
Dark Themes
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{darkThemes.map((theme) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={theme.id}
|
||||||
|
onClick={() => setTheme(theme.id)}
|
||||||
|
className="justify-between"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Palette className="mr-2 h-4 w-4" />
|
||||||
|
<span>{theme.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{currentTheme.id === theme.id && (
|
||||||
|
<Badge variant="secondary">Active</Badge>
|
||||||
|
)}
|
||||||
|
{theme.isCustom && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-auto p-1 hover:text-destructive"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
removeCustomTheme(theme.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
{/* Add Custom Theme */}
|
||||||
|
<DropdownMenuItem onClick={() => setIsCreateModalOpen(true)}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
<span>Create Theme</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
|
||||||
|
<CreateThemeModal
|
||||||
|
isOpen={isCreateModalOpen}
|
||||||
|
onClose={() => setIsCreateModalOpen(false)}
|
||||||
|
onSave={addCustomTheme}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
concord-client/src/components/ui/badge.tsx
Normal file
46
concord-client/src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "span"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
141
concord-client/src/components/ui/dialog.tsx
Normal file
141
concord-client/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Dialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DialogPortal data-slot="dialog-portal">
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
data-slot="dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
data-slot="dialog-close"
|
||||||
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
data-slot="dialog-title"
|
||||||
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
data-slot="dialog-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
}
|
||||||
183
concord-client/src/components/ui/select.tsx
Normal file
183
concord-client/src/components/ui/select.tsx
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Select({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
|
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectValue({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectTrigger({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
|
size?: "sm" | "default"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
data-slot="select-trigger"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
position = "popper",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
data-slot="select-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
data-slot="select-label"
|
||||||
|
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
data-slot="select-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
data-slot="select-separator"
|
||||||
|
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollUpButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollDownButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
}
|
||||||
28
concord-client/src/components/ui/separator.tsx
Normal file
28
concord-client/src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Separator({
|
||||||
|
className,
|
||||||
|
orientation = "horizontal",
|
||||||
|
decorative = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
data-slot="separator"
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Separator }
|
||||||
61
concord-client/src/components/ui/slider.tsx
Normal file
61
concord-client/src/components/ui/slider.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Slider({
|
||||||
|
className,
|
||||||
|
defaultValue,
|
||||||
|
value,
|
||||||
|
min = 0,
|
||||||
|
max = 100,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||||
|
const _values = React.useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: Array.isArray(defaultValue)
|
||||||
|
? defaultValue
|
||||||
|
: [min, max],
|
||||||
|
[value, defaultValue, min, max]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
data-slot="slider"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
value={value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track
|
||||||
|
data-slot="slider-track"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Range
|
||||||
|
data-slot="slider-range"
|
||||||
|
className={cn(
|
||||||
|
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
{Array.from({ length: _values.length }, (_, index) => (
|
||||||
|
<SliderPrimitive.Thumb
|
||||||
|
data-slot="slider-thumb"
|
||||||
|
key={index}
|
||||||
|
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Slider }
|
||||||
29
concord-client/src/components/ui/switch.tsx
Normal file
29
concord-client/src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Switch({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SwitchPrimitive.Root
|
||||||
|
data-slot="switch"
|
||||||
|
className={cn(
|
||||||
|
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SwitchPrimitive.Thumb
|
||||||
|
data-slot="switch-thumb"
|
||||||
|
className={cn(
|
||||||
|
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
18
concord-client/src/components/ui/textarea.tsx
Normal file
18
concord-client/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
@@ -1,72 +1,86 @@
|
|||||||
// src/hooks/useChannels.ts
|
import { Message } from "@/types/database";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { CategoryWithChannels } from "@/types/api";
|
|
||||||
|
|
||||||
// Placeholder hook for channels by instance
|
// Sample messages data
|
||||||
export const useChannels = (instanceId?: string) => {
|
export const SAMPLE_MESSAGES = [
|
||||||
return useQuery({
|
|
||||||
queryKey: ["channels", instanceId],
|
|
||||||
queryFn: async (): Promise<CategoryWithChannels[]> => {
|
|
||||||
if (!instanceId || instanceId === "@me") return [];
|
|
||||||
|
|
||||||
// TODO: Replace with actual API call
|
|
||||||
return [
|
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "Text Channels",
|
content: "Hey everyone! Just finished the new theme system. Check it out!",
|
||||||
instanceId: instanceId,
|
channelId: "1", // general channel
|
||||||
position: 0,
|
userId: "1",
|
||||||
createdAt: new Date().toISOString(),
|
edited: false,
|
||||||
updatedAt: new Date().toISOString(),
|
createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
|
||||||
channels: [
|
updatedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
name: "general",
|
|
||||||
type: "text",
|
|
||||||
categoryId: "1",
|
|
||||||
instanceId: instanceId,
|
|
||||||
position: 0,
|
|
||||||
topic: "General discussion",
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "2",
|
id: "2",
|
||||||
name: "random",
|
content:
|
||||||
type: "text",
|
"Looking great! The dark mode especially feels much better on the eyes 👀",
|
||||||
categoryId: "1",
|
channelId: "1",
|
||||||
instanceId: instanceId,
|
userId: "2",
|
||||||
position: 1,
|
edited: false,
|
||||||
topic: "Random chat",
|
createdAt: new Date(Date.now() - 4 * 60 * 1000).toISOString(),
|
||||||
createdAt: new Date().toISOString(),
|
updatedAt: new Date(Date.now() - 4 * 60 * 1000).toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
name: "Voice Channels",
|
|
||||||
instanceId: instanceId,
|
|
||||||
position: 1,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
channels: [
|
|
||||||
{
|
{
|
||||||
id: "3",
|
id: "3",
|
||||||
name: "General",
|
content: "Can we add a **high contrast mode** for accessibility?",
|
||||||
type: "voice",
|
channelId: "1",
|
||||||
categoryId: "2",
|
userId: "3",
|
||||||
instanceId: instanceId,
|
edited: false,
|
||||||
position: 0,
|
createdAt: new Date(Date.now() - 3 * 60 * 1000).toISOString(),
|
||||||
topic: "",
|
updatedAt: new Date(Date.now() - 3 * 60 * 1000).toISOString(),
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
|
id: "4",
|
||||||
|
content:
|
||||||
|
"```typescript\nconst theme = {\n primary: 'oklch(0.6 0.2 240)',\n secondary: 'oklch(0.8 0.1 60)'\n};\n```\nHere's how the new color system works!",
|
||||||
|
channelId: "1",
|
||||||
|
userId: "3",
|
||||||
|
edited: false,
|
||||||
|
createdAt: new Date(Date.now() - 2 * 60 * 1000).toISOString(),
|
||||||
|
updatedAt: new Date(Date.now() - 2 * 60 * 1000).toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
content:
|
||||||
|
"Perfect timing! I was just about to ask about the color format. _OKLCH_ is so much better than HSL for this.",
|
||||||
|
channelId: "1",
|
||||||
|
userId: "1",
|
||||||
|
edited: false,
|
||||||
|
createdAt: new Date(Date.now() - 1 * 60 * 1000).toISOString(),
|
||||||
|
updatedAt: new Date(Date.now() - 1 * 60 * 1000).toISOString(),
|
||||||
|
},
|
||||||
|
// Messages for random channel
|
||||||
|
{
|
||||||
|
id: "6",
|
||||||
|
content: "Anyone up for a game tonight?",
|
||||||
|
channelId: "2", // random channel
|
||||||
|
userId: "2",
|
||||||
|
edited: false,
|
||||||
|
createdAt: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
|
||||||
|
updatedAt: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
content: "I'm in! What are we playing?",
|
||||||
|
channelId: "2",
|
||||||
|
userId: "1",
|
||||||
|
edited: false,
|
||||||
|
createdAt: new Date(Date.now() - 25 * 60 * 1000).toISOString(),
|
||||||
|
updatedAt: new Date(Date.now() - 25 * 60 * 1000).toISOString(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const useChannelMessages = (channelId?: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["messages", channelId],
|
||||||
|
queryFn: async (): Promise<Message[]> => {
|
||||||
|
if (!channelId) return [];
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
return SAMPLE_MESSAGES.filter((msg) => msg.channelId === channelId);
|
||||||
},
|
},
|
||||||
enabled: !!instanceId && instanceId !== "@me",
|
enabled: !!channelId,
|
||||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
staleTime: 1000 * 60 * 1,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { useUiStore } from "@/stores/uiStore";
|
|
||||||
|
|
||||||
export const useResponsive = () => {
|
|
||||||
const { screenWidth, isMobile, setScreenWidth, updateIsMobile } =
|
|
||||||
useUiStore();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleResize = () => {
|
|
||||||
const width = window.innerWidth;
|
|
||||||
setScreenWidth(width);
|
|
||||||
updateIsMobile();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set initial values
|
|
||||||
handleResize();
|
|
||||||
|
|
||||||
// Add event listener
|
|
||||||
window.addEventListener("resize", handleResize);
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
return () => window.removeEventListener("resize", handleResize);
|
|
||||||
}, [setScreenWidth, updateIsMobile]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
screenWidth,
|
|
||||||
isMobile,
|
|
||||||
isTablet: screenWidth >= 768 && screenWidth < 1024,
|
|
||||||
isDesktop: screenWidth >= 1024,
|
|
||||||
isLargeDesktop: screenWidth >= 1280,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,78 +1,359 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Instance, InstanceWithDetails, UserWithRoles } from "@/types/api";
|
import { Instance, User } from "@/types/database";
|
||||||
|
import { InstanceWithDetails } from "@/types/api";
|
||||||
|
import { CategoryWithChannels } from "@/types/api";
|
||||||
|
|
||||||
|
// Sample users data with proper Role structure
|
||||||
|
export const SAMPLE_USERS: User[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
username: "alice_dev",
|
||||||
|
nickname: "Alice",
|
||||||
|
bio: "Frontend developer who loves React",
|
||||||
|
picture: null,
|
||||||
|
banner: null,
|
||||||
|
status: "online" as const,
|
||||||
|
hashPassword: "",
|
||||||
|
admin: false,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
roles: [], // Will be populated per instance
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
username: "bob_designer",
|
||||||
|
nickname: "Bob",
|
||||||
|
bio: "UI/UX Designer & Coffee Enthusiast",
|
||||||
|
picture:
|
||||||
|
"https://media.istockphoto.com/id/1682296067/photo/happy-studio-portrait-or-professional-man-real-estate-agent-or-asian-businessman-smile-for.jpg?s=612x612&w=0&k=20&c=9zbG2-9fl741fbTWw5fNgcEEe4ll-JegrGlQQ6m54rg=",
|
||||||
|
banner: null,
|
||||||
|
status: "away" as const,
|
||||||
|
hashPassword: "",
|
||||||
|
admin: false,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
username: "charlie_backend",
|
||||||
|
nickname: "Charlie",
|
||||||
|
bio: "Backend wizard, scaling systems since 2018",
|
||||||
|
picture: null,
|
||||||
|
banner: null,
|
||||||
|
status: "busy" as const,
|
||||||
|
hashPassword: "",
|
||||||
|
admin: false,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "current",
|
||||||
|
username: "you",
|
||||||
|
nickname: "You",
|
||||||
|
bio: "That's you!",
|
||||||
|
picture: null,
|
||||||
|
banner: null,
|
||||||
|
status: "online" as const,
|
||||||
|
hashPassword: "",
|
||||||
|
admin: true,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Sample servers data
|
||||||
|
const SAMPLE_SERVERS: Instance[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
name: "Dev Team",
|
||||||
|
icon: null,
|
||||||
|
description: "Our development team server",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
name: "Gaming Squad",
|
||||||
|
icon: null,
|
||||||
|
description: "Gaming and fun times",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
name: "Book Club",
|
||||||
|
icon: null,
|
||||||
|
description: "Monthly book discussions",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Sample messages data
|
||||||
|
export const SAMPLE_MESSAGES = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
content: "Hey everyone! Just finished the new theme system. Check it out!",
|
||||||
|
channelId: "1", // general channel
|
||||||
|
userId: "1",
|
||||||
|
edited: false,
|
||||||
|
createdAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
|
||||||
|
updatedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
content:
|
||||||
|
"Looking great! The dark mode especially feels much better on the eyes 👀",
|
||||||
|
channelId: "1",
|
||||||
|
userId: "2",
|
||||||
|
edited: false,
|
||||||
|
createdAt: new Date(Date.now() - 4 * 60 * 1000).toISOString(),
|
||||||
|
updatedAt: new Date(Date.now() - 4 * 60 * 1000).toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
content: "Can we add a **high contrast mode** for accessibility?",
|
||||||
|
channelId: "1",
|
||||||
|
userId: "3",
|
||||||
|
edited: false,
|
||||||
|
createdAt: new Date(Date.now() - 3 * 60 * 1000).toISOString(),
|
||||||
|
updatedAt: new Date(Date.now() - 3 * 60 * 1000).toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
content:
|
||||||
|
"```typescript\nconst theme = {\n primary: 'oklch(0.6 0.2 240)',\n secondary: 'oklch(0.8 0.1 60)'\n};\n```\nHere's how the new color system works!",
|
||||||
|
channelId: "1",
|
||||||
|
userId: "3",
|
||||||
|
edited: false,
|
||||||
|
createdAt: new Date(Date.now() - 2 * 60 * 1000).toISOString(),
|
||||||
|
updatedAt: new Date(Date.now() - 2 * 60 * 1000).toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
content:
|
||||||
|
"Perfect timing! I was just about to ask about the color format. _OKLCH_ is so much better than HSL for this.",
|
||||||
|
channelId: "1",
|
||||||
|
userId: "1",
|
||||||
|
edited: false,
|
||||||
|
createdAt: new Date(Date.now() - 1 * 60 * 1000).toISOString(),
|
||||||
|
updatedAt: new Date(Date.now() - 1 * 60 * 1000).toISOString(),
|
||||||
|
},
|
||||||
|
// Messages for random channel
|
||||||
|
{
|
||||||
|
id: "6",
|
||||||
|
content: "Anyone up for a game tonight?",
|
||||||
|
channelId: "2", // random channel
|
||||||
|
userId: "2",
|
||||||
|
edited: false,
|
||||||
|
createdAt: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
|
||||||
|
updatedAt: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
content: "I'm in! What are we playing?",
|
||||||
|
channelId: "2",
|
||||||
|
userId: "1",
|
||||||
|
edited: false,
|
||||||
|
createdAt: new Date(Date.now() - 25 * 60 * 1000).toISOString(),
|
||||||
|
updatedAt: new Date(Date.now() - 25 * 60 * 1000).toISOString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Sample categories with channels
|
||||||
|
const createSampleCategories = (instanceId: string): CategoryWithChannels[] => [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
name: "Text Channels",
|
||||||
|
instanceId: instanceId,
|
||||||
|
position: 0,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
channels: [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
name: "general",
|
||||||
|
type: "text",
|
||||||
|
categoryId: "1",
|
||||||
|
instanceId: instanceId,
|
||||||
|
position: 0,
|
||||||
|
description: "General discussion about development and projects",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
name: "random",
|
||||||
|
type: "text",
|
||||||
|
categoryId: "1",
|
||||||
|
instanceId: instanceId,
|
||||||
|
position: 1,
|
||||||
|
description: "Random chat and off-topic discussions",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
name: "announcements",
|
||||||
|
type: "text",
|
||||||
|
categoryId: "1",
|
||||||
|
instanceId: instanceId,
|
||||||
|
position: 2,
|
||||||
|
description: "Important announcements and updates",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
name: "Voice Channels",
|
||||||
|
instanceId: instanceId,
|
||||||
|
position: 1,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
channels: [
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
name: "General",
|
||||||
|
type: "voice",
|
||||||
|
categoryId: "2",
|
||||||
|
instanceId: instanceId,
|
||||||
|
position: 0,
|
||||||
|
description: "",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
name: "Focus Room",
|
||||||
|
type: "voice",
|
||||||
|
categoryId: "2",
|
||||||
|
instanceId: instanceId,
|
||||||
|
position: 1,
|
||||||
|
description: "",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
name: "Project Channels",
|
||||||
|
instanceId: instanceId,
|
||||||
|
position: 2,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
channels: [
|
||||||
|
{
|
||||||
|
id: "6",
|
||||||
|
name: "frontend",
|
||||||
|
type: "text",
|
||||||
|
categoryId: "3",
|
||||||
|
instanceId: instanceId,
|
||||||
|
position: 0,
|
||||||
|
description: "Frontend development discussions",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
name: "backend",
|
||||||
|
type: "text",
|
||||||
|
categoryId: "3",
|
||||||
|
instanceId: instanceId,
|
||||||
|
position: 1,
|
||||||
|
description: "Backend and API development",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Placeholder hook for channels by instance
|
||||||
|
export const useChannels = (instanceId?: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["channels", instanceId],
|
||||||
|
queryFn: async (): Promise<CategoryWithChannels[]> => {
|
||||||
|
if (!instanceId) return [];
|
||||||
|
|
||||||
|
return createSampleCategories(instanceId);
|
||||||
|
},
|
||||||
|
enabled: !!instanceId,
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook for getting messages in a channel
|
||||||
|
export const useChannelMessages = (channelId?: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["messages", channelId],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!channelId) return [];
|
||||||
|
|
||||||
|
// Return messages for this channel
|
||||||
|
return SAMPLE_MESSAGES.filter((msg) => msg.channelId === channelId);
|
||||||
|
},
|
||||||
|
enabled: !!channelId,
|
||||||
|
staleTime: 1000 * 60 * 1, // 1 minute (messages are more dynamic)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Placeholder hook for servers/instances
|
// Placeholder hook for servers/instances
|
||||||
export const useServers = () => {
|
export const useServers = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["servers"],
|
queryKey: ["servers"],
|
||||||
queryFn: async (): Promise<Instance[]> => {
|
queryFn: async (): Promise<Instance[]> => {
|
||||||
// TODO: Replace with actual API call
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
return [
|
return SAMPLE_SERVERS;
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
name: "My Server",
|
|
||||||
icon: null,
|
|
||||||
description: "A cool server",
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
},
|
},
|
||||||
];
|
|
||||||
},
|
|
||||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Placeholder hook for instance details
|
|
||||||
export const useInstanceDetails = (instanceId?: string) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["instance", instanceId],
|
|
||||||
queryFn: async (): Promise<InstanceWithDetails | null> => {
|
|
||||||
if (!instanceId || instanceId === "@me") return null;
|
|
||||||
|
|
||||||
// TODO: Replace with actual API call
|
|
||||||
return {
|
|
||||||
id: instanceId,
|
|
||||||
name: "My Server",
|
|
||||||
icon: null,
|
|
||||||
description: "A cool server",
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
categories: [],
|
|
||||||
memberCount: 1,
|
|
||||||
roles: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
enabled: !!instanceId && instanceId !== "@me",
|
|
||||||
staleTime: 1000 * 60 * 5,
|
staleTime: 1000 * 60 * 5,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Placeholder hook for instance members
|
export const useInstanceDetails = (instanceId?: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["instance", instanceId],
|
||||||
|
queryFn: async (): Promise<InstanceWithDetails | null> => {
|
||||||
|
if (!instanceId) return null;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
const server = SAMPLE_SERVERS.find((s) => s.id === instanceId);
|
||||||
|
if (!server) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...server,
|
||||||
|
categories: createSampleCategories(instanceId),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
enabled: !!instanceId,
|
||||||
|
staleTime: 1000 * 60 * 5,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook for getting all users from an instance with their roles
|
||||||
export const useInstanceMembers = (instanceId?: string) => {
|
export const useInstanceMembers = (instanceId?: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["instance", instanceId, "members"],
|
queryKey: ["instance", instanceId, "members"],
|
||||||
queryFn: async (): Promise<UserWithRoles[]> => {
|
queryFn: async (): Promise<User[]> => {
|
||||||
if (!instanceId || instanceId === "@me") return [];
|
if (!instanceId) return [];
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
// TODO: Replace with actual API call
|
return SAMPLE_USERS.map((user, index) => ({
|
||||||
return [
|
...user,
|
||||||
|
roles: [
|
||||||
{
|
{
|
||||||
id: "1",
|
instanceId: instanceId,
|
||||||
username: "testuser",
|
role: index === 0 ? "admin" : index === 1 ? "mod" : "member",
|
||||||
nickname: "Test User",
|
|
||||||
bio: "Just testing",
|
|
||||||
picture: null,
|
|
||||||
banner: null,
|
|
||||||
hashPassword: "",
|
|
||||||
algorithms: "{}",
|
|
||||||
status: "online",
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
roles: [],
|
|
||||||
},
|
},
|
||||||
];
|
],
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
enabled: !!instanceId && instanceId !== "@me",
|
enabled: !!instanceId,
|
||||||
staleTime: 1000 * 60 * 2, // 2 minutes (members change more frequently)
|
staleTime: 1000 * 60 * 2,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,6 +39,23 @@
|
|||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
|
||||||
|
/* concord-specific custom properties */
|
||||||
|
--color-text-primary: var(--foreground);
|
||||||
|
--color-text-secondary: var(--muted-foreground);
|
||||||
|
--color-text-muted: var(--muted-foreground);
|
||||||
|
--color-interactive-normal: var(--muted-foreground);
|
||||||
|
--color-interactive-hover: var(--foreground);
|
||||||
|
--color-interactive-active: var(--primary);
|
||||||
|
--color-background-primary: var(--background);
|
||||||
|
--color-background-secondary: var(--card);
|
||||||
|
--color-background-tertiary: var(--muted);
|
||||||
|
|
||||||
|
/* Status colors - these remain consistent across themes */
|
||||||
|
--color-status-online: oklch(0.6 0.15 140);
|
||||||
|
--color-status-away: oklch(0.75 0.15 85);
|
||||||
|
--color-status-busy: oklch(0.65 0.2 25);
|
||||||
|
--color-status-offline: var(--muted-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@@ -112,9 +129,179 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
border-color: var(--border);
|
||||||
|
outline-color: var(--ring, oklch(0.708 0 0)) / 50%;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
background-color: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Background utilities */
|
||||||
|
@utility bg-concord-primary {
|
||||||
|
background-color: var(--color-background-primary);
|
||||||
|
}
|
||||||
|
@utility bg-concord-secondary {
|
||||||
|
background-color: var(--color-background-secondary);
|
||||||
|
}
|
||||||
|
@utility bg-concord-tertiary {
|
||||||
|
background-color: var(--color-background-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text utilities */
|
||||||
|
@utility text-concord-primary {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
@utility text-concord-secondary {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
@utility text-concord-muted {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interactive utilities */
|
||||||
|
@utility text-interactive-normal {
|
||||||
|
color: var(--color-interactive-normal);
|
||||||
|
}
|
||||||
|
@utility text-interactive-hover {
|
||||||
|
color: var(--color-interactive-hover);
|
||||||
|
}
|
||||||
|
@utility text-interactive-active {
|
||||||
|
color: var(--color-interactive-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status utilities */
|
||||||
|
@utility text-status-online {
|
||||||
|
color: var(--color-status-online);
|
||||||
|
}
|
||||||
|
@utility text-status-away {
|
||||||
|
color: var(--color-status-away);
|
||||||
|
}
|
||||||
|
@utility text-status-busy {
|
||||||
|
color: var(--color-status-busy);
|
||||||
|
}
|
||||||
|
@utility text-status-offline {
|
||||||
|
color: var(--color-status-offline);
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility bg-status-online {
|
||||||
|
background-color: var(--color-status-online);
|
||||||
|
}
|
||||||
|
@utility bg-status-away {
|
||||||
|
background-color: var(--color-status-away);
|
||||||
|
}
|
||||||
|
@utility bg-status-busy {
|
||||||
|
background-color: var(--color-status-busy);
|
||||||
|
}
|
||||||
|
@utility bg-status-offline {
|
||||||
|
background-color: var(--color-status-offline);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Border utilities */
|
||||||
|
@utility border-concord {
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
@utility border-sidebar {
|
||||||
|
border-color: var(--color-sidebar-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility interactive-hover {
|
||||||
|
transition-property: color;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
transition-timing-function: ease-in-out;
|
||||||
|
color: var(--color-interactive-normal);
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-interactive-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility spacing-xs {
|
||||||
|
padding: var(--tw-spacing-1, 0.25rem);
|
||||||
|
gap: var(--tw-spacing-1, 0.25rem);
|
||||||
|
}
|
||||||
|
@utility spacing-sm {
|
||||||
|
padding: var(--tw-spacing-2, 0.5rem);
|
||||||
|
gap: var(--tw-spacing-2, 0.5rem);
|
||||||
|
}
|
||||||
|
@utility spacing-md {
|
||||||
|
padding: var(--tw-spacing-4, 1rem);
|
||||||
|
gap: var(--tw-spacing-4, 1rem);
|
||||||
|
}
|
||||||
|
@utility spacing-lg {
|
||||||
|
padding: var(--tw-spacing-6, 1.5rem);
|
||||||
|
gap: var(--tw-spacing-6, 1.5rem);
|
||||||
|
}
|
||||||
|
@utility spacing-xl {
|
||||||
|
padding: var(--tw-spacing-8, 2rem);
|
||||||
|
gap: var(--tw-spacing-8, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility spacing-x-xs {
|
||||||
|
padding-left: var(--tw-spacing-1, 0.25rem);
|
||||||
|
padding-right: var(--tw-spacing-1, 0.25rem);
|
||||||
|
}
|
||||||
|
@utility spacing-x-sm {
|
||||||
|
padding-left: var(--tw-spacing-2, 0.5rem);
|
||||||
|
padding-right: var(--tw-spacing-2, 0.5rem);
|
||||||
|
}
|
||||||
|
@utility spacing-x-md {
|
||||||
|
padding-left: var(--tw-spacing-4, 1rem);
|
||||||
|
padding-right: var(--tw-spacing-4, 1rem);
|
||||||
|
}
|
||||||
|
@utility spacing-x-lg {
|
||||||
|
padding-left: var(--tw-spacing-6, 1.5rem);
|
||||||
|
padding-right: var(--tw-spacing-6, 1.5rem);
|
||||||
|
}
|
||||||
|
@utility spacing-x-xl {
|
||||||
|
padding-left: var(--tw-spacing-8, 2rem);
|
||||||
|
padding-right: var(--tw-spacing-8, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility spacing-y-xs {
|
||||||
|
padding-top: var(--tw-spacing-1, 0.25rem);
|
||||||
|
padding-bottom: var(--tw-spacing-1, 0.25rem);
|
||||||
|
}
|
||||||
|
@utility spacing-y-sm {
|
||||||
|
padding-top: var(--tw-spacing-2, 0.5rem);
|
||||||
|
padding-bottom: var(--tw-spacing-2, 0.5rem);
|
||||||
|
}
|
||||||
|
@utility spacing-y-md {
|
||||||
|
padding-top: var(--tw-spacing-4, 1rem);
|
||||||
|
padding-bottom: var(--tw-spacing-4, 1rem);
|
||||||
|
}
|
||||||
|
@utility spacing-y-lg {
|
||||||
|
padding-top: var(--tw-spacing-6, 1.5rem);
|
||||||
|
padding-bottom: var(--tw-spacing-6, 1.5rem);
|
||||||
|
}
|
||||||
|
@utility spacing-y-xl {
|
||||||
|
padding-top: var(--tw-spacing-8, 2rem);
|
||||||
|
padding-bottom: var(--tw-spacing-8, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
/* Sidebar styling */
|
||||||
|
.sidebar-primary {
|
||||||
|
background-color: var(--color-sidebar);
|
||||||
|
border-right-width: 1px;
|
||||||
|
border-color: var(--color-sidebar-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-secondary {
|
||||||
|
background-color: var(--color-background-secondary);
|
||||||
|
border-right-width: 1px;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-button {
|
||||||
|
display: flex;
|
||||||
|
width: full;
|
||||||
|
flex-grow: 1;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin: 4px 8px;
|
||||||
|
padding: 8px;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-tertiary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,121 +1,476 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { useParams } from "react-router";
|
import { useNavigate, useParams } from "react-router";
|
||||||
import { Hash, Volume2, Users, HelpCircle, Inbox, Pin } from "lucide-react";
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import { Hash, Volume2, Users, Pin } from "lucide-react";
|
||||||
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { useInstanceDetails } from "@/hooks/useServers";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { useChannels } from "@/hooks/useChannel";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Copy, Edit, Trash2, Reply, MoreHorizontal } from "lucide-react";
|
||||||
|
import { useInstanceDetails, useInstanceMembers } from "@/hooks/useServers";
|
||||||
|
import { useChannelMessages } from "@/hooks/useChannel";
|
||||||
import { useUiStore } from "@/stores/uiStore";
|
import { useUiStore } from "@/stores/uiStore";
|
||||||
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
|
import { Message, User } from "@/types/database";
|
||||||
|
import { MessageProps } from "@/components/message/Message";
|
||||||
|
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||||
|
import {
|
||||||
|
dark,
|
||||||
|
solarizedLight,
|
||||||
|
} from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||||
|
import { useTheme } from "@/components/theme-provider";
|
||||||
|
|
||||||
|
const MessageComponent: React.FC<MessageProps> = ({
|
||||||
|
message,
|
||||||
|
user,
|
||||||
|
currentUser,
|
||||||
|
replyTo,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onReply,
|
||||||
|
isGrouped,
|
||||||
|
}) => {
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
|
const formatTimestamp = (timestamp: string) => {
|
||||||
|
return formatDistanceToNow(new Date(timestamp), { addSuffix: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOwnMessage = currentUser?.id === message.userId;
|
||||||
|
const { mode } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`group relative px-4 hover:bg-concord-secondary/50 transition-colors ${
|
||||||
|
isGrouped ? "mt-0 py-0" : "mt-4"
|
||||||
|
}`}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{/* Avatar - only show if not grouped */}
|
||||||
|
<div className="w-10 flex-shrink-0">
|
||||||
|
{!isGrouped && (
|
||||||
|
<Avatar className="h-10 w-10">
|
||||||
|
<AvatarImage
|
||||||
|
src={user.picture || undefined}
|
||||||
|
alt={user.username}
|
||||||
|
/>
|
||||||
|
<AvatarFallback className="text-sm bg-primary text-primary-foreground">
|
||||||
|
{user.username.slice(0, 2).toUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Message content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
{/* Reply line and reference */}
|
||||||
|
{replyTo && (
|
||||||
|
<div className="flex items-center gap-2 mb-2 text-xs text-concord-secondary">
|
||||||
|
<div className="w-6 h-3 border-l-2 border-t-2 border-concord-secondary/50 rounded-tl-md ml-2" />
|
||||||
|
<span className="font-medium text-concord-primary">
|
||||||
|
{replyTo?.user?.nickname || replyTo?.user?.username}
|
||||||
|
</span>
|
||||||
|
<span className="truncate max-w-xs opacity-75">
|
||||||
|
{replyTo.content.replace(/```[\s\S]*?```/g, "[code]")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Header - only show if not grouped */}
|
||||||
|
{!isGrouped && (
|
||||||
|
<div className="flex items-baseline gap-2 mb-1">
|
||||||
|
<span className="font-semibold text-concord-primary">
|
||||||
|
{user.nickname || user.username}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-concord-secondary">
|
||||||
|
{formatTimestamp(message.createdAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Message content with markdown */}
|
||||||
|
<div className="text-concord-primary leading-relaxed prose prose-sm max-w-none dark:prose-invert">
|
||||||
|
<ReactMarkdown
|
||||||
|
components={{
|
||||||
|
code: ({ node, className, children, ...props }) => {
|
||||||
|
const match = /language-(\w+)/.exec(className || "");
|
||||||
|
return match ? (
|
||||||
|
<div className="flex flex-row flex-1 max-w-2/3 flex-wrap !bg-transparent">
|
||||||
|
<SyntaxHighlighter
|
||||||
|
PreTag="div"
|
||||||
|
children={String(children).replace(/\n$/, "")}
|
||||||
|
language={match[1]}
|
||||||
|
style={mode === "light" ? solarizedLight : dark}
|
||||||
|
className="!bg-concord-secondary p-2 border-2 concord-border rounded-xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<code className={className}>{children}</code>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
blockquote: ({ children }) => (
|
||||||
|
<blockquote className="border-l-4 border-primary pl-4 my-2 italic text-concord-secondary bg-concord-secondary/30 py-2 rounded-r">
|
||||||
|
{children}
|
||||||
|
</blockquote>
|
||||||
|
),
|
||||||
|
p: ({ children }) => (
|
||||||
|
<p className="my-1 text-concord-primary">{children}</p>
|
||||||
|
),
|
||||||
|
strong: ({ children }) => (
|
||||||
|
<strong className="font-semibold text-concord-primary">
|
||||||
|
{children}
|
||||||
|
</strong>
|
||||||
|
),
|
||||||
|
em: ({ children }) => (
|
||||||
|
<em className="italic text-concord-primary">{children}</em>
|
||||||
|
),
|
||||||
|
ul: ({ children }) => (
|
||||||
|
<ul className="list-disc list-inside my-2 text-concord-primary">
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
),
|
||||||
|
ol: ({ children }) => (
|
||||||
|
<ol className="list-decimal list-inside my-2 text-concord-primary">
|
||||||
|
{children}
|
||||||
|
</ol>
|
||||||
|
),
|
||||||
|
h1: ({ children }) => (
|
||||||
|
<h1 className="text-xl font-bold my-2 text-concord-primary">
|
||||||
|
{children}
|
||||||
|
</h1>
|
||||||
|
),
|
||||||
|
h2: ({ children }) => (
|
||||||
|
<h2 className="text-lg font-bold my-2 text-concord-primary">
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
),
|
||||||
|
h3: ({ children }) => (
|
||||||
|
<h3 className="text-base font-bold my-2 text-concord-primary">
|
||||||
|
{children}
|
||||||
|
</h3>
|
||||||
|
),
|
||||||
|
a: ({ children, href }) => (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message.content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Message actions */}
|
||||||
|
{isHovered && (
|
||||||
|
<div className="absolute top-0 right-4 bg-concord-secondary border border-border rounded-md shadow-md flex">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0 interactive-hover"
|
||||||
|
onClick={() => onReply?.(message.id)}
|
||||||
|
>
|
||||||
|
<Reply className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0 interactive-hover"
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => navigator.clipboard.writeText(message.content)}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4 mr-2" />
|
||||||
|
Copy Text
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{isOwnMessage && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem onClick={() => onEdit?.(message.id)}>
|
||||||
|
<Edit className="h-4 w-4 mr-2" />
|
||||||
|
Edit Message
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onDelete?.(message.id)}
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Delete Message
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Message Input Component
|
||||||
|
interface MessageInputProps {
|
||||||
|
channelName?: string;
|
||||||
|
onSendMessage: (content: string) => void;
|
||||||
|
replyingTo?: Message | null;
|
||||||
|
onCancelReply?: () => void;
|
||||||
|
replyingToUser: User | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageInput: React.FC<MessageInputProps> = ({
|
||||||
|
channelName,
|
||||||
|
onSendMessage,
|
||||||
|
replyingTo,
|
||||||
|
onCancelReply,
|
||||||
|
replyingToUser,
|
||||||
|
}) => {
|
||||||
|
const [content, setContent] = useState("");
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
|
// Auto-resize textarea
|
||||||
|
useEffect(() => {
|
||||||
|
if (textareaRef.current) {
|
||||||
|
textareaRef.current.style.height = "auto";
|
||||||
|
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
|
||||||
|
}
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (content.trim()) {
|
||||||
|
onSendMessage(content.trim());
|
||||||
|
setContent("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
formRef.current?.requestSubmit(); // <-- Programmatically submit form
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-4 pb-4">
|
||||||
|
{replyingTo && replyingToUser && (
|
||||||
|
<div className="mb-2 p-3 bg-concord-secondary rounded-lg border border-b-0 border-border">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-6 h-4 border-l-2 border-t-2 border-concord-secondary/50 rounded-tl-md ml-2" />
|
||||||
|
<span className="font-medium text-concord-primary">
|
||||||
|
{replyingToUser.nickname || replyingToUser.username}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-auto p-1 text-concord-secondary hover:text-concord-primary"
|
||||||
|
onClick={onCancelReply}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-concord-primary truncate pl-2">
|
||||||
|
{replyingTo.content.replace(/```[\s\S]*?```/g, "[code]")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form ref={formRef} onSubmit={handleSubmit}>
|
||||||
|
<div className="relative">
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={`Message #${channelName || "channel"}`}
|
||||||
|
className="w-full bg-concord-tertiary border border-border rounded-lg px-4 py-3 text-concord-primary placeholder-concord-muted resize-none focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||||
|
style={{
|
||||||
|
minHeight: "44px",
|
||||||
|
maxHeight: "200px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="absolute right-3 bottom-3 text-xs text-concord-secondary">
|
||||||
|
Press Enter to send • Shift+Enter for new line
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ChatPage: React.FC = () => {
|
const ChatPage: React.FC = () => {
|
||||||
const { instanceId, channelId } = useParams();
|
const { instanceId, channelId } = useParams();
|
||||||
const { instance } = useInstanceDetails(instanceId);
|
const { data: instance } = useInstanceDetails(instanceId);
|
||||||
const { categories } = useChannels(instanceId);
|
const categories = instance?.categories;
|
||||||
|
const { data: channelMessages } = useChannelMessages(channelId);
|
||||||
const { toggleMemberList, showMemberList } = useUiStore();
|
const { toggleMemberList, showMemberList } = useUiStore();
|
||||||
|
const { user: currentUser } = useAuthStore();
|
||||||
|
const { data: users } = useInstanceMembers(instanceId);
|
||||||
|
|
||||||
|
// State for messages and interactions
|
||||||
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
|
const [replyingTo, setReplyingTo] = useState<Message | null>(null);
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Use sample current user if none exists
|
||||||
|
const displayCurrentUser =
|
||||||
|
currentUser || users?.find((u) => u.id === "current");
|
||||||
|
|
||||||
// Find current channel
|
// Find current channel
|
||||||
const currentChannel = categories
|
const currentChannel = categories
|
||||||
?.flatMap((cat) => cat.channels)
|
?.flatMap((cat) => cat.channels)
|
||||||
?.find((ch) => ch.id === channelId);
|
?.find((ch) => ch.id === channelId);
|
||||||
|
|
||||||
// Handle Direct Messages view
|
// Update messages when channel messages load
|
||||||
// if (!instanceId || instanceId === '@me') {
|
useEffect(() => {
|
||||||
// return (
|
if (channelMessages) {
|
||||||
// <div className="flex flex-col h-full">
|
setMessages(channelMessages.map((msg) => ({ ...msg, replyToId: null })));
|
||||||
// {/* DM Header */}
|
|
||||||
// <div className="flex items-center justify-between px-4 py-3 border-b border-gray-700 bg-gray-800">
|
|
||||||
// <div className="flex items-center space-x-2">
|
|
||||||
// <Inbox size={20} className="text-gray-400" />
|
|
||||||
// <span className="font-semibold text-white">Direct Messages</span>
|
|
||||||
// </div>
|
|
||||||
// <div className="flex items-center space-x-2">
|
|
||||||
// <Button variant="ghost" size="icon" className="h-8 w-8">
|
|
||||||
// <HelpCircle size={16} />
|
|
||||||
// </Button>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
|
|
||||||
// {/* DM Content */}
|
|
||||||
// <div className="flex-1 flex items-center justify-center">
|
|
||||||
// <div className="text-center text-gray-400 max-w-md">
|
|
||||||
// <div className="w-16 h-16 mx-auto mb-4 bg-gray-700 rounded-full flex items-center justify-center">
|
|
||||||
// <Inbox size={24} />
|
|
||||||
// </div>
|
|
||||||
// <h2 className="text-xl font-semibold mb-2 text-white">No Direct Messages</h2>
|
|
||||||
// <p className="text-sm">
|
|
||||||
// When someone sends you a direct message, it will show up here.
|
|
||||||
// </p>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (!currentChannel && channelId) {
|
|
||||||
return (
|
|
||||||
<div className="flex-1 flex items-center justify-center">
|
|
||||||
<div className="text-center text-gray-400">
|
|
||||||
<h2 className="text-xl font-semibold mb-2">Channel not found</h2>
|
|
||||||
<p>
|
|
||||||
The channel you're looking for doesn't exist or you don't have
|
|
||||||
access to it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}, [channelMessages]);
|
||||||
|
|
||||||
// Default channel view (when just /channels/instanceId)
|
// Scroll to bottom when messages change
|
||||||
if (!channelId && instance) {
|
useEffect(() => {
|
||||||
const firstChannel = categories?.[0]?.channels?.[0];
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
// Require both instanceId and channelId for chat
|
||||||
|
if (!instanceId) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex items-center justify-center">
|
<div className="flex-1 flex items-center justify-center bg-concord-primary">
|
||||||
<div className="text-center text-gray-400 max-w-md">
|
<div className="text-center text-concord-secondary">
|
||||||
<div className="w-16 h-16 mx-auto mb-4 bg-gray-700 rounded-full flex items-center justify-center">
|
<h2 className="text-xl font-semibold mb-2 text-concord-primary">
|
||||||
<Hash size={24} />
|
No Channel Selected
|
||||||
</div>
|
|
||||||
<h2 className="text-xl font-semibold mb-2 text-white">
|
|
||||||
Welcome to {instance.name}!
|
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm mb-4">
|
<p>Select a channel from the sidebar to start chatting.</p>
|
||||||
{firstChannel
|
</div>
|
||||||
? `Select a channel from the sidebar to start chatting, or head to #${firstChannel.name} to get started.`
|
</div>
|
||||||
: "This server doesn't have any channels yet. Create one to get started!"}
|
);
|
||||||
</p>
|
} else if (!channelId || !currentChannel) {
|
||||||
|
const existingChannelId = categories
|
||||||
|
?.flatMap((cat) => cat.channels)
|
||||||
|
?.find((channel) => channel.position === 0)?.id;
|
||||||
|
|
||||||
|
if (existingChannelId)
|
||||||
|
navigate(`/channels/${instanceId}/${existingChannelId}`);
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex items-center justify-center bg-concord-primary">
|
||||||
|
<div className="text-center text-concord-secondary">
|
||||||
|
<h2 className="text-xl font-semibold mb-2 text-concord-primary">
|
||||||
|
No channels exist yet!
|
||||||
|
</h2>
|
||||||
|
<p>Ask an admin to create a channel</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChannelIcon = currentChannel?.type === "voice" ? Volume2 : Hash;
|
const ChannelIcon = currentChannel?.type === "voice" ? Volume2 : Hash;
|
||||||
|
|
||||||
|
// Message handlers
|
||||||
|
const handleSendMessage = (content: string) => {
|
||||||
|
if (!displayCurrentUser) return;
|
||||||
|
|
||||||
|
const newMessage: Message = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
content,
|
||||||
|
channelId: channelId || "",
|
||||||
|
userId: displayCurrentUser.id,
|
||||||
|
edited: false,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
replyTo: replyingTo || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages((prev) => [...prev, newMessage]);
|
||||||
|
setReplyingTo(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReply = (messageId: string) => {
|
||||||
|
const message = messages.find((m) => m.id === messageId);
|
||||||
|
if (message) {
|
||||||
|
setReplyingTo(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (messageId: string) => {
|
||||||
|
// TODO: Implement edit functionality
|
||||||
|
console.log("Edit message:", messageId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (messageId: string) => {
|
||||||
|
setMessages((prev) => prev.filter((m) => m.id !== messageId));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Group messages by user and time
|
||||||
|
const groupedMessages = messages.reduce((acc, message, index) => {
|
||||||
|
const prevMessage = index > 0 ? messages[index - 1] : null;
|
||||||
|
const isGrouped =
|
||||||
|
prevMessage &&
|
||||||
|
prevMessage.userId === message.userId &&
|
||||||
|
!message.replyTo && // Don't group replies
|
||||||
|
!prevMessage.replyTo && // Don't group if previous was a reply
|
||||||
|
new Date(message.createdAt).getTime() -
|
||||||
|
new Date(prevMessage.createdAt).getTime() <
|
||||||
|
5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
|
acc.push({ ...message, isGrouped });
|
||||||
|
return acc;
|
||||||
|
}, [] as Message[]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col flex-shrink h-full bg-concord-primary">
|
||||||
{/* Channel Header */}
|
{/* Channel Header */}
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700 bg-gray-800">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-concord bg-concord-secondary">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<ChannelIcon size={20} className="text-gray-400" />
|
<ChannelIcon size={20} className="text-concord-secondary" />
|
||||||
<span className="font-semibold text-white">
|
<span className="font-semibold text-concord-primary">
|
||||||
{currentChannel?.name}
|
{currentChannel?.name}
|
||||||
</span>
|
</span>
|
||||||
{currentChannel?.topic && (
|
{currentChannel?.description && (
|
||||||
<>
|
<>
|
||||||
<div className="w-px h-4 bg-gray-600" />
|
<div className="w-px h-4 bg-border" />
|
||||||
<span className="text-sm text-gray-400 truncate max-w-xs">
|
<span className="text-sm text-concord-secondary truncate max-w-xs">
|
||||||
{currentChannel.topic}
|
{currentChannel.description}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 interactive-hover"
|
||||||
|
>
|
||||||
<Pin size={16} />
|
<Pin size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={`h-8 w-8 ${showMemberList ? "text-white bg-gray-700" : "text-gray-400"}`}
|
className={`h-8 w-8 ${showMemberList ? "text-interactive-active bg-concord-tertiary" : "interactive-hover"}`}
|
||||||
onClick={toggleMemberList}
|
onClick={toggleMemberList}
|
||||||
>
|
>
|
||||||
<Users size={16} />
|
<Users size={16} />
|
||||||
@@ -123,66 +478,88 @@ const ChatPage: React.FC = () => {
|
|||||||
<div className="w-40">
|
<div className="w-40">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
className="h-8 bg-gray-700 border-none text-sm"
|
className="h-8 bg-concord-tertiary border-none text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
||||||
<Inbox size={16} />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
||||||
<HelpCircle size={16} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chat Content */}
|
{/* Chat Content */}
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
{/* Messages Area */}
|
{/* Messages Area */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<ScrollArea className="flex-1 min-h-0">
|
||||||
<div className="p-4">
|
|
||||||
{/* Welcome Message */}
|
{/* Welcome Message */}
|
||||||
<div className="flex flex-col space-y-2 mb-4">
|
<div className="px-4 py-6 border-b border-concord/50 flex-shrink-0">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-3 mb-3">
|
||||||
<div className="w-16 h-16 bg-blue-600 rounded-full flex items-center justify-center">
|
<div className="w-16 h-16 bg-primary rounded-full flex items-center justify-center">
|
||||||
<ChannelIcon size={24} className="text-white" />
|
<ChannelIcon size={24} className="text-primary-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-2xl font-bold text-white">
|
<h3 className="text-2xl font-bold text-concord-primary">
|
||||||
Welcome to #{currentChannel?.name}!
|
Welcome to #{currentChannel?.name}!
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-400">
|
|
||||||
This is the start of the #{currentChannel?.name} channel.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{currentChannel?.topic && (
|
{currentChannel?.description && (
|
||||||
<div className="text-gray-400 bg-gray-800 p-3 rounded border-l-4 border-gray-600">
|
<div className="text-concord-secondary bg-concord-secondary/50 p-3 rounded border-l-4 border-primary">
|
||||||
<strong>Topic:</strong> {currentChannel.topic}
|
{currentChannel.description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Placeholder messages */}
|
<div className="pb-4">
|
||||||
<div className="space-y-4 text-gray-400 text-center">
|
{/* Messages */}
|
||||||
|
{groupedMessages.length > 0 ? (
|
||||||
|
<div>
|
||||||
|
{groupedMessages.map((message) => {
|
||||||
|
const user = users?.find((u) => u.id === message.userId);
|
||||||
|
const replyToMessage = messages.find(
|
||||||
|
(m) => m.id === message.replyTo?.id,
|
||||||
|
);
|
||||||
|
const replyToUser = replyToMessage?.user;
|
||||||
|
|
||||||
|
if (!user) return null;
|
||||||
|
return (
|
||||||
|
<MessageComponent
|
||||||
|
key={message.id}
|
||||||
|
message={message}
|
||||||
|
user={user}
|
||||||
|
currentUser={displayCurrentUser}
|
||||||
|
replyTo={replyToMessage}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onReply={handleReply}
|
||||||
|
replyToUser={replyToUser}
|
||||||
|
isGrouped={message.isGrouped}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-center text-concord-secondary">
|
||||||
<p>No messages yet. Start the conversation!</p>
|
<p>No messages yet. Start the conversation!</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
{/* Message Input */}
|
{/* Message Input */}
|
||||||
<div className="p-4 bg-gray-800">
|
<div className="flex-shrink-0">
|
||||||
<div className="relative">
|
<MessageInput
|
||||||
<Input
|
channelName={currentChannel?.name}
|
||||||
placeholder={`Message #${currentChannel?.name || "channel"}`}
|
onSendMessage={handleSendMessage}
|
||||||
className="w-full bg-gray-700 border-none text-white placeholder-gray-400 pr-12"
|
replyingTo={replyingTo}
|
||||||
|
onCancelReply={() => setReplyingTo(null)}
|
||||||
|
replyingToUser={
|
||||||
|
replyingTo
|
||||||
|
? users?.find((u) => u.id === replyingTo.userId) || null
|
||||||
|
: null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
{/* Emoji picker, file upload, etc. would go here */}
|
|
||||||
<div className="text-gray-400 text-sm">Press Enter to send</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,13 +36,14 @@ const LoginPage: React.FC = () => {
|
|||||||
username,
|
username,
|
||||||
nickname: username,
|
nickname: username,
|
||||||
bio: "Test user",
|
bio: "Test user",
|
||||||
picture: null,
|
picture: "",
|
||||||
banner: null,
|
banner: "",
|
||||||
hashPassword: "",
|
hashPassword: "",
|
||||||
algorithms: "{}",
|
admin: false,
|
||||||
status: "online",
|
status: "online",
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
|
roles: [],
|
||||||
},
|
},
|
||||||
"fake-token",
|
"fake-token",
|
||||||
"fake-refresh-token",
|
"fake-refresh-token",
|
||||||
@@ -56,20 +57,20 @@ const LoginPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
<div className="min-h-screen bg-concord-primary flex items-center justify-center p-4">
|
||||||
<Card className="w-full max-w-md bg-gray-800 border-gray-700">
|
<Card className="w-full max-w-md bg-concord-secondary border-concord">
|
||||||
<CardHeader className="text-center">
|
<CardHeader className="text-center">
|
||||||
<CardTitle className="text-2xl font-bold text-white">
|
<CardTitle className="text-2xl font-bold text-concord-primary">
|
||||||
Welcome back!
|
Welcome back!
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-gray-400">
|
<CardDescription className="text-concord-secondary">
|
||||||
We're so excited to see you again!
|
We're so excited to see you again!
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={handleLogin} className="space-y-4">
|
<form onSubmit={handleLogin} className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="username" className="text-gray-300">
|
<Label htmlFor="username" className="text-concord-primary">
|
||||||
Username
|
Username
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -77,13 +78,13 @@ const LoginPage: React.FC = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
className="bg-gray-700 border-gray-600 text-white"
|
className="bg-concord-tertiary border-concord text-concord-primary"
|
||||||
placeholder="Enter your username"
|
placeholder="Enter your username"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="password" className="text-gray-300">
|
<Label htmlFor="password" className="text-concord-primary">
|
||||||
Password
|
Password
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -91,7 +92,7 @@ const LoginPage: React.FC = () => {
|
|||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className="bg-gray-700 border-gray-600 text-white"
|
className="bg-concord-tertiary border-concord text-concord-primary"
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
const NotFoundPage: React.FC = () => {
|
const NotFoundPage: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
<div className="min-h-screen bg-concord-primary flex items-center justify-center">
|
||||||
<div className="text-center text-gray-400">
|
<div className="text-center text-concord-secondary">
|
||||||
<h1 className="text-4xl font-bold mb-4">404</h1>
|
<h1 className="text-4xl font-bold mb-4 text-concord-primary">404</h1>
|
||||||
<p className="text-xl">Page not found</p>
|
<p className="text-xl mb-6">Page not found</p>
|
||||||
|
<Button asChild>
|
||||||
|
<a href="/channels/@me">Go Home</a>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,785 @@
|
|||||||
const SettingsPage: React.FC = () => {
|
import React, { useState } from "react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import {
|
||||||
|
Palette,
|
||||||
|
User,
|
||||||
|
Shield,
|
||||||
|
Mic,
|
||||||
|
Settings,
|
||||||
|
ChevronRight,
|
||||||
|
Eye,
|
||||||
|
Moon,
|
||||||
|
Sun,
|
||||||
|
Monitor,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { ThemeSelector } from "@/components/theme-selector";
|
||||||
|
import { useTheme } from "@/components/theme-provider";
|
||||||
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
|
||||||
|
type SettingsSection = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SETTINGS_SECTIONS: SettingsSection[] = [
|
||||||
|
{
|
||||||
|
id: "account",
|
||||||
|
title: "My Account",
|
||||||
|
icon: User,
|
||||||
|
description: "Profile, privacy, and account settings",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "appearance",
|
||||||
|
title: "Appearance",
|
||||||
|
icon: Palette,
|
||||||
|
description: "Themes, display, and accessibility",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "voice",
|
||||||
|
title: "Voice & Video",
|
||||||
|
icon: Mic,
|
||||||
|
description: "Audio and video settings",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const AppearanceSettings: React.FC = () => {
|
||||||
|
const {
|
||||||
|
currentLightTheme,
|
||||||
|
currentDarkTheme,
|
||||||
|
getThemesForMode,
|
||||||
|
mode,
|
||||||
|
setMode,
|
||||||
|
setTheme,
|
||||||
|
} = useTheme();
|
||||||
|
const [compactMode, setCompactMode] = useState(false);
|
||||||
|
const [showTimestamps, setShowTimestamps] = useState(true);
|
||||||
|
const [animationsEnabled, setAnimationsEnabled] = useState(true);
|
||||||
|
const [reduceMotion, setReduceMotion] = useState(false);
|
||||||
|
const [highContrast, setHighContrast] = useState(false);
|
||||||
|
|
||||||
|
const lightThemes = getThemesForMode("light");
|
||||||
|
const darkThemes = getThemesForMode("dark");
|
||||||
|
|
||||||
|
const getModeIcon = (themeMode: "light" | "dark" | "system") => {
|
||||||
|
switch (themeMode) {
|
||||||
|
case "light":
|
||||||
|
return <Sun className="h-4 w-4" />;
|
||||||
|
case "dark":
|
||||||
|
return <Moon className="h-4 w-4" />;
|
||||||
|
default:
|
||||||
|
return <Monitor className="h-4 w-4" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex items-center justify-center">
|
<div className="space-y-6 flex flex-col justify-center self-center items-center w-full">
|
||||||
<div className="text-center text-gray-400">
|
{/* Theme Mode Selection */}
|
||||||
<h2 className="text-xl font-semibold mb-2">Settings</h2>
|
<Card className="w-full p-6">
|
||||||
<p>User settings will go here</p>
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
{getModeIcon(mode)}
|
||||||
|
Theme Mode
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Choose between light, dark, or system preference.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setMode("light")}
|
||||||
|
className={`p-3 rounded-lg border-2 transition-all text-left ${
|
||||||
|
mode === "light"
|
||||||
|
? "border-primary bg-primary/10"
|
||||||
|
: "border-border hover:border-primary/50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center mb-2">
|
||||||
|
<Sun className="h-6 w-6 text-yellow-500" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="font-medium text-sm">Light</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Always use light theme
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setMode("dark")}
|
||||||
|
className={`p-3 rounded-lg border-2 transition-all text-left ${
|
||||||
|
mode === "dark"
|
||||||
|
? "border-primary bg-primary/10"
|
||||||
|
: "border-border hover:border-primary/50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center mb-2">
|
||||||
|
<Moon className="h-6 w-6 text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="font-medium text-sm">Dark</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Always use dark theme
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setMode("system")}
|
||||||
|
className={`p-3 rounded-lg border-2 transition-all text-left ${
|
||||||
|
mode === "system"
|
||||||
|
? "border-primary bg-primary/10"
|
||||||
|
: "border-border hover:border-primary/50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center mb-2">
|
||||||
|
<Monitor className="h-6 w-6 text-gray-500" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="font-medium text-sm">System</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Match system preference
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Theme Selection */}
|
||||||
|
<Card className="w-full p-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Palette className="h-5 w-5" />
|
||||||
|
Theme Selection
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Choose themes for light and dark mode. You can also create custom
|
||||||
|
themes.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-medium">
|
||||||
|
Quick Theme Selector
|
||||||
|
</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Access the theme selector with custom theme creation
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ThemeSelector />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Current Theme Display */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="rounded-lg border p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Sun className="h-4 w-4 text-yellow-500" />
|
||||||
|
<Label className="text-sm font-medium">Light Theme</Label>
|
||||||
|
</div>
|
||||||
|
<div className="font-medium">{currentLightTheme.name}</div>
|
||||||
|
{currentLightTheme.description && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{currentLightTheme.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-1 mt-2">
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{ backgroundColor: currentLightTheme.colors.primary }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: currentLightTheme.colors.secondary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{ backgroundColor: currentLightTheme.colors.accent }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Moon className="h-4 w-4 text-blue-400" />
|
||||||
|
<Label className="text-sm font-medium">Dark Theme</Label>
|
||||||
|
</div>
|
||||||
|
<div className="font-medium">{currentDarkTheme.name}</div>
|
||||||
|
{currentDarkTheme.description && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{currentDarkTheme.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-1 mt-2">
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{ backgroundColor: currentDarkTheme.colors.primary }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{ backgroundColor: currentDarkTheme.colors.secondary }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{ backgroundColor: currentDarkTheme.colors.accent }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Theme Grid */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<Label className="text-sm font-medium">Available Themes</Label>
|
||||||
|
<div className="mt-3 grid grid-cols-2 gap-3">
|
||||||
|
{/* Light Themes */}
|
||||||
|
{lightThemes.map((theme) => (
|
||||||
|
<button
|
||||||
|
key={theme.id}
|
||||||
|
onClick={() => setTheme(theme.id)}
|
||||||
|
className={`p-3 rounded-lg border-2 transition-all text-left ${
|
||||||
|
currentLightTheme.id === theme.id
|
||||||
|
? "border-primary bg-primary/10"
|
||||||
|
: "border-border hover:border-primary/50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="font-medium text-sm">{theme.name}</span>
|
||||||
|
<Sun className="h-4 w-4 text-yellow-500" />
|
||||||
|
</div>
|
||||||
|
{theme.description && (
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">
|
||||||
|
{theme.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{ backgroundColor: theme.colors.primary }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{ backgroundColor: theme.colors.secondary }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{ backgroundColor: theme.colors.accent }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Dark Themes */}
|
||||||
|
{darkThemes.map((theme) => (
|
||||||
|
<button
|
||||||
|
key={theme.id}
|
||||||
|
onClick={() => setTheme(theme.id)}
|
||||||
|
className={`p-3 rounded-lg border-2 transition-all text-left ${
|
||||||
|
currentDarkTheme.id === theme.id
|
||||||
|
? "border-primary bg-primary/10"
|
||||||
|
: "border-border hover:border-primary/50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="font-medium text-sm">{theme.name}</span>
|
||||||
|
<Moon className="h-4 w-4 text-blue-400" />
|
||||||
|
</div>
|
||||||
|
{theme.description && (
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">
|
||||||
|
{theme.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{ backgroundColor: theme.colors.primary }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{ backgroundColor: theme.colors.secondary }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{ backgroundColor: theme.colors.accent }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Theme Stats */}
|
||||||
|
<div className="mt-4 grid grid-cols-2 gap-4">
|
||||||
|
<div className="text-center p-3 bg-muted/50 rounded-lg">
|
||||||
|
<div className="text-lg font-semibold">{lightThemes.length}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">Light Themes</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-3 bg-muted/50 rounded-lg">
|
||||||
|
<div className="text-lg font-semibold">{darkThemes.length}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">Dark Themes</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Display Settings */}
|
||||||
|
<Card className="w-full p-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Eye className="h-5 w-5" />
|
||||||
|
Display Settings
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Customize how content is displayed in the app.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-medium">Compact Mode</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Use less space between messages and interface elements
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch checked={compactMode} onCheckedChange={setCompactMode} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-medium">
|
||||||
|
Show Message Timestamps
|
||||||
|
</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Display timestamps next to messages
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={showTimestamps}
|
||||||
|
onCheckedChange={setShowTimestamps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-medium">Enable Animations</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Play animations throughout the interface
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={animationsEnabled}
|
||||||
|
onCheckedChange={setAnimationsEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Accessibility */}
|
||||||
|
<Card className="w-full p-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Accessibility</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Settings to improve accessibility and usability.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-medium">Reduce Motion</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Reduce motion and animations for users with vestibular disorders
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch checked={reduceMotion} onCheckedChange={setReduceMotion} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-medium">High Contrast</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Increase contrast for better visibility
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch checked={highContrast} onCheckedChange={setHighContrast} />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccountSettings: React.FC = () => {
|
||||||
|
const { user } = useAuthStore();
|
||||||
|
const [username, setUsername] = useState(user?.username || "");
|
||||||
|
const [nickname, setNickname] = useState(user?.nickname || "");
|
||||||
|
const [bio, setBio] = useState(user?.bio || "");
|
||||||
|
const [email, setEmail] = useState("user@example.com");
|
||||||
|
const [isChanged, setIsChanged] = useState(false);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
console.log("Saving profile changes:", { username, nickname, bio, email });
|
||||||
|
setIsChanged(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = () => {
|
||||||
|
setIsChanged(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 flex flex-col justify-center self-center items-stretch w-2/3">
|
||||||
|
<Card className="w-full p-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<User className="h-5 w-5" />
|
||||||
|
Profile
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Update your profile information and display settings.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="email">Email</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => {
|
||||||
|
setEmail(e.target.value);
|
||||||
|
handleChange();
|
||||||
|
}}
|
||||||
|
className="max-w-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="username">Username</Label>
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => {
|
||||||
|
setUsername(e.target.value);
|
||||||
|
handleChange();
|
||||||
|
}}
|
||||||
|
className="max-w-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="nickname">Display Name</Label>
|
||||||
|
<Input
|
||||||
|
id="nickname"
|
||||||
|
value={nickname}
|
||||||
|
onChange={(e) => {
|
||||||
|
setNickname(e.target.value);
|
||||||
|
handleChange();
|
||||||
|
}}
|
||||||
|
className="max-w-sm"
|
||||||
|
placeholder="How others see your name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="bio">Bio</Label>
|
||||||
|
<Input
|
||||||
|
id="bio"
|
||||||
|
value={bio}
|
||||||
|
onChange={(e) => {
|
||||||
|
setBio(e.target.value);
|
||||||
|
handleChange();
|
||||||
|
}}
|
||||||
|
className="max-w-sm"
|
||||||
|
placeholder="Tell others about yourself"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex gap-2">
|
||||||
|
<Button onClick={handleSave} disabled={!isChanged}>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
{isChanged && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setUsername(user?.username || "");
|
||||||
|
setNickname(user?.nickname || "");
|
||||||
|
setBio(user?.bio || "");
|
||||||
|
setEmail("user@example.com");
|
||||||
|
setIsChanged(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="w-full p-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Shield className="h-5 w-5" />
|
||||||
|
Privacy
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Control who can contact you and see your information.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-medium">
|
||||||
|
Allow Direct Messages
|
||||||
|
</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Let other users send you direct messages
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-medium">
|
||||||
|
Show Online Status
|
||||||
|
</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Display when you're online to other users
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const VoiceSettings: React.FC = () => {
|
||||||
|
const [inputVolume, setInputVolume] = useState(75);
|
||||||
|
const [outputVolume, setOutputVolume] = useState(100);
|
||||||
|
const [pushToTalk, setPushToTalk] = useState(false);
|
||||||
|
const [noiseSuppression, setNoiseSuppression] = useState(true);
|
||||||
|
const [echoCancellation, setEchoCancellation] = useState(true);
|
||||||
|
const [autoGainControl, setAutoGainControl] = useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 flex flex-col justify-center self-center items-stretch w-full">
|
||||||
|
<Card className="w-full p-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Mic className="h-5 w-5" />
|
||||||
|
Voice Settings
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Configure your microphone and audio settings.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Input Volume: {inputVolume}%</Label>
|
||||||
|
<Slider
|
||||||
|
defaultValue={[inputVolume]}
|
||||||
|
value={[inputVolume]}
|
||||||
|
max={100}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
setInputVolume(v[0]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Output Volume: {outputVolume}%</Label>
|
||||||
|
<Slider
|
||||||
|
defaultValue={[outputVolume]}
|
||||||
|
value={[outputVolume]}
|
||||||
|
max={100}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
setOutputVolume(v[0]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-medium">Push to Talk</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Use a key to transmit voice instead of voice activity
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch checked={pushToTalk} onCheckedChange={setPushToTalk} />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="w-full p-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Audio Processing</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Advanced audio processing features to improve call quality.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-medium">Noise Suppression</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Reduce background noise during calls
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={noiseSuppression}
|
||||||
|
onCheckedChange={setNoiseSuppression}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-medium">Echo Cancellation</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Prevent audio feedback and echo
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={echoCancellation}
|
||||||
|
onCheckedChange={setEchoCancellation}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Label className="text-base font-medium">Auto Gain Control</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Automatically adjust microphone sensitivity
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={autoGainControl}
|
||||||
|
onCheckedChange={setAutoGainControl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingsPage: React.FC = () => {
|
||||||
|
const { section } = useParams();
|
||||||
|
const currentSection = section || "appearance";
|
||||||
|
|
||||||
|
const renderSettingsContent = () => {
|
||||||
|
switch (currentSection) {
|
||||||
|
case "account":
|
||||||
|
return <AccountSettings />;
|
||||||
|
case "appearance":
|
||||||
|
return <AppearanceSettings />;
|
||||||
|
case "voice":
|
||||||
|
return <VoiceSettings />;
|
||||||
|
default:
|
||||||
|
return <AppearanceSettings />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<div className="w-1/4 flex flex-col bg-concord-secondary border-r border-concord">
|
||||||
|
<div className="px-4 py-4 border-b border-concord">
|
||||||
|
<h1 className="text-2xl font-semibold text-concord-primary">
|
||||||
|
Settings
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea className="flex-1">
|
||||||
|
<div className="p-2">
|
||||||
|
{SETTINGS_SECTIONS.map((settingsSection) => {
|
||||||
|
const Icon = settingsSection.icon;
|
||||||
|
const isActive = currentSection === settingsSection.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={settingsSection.id}
|
||||||
|
variant={isActive ? "secondary" : "ghost"}
|
||||||
|
className="w-full justify-start mb-1 h-auto p-2"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<a href={`/settings/${settingsSection.id}`}>
|
||||||
|
<Icon className="mr-2 h-4 w-4" />
|
||||||
|
<div className="flex-1 text-left">
|
||||||
|
<div className="font-medium">{settingsSection.title}</div>
|
||||||
|
{settingsSection.description && (
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{settingsSection.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex-1 flex flex-col min-h-0">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-6 py-4 border-b border-concord">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{(() => {
|
||||||
|
const section = SETTINGS_SECTIONS.find(
|
||||||
|
(s) => s.id === currentSection,
|
||||||
|
);
|
||||||
|
const Icon = section?.icon || Settings;
|
||||||
|
return <Icon className="h-5 w-5" />;
|
||||||
|
})()}
|
||||||
|
<h1 className="text-2xl font-bold text-concord-primary">
|
||||||
|
{SETTINGS_SECTIONS.find((s) => s.id === currentSection)?.title ||
|
||||||
|
"Settings"}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<ScrollArea className="min-h-0 w-full">
|
||||||
|
<div className="p-6 flex w-full">{renderSettingsContent()}</div>
|
||||||
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface AuthState {
|
|||||||
|
|
||||||
export const useAuthStore = create<AuthState>()(
|
export const useAuthStore = create<AuthState>()(
|
||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set) => ({
|
||||||
user: null,
|
user: null,
|
||||||
token: null,
|
token: null,
|
||||||
refreshToken: null,
|
refreshToken: null,
|
||||||
|
|||||||
@@ -6,13 +6,6 @@ interface UiState {
|
|||||||
showMemberList: boolean;
|
showMemberList: boolean;
|
||||||
sidebarCollapsed: boolean;
|
sidebarCollapsed: boolean;
|
||||||
|
|
||||||
// Responsive
|
|
||||||
isMobile: boolean;
|
|
||||||
screenWidth: number;
|
|
||||||
|
|
||||||
// Theme
|
|
||||||
theme: "dark" | "light";
|
|
||||||
|
|
||||||
// Modal states
|
// Modal states
|
||||||
showUserSettings: boolean;
|
showUserSettings: boolean;
|
||||||
showServerSettings: boolean;
|
showServerSettings: boolean;
|
||||||
@@ -20,20 +13,16 @@ interface UiState {
|
|||||||
showCreateServer: boolean;
|
showCreateServer: boolean;
|
||||||
showInviteModal: boolean;
|
showInviteModal: boolean;
|
||||||
|
|
||||||
// Chat states
|
|
||||||
// isTyping: boolean;
|
|
||||||
// typingUsers: string[];
|
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
activeChannelId: string | null;
|
activeChannelId: string | null;
|
||||||
activeInstanceId: string | null;
|
activeInstanceId: string | null;
|
||||||
|
|
||||||
|
// Computed: Should show channel sidebar
|
||||||
|
shouldShowChannelSidebar: boolean;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
toggleMemberList: () => void;
|
toggleMemberList: () => void;
|
||||||
toggleSidebar: () => void;
|
toggleSidebar: () => void;
|
||||||
setTheme: (theme: "dark" | "light") => void;
|
|
||||||
setScreenWidth: (width: number) => void;
|
|
||||||
updateIsMobile: () => void;
|
|
||||||
|
|
||||||
// Modal actions
|
// Modal actions
|
||||||
openUserSettings: () => void;
|
openUserSettings: () => void;
|
||||||
@@ -47,53 +36,50 @@ interface UiState {
|
|||||||
openInviteModal: () => void;
|
openInviteModal: () => void;
|
||||||
closeInviteModal: () => void;
|
closeInviteModal: () => void;
|
||||||
|
|
||||||
// Chat actions
|
|
||||||
// setTyping: (isTyping: boolean) => void;
|
|
||||||
// addTypingUser: (userId: string) => void;
|
|
||||||
// removeTypingUser: (userId: string) => void;
|
|
||||||
// clearTypingUsers: () => void;
|
|
||||||
|
|
||||||
// Navigation actions
|
// Navigation actions
|
||||||
setActiveChannel: (channelId: string | null) => void;
|
setActiveChannel: (channelId: string | null) => void;
|
||||||
setActiveInstance: (instanceId: string | null) => void;
|
setActiveInstance: (instanceId: string | null) => void;
|
||||||
|
selectedChannelsByInstance: Record<string, string>;
|
||||||
|
setSelectedChannelForInstance: (
|
||||||
|
instanceId: string,
|
||||||
|
channelId: string,
|
||||||
|
) => void;
|
||||||
|
getSelectedChannelForInstance: (instanceId: string) => string | null;
|
||||||
|
updateSidebarVisibility: (pathname: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to determine if channel sidebar should be shown
|
||||||
|
const shouldShowChannelSidebar = (pathname: string): boolean => {
|
||||||
|
// Show channel sidebar for server pages (not settings, home, etc.)
|
||||||
|
const pathParts = pathname.split("/");
|
||||||
|
const isChannelsRoute = pathParts[1] === "channels";
|
||||||
|
const isSettingsRoute = pathname.includes("/settings");
|
||||||
|
|
||||||
|
return isChannelsRoute && !isSettingsRoute;
|
||||||
|
};
|
||||||
|
|
||||||
export const useUiStore = create<UiState>()(
|
export const useUiStore = create<UiState>()(
|
||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
// Initial state
|
// Initial state
|
||||||
showMemberList: true,
|
showMemberList: true,
|
||||||
sidebarCollapsed: false,
|
sidebarCollapsed: false,
|
||||||
isMobile: typeof window !== "undefined" ? window.innerWidth < 768 : false,
|
|
||||||
screenWidth: typeof window !== "undefined" ? window.innerWidth : 1024,
|
screenWidth: typeof window !== "undefined" ? window.innerWidth : 1024,
|
||||||
theme: "dark",
|
|
||||||
showUserSettings: false,
|
showUserSettings: false,
|
||||||
showServerSettings: false,
|
showServerSettings: false,
|
||||||
showCreateChannel: false,
|
showCreateChannel: false,
|
||||||
showCreateServer: false,
|
showCreateServer: false,
|
||||||
showInviteModal: false,
|
showInviteModal: false,
|
||||||
isTyping: false,
|
|
||||||
typingUsers: [],
|
|
||||||
activeChannelId: null,
|
activeChannelId: null,
|
||||||
activeInstanceId: null,
|
activeInstanceId: null,
|
||||||
|
shouldShowChannelSidebar: false,
|
||||||
|
selectedChannelsByInstance: {},
|
||||||
|
|
||||||
// Sidebar actions
|
// Sidebar actions
|
||||||
toggleMemberList: () =>
|
toggleMemberList: () =>
|
||||||
set((state) => ({ showMemberList: !state.showMemberList })),
|
set((state) => ({ showMemberList: !state.showMemberList })),
|
||||||
toggleSidebar: () =>
|
toggleSidebar: () =>
|
||||||
set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
|
set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
|
||||||
setTheme: (theme) => set({ theme }),
|
|
||||||
setScreenWidth: (screenWidth) => set({ screenWidth }),
|
|
||||||
updateIsMobile: () =>
|
|
||||||
set((state) => ({
|
|
||||||
isMobile: state.screenWidth < 768,
|
|
||||||
// Auto-collapse sidebar on mobile
|
|
||||||
sidebarCollapsed:
|
|
||||||
state.screenWidth < 768 ? true : state.sidebarCollapsed,
|
|
||||||
// Hide member list on small screens
|
|
||||||
showMemberList:
|
|
||||||
state.screenWidth < 1024 ? false : state.showMemberList,
|
|
||||||
})),
|
|
||||||
|
|
||||||
// Modal actions
|
// Modal actions
|
||||||
openUserSettings: () => set({ showUserSettings: true }),
|
openUserSettings: () => set({ showUserSettings: true }),
|
||||||
@@ -107,23 +93,39 @@ export const useUiStore = create<UiState>()(
|
|||||||
openInviteModal: () => set({ showInviteModal: true }),
|
openInviteModal: () => set({ showInviteModal: true }),
|
||||||
closeInviteModal: () => set({ showInviteModal: false }),
|
closeInviteModal: () => set({ showInviteModal: false }),
|
||||||
|
|
||||||
// Chat actions
|
|
||||||
// setTyping: (isTyping) => set({ isTyping }),
|
|
||||||
// addTypingUser: (userId) =>
|
|
||||||
// set((state) => ({
|
|
||||||
// typingUsers: state.typingUsers.includes(userId)
|
|
||||||
// ? state.typingUsers
|
|
||||||
// : [...state.typingUsers, userId],
|
|
||||||
// })),
|
|
||||||
// removeTypingUser: (userId) =>
|
|
||||||
// set((state) => ({
|
|
||||||
// typingUsers: state.typingUsers.filter((id) => id !== userId),
|
|
||||||
// })),
|
|
||||||
// clearTypingUsers: () => set({ typingUsers: [] }),
|
|
||||||
|
|
||||||
// Navigation actions
|
// Navigation actions
|
||||||
setActiveChannel: (channelId) => set({ activeChannelId: channelId }),
|
setActiveChannel: (channelId) => set({ activeChannelId: channelId }),
|
||||||
setActiveInstance: (instanceId) => set({ activeInstanceId: instanceId }),
|
setActiveInstance: (instanceId) => set({ activeInstanceId: instanceId }),
|
||||||
|
|
||||||
|
setSelectedChannelForInstance: (instanceId, channelId) =>
|
||||||
|
set((state) => ({
|
||||||
|
selectedChannelsByInstance: {
|
||||||
|
...state.selectedChannelsByInstance,
|
||||||
|
[instanceId]: channelId,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
getSelectedChannelForInstance: (instanceId) => {
|
||||||
|
const state = get();
|
||||||
|
return state.selectedChannelsByInstance[instanceId] || null;
|
||||||
|
},
|
||||||
|
updateSidebarVisibility: (pathname) => {
|
||||||
|
const showChannelSidebar = shouldShowChannelSidebar(pathname);
|
||||||
|
const pathParts = pathname.split("/");
|
||||||
|
const instanceId = pathParts[2] || null;
|
||||||
|
const channelId = pathParts[3] || null;
|
||||||
|
|
||||||
|
set({
|
||||||
|
shouldShowChannelSidebar: showChannelSidebar,
|
||||||
|
activeInstanceId: instanceId,
|
||||||
|
activeChannelId: channelId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store the selected channel for this instance if we have both
|
||||||
|
if (instanceId && channelId) {
|
||||||
|
get().setSelectedChannelForInstance(instanceId, channelId);
|
||||||
|
}
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "concord-ui-store",
|
name: "concord-ui-store",
|
||||||
@@ -131,7 +133,8 @@ export const useUiStore = create<UiState>()(
|
|||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
showMemberList: state.showMemberList,
|
showMemberList: state.showMemberList,
|
||||||
sidebarCollapsed: state.sidebarCollapsed,
|
sidebarCollapsed: state.sidebarCollapsed,
|
||||||
theme: state.theme,
|
|
||||||
|
selectedChannelsByInstance: state.selectedChannelsByInstance,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Instance, Category, Channel, User, Role, Message } from "./database";
|
import { Instance, Category, Channel, User, Message } from "./database";
|
||||||
|
|
||||||
// API Response wrappers
|
// API Response wrappers
|
||||||
export interface ApiResponse<T> {
|
export interface ApiResponse<T> {
|
||||||
@@ -30,18 +30,12 @@ export interface CategoryWithChannels extends Category {
|
|||||||
|
|
||||||
export interface InstanceWithDetails extends Instance {
|
export interface InstanceWithDetails extends Instance {
|
||||||
categories: CategoryWithChannels[];
|
categories: CategoryWithChannels[];
|
||||||
memberCount: number;
|
|
||||||
roles: Role[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageWithUser extends Message {
|
export interface MessageWithUser extends Message {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserWithRoles extends User {
|
|
||||||
roles: Role[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request types
|
// Request types
|
||||||
export interface CreateInstanceRequest {
|
export interface CreateInstanceRequest {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export interface Instance {
|
export interface Instance {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon?: string;
|
icon?: string | null;
|
||||||
description?: string;
|
description?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -23,7 +23,7 @@ export interface Channel {
|
|||||||
categoryId: string;
|
categoryId: string;
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
position: number;
|
position: number;
|
||||||
topic?: string;
|
description?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
@@ -33,24 +33,19 @@ export interface User {
|
|||||||
username: string;
|
username: string;
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
picture?: string;
|
picture?: string | null;
|
||||||
banner?: string;
|
banner?: string | null;
|
||||||
hashPassword: string; // Won't be sent to client
|
hashPassword: string; // Won't be sent to client
|
||||||
admin: boolean;
|
admin: boolean;
|
||||||
status: "online" | "away" | "busy" | "offline";
|
status: "online" | "away" | "busy" | "offline";
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
roles: Role[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Role {
|
export interface Role {
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
color?: string;
|
|
||||||
permissions: string; // JSON string of permissions
|
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
position: number;
|
role: "admin" | "mod" | "member";
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
@@ -61,9 +56,13 @@ export interface Message {
|
|||||||
edited: boolean;
|
edited: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
isGrouped?: boolean | null;
|
||||||
|
replyTo?: Message | null;
|
||||||
// Relations
|
// Relations
|
||||||
user?: User;
|
user?: User;
|
||||||
channel?: Channel;
|
channel?: Channel;
|
||||||
|
|
||||||
|
replyToId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct messages
|
// Direct messages
|
||||||
|
|||||||
Reference in New Issue
Block a user