Compare commits

...

5 Commits

Author SHA1 Message Date
b07d8a0839 Chart 2025-05-26 05:06:45 +02:00
521e53fadd Basic functionality 2025-05-26 04:22:15 +02:00
2f523a9219 Remove old control characters 2025-05-26 04:22:01 +02:00
339a98d8ab Add theming boilerplate 2025-05-26 02:37:54 +02:00
cd65f06c5f Add API code 2025-05-26 02:37:43 +02:00
18 changed files with 1055 additions and 5 deletions

604
package-lock.json generated
View File

@ -12,6 +12,8 @@
"@emotion/styled": "^11.14.0",
"@fontsource/inter": "^5.2.5",
"@mui/joy": "^5.0.0-beta.52",
"@mui/material": "^7.1.0",
"@mui/x-charts": "^8.4.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
@ -1211,6 +1213,206 @@
}
}
},
"node_modules/@mui/material": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.1.0.tgz",
"integrity": "sha512-ahUJdrhEv+mCp4XHW+tHIEYzZMSRLg8z4AjUOsj44QpD1ZaMxQoVOG2xiHvLFdcsIPbgSRx1bg1eQSheHBgvtg==",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/core-downloads-tracker": "^7.1.0",
"@mui/system": "^7.1.0",
"@mui/types": "^7.4.2",
"@mui/utils": "^7.1.0",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^19.1.0",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "^7.1.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@mui/material-pigment-css": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/@mui/core-downloads-tracker": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.0.tgz",
"integrity": "sha512-E0OqhZv548Qdc0PwWhLVA2zmjJZSTvaL4ZhoswmI8NJEC1tpW2js6LLP827jrW9MEiXYdz3QS6+hask83w74yQ==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/material/node_modules/@mui/private-theming": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz",
"integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/utils": "^7.1.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/@mui/styled-engine": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz",
"integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@emotion/cache": "^11.13.5",
"@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/@mui/system": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.0.tgz",
"integrity": "sha512-iedAWgRJMCxeMHvkEhsDlbvkK+qKf9me6ofsf7twk/jfT4P1ImVf7Rwb5VubEA0sikrVL+1SkoZM41M4+LNAVA==",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/private-theming": "^7.1.0",
"@mui/styled-engine": "^7.1.0",
"@mui/types": "^7.4.2",
"@mui/utils": "^7.1.0",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/@mui/types": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.2.tgz",
"integrity": "sha512-edRc5JcLPsrlNFYyTPxds+d5oUovuUxnnDtpJUbP6WMeV4+6eaX/mqai1ZIWT62lCOe0nlrON0s9HDiv5en5bA==",
"dependencies": {
"@babel/runtime": "^7.27.1"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/@mui/utils": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz",
"integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/types": "^7.4.2",
"@types/prop-types": "^15.7.14",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^19.1.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/private-theming": {
"version": "5.17.1",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.17.1.tgz",
@ -1349,6 +1551,174 @@
}
}
},
"node_modules/@mui/x-charts": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.4.0.tgz",
"integrity": "sha512-XXXt6cHgpTTkLWIImBy0OPD0FwuOdux4AprP/0Zvs0PXuM9D9eeN1piZvo5gjZbPHSsCzIyZzwx9PGncFScgEQ==",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/utils": "^7.0.2",
"@mui/x-charts-vendor": "8.4.0",
"@mui/x-internals": "8.4.0",
"bezier-easing": "^2.1.0",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"reselect": "^5.1.1",
"use-sync-external-store": "^1.5.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
"@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/x-charts-vendor": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.4.0.tgz",
"integrity": "sha512-0VvwJSeFezJTnjoKg5YUbbI82a60+VZfH2RyqaosmKH516lKYKSCuAFmkj4vUBP6+ZJPZDW5mWI3VhwD4DN6hg==",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@types/d3-color": "^3.1.3",
"@types/d3-delaunay": "^6.0.4",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-scale": "^4.0.9",
"@types/d3-shape": "^3.1.7",
"@types/d3-time": "^3.0.4",
"@types/d3-timer": "^3.0.2",
"d3-color": "^3.1.0",
"d3-delaunay": "^6.0.4",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.2.0",
"d3-time": "^3.1.0",
"d3-timer": "^3.0.1",
"delaunator": "^5.0.1",
"robust-predicates": "^3.0.2"
}
},
"node_modules/@mui/x-charts/node_modules/@mui/types": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.2.tgz",
"integrity": "sha512-edRc5JcLPsrlNFYyTPxds+d5oUovuUxnnDtpJUbP6WMeV4+6eaX/mqai1ZIWT62lCOe0nlrON0s9HDiv5en5bA==",
"dependencies": {
"@babel/runtime": "^7.27.1"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/x-charts/node_modules/@mui/utils": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz",
"integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/types": "^7.4.2",
"@types/prop-types": "^15.7.14",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^19.1.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/x-internals": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.4.0.tgz",
"integrity": "sha512-Z7FCahC4MLfTVzEwnKOB7P1fiR9DzFuMzHOPRNaMXc/rsS7unbtBKAG94yvsRzReCyjzZUVA7h37lnQ1DoPKJw==",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/utils": "^7.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@mui/x-internals/node_modules/@mui/types": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.2.tgz",
"integrity": "sha512-edRc5JcLPsrlNFYyTPxds+d5oUovuUxnnDtpJUbP6WMeV4+6eaX/mqai1ZIWT62lCOe0nlrON0s9HDiv5en5bA==",
"dependencies": {
"@babel/runtime": "^7.27.1"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/x-internals/node_modules/@mui/utils": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz",
"integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/types": "^7.4.2",
"@types/prop-types": "^15.7.14",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^19.1.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -1700,6 +2070,55 @@
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
},
"node_modules/@types/d3-delaunay": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
"integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
},
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@ -1726,7 +2145,6 @@
"version": "19.1.5",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz",
"integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==",
"devOptional": true,
"dependencies": {
"csstype": "^3.0.2"
}
@ -1740,6 +2158,14 @@
"@types/react": "^19.0.0"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
@ -2055,6 +2481,11 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/bezier-easing": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
"integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -2233,6 +2664,119 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-delaunay": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
"integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
"dependencies": {
"delaunator": "5"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"engines": {
"node": ">=12"
}
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@ -2255,6 +2799,23 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"node_modules/delaunator": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
"integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
"dependencies": {
"robust-predicates": "^3.0.2"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.157",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.157.tgz",
@ -2743,6 +3304,14 @@
"node": ">=0.8.19"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"engines": {
"node": ">=12"
}
},
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@ -3243,6 +3812,26 @@
"node": ">=0.10.0"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@ -3280,6 +3869,11 @@
"node": ">=0.10.0"
}
},
"node_modules/robust-predicates": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
},
"node_modules/rollup": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz",
@ -3586,6 +4180,14 @@
"punycode": "^2.1.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",

View File

@ -14,6 +14,8 @@
"@emotion/styled": "^11.14.0",
"@fontsource/inter": "^5.2.5",
"@mui/joy": "^5.0.0-beta.52",
"@mui/material": "^7.1.0",
"@mui/x-charts": "^8.4.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},

View File

@ -1,9 +1,66 @@
import './App.css'
import {PlayersContext} from "./api/contexts/PlayersContext.tsx";
import type IPlayer from "./api/types/IPlayer.ts";
import {useEffect, useState} from "react";
import type IGame from "./api/types/IGame.ts";
import {GamesContext} from "./api/contexts/GamesContext.tsx";
import {ApiUriContext} from "./api/fetchApi.tsx";
import {GetGames, GetPlayers} from "./api/endpoints/Data.tsx";
import {
AccordionGroup,
Box,
Divider,
Input,
Stack,
Typography
} from "@mui/joy";
import GameAccordionItem from "./components/GameAccordionItem.tsx";
import PlayerAccordionItem from "./components/PlayerAccordionItem.tsx";
import StatsDrawer from "./components/StatsDrawer.tsx";
export default function App() {
const [apiUri, setApiUri] = useState<string>("http://localhost:5239");
const [players, setPlayers] = useState<IPlayer[]>([]);
const [games, setGames] = useState<IGame[]>([]);
const [selectedPlayer, setSelectedPlayer] = useState<IPlayer | null>(null);
const [selectedGame, setSelectedGame] = useState<IGame | null>(null);
const [open, setOpen] = useState<boolean>(false);
const OpenDrawer = (player: IPlayer, game: IGame) => {
setSelectedPlayer(player);
setSelectedGame(game);
setOpen(true);
}
useEffect(() => {
GetPlayers(apiUri).then(setPlayers);
GetGames(apiUri).then(setGames);
}, [apiUri]);
return (
<>
</>
<ApiUriContext value={apiUri}>
<PlayersContext.Provider value={{players: players}}>
<GamesContext value={{games: games}}>
<Input type={"text"} placeholder={"Api Uri"} value={apiUri} onChange={(e) => setApiUri(e.target.value)} />
<Stack direction={"row"} spacing={2}>
<Box sx={{width:'50%'}}>
<Typography level={"h2"}>Players</Typography>
<AccordionGroup>
{players?.map((player) => <PlayerAccordionItem key={player.steamId} player={player} OpenDrawer={OpenDrawer} />)}
</AccordionGroup>
</Box>
<Divider />
<Box sx={{width:'50%'}}>
<Typography level={"h2"}>Games</Typography>
<AccordionGroup>
{games?.map((game) => <GameAccordionItem key={game.appId} game={game} OpenDrawer={OpenDrawer} />)}
</AccordionGroup>
</Box>
</Stack>
<StatsDrawer player={selectedPlayer} game={selectedGame} open={open} setOpen={setOpen} />
</GamesContext>
</PlayersContext.Provider>
</ApiUriContext>
)
}

View File

@ -0,0 +1,8 @@
import {createContext} from "react";
import type IGame from "../types/IGame.ts";
export const GamesContext = createContext<{games: IGame[]}>(
{
games: []
}
);

View File

@ -0,0 +1,8 @@
import {createContext} from "react";
import type IPlayer from "../types/IPlayer.ts";
export const PlayersContext = createContext<{players: IPlayer[]}>(
{
players: []
}
);

View File

@ -0,0 +1,30 @@
import {deleteData, postData, putData} from "../fetchApi.tsx";
import type IPlayer from "../types/IPlayer.ts";
export function AddPlayer(apiUri: string, steamId: bigint){
return putData(`${apiUri}/Actions/Player/${steamId}`) as Promise<IPlayer>;
}
export function DeletePlayer(apiUri: string, steamId: bigint){
return deleteData(`${apiUri}/Actions/Player/${steamId}`) as Promise<void>;
}
export function UpdatePlayerData(apiUri: string, steamId: bigint){
return postData(`${apiUri}/Actions/Update/Player/${steamId}/All`) as Promise<void>;
}
export function UpdatePlayerInfo(apiUri: string, steamId: bigint){
return postData(`${apiUri}/Actions/Update/Player/${steamId}/Info`) as Promise<void>;
}
export function UpdatePlayerOwnedGames(apiUri: string, steamId: bigint){
return postData(`${apiUri}/Actions/Update/Player/${steamId}/OwnedGames`) as Promise<void>;
}
export function UpdatePlayerTimeTracked(apiUri: string, steamId: bigint){
return postData(`${apiUri}/Actions/Update/Player/${steamId}/TimeTracked`) as Promise<void>;
}
export function UpdateAll(apiUri: string){
return postData(`${apiUri}/Actions/All`) as Promise<void>;
}

View File

@ -0,0 +1,27 @@
import {getData} from "../fetchApi.tsx";
import type IPlayer from "../types/IPlayer.ts";
import type IGame from "../types/IGame.ts";
export function GetPlayers(apiUri: string){
return getData(`${apiUri}/Data/Players`) as Promise<IPlayer[]>;
}
export function GetPlayer(apiUri: string, steamdId: bigint){
return getData(`${apiUri}/Data/Player/${steamdId}`) as Promise<IPlayer>;
}
export function GetGamesOfPlayer(apiUri: string, steamdId: bigint){
return getData(`${apiUri}/Data/Player/${steamdId}/Games`) as Promise<IGame[]>;
}
export function GetGames(apiUri: string){
return getData(`${apiUri}/Data/Games`) as Promise<IGame[]>;
}
export function GetGame(apiUri: string, appId: bigint){
return getData(`${apiUri}/Data/Game/${appId}`) as Promise<IGame>;
}
export function GetPlayersOfGame(apiUri: string, appId: bigint){
return getData(`${apiUri}/Data/Game/${appId}/Players`) as Promise<IPlayer[]>;
}

View File

@ -0,0 +1,18 @@
import {getData} from "../fetchApi.tsx";
import type ITrackedTime from "../types/ITrackedTime.ts";
export function GetTimelines(apiUri: string, steamId: bigint){
return getData(`${apiUri}/TimeTrack/${steamId}`) as Promise<Map<bigint, ITrackedTime[]>>;
}
export function GetTimelineGame(apiUri: string, steamId: bigint, appId: bigint){
return getData(`${apiUri}/TimeTrack/${steamId}/${appId}`) as Promise<ITrackedTime[]>;
}
export function GetTotal(apiUri: string, steamId: bigint){
return getData(`${apiUri}/TimeTrack/${steamId}/Total`) as unknown as Promise<bigint>;
}
export function GetTotalPerGame(apiUri: string, steamId: bigint){
return getData(`${apiUri}/TimeTrack/${steamId}/Total/PerGame`) as Promise<Map<bigint, bigint>>;
}

121
src/api/fetchApi.tsx Normal file
View File

@ -0,0 +1,121 @@
import {createContext} from "react";
export const ApiUriContext = createContext<string>("");
export function getData(uri: string) : Promise<object | undefined> {
return makeRequestWrapper("GET", uri, null);
}
export function postData(uri: string, content?: object | string | number | boolean | null) : Promise<object | undefined> {
return makeRequestWrapper("POST", uri, content);
}
export function deleteData(uri: string) : Promise<void> {
return makeRequestWrapper("DELETE", uri, null) as Promise<void>;
}
export function patchData(uri: string, content: object | string | number | boolean) : Promise<object | undefined> {
return makeRequestWrapper("patch", uri, content);
}
export function putData(uri: string, content?: object | string | number | boolean | null) : Promise<object | undefined> {
return makeRequestWrapper("PUT", uri, content);
}
function makeRequestWrapper(method: string, uri: string, content?: object | string | number | null | boolean) : Promise<object | undefined>{
return makeRequest(method, uri, content)
.then((result) => result as Promise<object>)
.catch((e) => {
console.warn(e);
return Promise.resolve(undefined);
});
}
let currentlyRequestedEndpoints: string[] = [];
function makeRequest(method: string, uri: string, content?: object | string | number | null | boolean) : Promise<object | void> {
const id = method + uri;
if(currentlyRequestedEndpoints.find(x => x == id) != undefined)
return Promise.reject(`Already requested: ${method} ${uri}`);
currentlyRequestedEndpoints.push(id);
return fetch(uri,
{
method: method,
headers : {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: content ? JSON.stringify(content) : null
})
.then(function(response){
if(!response.ok){
if(response.status === 503){
currentlyRequestedEndpoints.splice(currentlyRequestedEndpoints.indexOf(id), 1)
let retryHeaderVal = response.headers.get("Retry-After");
let seconds = 10;
if(retryHeaderVal === null){
return response.text().then(text => {
seconds = parseInt(text);
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
.then(() => {
return makeRequest(method, uri, content);
});
});
}else {
seconds = parseInt(retryHeaderVal);
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
.then(() => {
return makeRequest(method, uri, content);
});
}
}else
throw new Error(response.statusText);
}
return response.text().then(text => JSON.parseBigInt(text) as object);
})
.catch(function(err : Error){
console.error(`Error ${method}ing Data ${uri}\n${err}`);
return Promise.reject();
}).finally(() => currentlyRequestedEndpoints.splice(currentlyRequestedEndpoints.indexOf(id), 1));
}
declare global {
interface JSON {
parseBigInt: (jsonStr: string, options?: { minDigits?: number; fallbackToString?: boolean; }) => unknown;
}
}
function quoteLargeNumbers(jsonStr: string, minDigits: number): string {
const regex = new RegExp(`(-?\\d{${minDigits},})(?=\\s*[,}\\]])`, 'g');
return jsonStr.replace(regex, '"$1"');
}
JSON.parseBigInt = function (jsonStr: string, options?: { minDigits?: number; fallbackToString?: boolean;}): unknown {
const minDigits = options?.minDigits ?? 15;
const fallbackToString = options?.fallbackToString ?? true;
const safeStr = quoteLargeNumbers(jsonStr, minDigits);
return JSON.parse(safeStr, (_key, value) => {
if (typeof value === 'string') {
const bigintRegex = new RegExp(`^-?\\d{${minDigits},}$`);
if (bigintRegex.test(value)) {
try {
if (typeof BigInt !== 'undefined') return BigInt(value);
else if (fallbackToString) return value;
} catch {
return value;
}
}
}
return value;
});
};
export function isValidUri(uri: string) : boolean{
try {
new URL(uri);
return true;
} catch (err) {
return false;
}
}

4
src/api/types/IGame.ts Normal file
View File

@ -0,0 +1,4 @@
export default interface IGame {
appId: bigint,
name: string
}

7
src/api/types/IPlayer.ts Normal file
View File

@ -0,0 +1,7 @@
export default interface IPlayer {
steamId: bigint,
name: string,
profileUrl: string,
avatarUrl: string,
updatedAt: Date
}

View File

@ -0,0 +1,4 @@
export default interface ITrackedTime {
timeStamp: Date,
timePlayed: bigint
}

View File

@ -0,0 +1,36 @@
import type IGame from "../api/types/IGame.ts";
import {
Accordion,
AccordionDetails,
AccordionSummary,
Stack
} from "@mui/joy";
import type IPlayer from "../api/types/IPlayer.ts";
import {useContext, useEffect, useState} from "react";
import {GetPlayersOfGame} from "../api/endpoints/Data.tsx";
import {ApiUriContext} from "../api/fetchApi.tsx";
import PlayerCard from "./PlayerCard.tsx";
export default function GameAccordionItem({game, OpenDrawer} : {game: IGame, OpenDrawer : (player: IPlayer, game: IGame) => void}) {
const apiUri = useContext(ApiUriContext);
const [players, setPlayers] = useState<IPlayer[]>([]);
const [expanded, setExpanded] = useState<boolean>(false);
useEffect(() => {
if(!expanded)
return;
GetPlayersOfGame(apiUri, game.appId).then(setPlayers);
}, [expanded]);
return (
<Accordion key={game.appId} onChange={(_, expanded) => setExpanded(expanded)}>
<AccordionSummary>{game.name}</AccordionSummary>
<AccordionDetails>
<Stack flexWrap={"wrap"} direction={"row"} useFlexGap={true} spacing={1}>
{players?.map((player) => <PlayerCard player={player} onClick={() => OpenDrawer(player, game)} />)}
</Stack>
</AccordionDetails>
</Accordion>
);
}

View File

@ -0,0 +1,46 @@
import type IGame from "../api/types/IGame.ts";
import {
Accordion,
AccordionDetails,
AccordionSummary,
AspectRatio,
Card,
CardContent,
Stack, Typography
} from "@mui/joy";
import type IPlayer from "../api/types/IPlayer.ts";
import {useContext, useEffect, useState} from "react";
import {ApiUriContext} from "../api/fetchApi.tsx";
import {GetGamesOfPlayer} from "../api/endpoints/Data.tsx";
export default function PlayerAccordionItem({player, OpenDrawer} : {player: IPlayer, OpenDrawer : (player: IPlayer, game: IGame) => void}) {
const apiUri = useContext(ApiUriContext);
const [games, setGames] = useState<IGame[]>([]);
const [expanded, setExpanded] = useState<boolean>(false);
useEffect(() => {
if(!expanded)
return;
GetGamesOfPlayer(apiUri, player.steamId).then(setGames);
}, [expanded]);
return (
<Accordion key={player.steamId} onChange={(_, expanded) => setExpanded(expanded)}>
<AccordionSummary><img src={player.avatarUrl} />{player.name}</AccordionSummary>
<AccordionDetails>
<Stack flexWrap={"wrap"} direction={"row"} useFlexGap={true} spacing={1}>
{games?.map((game) =>
<AspectRatio sx={{width: "128px"}} ratio={2/3}>
<Card key={game.appId} onClick={() => OpenDrawer(player, game)}>
<CardContent>
<Typography level={"body-lg"}>{game.name}</Typography>
</CardContent>
</Card>
</AspectRatio>
)}
</Stack>
</AccordionDetails>
</Accordion>
);
}

View File

@ -0,0 +1,19 @@
import {AspectRatio, Card, CardContent, Stack, Typography} from "@mui/joy";
import type IPlayer from "../api/types/IPlayer.ts";
export default function PlayerCard({player, onClick} : {player: IPlayer | null, onClick?: React.MouseEventHandler<HTMLDivElement> | undefined}) {
return (
<AspectRatio ratio={3} sx={{width: '192px'}}>
<Card onClick={onClick}>
<CardContent sx={{width: "100%"}}>
<Stack direction="row" spacing={1} justifyContent={"flex-start"} sx={{width: "100%"}} alignContent={"center"}>
<AspectRatio ratio={1} sx={{width: '64px'}}>
<img src={player?.avatarUrl} />
</AspectRatio>
<Typography level={"h4"} alignContent={"center"}>{player?.name}</Typography>
</Stack>
</CardContent>
</Card>
</AspectRatio>
);
}

View File

@ -0,0 +1,37 @@
import type IPlayer from "../api/types/IPlayer.ts";
import type IGame from "../api/types/IGame.ts";
import {DialogContent, DialogTitle, Drawer, ModalClose, Typography} from "@mui/joy";
import {type Dispatch, useContext, useEffect, useState} from "react";
import {ApiUriContext} from "../api/fetchApi.tsx";
import {GetTimelineGame} from "../api/endpoints/TimeTrack.tsx";
import type ITrackedTime from "../api/types/ITrackedTime.ts";
import {LineChart} from "@mui/x-charts";
import PlayerCard from "./PlayerCard.tsx";
export default function StatsDrawer({player, game, open, setOpen} : {player: IPlayer | null, game: IGame | null, open: boolean, setOpen: Dispatch<boolean>}) {
const apiUri = useContext(ApiUriContext);
const [trackedTime, setTrackedTime] = useState<ITrackedTime[]>();
useEffect(() => {
if(!open || !game || !player)
return;
GetTimelineGame(apiUri, player.steamId, game.appId).then(setTrackedTime);
}, [open]);
return (
<Drawer anchor={"bottom"} size={"lg"} open={open} onClose={() => setOpen(false)}>
<ModalClose />
<DialogTitle>
<Typography level={"h4"} alignContent={"center"}>{game?.name}</Typography>
<PlayerCard player={player} />
</DialogTitle>
<DialogContent>
<LineChart xAxis={[{data : trackedTime?.map(t => t.timeStamp)??[], scaleType: "utc", label: "Date"}]}
series={[{data: trackedTime?.map(t => Number(t.timePlayed))??[], label: "Minutes Played"}]}
sx={{height: "80%"}}/>
</DialogContent>
</Drawer>
);
}

View File

@ -2,9 +2,13 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { CssVarsProvider } from '@mui/joy'
import theme from "./theme.ts";
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
<CssVarsProvider theme={theme}>
<App />
</CssVarsProvider>
</StrictMode>
)

20
src/theme.ts Normal file
View File

@ -0,0 +1,20 @@
import { extendTheme } from '@mui/joy/styles';
declare module '@mui/joy/styles' {
// No custom tokens found, you can skip the theme augmentation.
}
const theme = extendTheme({
"colorSchemes": {
"light": {
"palette": {}
},
"dark": {
"palette": {}
}
}
})
export default theme;