mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix: add nous-research/ui package
This commit is contained in:
parent
957ca79e8e
commit
923539a46b
26 changed files with 798 additions and 637 deletions
236
web/package-lock.json
generated
236
web/package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
|||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@nous-research/ui": "^0.3.0",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
|
@ -64,6 +65,7 @@
|
|||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
|
|
@ -985,6 +987,66 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@nanostores/react": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@nanostores/react/-/react-1.1.0.tgz",
|
||||
"integrity": "sha512-MbH35fjhcf7LAubYX5vhOChYUfTLzNLqH/mBGLVsHkcvjy0F8crO1WQwdmQ2xKbAmtpalDa2zBt3Hlg5kqr8iw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.0.0 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"nanostores": "^1.2.0",
|
||||
"react": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nous-research/ui": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@nous-research/ui/-/ui-0.3.0.tgz",
|
||||
"integrity": "sha512-konGgtV9lkzqYkWuoUGnROqavq1svTnGbERLKItvEXmsRz4xRtbAMHI8rK6sjGpHDpwvOUN3olcOhRLTGuVfcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nanostores/react": "^1.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"nanostores": "^1.0.1",
|
||||
"sanitize-html": "^2.16.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@observablehq/plot": "^0.6.17",
|
||||
"@react-three/fiber": "^9.4.0",
|
||||
"gsap": "^3.13.0",
|
||||
"leva": "^0.10.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"three": "^0.180.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@observablehq/plot": {
|
||||
"optional": true
|
||||
},
|
||||
"@react-three/fiber": {
|
||||
"optional": true
|
||||
},
|
||||
"gsap": {
|
||||
"optional": true
|
||||
},
|
||||
"leva": {
|
||||
"optional": true
|
||||
},
|
||||
"three": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
||||
|
|
@ -1638,6 +1700,7 @@
|
|||
"integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
|
|
@ -1648,6 +1711,7 @@
|
|||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
|
|
@ -1707,6 +1771,7 @@
|
|||
"integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.57.0",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
|
|
@ -1984,6 +2049,7 @@
|
|||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -2092,6 +2158,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
|
|
@ -2269,6 +2336,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
|
|
@ -2278,6 +2354,73 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer/node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.313",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz",
|
||||
|
|
@ -2298,6 +2441,18 @@
|
|||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
|
||||
|
|
@ -2353,7 +2508,6 @@
|
|||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
|
|
@ -2368,6 +2522,7 @@
|
|||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
|
|
@ -2718,6 +2873,25 @@
|
|||
"hermes-estree": "0.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
|
||||
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.2.2",
|
||||
"entities": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
|
|
@ -2778,6 +2952,15 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-object": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
|
|
@ -3223,6 +3406,22 @@
|
|||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/nanostores": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.3.0.tgz",
|
||||
"integrity": "sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.0.0 || >=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
|
|
@ -3300,6 +3499,12 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-srcset": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
|
|
@ -3331,6 +3536,7 @@
|
|||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -3391,6 +3597,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -3400,6 +3607,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
|
|
@ -3509,6 +3717,20 @@
|
|||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/sanitize-html": {
|
||||
"version": "2.17.3",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.3.tgz",
|
||||
"integrity": "sha512-Kn4srCAo2+wZyvCNKCSyB2g8RQ8IkX/gQs2uqoSRNu5t9I2qvUyAVvRDiFUVAiX3N3PNuwStY0eNr+ooBHVWEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.2.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"htmlparser2": "^10.1.0",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"parse-srcset": "^1.0.2",
|
||||
"postcss": "^8.3.11"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
|
|
@ -3647,6 +3869,15 @@
|
|||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tw-animate-css": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
|
||||
"integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Wombosvideo"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
|
@ -3666,6 +3897,7 @@
|
|||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -3751,6 +3983,7 @@
|
|||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
@ -3872,6 +4105,7 @@
|
|||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,16 @@
|
|||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"sync-assets": "rm -rf public/fonts public/ds-assets && cp -r node_modules/@nous-research/ui/dist/fonts public/fonts && cp -r node_modules/@nous-research/ui/dist/assets public/ds-assets",
|
||||
"predev": "npm run sync-assets",
|
||||
"prebuild": "npm run sync-assets",
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nous-research/ui": "^0.3.0",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
107
web/src/App.tsx
107
web/src/App.tsx
|
|
@ -6,6 +6,9 @@ import {
|
|||
Sparkles, Terminal, Globe, Database, Shield,
|
||||
Wrench, Zap, Heart, Star, Code, Eye,
|
||||
} from "lucide-react";
|
||||
import { SelectionSwitcher } from "@nous-research/ui/ui/components/selection-switcher";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Backdrop } from "@/components/Backdrop";
|
||||
import StatusPage from "@/pages/StatusPage";
|
||||
import ConfigPage from "@/pages/ConfigPage";
|
||||
import EnvPage from "@/pages/EnvPage";
|
||||
|
|
@ -20,17 +23,6 @@ import { useI18n } from "@/i18n";
|
|||
import { usePlugins } from "@/plugins";
|
||||
import type { RegisteredPlugin } from "@/plugins";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Built-in nav items
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface NavItem {
|
||||
path: string;
|
||||
label: string;
|
||||
labelKey?: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
}
|
||||
|
||||
const BUILTIN_NAV: NavItem[] = [
|
||||
{ path: "/", labelKey: "status", label: "Status", icon: Activity },
|
||||
{ path: "/sessions", labelKey: "sessions", label: "Sessions", icon: MessageSquare },
|
||||
|
|
@ -42,11 +34,8 @@ const BUILTIN_NAV: NavItem[] = [
|
|||
{ path: "/env", labelKey: "keys", label: "Keys", icon: KeyRound },
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Map of icon names plugins can use. Covers common choices without importing all of lucide. */
|
||||
// Plugins can reference any of these by name in their manifest — keeps bundle
|
||||
// size sane vs. importing the full lucide-react set.
|
||||
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
Activity, BarChart3, Clock, FileText, KeyRound,
|
||||
MessageSquare, Package, Settings, Puzzle,
|
||||
|
|
@ -54,12 +43,10 @@ const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
|
|||
Wrench, Zap, Heart, Star, Code, Eye,
|
||||
};
|
||||
|
||||
/** Resolve a Lucide icon name to a component, fallback to Puzzle. */
|
||||
function resolveIcon(name: string): React.ComponentType<{ className?: string }> {
|
||||
return ICON_MAP[name] ?? Puzzle;
|
||||
}
|
||||
|
||||
/** Insert plugin nav items at the position specified in their manifest. */
|
||||
function buildNavItems(builtIn: NavItem[], plugins: RegisteredPlugin[]): NavItem[] {
|
||||
const items = [...builtIn];
|
||||
|
||||
|
|
@ -89,10 +76,6 @@ function buildNavItems(builtIn: NavItem[], plugins: RegisteredPlugin[]): NavItem
|
|||
return items;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// App
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default function App() {
|
||||
const { t } = useI18n();
|
||||
const { plugins } = usePlugins();
|
||||
|
|
@ -103,15 +86,26 @@ export default function App() {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-background text-foreground overflow-x-hidden">
|
||||
<div className="noise-overlay" />
|
||||
<div className="warm-glow" />
|
||||
<div className="text-midground font-mondwest bg-black min-h-screen flex flex-col uppercase antialiased overflow-x-hidden">
|
||||
<SelectionSwitcher />
|
||||
<Backdrop />
|
||||
|
||||
<header className="fixed top-0 left-0 right-0 z-40 border-b border-border bg-background/90 backdrop-blur-sm">
|
||||
<div className="mx-auto flex h-12 max-w-[1400px] items-stretch">
|
||||
<div className="flex items-center border-r border-border px-3 sm:px-5 shrink-0">
|
||||
<span className="font-collapse text-lg sm:text-xl font-bold tracking-wider uppercase blend-lighter">
|
||||
H<span className="hidden sm:inline">ermes </span>A<span className="hidden sm:inline">gent</span>
|
||||
<header
|
||||
className={cn(
|
||||
"fixed top-0 left-0 right-0 z-40",
|
||||
"border-b border-current/20",
|
||||
"bg-background-base/90 backdrop-blur-sm",
|
||||
)}
|
||||
>
|
||||
<div className="mx-auto flex h-12 max-w-[1600px] items-stretch">
|
||||
<div className="flex items-center border-r border-current/20 px-3 sm:px-5 shrink-0">
|
||||
<span
|
||||
className="font-sans font-bold text-[1.0625rem] sm:text-[1.125rem] leading-[0.95] tracking-[0.0525rem] text-midground"
|
||||
style={{ mixBlendMode: "plus-lighter" }}
|
||||
>
|
||||
Hermes
|
||||
<br />
|
||||
Agent
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -122,22 +116,36 @@ export default function App() {
|
|||
to={path}
|
||||
end={path === "/"}
|
||||
className={({ isActive }) =>
|
||||
`group relative inline-flex items-center gap-1 sm:gap-1.5 border-r border-border px-2.5 sm:px-4 py-2 font-display text-[0.65rem] sm:text-[0.8rem] tracking-[0.12em] uppercase whitespace-nowrap transition-colors cursor-pointer shrink-0 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring ${
|
||||
cn(
|
||||
"group relative inline-flex items-center gap-1.5 shrink-0",
|
||||
"border-r border-current/20 px-2.5 sm:px-4 py-2",
|
||||
"font-mondwest text-[0.65rem] sm:text-[0.8rem] tracking-[0.12em]",
|
||||
"whitespace-nowrap transition-colors cursor-pointer",
|
||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground",
|
||||
isActive
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`
|
||||
? "text-midground"
|
||||
: "opacity-60 hover:opacity-100",
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<Icon className="h-4 w-4 sm:h-3.5 sm:w-3.5 shrink-0" />
|
||||
<Icon className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="hidden sm:inline">
|
||||
{labelKey ? (t.app.nav as Record<string, string>)[labelKey] ?? label : label}
|
||||
</span>
|
||||
<span className="absolute inset-0 bg-foreground pointer-events-none transition-opacity duration-150 group-hover:opacity-5 opacity-0" />
|
||||
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute inset-1 bg-midground opacity-0 pointer-events-none transition-opacity duration-200 group-hover:opacity-5"
|
||||
/>
|
||||
|
||||
{isActive && (
|
||||
<span className="absolute bottom-0 left-0 right-0 h-px bg-foreground" />
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute bottom-0 left-0 right-0 h-px bg-midground"
|
||||
style={{ mixBlendMode: "plus-lighter" }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
|
@ -145,17 +153,17 @@ export default function App() {
|
|||
))}
|
||||
</nav>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2 px-2 sm:px-4">
|
||||
<div className="ml-auto flex items-center gap-2 border-l border-current/20 px-2 sm:px-4">
|
||||
<ThemeSwitcher />
|
||||
<LanguageSwitcher />
|
||||
<span className="hidden sm:inline font-display text-[0.7rem] tracking-[0.15em] uppercase opacity-50">
|
||||
<span className="hidden sm:inline font-mondwest text-[0.7rem] tracking-[0.15em] opacity-50">
|
||||
{t.app.webUi}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="relative z-2 mx-auto w-full max-w-[1400px] flex-1 px-3 sm:px-6 pt-16 sm:pt-20 pb-4 sm:pb-8">
|
||||
<main className="relative z-2 mx-auto w-full max-w-[1600px] flex-1 px-3 sm:px-6 pt-16 sm:pt-20 pb-4 sm:pb-8">
|
||||
<Routes>
|
||||
<Route path="/" element={<StatusPage />} />
|
||||
<Route path="/sessions" element={<SessionsPage />} />
|
||||
|
|
@ -166,7 +174,6 @@ export default function App() {
|
|||
<Route path="/config" element={<ConfigPage />} />
|
||||
<Route path="/env" element={<EnvPage />} />
|
||||
|
||||
{/* Plugin routes */}
|
||||
{plugins.map(({ manifest, component: PluginComponent }) => (
|
||||
<Route
|
||||
key={manifest.name}
|
||||
|
|
@ -179,12 +186,15 @@ export default function App() {
|
|||
</Routes>
|
||||
</main>
|
||||
|
||||
<footer className="relative z-2 border-t border-border">
|
||||
<div className="mx-auto flex max-w-[1400px] items-center justify-between px-3 sm:px-6 py-3">
|
||||
<span className="font-display text-[0.7rem] sm:text-[0.8rem] tracking-[0.12em] uppercase opacity-50">
|
||||
<footer className="relative z-2 border-t border-current/20">
|
||||
<div className="mx-auto flex max-w-[1600px] items-center justify-between px-3 sm:px-6 py-3">
|
||||
<span className="font-mondwest text-[0.7rem] sm:text-[0.8rem] tracking-[0.12em] opacity-60">
|
||||
{t.app.footer.name}
|
||||
</span>
|
||||
<span className="font-display text-[0.6rem] sm:text-[0.7rem] tracking-[0.15em] uppercase text-foreground/40">
|
||||
<span
|
||||
className="font-mondwest text-[0.6rem] sm:text-[0.7rem] tracking-[0.15em] text-midground"
|
||||
style={{ mixBlendMode: "plus-lighter" }}
|
||||
>
|
||||
{t.app.footer.org}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -192,3 +202,10 @@ export default function App() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface NavItem {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
label: string;
|
||||
labelKey?: string;
|
||||
path: string;
|
||||
}
|
||||
|
|
|
|||
77
web/src/components/Backdrop.tsx
Normal file
77
web/src/components/Backdrop.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { useGpuTier } from "@nous-research/ui/hooks/use-gpu-tier";
|
||||
|
||||
/**
|
||||
* Replicates the visual layer stack of `<Overlays dark />` from
|
||||
* `@nous-research/ui` without pulling in its leva / gsap / three peer deps.
|
||||
*
|
||||
* See `design-language/src/ui/components/overlays/index.tsx` for the source of
|
||||
* truth. Defaults match LENS_0 (the Hermes teal dark preset); the deep canvas
|
||||
* and the warm vignette both read theme-switchable CSS custom properties so
|
||||
* `ThemeProvider` can repaint the stack without remounting.
|
||||
*
|
||||
* z-1 bg = `var(--background-base)`, mix-blend-mode: difference
|
||||
* z-2 filler-bg jpeg, inverted, opacity 0.033, difference
|
||||
* z-99 warm top-left vignette (`var(--warm-glow)`), opacity 0.22, lighten
|
||||
* z-101 noise grain (SVG, ~55% opacity × `--noise-opacity-mul`,
|
||||
* color-dodge) — gated on GPU tier
|
||||
*
|
||||
* `useGpuTier` returns 0 when WebGL is unavailable, the renderer is a
|
||||
* software rasterizer (SwiftShader/llvmpipe), or the user has
|
||||
* `prefers-reduced-motion: reduce` set. We skip the animated noise layer
|
||||
* in that case so low-power / accessibility-conscious sessions stay crisp,
|
||||
* mirroring the DS `<Noise />` component's own opt-out.
|
||||
*/
|
||||
export function Backdrop() {
|
||||
const gpuTier = useGpuTier();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none fixed inset-0 z-[1]"
|
||||
style={{
|
||||
backgroundColor: "var(--background-base)",
|
||||
mixBlendMode: "difference",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none fixed inset-0 z-[2]"
|
||||
style={{ mixBlendMode: "difference", opacity: 0.033 }}
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
className="h-[150dvh] w-auto min-w-[100dvw] object-cover object-top-left invert"
|
||||
fetchPriority="low"
|
||||
src="/ds-assets/filler-bg0.jpg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none fixed inset-0 z-[99]"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(ellipse at 0% 0%, transparent 60%, var(--warm-glow) 100%)",
|
||||
mixBlendMode: "lighten",
|
||||
opacity: 0.22,
|
||||
}}
|
||||
/>
|
||||
|
||||
{gpuTier > 0 && (
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none fixed inset-0 z-[101]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' fill='%23eaeaea' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E\")",
|
||||
backgroundSize: "512px 512px",
|
||||
mixBlendMode: "color-dodge",
|
||||
opacity: "calc(0.55 * var(--noise-opacity-mul, 1))",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ export function LanguageSwitcher() {
|
|||
>
|
||||
{/* Show the *current* language's flag — tooltip advertises the click action */}
|
||||
<span className="text-base leading-none">{locale === "en" ? "🇬🇧" : "🇨🇳"}</span>
|
||||
<span className="hidden sm:inline font-display tracking-wide uppercase text-[0.65rem]">
|
||||
<span className="hidden sm:inline font-mondwest tracking-wide uppercase text-[0.65rem]">
|
||||
{locale === "en" ? "EN" : "中文"}
|
||||
</span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
|
|||
</button>
|
||||
<div className="p-6 flex flex-col gap-4">
|
||||
<div>
|
||||
<h2 id="oauth-modal-title" className="font-display text-base tracking-wider uppercase">
|
||||
<h2 id="oauth-modal-title" className="font-mondwest text-base tracking-wider uppercase">
|
||||
{t.oauth.connect} {provider.name}
|
||||
</h2>
|
||||
{secondsLeft !== null && phase !== "approved" && phase !== "error" && (
|
||||
|
|
|
|||
|
|
@ -158,11 +158,11 @@ export function OAuthProvidersCard({ onError, onSuccess }: Props) {
|
|||
)}
|
||||
</div>
|
||||
{p.status.logged_in && p.status.token_preview && (
|
||||
<code className="text-xs text-muted-foreground font-mono-ui truncate">
|
||||
token{" "}
|
||||
<span className="text-foreground">{p.status.token_preview}</span>
|
||||
<code className="text-xs font-mono-ui truncate">
|
||||
<span className="opacity-50">token{" "}</span>
|
||||
{p.status.token_preview}
|
||||
{p.status.source_label && (
|
||||
<span className="text-muted-foreground/70">
|
||||
<span className="opacity-40">
|
||||
{" "}· {p.status.source_label}
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,53 +1,54 @@
|
|||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Palette, Check } from "lucide-react";
|
||||
import { useTheme } from "@/themes";
|
||||
import { BUILTIN_THEMES, useTheme } from "@/themes";
|
||||
import { useI18n } from "@/i18n";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Compact theme picker for the dashboard header.
|
||||
* Shows a palette icon + current theme name; opens a dropdown of all
|
||||
* available themes with color swatches for instant preview.
|
||||
* Compact theme picker mounted next to the language switcher in the header.
|
||||
* Each dropdown row shows a 3-stop swatch (background / midground / warm
|
||||
* glow) so users can preview the palette before committing. User-defined
|
||||
* themes from `~/.hermes/dashboard-themes/*.yaml` that aren't in
|
||||
* `BUILTIN_THEMES` render without swatches and apply the default palette.
|
||||
*/
|
||||
export function ThemeSwitcher() {
|
||||
const { themeName, availableThemes, setTheme } = useTheme();
|
||||
const { t } = useI18n();
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const close = useCallback(() => setOpen(false), []);
|
||||
|
||||
// Close on outside click.
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) close();
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
|
||||
close();
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handler);
|
||||
return () => document.removeEventListener("mousedown", handler);
|
||||
}, [open, close]);
|
||||
|
||||
// Close on Escape.
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") close();
|
||||
};
|
||||
document.addEventListener("keydown", handler);
|
||||
return () => document.removeEventListener("keydown", handler);
|
||||
document.addEventListener("mousedown", onMouseDown);
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", onMouseDown);
|
||||
document.removeEventListener("keydown", onKey);
|
||||
};
|
||||
}, [open, close]);
|
||||
|
||||
const current = availableThemes.find((t) => t.name === themeName);
|
||||
const current = availableThemes.find((th) => th.name === themeName);
|
||||
const label = current?.label ?? themeName;
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative">
|
||||
<div ref={wrapperRef} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
className={cn(
|
||||
"group relative inline-flex items-center gap-1.5 px-2 py-1 text-xs",
|
||||
"text-muted-foreground hover:text-foreground transition-colors",
|
||||
"cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
||||
"text-muted-foreground hover:text-foreground transition-colors cursor-pointer",
|
||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground",
|
||||
)}
|
||||
title={t.theme?.switchTheme ?? "Switch theme"}
|
||||
aria-label={t.theme?.switchTheme ?? "Switch theme"}
|
||||
|
|
@ -55,56 +56,66 @@ export function ThemeSwitcher() {
|
|||
aria-haspopup="listbox"
|
||||
>
|
||||
<Palette className="h-3.5 w-3.5" />
|
||||
<span className="hidden sm:inline font-display tracking-wide uppercase text-[0.65rem]">
|
||||
{current?.label ?? themeName}
|
||||
<span className="hidden sm:inline font-mondwest tracking-wide uppercase text-[0.65rem]">
|
||||
{label}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
role="listbox"
|
||||
aria-label={t.theme?.title ?? "Theme"}
|
||||
className={cn(
|
||||
"absolute right-0 top-full mt-1 z-50 min-w-[200px]",
|
||||
"border border-border bg-popover text-popover-foreground shadow-lg",
|
||||
"animate-[fade-in_100ms_ease-out]",
|
||||
"absolute right-0 top-full mt-1 z-50 min-w-[240px]",
|
||||
"border border-current/20 bg-background-base/95 backdrop-blur-sm",
|
||||
"shadow-[0_12px_32px_-8px_rgba(0,0,0,0.6)]",
|
||||
)}
|
||||
>
|
||||
<div className="px-3 py-2 border-b border-border">
|
||||
<span className="font-display text-[0.7rem] tracking-[0.12em] uppercase text-muted-foreground">
|
||||
<div className="border-b border-current/20 px-3 py-2">
|
||||
<span className="font-mondwest text-[0.65rem] tracking-[0.15em] uppercase text-midground/70">
|
||||
{t.theme?.title ?? "Theme"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{availableThemes.map((theme) => {
|
||||
const isActive = theme.name === themeName;
|
||||
{availableThemes.map((th) => {
|
||||
const isActive = th.name === themeName;
|
||||
const preset = BUILTIN_THEMES[th.name];
|
||||
|
||||
return (
|
||||
<button
|
||||
key={theme.name}
|
||||
key={th.name}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={isActive}
|
||||
onClick={() => {
|
||||
setTheme(theme.name);
|
||||
setTheme(th.name);
|
||||
close();
|
||||
}}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm transition-colors cursor-pointer",
|
||||
"hover:bg-foreground/10",
|
||||
isActive ? "text-foreground" : "text-muted-foreground",
|
||||
"flex w-full items-center gap-3 px-3 py-2 text-left transition-colors cursor-pointer",
|
||||
"hover:bg-midground/10",
|
||||
isActive ? "text-midground" : "text-midground/60",
|
||||
)}
|
||||
>
|
||||
{preset ? <ThemeSwatch theme={preset.name} /> : <PlaceholderSwatch />}
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<span className="truncate font-mondwest text-[0.75rem] tracking-wide uppercase">
|
||||
{th.label}
|
||||
</span>
|
||||
{th.description && (
|
||||
<span className="truncate font-sans text-[0.65rem] normal-case tracking-normal text-midground/50">
|
||||
{th.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Check
|
||||
className={cn(
|
||||
"h-3 w-3 shrink-0",
|
||||
"h-3 w-3 shrink-0 text-midground",
|
||||
isActive ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col gap-0.5 min-w-0">
|
||||
<span className="font-medium text-xs truncate">{theme.label}</span>
|
||||
<span className="text-[0.65rem] text-muted-foreground truncate">
|
||||
{theme.description}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
|
@ -113,3 +124,28 @@ export function ThemeSwitcher() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ThemeSwatch({ theme }: { theme: string }) {
|
||||
const preset = BUILTIN_THEMES[theme];
|
||||
if (!preset) return <PlaceholderSwatch />;
|
||||
const { background, midground, warmGlow } = preset.palette;
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
className="flex h-4 w-9 shrink-0 overflow-hidden border border-current/20"
|
||||
>
|
||||
<span className="flex-1" style={{ background: background.hex }} />
|
||||
<span className="flex-1" style={{ background: midground.hex }} />
|
||||
<span className="flex-1" style={{ background: warmGlow }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PlaceholderSwatch() {
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
className="h-4 w-9 shrink-0 border border-dashed border-current/20"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap font-display text-xs tracking-[0.1em] uppercase transition-colors cursor-pointer"
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap font-mondwest text-xs tracking-[0.1em] uppercase transition-colors cursor-pointer"
|
||||
+ " disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHead
|
|||
}
|
||||
|
||||
export function CardDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
||||
return <p className={cn("font-display text-xs text-muted-foreground", className)} {...props} />;
|
||||
return <p className={cn("font-mondwest text-xs text-muted-foreground", className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export function Label({ className, ...props }: React.LabelHTMLAttributes<HTMLLab
|
|||
return (
|
||||
<label
|
||||
className={cn(
|
||||
"font-display text-xs tracking-[0.1em] uppercase leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
"font-mondwest text-xs tracking-[0.1em] uppercase leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export function TabsTrigger({
|
|||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center whitespace-nowrap px-3 py-1.5 font-display text-xs tracking-[0.1em] uppercase transition-all cursor-pointer",
|
||||
"relative inline-flex items-center justify-center whitespace-nowrap px-3 py-1.5 font-mondwest text-xs tracking-[0.1em] uppercase transition-all cursor-pointer",
|
||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
||||
active
|
||||
? "text-foreground after:absolute after:bottom-0 after:left-0 after:right-0 after:h-px after:bg-foreground"
|
||||
|
|
|
|||
|
|
@ -1,132 +1,74 @@
|
|||
@import "tailwindcss";
|
||||
@import 'tailwindcss';
|
||||
@import '@nous-research/ui/styles/globals.css';
|
||||
|
||||
/* Scan the published design-system bundle so its utility classes survive
|
||||
Tailwind's JIT purge. */
|
||||
@source '../node_modules/@nous-research/ui/dist';
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Hermes Agent — Design tokens */
|
||||
/* Matched to hermes-agent.nousresearch.com (dark teal theme) */
|
||||
/* Hermes Agent — Nous DS with the LENS_0 (Hermes teal) lens applied */
|
||||
/* statically. Mirrors nousnet-web/(hermes-agent)/layout.tsx so the */
|
||||
/* canonical Hermes palette is the default — teal canvas + cream */
|
||||
/* accent — without relying on leva/gsap at runtime. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* --- Font faces --- */
|
||||
@font-face { font-family: "Collapse"; src: url("/fonts/Collapse-Regular.woff2") format("woff2"); font-weight: 400; font-display: swap; }
|
||||
@font-face { font-family: "Collapse"; src: url("/fonts/Collapse-Bold.woff2") format("woff2"); font-weight: 700; font-display: swap; }
|
||||
@font-face { font-family: "Courier Prime"; src: url("/fonts/CourierPrime-Regular.woff2") format("woff2"); font-weight: 400; font-display: swap; }
|
||||
@font-face { font-family: "Courier Prime"; src: url("/fonts/CourierPrime-Bold.woff2") format("woff2"); font-weight: 700; font-display: swap; }
|
||||
@font-face { font-family: "RulesCompressed"; src: url("/fonts/RulesCompressed-Regular.woff2") format("woff2"); font-weight: 400; font-display: swap; }
|
||||
@font-face { font-family: "RulesCompressed"; src: url("/fonts/RulesCompressed-Medium.woff2") format("woff2"); font-weight: 600; font-display: swap; }
|
||||
@font-face { font-family: "RulesExpanded"; src: url("/fonts/RulesExpanded-Regular.woff2") format("woff2"); font-weight: 400; font-display: swap; }
|
||||
@font-face { font-family: "RulesExpanded"; src: url("/fonts/RulesExpanded-Bold.woff2") format("woff2"); font-weight: 700; font-display: swap; }
|
||||
@font-face { font-family: "Mondwest"; src: url("/fonts/Mondwest-Regular.woff2") format("woff2"); font-weight: 400; font-display: swap; }
|
||||
:root {
|
||||
/* LENS_0 — from design-language/src/ui/components/overlays/index.tsx.
|
||||
These are the defaults for the `default` (Hermes Teal) dashboard theme;
|
||||
ThemeProvider rewrites them as inline styles when a user switches themes. */
|
||||
--foreground: color-mix(in srgb, #ffffff 0%, transparent);
|
||||
--foreground-base: #ffffff;
|
||||
--foreground-alpha: 0;
|
||||
--midground: color-mix(in srgb, #ffe6cb 100%, transparent);
|
||||
--midground-base: #ffe6cb;
|
||||
--midground-alpha: 1;
|
||||
--background: color-mix(in srgb, #041c1c 100%, transparent);
|
||||
--background-base: #041c1c;
|
||||
--background-alpha: 1;
|
||||
|
||||
@theme {
|
||||
/* ---- Hermes palette (dark teal, from live site) ---- */
|
||||
--color-background: #041C1C;
|
||||
--color-foreground: #ffe6cb;
|
||||
--color-card: #062424;
|
||||
--color-card-foreground: #ffe6cb;
|
||||
--color-primary: #ffe6cb;
|
||||
--color-primary-foreground: #041C1C;
|
||||
--color-secondary: #0a2e2e;
|
||||
--color-secondary-foreground: #ffe6cb;
|
||||
--color-muted: #083030;
|
||||
--color-muted-foreground: #8aaa9a;
|
||||
--color-accent: #0c3838;
|
||||
--color-accent-foreground: #ffe6cb;
|
||||
/* Consumed by <Backdrop />; also theme-switchable. */
|
||||
--warm-glow: rgba(255, 189, 56, 0.35);
|
||||
--noise-opacity-mul: 1;
|
||||
}
|
||||
|
||||
/* Nousnet's hermes-agent layout bumps `small` and `code` to readable
|
||||
dashboard sizes. Keep in sync. */
|
||||
small { font-size: 1.0625rem; }
|
||||
code { font-size: 0.875rem; }
|
||||
|
||||
/* Shadcn-compat tokens.
|
||||
The dashboard's page code predates the Nous DS and uses shadcn-style
|
||||
utility classes (bg-card, text-muted-foreground, border-border, etc.)
|
||||
extensively. Rather than rewrite every call site, we expose those
|
||||
tokens on top of the Nous palette so classes continue to resolve. */
|
||||
@theme inline {
|
||||
/* Remap foreground to midground so `text-foreground` / `bg-foreground`
|
||||
stay visible — in LENS_0, `--foreground` itself has alpha 0. */
|
||||
--color-foreground: var(--midground);
|
||||
|
||||
--color-card: color-mix(in srgb, var(--midground-base) 4%, var(--background-base));
|
||||
--color-card-foreground: var(--midground);
|
||||
--color-primary: var(--midground);
|
||||
--color-primary-foreground: var(--background-base);
|
||||
--color-secondary: color-mix(in srgb, var(--midground-base) 6%, var(--background-base));
|
||||
--color-secondary-foreground: var(--midground);
|
||||
--color-muted: color-mix(in srgb, var(--midground-base) 8%, var(--background-base));
|
||||
--color-muted-foreground: color-mix(in srgb, var(--midground-base) 55%, transparent);
|
||||
--color-accent: color-mix(in srgb, var(--midground-base) 10%, var(--background-base));
|
||||
--color-accent-foreground: var(--midground);
|
||||
--color-destructive: #fb2c36;
|
||||
--color-destructive-foreground: #fff;
|
||||
--color-destructive-foreground: #ffffff;
|
||||
--color-success: #4ade80;
|
||||
--color-warning: #ffbd38;
|
||||
--color-border: color-mix(in srgb, #ffe6cb 15%, transparent);
|
||||
--color-input: color-mix(in srgb, #ffe6cb 15%, transparent);
|
||||
--color-ring: #ffe6cb;
|
||||
--color-popover: #062424;
|
||||
--color-popover-foreground: #ffe6cb;
|
||||
|
||||
/* ---- Font stacks ---- */
|
||||
--font-sans: "Mondwest", Arial, sans-serif;
|
||||
--font-mono: "Courier Prime", "Courier New", monospace;
|
||||
--font-display: "Mondwest", Arial, sans-serif;
|
||||
--font-expanded: "RulesExpanded", Arial, sans-serif;
|
||||
--font-compressed: "RulesCompressed", Arial, sans-serif;
|
||||
--color-border: color-mix(in srgb, var(--midground-base) 15%, transparent);
|
||||
--color-input: color-mix(in srgb, var(--midground-base) 15%, transparent);
|
||||
--color-ring: var(--midground);
|
||||
--color-popover: color-mix(in srgb, var(--midground-base) 4%, var(--background-base));
|
||||
--color-popover-foreground: var(--midground);
|
||||
}
|
||||
|
||||
/* ---- Global body ---- */
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Mondwest", Arial, sans-serif;
|
||||
background: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
/* ---- Selection ---- */
|
||||
::selection {
|
||||
background: var(--color-foreground);
|
||||
color: var(--color-background);
|
||||
}
|
||||
|
||||
/* ---- Scrollbars (thin, subtle) ---- */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: transparent transparent;
|
||||
}
|
||||
*:hover {
|
||||
scrollbar-color: color-mix(in srgb, var(--color-foreground) 15%, transparent) transparent;
|
||||
}
|
||||
html, body {
|
||||
overflow-x: hidden;
|
||||
scrollbar-color: color-mix(in srgb, var(--color-foreground) 25%, transparent) transparent;
|
||||
}
|
||||
::-webkit-scrollbar { width: 4px; height: 4px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in srgb, var(--color-foreground) 20%, transparent);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: color-mix(in srgb, var(--color-foreground) 35%, transparent);
|
||||
}
|
||||
|
||||
/* ---- Hide scrollbar utility ---- */
|
||||
.scrollbar-none {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.scrollbar-none::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ---- Code blocks ---- */
|
||||
code {
|
||||
font-family: "Courier Prime", "Courier New", monospace;
|
||||
font-size: 0.85em;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 0;
|
||||
background: color-mix(in srgb, var(--color-foreground) 8%, transparent);
|
||||
}
|
||||
|
||||
/* ---- Dither texture ---- */
|
||||
.dither {
|
||||
background: repeating-conic-gradient(currentColor 0% 25%, #0000 0% 50%) 0 0 / 2px 2px;
|
||||
}
|
||||
|
||||
/* ---- Blink cursor (only on group hover, like canonical) ---- */
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
.blink {
|
||||
display: none;
|
||||
}
|
||||
.group:hover .blink {
|
||||
display: inline-block;
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
/* ---- Page transitions ---- */
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
/* Toast animations used by `components/Toast.tsx`. */
|
||||
@keyframes toast-in {
|
||||
from { opacity: 0; transform: translateX(16px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
|
|
@ -136,62 +78,38 @@ code {
|
|||
to { opacity: 0; transform: translateX(16px); }
|
||||
}
|
||||
|
||||
/* ---- Plus-lighter blend for headings ---- */
|
||||
/* Hide scrollbar utility — used by the header's overflow-x nav row. */
|
||||
.scrollbar-none {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.scrollbar-none::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Plus-lighter blend used by logos/titles for a subtle glow. */
|
||||
.blend-lighter {
|
||||
mix-blend-mode: plus-lighter;
|
||||
}
|
||||
|
||||
/* ---- Font utilities ---- */
|
||||
.font-display { font-family: "Mondwest", Arial, sans-serif; }
|
||||
.font-expanded { font-family: "RulesExpanded", Arial, sans-serif; }
|
||||
.font-compressed { font-family: "RulesCompressed", Arial, sans-serif; }
|
||||
.font-courier { font-family: "Courier Prime", "Courier New", monospace; }
|
||||
.font-collapse { font-family: "Collapse", Arial, sans-serif; }
|
||||
.font-mono-ui { font-family: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace; }
|
||||
/* System UI-monospace stack — distinct from `font-courier` (Courier
|
||||
Prime), used for dense data readouts where the display font would
|
||||
break the grid. */
|
||||
.font-mono-ui {
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Mono', Menlo, monospace;
|
||||
}
|
||||
|
||||
/* ---- Subtle grain overlay for badges ---- */
|
||||
/* Subtle grain overlay for badges. */
|
||||
.grain {
|
||||
position: relative;
|
||||
}
|
||||
.grain::after {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.12;
|
||||
pointer-events: none;
|
||||
background: repeating-conic-gradient(currentColor 0% 25%, #0000 0% 50%) 0 0 / 2px 2px;
|
||||
background: repeating-conic-gradient(currentColor 0% 25%, #0000 0% 50%) 0 0 /
|
||||
2px 2px;
|
||||
}
|
||||
|
||||
/* ---- Global noise grain (canonical: color-dodge, #eaeaea, high density) ---- */
|
||||
.noise-overlay {
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 101;
|
||||
mix-blend-mode: color-dodge;
|
||||
opacity: 0.10;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' fill='%23eaeaea' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E");
|
||||
background-size: 512px 512px;
|
||||
}
|
||||
|
||||
/* ---- Vignette (canonical: top-left amber radial, lighten blend) ---- */
|
||||
.warm-glow {
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 99;
|
||||
mix-blend-mode: lighten;
|
||||
opacity: 0.22;
|
||||
background: radial-gradient(ellipse at 0% 0%, rgba(255,189,56,0.35) 0%, rgba(255,189,56,0) 60%);
|
||||
}
|
||||
|
||||
/* ---- Reduced motion ---- */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,21 +183,21 @@ export const api = {
|
|||
);
|
||||
},
|
||||
|
||||
// Dashboard plugins
|
||||
getPlugins: () =>
|
||||
fetchJSON<PluginManifestResponse[]>("/api/dashboard/plugins"),
|
||||
rescanPlugins: () =>
|
||||
fetchJSON<{ ok: boolean; count: number }>("/api/dashboard/plugins/rescan"),
|
||||
|
||||
// Dashboard themes
|
||||
getThemes: () =>
|
||||
fetchJSON<ThemeListResponse>("/api/dashboard/themes"),
|
||||
fetchJSON<DashboardThemesResponse>("/api/dashboard/themes"),
|
||||
setTheme: (name: string) =>
|
||||
fetchJSON<{ ok: boolean; theme: string }>("/api/dashboard/theme", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name }),
|
||||
}),
|
||||
|
||||
// Dashboard plugins
|
||||
getPlugins: () =>
|
||||
fetchJSON<PluginManifestResponse[]>("/api/dashboard/plugins"),
|
||||
rescanPlugins: () =>
|
||||
fetchJSON<{ ok: boolean; count: number }>("/api/dashboard/plugins/rescan"),
|
||||
};
|
||||
|
||||
export interface PlatformStatus {
|
||||
|
|
@ -435,9 +435,15 @@ export interface OAuthPollResponse {
|
|||
|
||||
// ── Dashboard theme types ──────────────────────────────────────────────
|
||||
|
||||
export interface ThemeListResponse {
|
||||
themes: Array<{ name: string; label: string; description: string }>;
|
||||
export interface DashboardThemeSummary {
|
||||
description: string;
|
||||
label: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface DashboardThemesResponse {
|
||||
active: string;
|
||||
themes: DashboardThemeSummary[];
|
||||
}
|
||||
|
||||
// ── Dashboard plugin types ─────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { BrowserRouter } from "react-router-dom";
|
|||
import "./index.css";
|
||||
import App from "./App";
|
||||
import { I18nProvider } from "./i18n";
|
||||
import { ThemeProvider } from "./themes";
|
||||
import { exposePluginSDK } from "./plugins";
|
||||
import { ThemeProvider } from "./themes";
|
||||
|
||||
// Expose the plugin SDK before rendering so plugins loaded via <script>
|
||||
// can access React, components, etc. immediately.
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ export default function StatusPage() {
|
|||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold font-display truncate" title={value}>{value}</div>
|
||||
<div className="text-2xl font-bold font-mondwest truncate" title={value}>{value}</div>
|
||||
|
||||
{badgeText && (
|
||||
<Badge variant={badgeVariant} className="mt-2">
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import { Select, SelectOption } from "@/components/ui/select";
|
|||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useI18n } from "@/i18n";
|
||||
import { useTheme } from "@/themes";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin registry — plugins call register() to add their component.
|
||||
|
|
@ -126,6 +125,5 @@ export function exposePluginSDK() {
|
|||
|
||||
// Hooks
|
||||
useI18n,
|
||||
useTheme,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,167 +3,122 @@ import {
|
|||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import type { DashboardTheme, ThemeColors, ThemeOverlay } from "./types";
|
||||
import { BUILTIN_THEMES, defaultTheme } from "./presets";
|
||||
import type { DashboardTheme, ThemeLayer, ThemePalette } from "./types";
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
/** LocalStorage key — pre-applied before the React tree mounts to avoid
|
||||
* a visible flash of the default palette on theme-overridden installs. */
|
||||
const STORAGE_KEY = "hermes-dashboard-theme";
|
||||
|
||||
/** Apply a theme's color overrides to `document.documentElement`. */
|
||||
function applyColors(colors: ThemeColors) {
|
||||
/** Turn a ThemeLayer into the two CSS expressions the DS consumes:
|
||||
* `--<name>` (color-mix'd with alpha) and `--<name>-base` (opaque hex). */
|
||||
function layerVars(name: "background" | "midground" | "foreground", layer: ThemeLayer) {
|
||||
const pct = Math.round(layer.alpha * 100);
|
||||
return {
|
||||
[`--${name}`]: `color-mix(in srgb, ${layer.hex} ${pct}%, transparent)`,
|
||||
[`--${name}-base`]: layer.hex,
|
||||
[`--${name}-alpha`]: String(layer.alpha),
|
||||
};
|
||||
}
|
||||
|
||||
/** Write a theme's palette to `document.documentElement` as inline styles.
|
||||
* Inline styles beat the `:root { }` rule in index.css, so this cascades
|
||||
* into every shadcn-compat token defined over the DS triplet. */
|
||||
function applyPalette(palette: ThemePalette) {
|
||||
const root = document.documentElement;
|
||||
for (const [key, value] of Object.entries(colors)) {
|
||||
root.style.setProperty(`--color-${key}`, value);
|
||||
const vars = {
|
||||
...layerVars("background", palette.background),
|
||||
...layerVars("midground", palette.midground),
|
||||
...layerVars("foreground", palette.foreground),
|
||||
"--warm-glow": palette.warmGlow,
|
||||
"--noise-opacity-mul": String(palette.noiseOpacity),
|
||||
};
|
||||
for (const [k, v] of Object.entries(vars)) {
|
||||
root.style.setProperty(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
/** Apply overlay overrides (noise + warm-glow). */
|
||||
function applyOverlay(overlay: ThemeOverlay | undefined) {
|
||||
const noiseEl = document.querySelector<HTMLElement>(".noise-overlay");
|
||||
const glowEl = document.querySelector<HTMLElement>(".warm-glow");
|
||||
|
||||
if (noiseEl) {
|
||||
noiseEl.style.opacity = String(overlay?.noiseOpacity ?? 0.10);
|
||||
noiseEl.style.mixBlendMode = overlay?.noiseBlendMode ?? "color-dodge";
|
||||
}
|
||||
if (glowEl) {
|
||||
glowEl.style.opacity = String(overlay?.warmGlowOpacity ?? 0.22);
|
||||
if (overlay?.warmGlowColor) {
|
||||
glowEl.style.background = `radial-gradient(ellipse at 0% 0%, ${overlay.warmGlowColor} 0%, rgba(0,0,0,0) 60%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove all inline overrides — reverts to stylesheet defaults. */
|
||||
function clearOverrides() {
|
||||
const root = document.documentElement;
|
||||
// Clear color overrides
|
||||
for (const key of Object.keys(defaultTheme.colors)) {
|
||||
root.style.removeProperty(`--color-${key}`);
|
||||
}
|
||||
// Clear overlay overrides
|
||||
const noiseEl = document.querySelector<HTMLElement>(".noise-overlay");
|
||||
const glowEl = document.querySelector<HTMLElement>(".warm-glow");
|
||||
if (noiseEl) {
|
||||
noiseEl.style.opacity = "";
|
||||
noiseEl.style.mixBlendMode = "";
|
||||
}
|
||||
if (glowEl) {
|
||||
glowEl.style.opacity = "";
|
||||
glowEl.style.background = "";
|
||||
}
|
||||
}
|
||||
|
||||
function applyTheme(theme: DashboardTheme) {
|
||||
if (theme.name === "default") {
|
||||
clearOverrides();
|
||||
} else {
|
||||
applyColors(theme.colors);
|
||||
applyOverlay(theme.overlay);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Context
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ThemeContextValue {
|
||||
/** Currently active theme name. */
|
||||
themeName: string;
|
||||
/** Currently active theme object. */
|
||||
theme: DashboardTheme;
|
||||
/** Available theme names (built-in + any server-provided custom themes). */
|
||||
availableThemes: Array<{ name: string; label: string; description: string }>;
|
||||
/** Switch theme — applies CSS immediately and persists to config.yaml. */
|
||||
setTheme: (name: string) => void;
|
||||
/** True while initial theme is loading from server. */
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue>({
|
||||
themeName: "default",
|
||||
theme: defaultTheme,
|
||||
availableThemes: Object.values(BUILTIN_THEMES).map((t) => ({
|
||||
name: t.name,
|
||||
label: t.label,
|
||||
description: t.description,
|
||||
})),
|
||||
setTheme: () => {},
|
||||
loading: true,
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Provider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [themeName, setThemeName] = useState("default");
|
||||
const [availableThemes, setAvailableThemes] = useState(
|
||||
const [themeName, setThemeName] = useState<string>(() => {
|
||||
if (typeof window === "undefined") return "default";
|
||||
return window.localStorage.getItem(STORAGE_KEY) ?? "default";
|
||||
});
|
||||
const [availableThemes, setAvailableThemes] = useState<
|
||||
Array<{ description: string; label: string; name: string }>
|
||||
>(() =>
|
||||
Object.values(BUILTIN_THEMES).map((t) => ({
|
||||
name: t.name,
|
||||
label: t.label,
|
||||
description: t.description,
|
||||
})),
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Fetch active theme + available list from server on mount.
|
||||
useEffect(() => {
|
||||
const t = BUILTIN_THEMES[themeName] ?? defaultTheme;
|
||||
applyPalette(t.palette);
|
||||
}, [themeName]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
api
|
||||
.getThemes()
|
||||
.then((resp) => {
|
||||
if (resp.themes?.length) {
|
||||
setAvailableThemes(resp.themes);
|
||||
}
|
||||
if (resp.active && resp.active !== "default") {
|
||||
if (cancelled) return;
|
||||
if (resp.themes?.length) setAvailableThemes(resp.themes);
|
||||
if (resp.active && resp.active !== themeName) {
|
||||
setThemeName(resp.active);
|
||||
const t = BUILTIN_THEMES[resp.active];
|
||||
if (t) applyTheme(t);
|
||||
window.localStorage.setItem(STORAGE_KEY, resp.active);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Server might not support theme API yet — stay on default.
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const resolvedTheme = BUILTIN_THEMES[themeName] ?? defaultTheme;
|
||||
const setTheme = useCallback((name: string) => {
|
||||
const next = BUILTIN_THEMES[name] ? name : "default";
|
||||
setThemeName(next);
|
||||
window.localStorage.setItem(STORAGE_KEY, next);
|
||||
api.setTheme(next).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const setTheme = useCallback(
|
||||
(name: string) => {
|
||||
const t = BUILTIN_THEMES[name] ?? defaultTheme;
|
||||
setThemeName(t.name);
|
||||
applyTheme(t);
|
||||
// Persist to config.yaml — fire and forget.
|
||||
api.setTheme(t.name).catch(() => {});
|
||||
},
|
||||
[],
|
||||
const value = useMemo<ThemeContextValue>(
|
||||
() => ({
|
||||
theme: BUILTIN_THEMES[themeName] ?? defaultTheme,
|
||||
themeName,
|
||||
availableThemes,
|
||||
setTheme,
|
||||
}),
|
||||
[themeName, availableThemes, setTheme],
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
themeName,
|
||||
theme: resolvedTheme,
|
||||
availableThemes,
|
||||
setTheme,
|
||||
loading,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useTheme() {
|
||||
export function useTheme(): ThemeContextValue {
|
||||
return useContext(ThemeContext);
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue>({
|
||||
theme: defaultTheme,
|
||||
themeName: "default",
|
||||
availableThemes: Object.values(BUILTIN_THEMES).map((t) => ({
|
||||
name: t.name,
|
||||
label: t.label,
|
||||
description: t.description,
|
||||
})),
|
||||
setTheme: () => {},
|
||||
});
|
||||
|
||||
interface ThemeContextValue {
|
||||
availableThemes: Array<{ description: string; label: string; name: string }>;
|
||||
setTheme: (name: string) => void;
|
||||
theme: DashboardTheme;
|
||||
themeName: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export { ThemeProvider, useTheme } from "./context";
|
||||
export { BUILTIN_THEMES } from "./presets";
|
||||
export type { DashboardTheme, ThemeColors, ThemeOverlay, ThemeListResponse } from "./types";
|
||||
export { BUILTIN_THEMES, defaultTheme } from "./presets";
|
||||
export type { DashboardTheme, ThemeLayer, ThemeListResponse, ThemePalette } from "./types";
|
||||
|
|
|
|||
|
|
@ -3,43 +3,25 @@ import type { DashboardTheme } from "./types";
|
|||
/**
|
||||
* Built-in dashboard themes.
|
||||
*
|
||||
* The "default" theme matches the current index.css @theme values exactly,
|
||||
* so applying it is a no-op (CSS vars stay at their stylesheet defaults).
|
||||
* Other themes override only what they change.
|
||||
* The `default` theme mirrors LENS_0 (canonical Hermes teal) exactly — the
|
||||
* same triplet `src/index.css` declares on `:root`. Applying it should be a
|
||||
* visual no-op; other themes override the triplet + warm-glow and let the DS
|
||||
* cascade handle every derived surface.
|
||||
*
|
||||
* Theme names must stay in sync with the backend's
|
||||
* `_BUILTIN_DASHBOARD_THEMES` list in `hermes_cli/web_server.py`.
|
||||
*/
|
||||
|
||||
export const defaultTheme: DashboardTheme = {
|
||||
name: "default",
|
||||
label: "Hermes Teal",
|
||||
description: "Classic dark teal — the canonical Hermes look",
|
||||
colors: {
|
||||
background: "#041C1C",
|
||||
foreground: "#ffe6cb",
|
||||
card: "#062424",
|
||||
"card-foreground": "#ffe6cb",
|
||||
primary: "#ffe6cb",
|
||||
"primary-foreground": "#041C1C",
|
||||
secondary: "#0a2e2e",
|
||||
"secondary-foreground": "#ffe6cb",
|
||||
muted: "#083030",
|
||||
"muted-foreground": "#8aaa9a",
|
||||
accent: "#0c3838",
|
||||
"accent-foreground": "#ffe6cb",
|
||||
destructive: "#fb2c36",
|
||||
"destructive-foreground": "#fff",
|
||||
success: "#4ade80",
|
||||
warning: "#ffbd38",
|
||||
border: "color-mix(in srgb, #ffe6cb 15%, transparent)",
|
||||
input: "color-mix(in srgb, #ffe6cb 15%, transparent)",
|
||||
ring: "#ffe6cb",
|
||||
popover: "#062424",
|
||||
"popover-foreground": "#ffe6cb",
|
||||
},
|
||||
overlay: {
|
||||
noiseOpacity: 0.10,
|
||||
noiseBlendMode: "color-dodge",
|
||||
warmGlowOpacity: 0.22,
|
||||
warmGlowColor: "rgba(255,189,56,0.35)",
|
||||
palette: {
|
||||
background: { hex: "#041c1c", alpha: 1 },
|
||||
midground: { hex: "#ffe6cb", alpha: 1 },
|
||||
foreground: { hex: "#ffffff", alpha: 0 },
|
||||
warmGlow: "rgba(255, 189, 56, 0.35)",
|
||||
noiseOpacity: 1,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -47,34 +29,12 @@ export const midnightTheme: DashboardTheme = {
|
|||
name: "midnight",
|
||||
label: "Midnight",
|
||||
description: "Deep blue-violet with cool accents",
|
||||
colors: {
|
||||
background: "#0a0a1a",
|
||||
foreground: "#e0e0f0",
|
||||
card: "#10102a",
|
||||
"card-foreground": "#e0e0f0",
|
||||
primary: "#a78bfa",
|
||||
"primary-foreground": "#0a0a1a",
|
||||
secondary: "#151530",
|
||||
"secondary-foreground": "#e0e0f0",
|
||||
muted: "#1a1a3a",
|
||||
"muted-foreground": "#8888bb",
|
||||
accent: "#1e1e44",
|
||||
"accent-foreground": "#e0e0f0",
|
||||
destructive: "#f43f5e",
|
||||
"destructive-foreground": "#fff",
|
||||
success: "#34d399",
|
||||
warning: "#fbbf24",
|
||||
border: "color-mix(in srgb, #a78bfa 15%, transparent)",
|
||||
input: "color-mix(in srgb, #a78bfa 15%, transparent)",
|
||||
ring: "#a78bfa",
|
||||
popover: "#10102a",
|
||||
"popover-foreground": "#e0e0f0",
|
||||
},
|
||||
overlay: {
|
||||
noiseOpacity: 0.08,
|
||||
noiseBlendMode: "color-dodge",
|
||||
warmGlowOpacity: 0.15,
|
||||
warmGlowColor: "rgba(120,80,220,0.3)",
|
||||
palette: {
|
||||
background: { hex: "#0a0a1f", alpha: 1 },
|
||||
midground: { hex: "#d4c8ff", alpha: 1 },
|
||||
foreground: { hex: "#ffffff", alpha: 0 },
|
||||
warmGlow: "rgba(167, 139, 250, 0.32)",
|
||||
noiseOpacity: 0.8,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -82,34 +42,12 @@ export const emberTheme: DashboardTheme = {
|
|||
name: "ember",
|
||||
label: "Ember",
|
||||
description: "Warm crimson and bronze — forge vibes",
|
||||
colors: {
|
||||
background: "#1a0a0a",
|
||||
foreground: "#fde8d0",
|
||||
card: "#241010",
|
||||
"card-foreground": "#fde8d0",
|
||||
primary: "#f97316",
|
||||
"primary-foreground": "#1a0a0a",
|
||||
secondary: "#2a1515",
|
||||
"secondary-foreground": "#fde8d0",
|
||||
muted: "#301818",
|
||||
"muted-foreground": "#b08878",
|
||||
accent: "#381e1e",
|
||||
"accent-foreground": "#fde8d0",
|
||||
destructive: "#ef4444",
|
||||
"destructive-foreground": "#fff",
|
||||
success: "#4ade80",
|
||||
warning: "#fbbf24",
|
||||
border: "color-mix(in srgb, #f97316 15%, transparent)",
|
||||
input: "color-mix(in srgb, #f97316 15%, transparent)",
|
||||
ring: "#f97316",
|
||||
popover: "#241010",
|
||||
"popover-foreground": "#fde8d0",
|
||||
},
|
||||
overlay: {
|
||||
noiseOpacity: 0.10,
|
||||
noiseBlendMode: "color-dodge",
|
||||
warmGlowOpacity: 0.25,
|
||||
warmGlowColor: "rgba(249,115,22,0.3)",
|
||||
palette: {
|
||||
background: { hex: "#1a0a06", alpha: 1 },
|
||||
midground: { hex: "#ffd8b0", alpha: 1 },
|
||||
foreground: { hex: "#ffffff", alpha: 0 },
|
||||
warmGlow: "rgba(249, 115, 22, 0.38)",
|
||||
noiseOpacity: 1,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -117,34 +55,12 @@ export const monoTheme: DashboardTheme = {
|
|||
name: "mono",
|
||||
label: "Mono",
|
||||
description: "Clean grayscale — minimal and focused",
|
||||
colors: {
|
||||
background: "#111111",
|
||||
foreground: "#e0e0e0",
|
||||
card: "#1a1a1a",
|
||||
"card-foreground": "#e0e0e0",
|
||||
primary: "#e0e0e0",
|
||||
"primary-foreground": "#111111",
|
||||
secondary: "#1e1e1e",
|
||||
"secondary-foreground": "#e0e0e0",
|
||||
muted: "#222222",
|
||||
"muted-foreground": "#888888",
|
||||
accent: "#2a2a2a",
|
||||
"accent-foreground": "#e0e0e0",
|
||||
destructive: "#ef4444",
|
||||
"destructive-foreground": "#fff",
|
||||
success: "#4ade80",
|
||||
warning: "#fbbf24",
|
||||
border: "color-mix(in srgb, #e0e0e0 12%, transparent)",
|
||||
input: "color-mix(in srgb, #e0e0e0 12%, transparent)",
|
||||
ring: "#e0e0e0",
|
||||
popover: "#1a1a1a",
|
||||
"popover-foreground": "#e0e0e0",
|
||||
},
|
||||
overlay: {
|
||||
noiseOpacity: 0.06,
|
||||
noiseBlendMode: "color-dodge",
|
||||
warmGlowOpacity: 0.0,
|
||||
warmGlowColor: "rgba(255,255,255,0)",
|
||||
palette: {
|
||||
background: { hex: "#0e0e0e", alpha: 1 },
|
||||
midground: { hex: "#eaeaea", alpha: 1 },
|
||||
foreground: { hex: "#ffffff", alpha: 0 },
|
||||
warmGlow: "rgba(255, 255, 255, 0.1)",
|
||||
noiseOpacity: 0.6,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -152,34 +68,12 @@ export const cyberpunkTheme: DashboardTheme = {
|
|||
name: "cyberpunk",
|
||||
label: "Cyberpunk",
|
||||
description: "Neon green on black — matrix terminal",
|
||||
colors: {
|
||||
background: "#050505",
|
||||
foreground: "#00ff88",
|
||||
card: "#0a0a0a",
|
||||
"card-foreground": "#00ff88",
|
||||
primary: "#00ff88",
|
||||
"primary-foreground": "#050505",
|
||||
secondary: "#0e0e0e",
|
||||
"secondary-foreground": "#00ff88",
|
||||
muted: "#121212",
|
||||
"muted-foreground": "#00aa55",
|
||||
accent: "#161616",
|
||||
"accent-foreground": "#00ff88",
|
||||
destructive: "#ff0055",
|
||||
"destructive-foreground": "#fff",
|
||||
success: "#00ff88",
|
||||
warning: "#ffff00",
|
||||
border: "color-mix(in srgb, #00ff88 12%, transparent)",
|
||||
input: "color-mix(in srgb, #00ff88 12%, transparent)",
|
||||
ring: "#00ff88",
|
||||
popover: "#0a0a0a",
|
||||
"popover-foreground": "#00ff88",
|
||||
},
|
||||
overlay: {
|
||||
noiseOpacity: 0.12,
|
||||
noiseBlendMode: "color-dodge",
|
||||
warmGlowOpacity: 0.10,
|
||||
warmGlowColor: "rgba(0,255,136,0.15)",
|
||||
palette: {
|
||||
background: { hex: "#040608", alpha: 1 },
|
||||
midground: { hex: "#9bffcf", alpha: 1 },
|
||||
foreground: { hex: "#ffffff", alpha: 0 },
|
||||
warmGlow: "rgba(0, 255, 136, 0.22)",
|
||||
noiseOpacity: 1.2,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -187,38 +81,15 @@ export const roseTheme: DashboardTheme = {
|
|||
name: "rose",
|
||||
label: "Rosé",
|
||||
description: "Soft pink and warm ivory — easy on the eyes",
|
||||
colors: {
|
||||
background: "#1a1015",
|
||||
foreground: "#f5e6e0",
|
||||
card: "#221820",
|
||||
"card-foreground": "#f5e6e0",
|
||||
primary: "#f9a8d4",
|
||||
"primary-foreground": "#1a1015",
|
||||
secondary: "#281e28",
|
||||
"secondary-foreground": "#f5e6e0",
|
||||
muted: "#2e2230",
|
||||
"muted-foreground": "#b08898",
|
||||
accent: "#352838",
|
||||
"accent-foreground": "#f5e6e0",
|
||||
destructive: "#fb2c36",
|
||||
"destructive-foreground": "#fff",
|
||||
success: "#4ade80",
|
||||
warning: "#fbbf24",
|
||||
border: "color-mix(in srgb, #f9a8d4 14%, transparent)",
|
||||
input: "color-mix(in srgb, #f9a8d4 14%, transparent)",
|
||||
ring: "#f9a8d4",
|
||||
popover: "#221820",
|
||||
"popover-foreground": "#f5e6e0",
|
||||
},
|
||||
overlay: {
|
||||
noiseOpacity: 0.08,
|
||||
noiseBlendMode: "color-dodge",
|
||||
warmGlowOpacity: 0.18,
|
||||
warmGlowColor: "rgba(249,168,212,0.2)",
|
||||
palette: {
|
||||
background: { hex: "#1a0f15", alpha: 1 },
|
||||
midground: { hex: "#ffd4e1", alpha: 1 },
|
||||
foreground: { hex: "#ffffff", alpha: 0 },
|
||||
warmGlow: "rgba(249, 168, 212, 0.3)",
|
||||
noiseOpacity: 0.9,
|
||||
},
|
||||
};
|
||||
|
||||
/** All built-in themes, keyed by name. */
|
||||
export const BUILTIN_THEMES: Record<string, DashboardTheme> = {
|
||||
default: defaultTheme,
|
||||
midnight: midnightTheme,
|
||||
|
|
|
|||
|
|
@ -1,44 +1,44 @@
|
|||
/** Dashboard theme definition. Maps 1:1 to CSS custom properties in index.css. */
|
||||
export interface ThemeColors {
|
||||
background: string;
|
||||
foreground: string;
|
||||
card: string;
|
||||
"card-foreground": string;
|
||||
primary: string;
|
||||
"primary-foreground": string;
|
||||
secondary: string;
|
||||
"secondary-foreground": string;
|
||||
muted: string;
|
||||
"muted-foreground": string;
|
||||
accent: string;
|
||||
"accent-foreground": string;
|
||||
destructive: string;
|
||||
"destructive-foreground": string;
|
||||
success: string;
|
||||
warning: string;
|
||||
border: string;
|
||||
input: string;
|
||||
ring: string;
|
||||
popover: string;
|
||||
"popover-foreground": string;
|
||||
/**
|
||||
* Dashboard theme model.
|
||||
*
|
||||
* Unlike the pre-DS implementation (which overrode 21 shadcn tokens directly),
|
||||
* themes are now expressed in the Nous DS's own 3-triplet vocabulary —
|
||||
* `background`, `midground`, `foreground` — plus a warm-glow tint for the
|
||||
* vignette in <Backdrop />. All downstream shadcn-compat tokens
|
||||
* (`--color-card`, `--color-muted-foreground`, `--color-border`, etc.) are
|
||||
* defined in `src/index.css` as `color-mix()` expressions over the triplets,
|
||||
* so overriding the triplets at runtime cascades to every surface.
|
||||
*/
|
||||
|
||||
/** A color layer: hex base + alpha (0–1). */
|
||||
export interface ThemeLayer {
|
||||
alpha: number;
|
||||
hex: string;
|
||||
}
|
||||
|
||||
export interface ThemeOverlay {
|
||||
noiseOpacity?: number;
|
||||
noiseBlendMode?: string;
|
||||
warmGlowOpacity?: number;
|
||||
warmGlowColor?: string;
|
||||
export interface ThemePalette {
|
||||
/** Deepest canvas color (typically near-black). */
|
||||
background: ThemeLayer;
|
||||
/** Primary text + accent. Most UI chrome reads this. */
|
||||
midground: ThemeLayer;
|
||||
/** Top-layer highlight. In LENS_0 this is white @ alpha 0 — invisible by
|
||||
* default but still drives `--color-ring`-style accents. */
|
||||
foreground: ThemeLayer;
|
||||
/** Warm vignette color for <Backdrop />, as an rgba() string. */
|
||||
warmGlow: string;
|
||||
/** Scalar multiplier (0–1.2) on the noise overlay. Lower for softer themes
|
||||
* like Mono and Rosé, higher for grittier themes like Cyberpunk. */
|
||||
noiseOpacity: number;
|
||||
}
|
||||
|
||||
export interface DashboardTheme {
|
||||
name: string;
|
||||
label: string;
|
||||
description: string;
|
||||
colors: ThemeColors;
|
||||
overlay?: ThemeOverlay;
|
||||
label: string;
|
||||
name: string;
|
||||
palette: ThemePalette;
|
||||
}
|
||||
|
||||
export interface ThemeListResponse {
|
||||
themes: Array<{ name: string; label: string; description: string }>;
|
||||
active: string;
|
||||
themes: Array<{ description: string; label: string; name: string }>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,58 @@
|
|||
import { defineConfig } from "vite";
|
||||
import { defineConfig, type Plugin } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import path from "path";
|
||||
|
||||
const BACKEND = process.env.HERMES_DASHBOARD_URL ?? "http://127.0.0.1:9119";
|
||||
|
||||
/**
|
||||
* In production the Python `hermes dashboard` server injects a one-shot
|
||||
* session token into `index.html` (see `hermes_cli/web_server.py`). The
|
||||
* Vite dev server serves its own `index.html`, so unless we forward that
|
||||
* token, every protected `/api/*` call 401s.
|
||||
*
|
||||
* This plugin fetches the running dashboard's `index.html` on each dev page
|
||||
* load, scrapes the `window.__HERMES_SESSION_TOKEN__` assignment, and
|
||||
* re-injects it into the dev HTML. No-op in production builds.
|
||||
*/
|
||||
function hermesDevToken(): Plugin {
|
||||
const TOKEN_RE = /window\.__HERMES_SESSION_TOKEN__\s*=\s*"([^"]+)"/;
|
||||
|
||||
return {
|
||||
name: "hermes:dev-session-token",
|
||||
apply: "serve",
|
||||
async transformIndexHtml() {
|
||||
try {
|
||||
const res = await fetch(BACKEND, { headers: { accept: "text/html" } });
|
||||
const html = await res.text();
|
||||
const match = html.match(TOKEN_RE);
|
||||
if (!match) {
|
||||
console.warn(
|
||||
`[hermes] Could not find session token in ${BACKEND} — ` +
|
||||
`is \`hermes dashboard\` running? /api calls will 401.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
return [
|
||||
{
|
||||
tag: "script",
|
||||
injectTo: "head",
|
||||
children: `window.__HERMES_SESSION_TOKEN__="${match[1]}";`,
|
||||
},
|
||||
];
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`[hermes] Dashboard at ${BACKEND} unreachable — ` +
|
||||
`start it with \`hermes dashboard\` or set HERMES_DASHBOARD_URL. ` +
|
||||
`(${(err as Error).message})`,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
plugins: [react(), tailwindcss(), hermesDevToken()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
|
@ -16,7 +64,7 @@ export default defineConfig({
|
|||
},
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": "http://127.0.0.1:9119",
|
||||
"/api": BACKEND,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue