diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bc4b34cd..21896949 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -48,6 +48,7 @@ "eslint-plugin-react-refresh": "^0.4.0", "globals": "^15.0.0", "jsdom": "^25.0.1", + "tsx": "^4.19.2", "typescript": "~5.6.2", "typescript-eslint": "^8.0.0", "vite": "^6.0.5", @@ -225,6 +226,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", @@ -1847,6 +1849,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1870,6 +1873,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -3316,6 +3320,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.95.2.tgz", "integrity": "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.95.2" }, @@ -3439,8 +3444,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -3658,6 +3662,7 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -3669,6 +3674,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -3732,6 +3738,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", @@ -4170,6 +4177,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4541,6 +4549,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5124,7 +5133,8 @@ "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -5259,8 +5269,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -5546,6 +5555,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", @@ -6188,6 +6198,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -7293,7 +7316,8 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/leaflet-draw": { "version": "1.0.4", @@ -7427,7 +7451,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -8044,7 +8067,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -8060,7 +8082,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -8768,6 +8789,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -8792,6 +8814,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -8805,8 +8828,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-leaflet": { "version": "4.2.1", @@ -9117,6 +9139,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve-protobuf-schema": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", @@ -9132,6 +9164,7 @@ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -10177,6 +10210,510 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -10287,6 +10824,7 @@ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10510,6 +11048,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -10639,6 +11178,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -11078,6 +11618,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11252,6 +11793,7 @@ "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, diff --git a/frontend/package.json b/frontend/package.json index c3f41913..af0fc463 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,7 +5,9 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc -b && vite build", + "build": "npm run build:tokens && tsc -b && vite build", + "build:tokens": "tsx scripts/build-tokens.ts", + "build:tokens:watch": "tsx watch scripts/build-tokens.ts", "lint": "eslint .", "preview": "vite preview", "test": "vitest run --config vitest.config.ts", @@ -54,6 +56,7 @@ "jsdom": "^25.0.1", "typescript": "~5.6.2", "typescript-eslint": "^8.0.0", + "tsx": "^4.19.2", "vite": "^6.0.5", "vite-plugin-pwa": "^1.2.0", "vitest": "^3.0.5" diff --git a/frontend/scripts/build-tokens.ts b/frontend/scripts/build-tokens.ts new file mode 100644 index 00000000..7f621d94 --- /dev/null +++ b/frontend/scripts/build-tokens.ts @@ -0,0 +1,183 @@ +/** + * Token generator — TS → CSS. + * + * Reads `src/design-system/tokens/index.ts` and emits + * `src/styles/tokens.css` with: + * • theme-independent vars under `:root` + * • dark theme semantics under `:root` (default) and `[data-theme="dark"]` + * • light theme semantics under `[data-theme="light"]` + * + * Run: `npm run build:tokens` + */ + +import { writeFileSync, mkdirSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { + tokens, + themeTokens, + semanticDark, + semanticLight, + fontScale, + cropColors, +} from '../src/design-system/tokens/index.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const outPath = resolve(__dirname, '../src/styles/tokens.css'); + +mkdirSync(dirname(outPath), { recursive: true }); + +type CssVar = readonly [name: string, value: string, comment?: string]; + +const sectionHeader = (label: string): string => + `\n /* ── ${label} ─────────────────────────────────────────────────── */\n`; + +const renderVar = ([name, value]: CssVar): string => ` --${name}: ${value};`; + +const renderBlock = (selector: string, sections: Array<[string, CssVar[]]>): string => { + const body = sections + .filter(([, vars]) => vars.length > 0) + .map(([label, vars]) => sectionHeader(label) + vars.map(renderVar).join('\n')) + .join('\n'); + return `${selector} {${body}\n}\n`; +}; + +// ─── Theme-independent token blocks ──────────────────────────────────────── + +const spacingVars: CssVar[] = Object.entries(tokens.spacing).map( + ([k, v]) => [`space-${k.replace('_', '-')}`, v] as const +); + +const radiusVars: CssVar[] = Object.entries(tokens.radius).map( + ([k, v]) => [`radius-${k}`, v] as const +); + +const fontFamilyVars: CssVar[] = [ + ['font-sans', tokens.fontFamilies.sans], + ['font-mono', tokens.fontFamilies.mono], + ['font-tabular', tokens.fontFamilies.tabular], +]; + +const fontWeightVars: CssVar[] = Object.entries(tokens.fontWeights).map( + ([k, v]) => [`font-weight-${k}`, String(v)] as const +); + +const fontScaleVars: CssVar[] = Object.entries(fontScale).flatMap(([k, step]) => { + const out: CssVar[] = [ + [`font-size-${k}`, step.size], + [`line-height-${k}`, String(step.lineHeight)], + ]; + if (step.weight) out.push([`font-weight-${k}-step`, String(step.weight)]); + if (step.letterSpacing) out.push([`letter-spacing-${k}`, step.letterSpacing]); + return out; +}); + +const letterSpacingVars: CssVar[] = Object.entries(tokens.letterSpacing).map( + ([k, v]) => [`tracking-${k}`, v] as const +); + +const motionVars: CssVar[] = [ + ...Object.entries(tokens.duration).map(([k, v]) => [`duration-${k}`, v] as const), + ...Object.entries(tokens.easing).map(([k, v]) => [`easing-${k}`, v] as const), +]; + +const zIndexVars: CssVar[] = Object.entries(tokens.zIndex).map( + ([k, v]) => [`z-${k}`, String(v)] as const +); + +const breakpointVars: CssVar[] = Object.entries(tokens.breakpoints).map( + ([k, v]) => [`bp-${k}`, v] as const +); + +const cropVars: CssVar[] = Object.entries(cropColors).map( + ([k, v]) => [`crop-${k}`, v] as const +); + +const glowVars: CssVar[] = Object.entries(tokens.glow).map( + ([k, v]) => [`glow-${k}`, v] as const +); + +// ─── Theme-aware semantic blocks ─────────────────────────────────────────── + +const buildSemantic = (theme: typeof semanticDark | typeof semanticLight): CssVar[] => + Object.entries(theme).map(([k, v]) => [k, v] as const); + +const buildElevation = ( + theme: typeof themeTokens.dark.elevation | typeof themeTokens.light.elevation +): CssVar[] => + Object.entries(theme).map(([level, value]) => [`shadow-${level}`, value] as const); + +// ─── Legacy aliases (preserve every var name from the previous tokens.css) ── + +const legacyAliases: CssVar[] = [ + ['bg-app', 'var(--bg-page)'], + ['bg-overlay', 'var(--bg-hover)'], + ['bg-subtle', 'var(--bg-elevated)'], + ['bg-base', 'var(--bg-page)'], + ['color-page-bg', 'var(--bg-page)'], + ['color-card-bg', 'var(--bg-surface)'], + ['color-elevated-bg', 'var(--bg-elevated)'], + ['color-input-bg', 'var(--bg-elevated)'], + ['color-hover-bg', 'var(--bg-hover)'], + ['color-primary', 'var(--brand)'], + ['color-primary-hover', 'var(--brand-hover)'], + ['border-default', 'var(--border)'], + ['shadow-sm', 'var(--shadow-1)'], + ['shadow-md', 'var(--shadow-2)'], + ['shadow-lg', 'var(--shadow-4)'], + ['shadow-card', 'var(--shadow-2)'], + ['shadow-glow', 'var(--glow-brand)'], + ['transition-fast', 'var(--duration-fast) var(--easing-standard)'], + ['transition-base', 'var(--duration-base) var(--easing-standard)'], + ['transition-slow', 'var(--duration-slow) var(--easing-standard)'], +]; + +// ─── Render the file ─────────────────────────────────────────────────────── + +const banner = `/* + * AUTO-GENERATED — DO NOT EDIT + * + * Source of truth: src/design-system/tokens/*.ts + * Generator: scripts/build-tokens.ts + * Re-generate: npm run build:tokens + * + * This file is committed (not ignored) so HMR/SSR pick it up immediately. + * Always regenerate after editing the TS sources, or CI will fail the diff. + */ + +`; + +const rootBlock = renderBlock(':root', [ + ['Spacing scale (4 px grid)', spacingVars], + ['Radius', radiusVars], + ['Typography — families', fontFamilyVars], + ['Typography — weights', fontWeightVars], + ['Typography — fluid scale', fontScaleVars], + ['Typography — tracking', letterSpacingVars], + ['Motion', motionVars], + ['Breakpoints (informational)', breakpointVars], + ['Z-index', zIndexVars], + ['Crop palette', cropVars], + ['Glow effects', glowVars], + ['Theme — dark (default)', buildSemantic(semanticDark)], + ['Elevation — dark (default)', buildElevation(themeTokens.dark.elevation)], + ['Legacy aliases — DO NOT use in new code', legacyAliases], +]); + +const darkBlock = renderBlock(`[data-theme='dark']`, [ + ['Theme — dark (explicit)', buildSemantic(semanticDark)], + ['Elevation — dark (explicit)', buildElevation(themeTokens.dark.elevation)], +]); + +const lightBlock = renderBlock(`[data-theme='light']`, [ + ['Theme — light', buildSemantic(semanticLight)], + ['Elevation — light', buildElevation(themeTokens.light.elevation)], +]); + +const css = banner + rootBlock + '\n' + darkBlock + '\n' + lightBlock; + +writeFileSync(outPath, css, 'utf8'); + +const lineCount = css.split('\n').length; +console.log(`✓ tokens.css regenerated (${lineCount} lines) → ${outPath}`); diff --git a/frontend/src/design-system/tokens/breakpoints.ts b/frontend/src/design-system/tokens/breakpoints.ts new file mode 100644 index 00000000..d63fe42b --- /dev/null +++ b/frontend/src/design-system/tokens/breakpoints.ts @@ -0,0 +1,48 @@ +/** + * Responsive breakpoints (mobile-first min-widths). + * + * Conventions: + * xs — large phone, landscape + * sm — small tablet + * md — tablet + * lg — small desktop / laptop (sidebar collapses below this) + * xl — desktop + * 2xl — wide desktop / dashboard density floor + */ + +export const breakpoints = { + xs: '480px', + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', +} as const; + +export const breakpointsPx = { + xs: 480, + sm: 640, + md: 768, + lg: 1024, + xl: 1280, + '2xl': 1536, +} as const; + +export type BreakpointKey = keyof typeof breakpoints; + +/** + * Pre-baked media-query strings — for use in CSS-in-JS or template literals. + * Use container queries for component-level breakpoints where possible. + */ +export const media = { + xs: `(min-width: ${breakpoints.xs})`, + sm: `(min-width: ${breakpoints.sm})`, + md: `(min-width: ${breakpoints.md})`, + lg: `(min-width: ${breakpoints.lg})`, + xl: `(min-width: ${breakpoints.xl})`, + '2xl': `(min-width: ${breakpoints['2xl']})`, + hover: '(hover: hover) and (pointer: fine)', + reducedMotion: '(prefers-reduced-motion: reduce)', + prefersDark: '(prefers-color-scheme: dark)', + prefersLight: '(prefers-color-scheme: light)', +} as const; diff --git a/frontend/src/design-system/tokens/colors.ts b/frontend/src/design-system/tokens/colors.ts new file mode 100644 index 00000000..da7a1a45 --- /dev/null +++ b/frontend/src/design-system/tokens/colors.ts @@ -0,0 +1,179 @@ +/** + * Color primitives + theme-aware semantic tokens. + * + * Architecture: + * palette → raw colors (immutable, never used directly in CSS) + * semanticDark → semantic tokens (dark theme values) + * semanticLight → semantic tokens (light theme values) + * cropColors → domain-specific palette (theme-independent) + * + * Keys map 1:1 to CSS variable names (without `--` prefix). + */ + +export const palette = { + green: { 400: '#22C55E', 500: '#16A34A', 600: '#15803D', 700: '#14532D' }, + red: { 400: '#F87171', 500: '#EF4444', 600: '#DC2626' }, + amber: { 400: '#FBBF24', 500: '#F59E0B', 600: '#D97706' }, + blue: { 400: '#60A5FA', 500: '#3B82F6', 600: '#2563EB' }, + purple: { 400: '#C084FC', 500: '#A855F7', 600: '#9333EA' }, + teal: { 500: '#14B8A6' }, + sky: { 500: '#0EA5E9' }, + orange: { 500: '#F97316' }, + pink: { 500: '#EC4899' }, + indigo: { 500: '#6366F1' }, + navy: { 950: '#060B14', 900: '#0a0a0a', 850: '#101010', 800: '#161616', 700: '#1c1c1c' }, + slate: { 50: '#FAFAFA', 100: '#F5F5F5', 200: '#E5E5E5', 400: '#94A3B8' }, +} as const; + +/** Domain palette for crop visualisation (same in both themes). */ +export const cropColors = { + wheat: palette.amber[400], + sunflower: palette.orange[500], + corn: palette.green[400], + rapeseed: palette.purple[500], + barley: palette.sky[500], + soy: palette.teal[500], + fallow: palette.slate[400], +} as const; + +/** Recharts / data-viz palette ordered for max contrast. */ +export const chartColors = [ + palette.green[400], + palette.blue[500], + palette.amber[500], + palette.purple[500], + palette.teal[500], + palette.orange[500], + palette.pink[500], + palette.sky[500], +] as const; + +/** + * Semantic tokens — dark theme. + * Keys are CSS-var names verbatim. + */ +export const semanticDark = { + // Brand + 'brand': palette.green[400], + 'brand-hover': palette.green[500], + 'brand-active': palette.green[600], + 'brand-glow': 'rgba(34, 197, 94, 0.15)', + 'brand-muted': 'rgba(34, 197, 94, 0.08)', + 'brand-border': 'rgba(34, 197, 94, 0.20)', + + // Surfaces + 'bg-page': palette.navy[900], + 'bg-surface': palette.navy[850], + 'bg-elevated': palette.navy[800], + 'bg-hover': palette.navy[700], + + // Card decorations + 'card-bg': 'rgba(255, 255, 255, 0.03)', + 'card-gradient': 'linear-gradient(180deg, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0.01) 100%)', + + // Text + 'text-primary': 'rgba(255, 255, 255, 0.94)', + 'text-secondary': 'rgba(255, 255, 255, 0.58)', + 'text-tertiary': 'rgba(255, 255, 255, 0.38)', + 'text-disabled': 'rgba(255, 255, 255, 0.20)', + 'text-inverse': '#0a0a0a', + + // Borders + 'border': 'rgba(255, 255, 255, 0.08)', + 'border-hover': 'rgba(255, 255, 255, 0.14)', + 'border-strong': 'rgba(255, 255, 255, 0.22)', + 'border-focus': palette.green[400], + + // Status (foreground) + 'success': palette.green[400], + 'warning': palette.amber[500], + 'error': palette.red[500], + 'info': palette.blue[500], + 'neutral': palette.indigo[500], + + // Status (muted backgrounds for inline emphasis) + 'success-bg': 'rgba(34, 197, 94, 0.08)', + 'warning-bg': 'rgba(245, 158, 11, 0.08)', + 'error-bg': 'rgba(239, 68, 68, 0.08)', + 'info-bg': 'rgba(59, 130, 246, 0.08)', + + // Accent aliases (kept for analytics screens) + 'accent-revenue': palette.green[400], + 'accent-cost': palette.red[500], + 'accent-info': palette.blue[500], + 'accent-warning': palette.amber[500], + 'accent-premium': palette.purple[500], + + // Focus ring (for non-input elements) + 'focus-ring': '0 0 0 3px rgba(34, 197, 94, 0.35)', + 'focus-ring-error': '0 0 0 3px rgba(239, 68, 68, 0.35)', + + // Selection + 'selection-bg': 'rgba(34, 197, 94, 0.25)', +} as const; + +/** + * Semantic tokens — light theme. + * Same shape as semanticDark; values inverted for light surfaces. + */ +export const semanticLight: Record = { + // Brand (slightly darker on light to preserve contrast) + 'brand': palette.green[500], + 'brand-hover': palette.green[600], + 'brand-active': palette.green[700], + 'brand-glow': 'rgba(22, 163, 74, 0.18)', + 'brand-muted': 'rgba(22, 163, 74, 0.10)', + 'brand-border': 'rgba(22, 163, 74, 0.25)', + + // Surfaces + 'bg-page': palette.slate[50], + 'bg-surface': '#ffffff', + 'bg-elevated': '#ffffff', + 'bg-hover': palette.slate[100], + + // Card decorations + 'card-bg': '#ffffff', + 'card-gradient': 'linear-gradient(180deg, #ffffff 0%, #fcfcfd 100%)', + + // Text + 'text-primary': 'rgba(15, 23, 42, 0.92)', + 'text-secondary': 'rgba(15, 23, 42, 0.62)', + 'text-tertiary': 'rgba(15, 23, 42, 0.42)', + 'text-disabled': 'rgba(15, 23, 42, 0.24)', + 'text-inverse': '#ffffff', + + // Borders + 'border': 'rgba(15, 23, 42, 0.08)', + 'border-hover': 'rgba(15, 23, 42, 0.14)', + 'border-strong': 'rgba(15, 23, 42, 0.22)', + 'border-focus': palette.green[500], + + // Status + 'success': palette.green[500], + 'warning': palette.amber[600], + 'error': palette.red[600], + 'info': palette.blue[600], + 'neutral': palette.indigo[500], + + // Status (muted backgrounds) + 'success-bg': 'rgba(22, 163, 74, 0.10)', + 'warning-bg': 'rgba(217, 119, 6, 0.10)', + 'error-bg': 'rgba(220, 38, 38, 0.10)', + 'info-bg': 'rgba(37, 99, 235, 0.10)', + + // Accent aliases + 'accent-revenue': palette.green[500], + 'accent-cost': palette.red[600], + 'accent-info': palette.blue[600], + 'accent-warning': palette.amber[600], + 'accent-premium': palette.purple[600], + + // Focus ring + 'focus-ring': '0 0 0 3px rgba(22, 163, 74, 0.30)', + 'focus-ring-error': '0 0 0 3px rgba(220, 38, 38, 0.25)', + + // Selection + 'selection-bg': 'rgba(22, 163, 74, 0.18)', +}; + +export type SemanticTokenKey = keyof typeof semanticDark; diff --git a/frontend/src/design-system/tokens/elevation.ts b/frontend/src/design-system/tokens/elevation.ts new file mode 100644 index 00000000..4e550db0 --- /dev/null +++ b/frontend/src/design-system/tokens/elevation.ts @@ -0,0 +1,40 @@ +/** + * Elevation system — layered shadows. + * + * Each level combines: + * • a hairline border (1 px inset-style stroke) — defines the edge + * • a soft ambient shadow — defines the volume + * • on higher levels, a sharper key shadow — defines the lift + * + * Levels: + * 0 flush / no elevation + * 1 subtle hover state, inputs + * 2 cards, panels + * 3 popovers, dropdowns + * 4 modals, sheets + * 5 toasts, command palette (top of the stack) + */ + +export const elevationDark = { + 0: 'none', + 1: '0 1px 2px rgba(0, 0, 0, 0.30)', + 2: '0 0 0 1px rgba(255, 255, 255, 0.06), 0 4px 16px rgba(0, 0, 0, 0.40)', + 3: '0 0 0 1px rgba(255, 255, 255, 0.08), 0 8px 24px rgba(0, 0, 0, 0.45)', + 4: '0 0 0 1px rgba(255, 255, 255, 0.10), 0 16px 48px rgba(0, 0, 0, 0.50)', + 5: '0 0 0 1px rgba(255, 255, 255, 0.12), 0 24px 80px rgba(0, 0, 0, 0.55)', +} as const; + +export const elevationLight = { + 0: 'none', + 1: '0 1px 2px rgba(15, 23, 42, 0.06)', + 2: '0 1px 3px rgba(15, 23, 42, 0.05), 0 4px 12px rgba(15, 23, 42, 0.06)', + 3: '0 2px 6px rgba(15, 23, 42, 0.07), 0 8px 24px rgba(15, 23, 42, 0.08)', + 4: '0 4px 12px rgba(15, 23, 42, 0.09), 0 16px 48px rgba(15, 23, 42, 0.10)', + 5: '0 6px 20px rgba(15, 23, 42, 0.12), 0 24px 80px rgba(15, 23, 42, 0.14)', +} as const; + +export const glow = { + brand: '0 0 20px rgba(34, 197, 94, 0.15)', +} as const; + +export type ElevationLevel = keyof typeof elevationDark; diff --git a/frontend/src/design-system/tokens/index.ts b/frontend/src/design-system/tokens/index.ts new file mode 100644 index 00000000..543b49a7 --- /dev/null +++ b/frontend/src/design-system/tokens/index.ts @@ -0,0 +1,70 @@ +/** + * Design-system tokens — single source of truth. + * + * Edit any file in this folder and run `npm run build:tokens` to regenerate + * `src/styles/tokens.css`. Never edit `tokens.css` by hand. + * + * In TS code, prefer importing from this barrel: + * import { tokens, themeTokens, chartColors } from '@/design-system/tokens'; + */ + +import { + palette, + cropColors, + chartColors, + semanticDark, + semanticLight, +} from './colors'; +import { + fontFamilies, + fontWeights, + fontScale, + letterSpacing, +} from './typography'; +import { spacing } from './spacing'; +import { radius } from './radius'; +import { elevationDark, elevationLight, glow } from './elevation'; +import { duration, easing } from './motion'; +import { breakpoints, breakpointsPx, media } from './breakpoints'; +import { zIndex } from './zIndex'; + +export * from './colors'; +export * from './typography'; +export * from './spacing'; +export * from './radius'; +export * from './elevation'; +export * from './motion'; +export * from './breakpoints'; +export * from './zIndex'; + +/** + * Theme-independent tokens (same values across light/dark). + */ +export const tokens = { + palette, + cropColors, + chartColors, + spacing, + radius, + fontFamilies, + fontWeights, + fontScale, + letterSpacing, + duration, + easing, + breakpoints, + breakpointsPx, + media, + zIndex, + glow, +} as const; + +/** + * Theme-aware tokens — pick by current `data-theme` attribute. + */ +export const themeTokens = { + dark: { semantic: semanticDark, elevation: elevationDark }, + light: { semantic: semanticLight, elevation: elevationLight }, +} as const; + +export type ThemeName = keyof typeof themeTokens; diff --git a/frontend/src/design-system/tokens/motion.ts b/frontend/src/design-system/tokens/motion.ts new file mode 100644 index 00000000..fa36c431 --- /dev/null +++ b/frontend/src/design-system/tokens/motion.ts @@ -0,0 +1,37 @@ +/** + * Motion tokens — durations + easing curves. + * + * Duration semantics: + * instant — micro-interactions (hover state colour swap) + * fast — small property changes (button press, focus ring) + * base — most enter/exit transitions (popover, accordion) + * slow — page-level transitions (route change content fade) + * slower — emphasis transitions (sheet open, hero animation) + * + * Easing semantics: + * standard — neutral curve, suits most UI (Material "standard") + * decelerate — content arriving on screen + * accelerate — content leaving the screen + * emphasized — high-energy moments (sheet open, drawer slide) + * spring — playful overshoot (counter, badge pop) + */ + +export const duration = { + instant: '50ms', + fast: '120ms', + base: '180ms', + slow: '260ms', + slower: '400ms', +} as const; + +export const easing = { + linear: 'linear', + standard: 'cubic-bezier(0.2, 0, 0, 1)', + decelerate: 'cubic-bezier(0, 0, 0, 1)', + accelerate: 'cubic-bezier(0.3, 0, 1, 1)', + emphasized: 'cubic-bezier(0.05, 0.7, 0.1, 1)', + spring: 'cubic-bezier(0.34, 1.56, 0.64, 1)', +} as const; + +export type DurationKey = keyof typeof duration; +export type EasingKey = keyof typeof easing; diff --git a/frontend/src/design-system/tokens/radius.ts b/frontend/src/design-system/tokens/radius.ts new file mode 100644 index 00000000..39df0ee2 --- /dev/null +++ b/frontend/src/design-system/tokens/radius.ts @@ -0,0 +1,18 @@ +/** + * Border-radius tokens. + * `pill` is the canonical name for fully-rounded chips/badges. + */ + +export const radius = { + none: '0', + xs: '4px', + sm: '6px', + md: '8px', + lg: '12px', + xl: '16px', + '2xl': '24px', + pill: '9999px', + full: '9999px', +} as const; + +export type RadiusKey = keyof typeof radius; diff --git a/frontend/src/design-system/tokens/spacing.ts b/frontend/src/design-system/tokens/spacing.ts new file mode 100644 index 00000000..26be554a --- /dev/null +++ b/frontend/src/design-system/tokens/spacing.ts @@ -0,0 +1,29 @@ +/** + * Spacing scale on a 4 px base grid. + * + * Numeric keys map to multiples of 4: e.g. `space-2` = 8 px, `space-6` = 24 px. + * The half-step values (`0_5`, `1_5`) cover icon padding and tight inline gaps. + */ + +export const spacing = { + '0': '0', + 'px': '1px', + '0_5': '2px', + '1': '4px', + '1_5': '6px', + '2': '8px', + '3': '12px', + '4': '16px', + '5': '20px', + '6': '24px', + '7': '28px', + '8': '32px', + '10': '40px', + '12': '48px', + '14': '56px', + '16': '64px', + '20': '80px', + '24': '96px', +} as const; + +export type SpacingKey = keyof typeof spacing; diff --git a/frontend/src/design-system/tokens/typography.ts b/frontend/src/design-system/tokens/typography.ts new file mode 100644 index 00000000..9658dbd0 --- /dev/null +++ b/frontend/src/design-system/tokens/typography.ts @@ -0,0 +1,67 @@ +/** + * Typography tokens. + * + * Type scale uses clamp() for fluid responsive sizing across breakpoints. + * Lower bound = mobile size, upper bound = desktop size. + */ + +export const fontFamilies = { + sans: "'Geist', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", + mono: "'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace", + tabular: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif", +} as const; + +export const fontWeights = { + normal: 400, + medium: 500, + semibold: 600, + bold: 700, +} as const; + +/** + * Fluid type scale — `clamp(min, preferred, max)`. + * - `min` = floor on small viewports (≤ 360 px ish) + * - `max` = ceiling on large viewports (≥ 1280 px ish) + */ +export interface FontStep { + size: string; + lineHeight: number | string; + weight?: number; + letterSpacing?: string; + textTransform?: 'uppercase' | 'lowercase' | 'capitalize' | 'none'; +} + +export const fontScale: Record = { + // Body + xs: { size: 'clamp(11px, 0.69rem + 0.05vw, 12px)', lineHeight: 1.4 }, + sm: { size: 'clamp(12px, 0.75rem + 0.06vw, 13px)', lineHeight: 1.5 }, + base: { size: 'clamp(13px, 0.81rem + 0.07vw, 14px)', lineHeight: 1.5 }, + md: { size: 'clamp(14px, 0.88rem + 0.08vw, 15px)', lineHeight: 1.5 }, + lg: { size: 'clamp(15px, 0.94rem + 0.10vw, 17px)', lineHeight: 1.45 }, + + // Headings + h4: { size: 'clamp(15px, 0.94rem + 0.15vw, 17px)', lineHeight: 1.35, weight: 600 }, + h3: { size: 'clamp(17px, 1.05rem + 0.20vw, 19px)', lineHeight: 1.30, weight: 600 }, + h2: { size: 'clamp(20px, 1.20rem + 0.45vw, 24px)', lineHeight: 1.25, weight: 600 }, + h1: { size: 'clamp(24px, 1.50rem + 0.60vw, 32px)', lineHeight: 1.20, weight: 700, letterSpacing: '-0.01em' }, + display: { size: 'clamp(28px, 1.75rem + 1.00vw, 40px)', lineHeight: 1.10, weight: 700, letterSpacing: '-0.02em' }, + + // Specials + eyebrow: { + size: '11px', + lineHeight: 1.2, + weight: 600, + letterSpacing: '0.08em', + textTransform: 'uppercase', + }, +}; + +export const letterSpacing = { + tighter: '-0.02em', + tight: '-0.01em', + normal: '0', + wide: '0.04em', + wider: '0.08em', +} as const; + +export type FontScaleKey = keyof typeof fontScale; diff --git a/frontend/src/design-system/tokens/zIndex.ts b/frontend/src/design-system/tokens/zIndex.ts new file mode 100644 index 00000000..27b157a7 --- /dev/null +++ b/frontend/src/design-system/tokens/zIndex.ts @@ -0,0 +1,23 @@ +/** + * Z-index scale. + * + * Centralising these prevents the "stacking-context arms race". + * The numeric gaps allow inserting one-off layers without renumbering. + */ + +export const zIndex = { + hide: -1, + base: 0, + raised: 1, + dropdown: 1000, + sticky: 1100, + fixed: 1200, + drawer: 1300, + modal: 1400, + popover: 1500, + toast: 1600, + tooltip: 1700, + banner: 10000, +} as const; + +export type ZIndexKey = keyof typeof zIndex; diff --git a/frontend/src/styles/tokens.css b/frontend/src/styles/tokens.css index 467381f9..d8b35220 100644 --- a/frontend/src/styles/tokens.css +++ b/frontend/src/styles/tokens.css @@ -1,119 +1,311 @@ /* - * Design System Tokens — "Command Center" palette + * AUTO-GENERATED — DO NOT EDIT * - * Single source of truth for all CSS custom properties. - * :root = dark theme (current default). + * Source of truth: src/design-system/tokens/*.ts + * Generator: scripts/build-tokens.ts + * Re-generate: npm run build:tokens + * + * This file is committed (not ignored) so HMR/SSR pick it up immediately. + * Always regenerate after editing the TS sources, or CI will fail the diff. */ :root { - /* ── Brand ── */ - --brand: #22C55E; - --brand-hover: #16A34A; - --brand-glow: rgba(34, 197, 94, 0.15); - --brand-muted: rgba(34, 197, 94, 0.08); - --brand-border: rgba(34, 197, 94, 0.2); - - /* ── Backgrounds — 21st Dark Premium (matches AntD dark tokens) ── */ - --bg-page: #0a0a0a; - --bg-surface: #101010; - --bg-elevated: #161616; - --bg-hover: #1c1c1c; - - /* Legacy aliases (keep for compatibility) */ - --bg-app: var(--bg-page); - --bg-overlay: var(--bg-hover); - --bg-subtle: var(--bg-elevated); - --bg-base: var(--bg-page); - - /* Card-specific */ - --card-bg: rgba(255, 255, 255, 0.03); - --card-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.01) 100%); - - /* ── Text — aligned with AntD dark tokens ── */ - --text-primary: rgba(255, 255, 255, 0.94); + /* ── Spacing scale (4 px grid) ─────────────────────────────────────────────────── */ + --space-0: 0; + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-7: 28px; + --space-8: 32px; + --space-10: 40px; + --space-12: 48px; + --space-14: 56px; + --space-16: 64px; + --space-20: 80px; + --space-24: 96px; + --space-px: 1px; + --space-0-5: 2px; + --space-1-5: 6px; + + /* ── Radius ─────────────────────────────────────────────────── */ + --radius-none: 0; + --radius-xs: 4px; + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-2xl: 24px; + --radius-pill: 9999px; + --radius-full: 9999px; + + /* ── Typography — families ─────────────────────────────────────────────────── */ + --font-sans: 'Geist', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace; + --font-tabular: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + + /* ── Typography — weights ─────────────────────────────────────────────────── */ + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + + /* ── Typography — fluid scale ─────────────────────────────────────────────────── */ + --font-size-xs: clamp(11px, 0.69rem + 0.05vw, 12px); + --line-height-xs: 1.4; + --font-size-sm: clamp(12px, 0.75rem + 0.06vw, 13px); + --line-height-sm: 1.5; + --font-size-base: clamp(13px, 0.81rem + 0.07vw, 14px); + --line-height-base: 1.5; + --font-size-md: clamp(14px, 0.88rem + 0.08vw, 15px); + --line-height-md: 1.5; + --font-size-lg: clamp(15px, 0.94rem + 0.10vw, 17px); + --line-height-lg: 1.45; + --font-size-h4: clamp(15px, 0.94rem + 0.15vw, 17px); + --line-height-h4: 1.35; + --font-weight-h4-step: 600; + --font-size-h3: clamp(17px, 1.05rem + 0.20vw, 19px); + --line-height-h3: 1.3; + --font-weight-h3-step: 600; + --font-size-h2: clamp(20px, 1.20rem + 0.45vw, 24px); + --line-height-h2: 1.25; + --font-weight-h2-step: 600; + --font-size-h1: clamp(24px, 1.50rem + 0.60vw, 32px); + --line-height-h1: 1.2; + --font-weight-h1-step: 700; + --letter-spacing-h1: -0.01em; + --font-size-display: clamp(28px, 1.75rem + 1.00vw, 40px); + --line-height-display: 1.1; + --font-weight-display-step: 700; + --letter-spacing-display: -0.02em; + --font-size-eyebrow: 11px; + --line-height-eyebrow: 1.2; + --font-weight-eyebrow-step: 600; + --letter-spacing-eyebrow: 0.08em; + + /* ── Typography — tracking ─────────────────────────────────────────────────── */ + --tracking-tighter: -0.02em; + --tracking-tight: -0.01em; + --tracking-normal: 0; + --tracking-wide: 0.04em; + --tracking-wider: 0.08em; + + /* ── Motion ─────────────────────────────────────────────────── */ + --duration-instant: 50ms; + --duration-fast: 120ms; + --duration-base: 180ms; + --duration-slow: 260ms; + --duration-slower: 400ms; + --easing-linear: linear; + --easing-standard: cubic-bezier(0.2, 0, 0, 1); + --easing-decelerate: cubic-bezier(0, 0, 0, 1); + --easing-accelerate: cubic-bezier(0.3, 0, 1, 1); + --easing-emphasized: cubic-bezier(0.05, 0.7, 0.1, 1); + --easing-spring: cubic-bezier(0.34, 1.56, 0.64, 1); + + /* ── Breakpoints (informational) ─────────────────────────────────────────────────── */ + --bp-xs: 480px; + --bp-sm: 640px; + --bp-md: 768px; + --bp-lg: 1024px; + --bp-xl: 1280px; + --bp-2xl: 1536px; + + /* ── Z-index ─────────────────────────────────────────────────── */ + --z-hide: -1; + --z-base: 0; + --z-raised: 1; + --z-dropdown: 1000; + --z-sticky: 1100; + --z-fixed: 1200; + --z-drawer: 1300; + --z-modal: 1400; + --z-popover: 1500; + --z-toast: 1600; + --z-tooltip: 1700; + --z-banner: 10000; + + /* ── Crop palette ─────────────────────────────────────────────────── */ + --crop-wheat: #FBBF24; + --crop-sunflower: #F97316; + --crop-corn: #22C55E; + --crop-rapeseed: #A855F7; + --crop-barley: #0EA5E9; + --crop-soy: #14B8A6; + --crop-fallow: #94A3B8; + + /* ── Glow effects ─────────────────────────────────────────────────── */ + --glow-brand: 0 0 20px rgba(34, 197, 94, 0.15); + + /* ── Theme — dark (default) ─────────────────────────────────────────────────── */ + --brand: #22C55E; + --brand-hover: #16A34A; + --brand-active: #15803D; + --brand-glow: rgba(34, 197, 94, 0.15); + --brand-muted: rgba(34, 197, 94, 0.08); + --brand-border: rgba(34, 197, 94, 0.20); + --bg-page: #0a0a0a; + --bg-surface: #101010; + --bg-elevated: #161616; + --bg-hover: #1c1c1c; + --card-bg: rgba(255, 255, 255, 0.03); + --card-gradient: linear-gradient(180deg, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0.01) 100%); + --text-primary: rgba(255, 255, 255, 0.94); --text-secondary: rgba(255, 255, 255, 0.58); - --text-tertiary: rgba(255, 255, 255, 0.38); - --text-disabled: rgba(255, 255, 255, 0.2); - - /* ── Borders — aligned with AntD dark tokens ── */ - --border: rgba(255, 255, 255, 0.08); - --border-hover: rgba(255, 255, 255, 0.14); - --border-strong: rgba(255, 255, 255, 0.22); - --border-focus: #22C55E; - - /* ── Crop colors — vibrant ── */ - --crop-wheat: #FBBF24; - --crop-sunflower: #F97316; - --crop-corn: #22C55E; - --crop-rapeseed: #A855F7; - --crop-barley: #0EA5E9; - --crop-soy: #14B8A6; - --crop-fallow: #94A3B8; - - /* ── Semantic accents ── */ - --accent-revenue: #22C55E; - --accent-cost: #EF4444; - --accent-info: #3B82F6; - --accent-warning: #F59E0B; - --accent-premium: #A855F7; - - /* Status colors */ - --success: #22C55E; - --warning: #F59E0B; - --error: #EF4444; - --info: #3B82F6; - --neutral: #6366F1; - - /* Status muted backgrounds */ + --text-tertiary: rgba(255, 255, 255, 0.38); + --text-disabled: rgba(255, 255, 255, 0.20); + --text-inverse: #0a0a0a; + --border: rgba(255, 255, 255, 0.08); + --border-hover: rgba(255, 255, 255, 0.14); + --border-strong: rgba(255, 255, 255, 0.22); + --border-focus: #22C55E; + --success: #22C55E; + --warning: #F59E0B; + --error: #EF4444; + --info: #3B82F6; + --neutral: #6366F1; --success-bg: rgba(34, 197, 94, 0.08); --warning-bg: rgba(245, 158, 11, 0.08); - --error-bg: rgba(239, 68, 68, 0.08); - --info-bg: rgba(59, 130, 246, 0.08); + --error-bg: rgba(239, 68, 68, 0.08); + --info-bg: rgba(59, 130, 246, 0.08); + --accent-revenue: #22C55E; + --accent-cost: #EF4444; + --accent-info: #3B82F6; + --accent-warning: #F59E0B; + --accent-premium: #A855F7; + --focus-ring: 0 0 0 3px rgba(34, 197, 94, 0.35); + --focus-ring-error: 0 0 0 3px rgba(239, 68, 68, 0.35); + --selection-bg: rgba(34, 197, 94, 0.25); - /* ── Typography ── */ - --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; - --font-mono: 'JetBrains Mono', 'Fira Code', monospace; - --font-tabular: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + /* ── Elevation — dark (default) ─────────────────────────────────────────────────── */ + --shadow-0: none; + --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.30); + --shadow-2: 0 0 0 1px rgba(255, 255, 255, 0.06), 0 4px 16px rgba(0, 0, 0, 0.40); + --shadow-3: 0 0 0 1px rgba(255, 255, 255, 0.08), 0 8px 24px rgba(0, 0, 0, 0.45); + --shadow-4: 0 0 0 1px rgba(255, 255, 255, 0.10), 0 16px 48px rgba(0, 0, 0, 0.50); + --shadow-5: 0 0 0 1px rgba(255, 255, 255, 0.12), 0 24px 80px rgba(0, 0, 0, 0.55); - /* ── Spacing scale (4px grid) ── */ - --space-1: 4px; - --space-2: 8px; - --space-3: 12px; - --space-4: 16px; - --space-5: 20px; - --space-6: 24px; - --space-8: 32px; - --space-10: 40px; - --space-12: 48px; + /* ── Legacy aliases — DO NOT use in new code ─────────────────────────────────────────────────── */ + --bg-app: var(--bg-page); + --bg-overlay: var(--bg-hover); + --bg-subtle: var(--bg-elevated); + --bg-base: var(--bg-page); + --color-page-bg: var(--bg-page); + --color-card-bg: var(--bg-surface); + --color-elevated-bg: var(--bg-elevated); + --color-input-bg: var(--bg-elevated); + --color-hover-bg: var(--bg-hover); + --color-primary: var(--brand); + --color-primary-hover: var(--brand-hover); + --border-default: var(--border); + --shadow-sm: var(--shadow-1); + --shadow-md: var(--shadow-2); + --shadow-lg: var(--shadow-4); + --shadow-card: var(--shadow-2); + --shadow-glow: var(--glow-brand); + --transition-fast: var(--duration-fast) var(--easing-standard); + --transition-base: var(--duration-base) var(--easing-standard); + --transition-slow: var(--duration-slow) var(--easing-standard); +} - /* ── Radius ── */ - --radius-xs: 4px; - --radius-sm: 6px; - --radius-md: 8px; - --radius-lg: 12px; - --radius-xl: 16px; - --radius-2xl: 24px; +[data-theme='dark'] { + /* ── Theme — dark (explicit) ─────────────────────────────────────────────────── */ + --brand: #22C55E; + --brand-hover: #16A34A; + --brand-active: #15803D; + --brand-glow: rgba(34, 197, 94, 0.15); + --brand-muted: rgba(34, 197, 94, 0.08); + --brand-border: rgba(34, 197, 94, 0.20); + --bg-page: #0a0a0a; + --bg-surface: #101010; + --bg-elevated: #161616; + --bg-hover: #1c1c1c; + --card-bg: rgba(255, 255, 255, 0.03); + --card-gradient: linear-gradient(180deg, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0.01) 100%); + --text-primary: rgba(255, 255, 255, 0.94); + --text-secondary: rgba(255, 255, 255, 0.58); + --text-tertiary: rgba(255, 255, 255, 0.38); + --text-disabled: rgba(255, 255, 255, 0.20); + --text-inverse: #0a0a0a; + --border: rgba(255, 255, 255, 0.08); + --border-hover: rgba(255, 255, 255, 0.14); + --border-strong: rgba(255, 255, 255, 0.22); + --border-focus: #22C55E; + --success: #22C55E; + --warning: #F59E0B; + --error: #EF4444; + --info: #3B82F6; + --neutral: #6366F1; + --success-bg: rgba(34, 197, 94, 0.08); + --warning-bg: rgba(245, 158, 11, 0.08); + --error-bg: rgba(239, 68, 68, 0.08); + --info-bg: rgba(59, 130, 246, 0.08); + --accent-revenue: #22C55E; + --accent-cost: #EF4444; + --accent-info: #3B82F6; + --accent-warning: #F59E0B; + --accent-premium: #A855F7; + --focus-ring: 0 0 0 3px rgba(34, 197, 94, 0.35); + --focus-ring-error: 0 0 0 3px rgba(239, 68, 68, 0.35); + --selection-bg: rgba(34, 197, 94, 0.25); - /* ── Shadows ── */ - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); - --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.06); - --shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.08); - --shadow-glow: 0 0 20px rgba(34, 197, 94, 0.15); - --shadow-card: 0 0 0 1px rgba(255,255,255,0.06), 0 4px 16px rgba(0,0,0,0.4); - - /* ── Transitions ── */ - --transition-fast: 100ms ease; - --transition-base: 150ms ease; - --transition-slow: 200ms ease; - - /* ── Aliases for design-system spec compatibility ── */ - --color-page-bg: var(--bg-page); - --color-card-bg: var(--bg-surface); - --color-elevated-bg: var(--bg-elevated); - --color-input-bg: var(--bg-elevated); - --color-hover-bg: var(--bg-hover); - --color-primary: var(--brand); - --color-primary-hover: var(--brand-hover); - --border-default: var(--border); + /* ── Elevation — dark (explicit) ─────────────────────────────────────────────────── */ + --shadow-0: none; + --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.30); + --shadow-2: 0 0 0 1px rgba(255, 255, 255, 0.06), 0 4px 16px rgba(0, 0, 0, 0.40); + --shadow-3: 0 0 0 1px rgba(255, 255, 255, 0.08), 0 8px 24px rgba(0, 0, 0, 0.45); + --shadow-4: 0 0 0 1px rgba(255, 255, 255, 0.10), 0 16px 48px rgba(0, 0, 0, 0.50); + --shadow-5: 0 0 0 1px rgba(255, 255, 255, 0.12), 0 24px 80px rgba(0, 0, 0, 0.55); } +[data-theme='light'] { + /* ── Theme — light ─────────────────────────────────────────────────── */ + --brand: #16A34A; + --brand-hover: #15803D; + --brand-active: #14532D; + --brand-glow: rgba(22, 163, 74, 0.18); + --brand-muted: rgba(22, 163, 74, 0.10); + --brand-border: rgba(22, 163, 74, 0.25); + --bg-page: #FAFAFA; + --bg-surface: #ffffff; + --bg-elevated: #ffffff; + --bg-hover: #F5F5F5; + --card-bg: #ffffff; + --card-gradient: linear-gradient(180deg, #ffffff 0%, #fcfcfd 100%); + --text-primary: rgba(15, 23, 42, 0.92); + --text-secondary: rgba(15, 23, 42, 0.62); + --text-tertiary: rgba(15, 23, 42, 0.42); + --text-disabled: rgba(15, 23, 42, 0.24); + --text-inverse: #ffffff; + --border: rgba(15, 23, 42, 0.08); + --border-hover: rgba(15, 23, 42, 0.14); + --border-strong: rgba(15, 23, 42, 0.22); + --border-focus: #16A34A; + --success: #16A34A; + --warning: #D97706; + --error: #DC2626; + --info: #2563EB; + --neutral: #6366F1; + --success-bg: rgba(22, 163, 74, 0.10); + --warning-bg: rgba(217, 119, 6, 0.10); + --error-bg: rgba(220, 38, 38, 0.10); + --info-bg: rgba(37, 99, 235, 0.10); + --accent-revenue: #16A34A; + --accent-cost: #DC2626; + --accent-info: #2563EB; + --accent-warning: #D97706; + --accent-premium: #9333EA; + --focus-ring: 0 0 0 3px rgba(22, 163, 74, 0.30); + --focus-ring-error: 0 0 0 3px rgba(220, 38, 38, 0.25); + --selection-bg: rgba(22, 163, 74, 0.18); + + /* ── Elevation — light ─────────────────────────────────────────────────── */ + --shadow-0: none; + --shadow-1: 0 1px 2px rgba(15, 23, 42, 0.06); + --shadow-2: 0 1px 3px rgba(15, 23, 42, 0.05), 0 4px 12px rgba(15, 23, 42, 0.06); + --shadow-3: 0 2px 6px rgba(15, 23, 42, 0.07), 0 8px 24px rgba(15, 23, 42, 0.08); + --shadow-4: 0 4px 12px rgba(15, 23, 42, 0.09), 0 16px 48px rgba(15, 23, 42, 0.10); + --shadow-5: 0 6px 20px rgba(15, 23, 42, 0.12), 0 24px 80px rgba(15, 23, 42, 0.14); +} diff --git a/frontend/src/theme/lightTheme.ts b/frontend/src/theme/lightTheme.ts index cb0ff5c3..0604e97a 100644 --- a/frontend/src/theme/lightTheme.ts +++ b/frontend/src/theme/lightTheme.ts @@ -1 +1,204 @@ -export {}; +import { theme } from 'antd'; +import type { ThemeConfig } from 'antd'; + +/** + * Light-mode AntD theme. + * + * Mirrors `darkTheme.ts`: every color reference points at the same CSS custom + * property, which is re-bound to its light value by the + * `[data-theme='light']` block in `src/styles/tokens.css`. + * + * That means switching themes only requires toggling the `data-theme` + * attribute on `` — no React re-render, no AntD re-theme, no flash. + */ +export const lightTheme: ThemeConfig = { + token: { + colorBgBase: 'var(--bg-page)', + colorBgContainer: 'var(--bg-surface)', + colorBgElevated: 'var(--bg-elevated)', + colorBgLayout: 'var(--bg-page)', + colorBgSpotlight: 'var(--bg-elevated)', + colorBorder: 'var(--border)', + colorBorderSecondary: 'var(--border-hover)', + colorTextBase: 'var(--text-primary)', + colorText: 'var(--text-primary)', + colorTextSecondary: 'var(--text-secondary)', + colorTextTertiary: 'var(--text-tertiary)', + colorTextQuaternary: 'var(--text-disabled)', + colorPrimary: '#16A34A', + colorSuccess: '#16A34A', + colorError: '#DC2626', + colorWarning: '#D97706', + colorInfo: '#2563EB', + colorLink: '#16A34A', + colorLinkHover: '#15803D', + borderRadius: 8, + borderRadiusLG: 12, + borderRadiusSM: 6, + fontFamily: "'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", + fontSize: 13, + fontSizeLG: 15, + lineHeight: 1.5, + controlHeight: 36, + controlHeightLG: 40, + boxShadow: 'none', + boxShadowSecondary: 'none', + wireframe: false, + }, + components: { + Layout: { + siderBg: 'var(--bg-surface)', + headerBg: 'var(--bg-surface)', + bodyBg: 'var(--bg-page)', + triggerBg: 'var(--bg-hover)', + triggerColor: 'var(--text-secondary)', + }, + Menu: { + itemBg: 'var(--bg-surface)', + subMenuItemBg: 'var(--bg-surface)', + itemSelectedBg: 'var(--brand-muted)', + itemHoverBg: 'var(--bg-hover)', + itemColor: 'var(--text-secondary)', + itemSelectedColor: 'var(--brand)', + itemHoverColor: 'var(--text-primary)', + itemBorderRadius: 8, + itemHeight: 36, + itemMarginInline: 8, + }, + Card: { + colorBgContainer: 'var(--bg-surface)', + colorBorderSecondary: 'var(--border)', + borderRadiusLG: 12, + paddingLG: 20, + boxShadow: 'var(--shadow-1)', + }, + Table: { + colorBgContainer: 'transparent', + headerBg: 'rgba(15, 23, 42, 0.02)', + headerColor: 'var(--text-tertiary)', + headerSortActiveBg: 'rgba(15, 23, 42, 0.04)', + rowHoverBg: 'rgba(15, 23, 42, 0.03)', + borderColor: 'var(--border)', + colorText: 'var(--text-primary)', + colorTextHeading: 'var(--text-tertiary)', + }, + Button: { + colorPrimary: 'var(--brand)', + colorPrimaryHover: 'var(--brand-hover)', + colorPrimaryActive: 'var(--brand-active)', + primaryShadow: 'none', + primaryColor: '#ffffff', + borderRadius: 8, + borderRadiusSM: 6, + defaultBg: 'var(--bg-surface)', + defaultColor: 'var(--text-primary)', + defaultBorderColor: 'var(--border-hover)', + defaultHoverBg: 'var(--bg-hover)', + defaultHoverColor: 'var(--text-primary)', + defaultHoverBorderColor: 'var(--border-strong)', + }, + Input: { + colorBgContainer: 'var(--bg-elevated)', + colorBorder: 'var(--border-hover)', + colorText: 'var(--text-primary)', + colorTextPlaceholder: 'var(--text-tertiary)', + hoverBorderColor: 'var(--border-strong)', + activeBorderColor: 'var(--brand)', + activeShadow: '0 0 0 3px var(--brand-glow)', + }, + Select: { + colorBgContainer: 'var(--bg-elevated)', + colorBorder: 'var(--border-hover)', + colorText: 'var(--text-primary)', + colorTextPlaceholder: 'var(--text-tertiary)', + optionSelectedBg: 'var(--brand-muted)', + optionActiveBg: 'var(--bg-hover)', + colorBgElevated: 'var(--bg-surface)', + }, + Modal: { + colorBgElevated: 'var(--bg-surface)', + colorBorder: 'var(--border)', + titleColor: 'var(--text-primary)', + }, + Drawer: { + colorBgElevated: 'var(--bg-surface)', + }, + Dropdown: { + colorBgElevated: 'var(--bg-surface)', + colorText: 'var(--text-primary)', + controlItemBgHover: 'var(--bg-hover)', + }, + Tooltip: { + colorBgSpotlight: 'var(--bg-hover)', + colorTextLightSolid: 'var(--text-primary)', + }, + Pagination: { + colorBgContainer: 'var(--bg-elevated)', + colorText: 'var(--text-secondary)', + itemActiveBg: 'var(--brand-muted)', + }, + Statistic: { + contentFontSize: 28, + titleFontSize: 11, + }, + Badge: { + colorBgContainer: 'var(--bg-surface)', + }, + Tag: { + defaultBg: 'var(--bg-hover)', + defaultColor: 'var(--text-secondary)', + }, + Divider: { + colorSplit: 'var(--border)', + }, + Spin: { + colorPrimary: 'var(--brand)', + }, + Progress: { + colorSuccess: 'var(--brand)', + }, + Alert: { + colorSuccessBg: 'var(--success-bg)', + colorSuccessBorder: 'var(--brand-border)', + colorErrorBg: 'var(--error-bg)', + colorErrorBorder: 'rgba(220, 38, 38, 0.25)', + colorWarningBg: 'var(--warning-bg)', + colorWarningBorder: 'rgba(217, 119, 6, 0.25)', + colorInfoBg: 'var(--info-bg)', + colorInfoBorder: 'rgba(37, 99, 235, 0.25)', + }, + DatePicker: { + colorBgContainer: 'var(--bg-elevated)', + colorBorder: 'var(--border-hover)', + colorText: 'var(--text-primary)', + colorTextPlaceholder: 'var(--text-tertiary)', + colorBgElevated: 'var(--bg-surface)', + }, + Form: { + labelColor: 'var(--text-secondary)', + labelFontSize: 12, + }, + Typography: { + colorText: 'var(--text-primary)', + colorTextSecondary: 'var(--text-secondary)', + colorTextDisabled: 'var(--text-tertiary)', + colorLink: 'var(--brand)', + titleMarginBottom: '0.5em', + }, + Result: { + colorTextDescription: 'var(--text-secondary)', + }, + Empty: { + colorTextDisabled: 'var(--text-tertiary)', + }, + Notification: { + colorBgElevated: 'var(--bg-surface)', + colorText: 'var(--text-primary)', + }, + Message: { + colorBgElevated: 'var(--bg-surface)', + colorText: 'var(--text-primary)', + }, + }, + algorithm: theme.defaultAlgorithm, +};