From 2c73dd9ec48bd171ae90cc8f2afe478c78c066ac Mon Sep 17 00:00:00 2001 From: Aaron Wood Date: Thu, 9 Apr 2026 01:03:40 -0400 Subject: [PATCH] Initial commit: Shadowdark character sheet manager with item/talent databases, view/edit modes, real-time sync --- .gitignore | 3 + .vscode/settings.json | 74 + client/index.html | 12 + client/package-lock.json | 1986 ++++++++++ client/package.json | 22 + client/src/App.module.css | 32 + client/src/App.tsx | 20 + client/src/api.ts | 109 + client/src/components/AcDisplay.module.css | 67 + client/src/components/AcDisplay.tsx | 92 + client/src/components/AttackBlock.module.css | 73 + client/src/components/AttackBlock.tsx | 43 + .../src/components/CharacterCard.module.css | 78 + client/src/components/CharacterCard.tsx | 63 + .../src/components/CharacterDetail.module.css | 82 + client/src/components/CharacterDetail.tsx | 74 + .../src/components/CharacterSheet.module.css | 157 + client/src/components/CharacterSheet.tsx | 212 ++ client/src/components/CurrencyRow.module.css | 70 + client/src/components/CurrencyRow.tsx | 80 + client/src/components/GearList.module.css | 153 + client/src/components/GearList.tsx | 184 + client/src/components/GearPanel.module.css | 6 + client/src/components/GearPanel.tsx | 50 + client/src/components/HpBar.module.css | 60 + client/src/components/HpBar.tsx | 30 + client/src/components/HpDisplay.module.css | 143 + client/src/components/HpDisplay.tsx | 84 + client/src/components/InfoPanel.module.css | 112 + client/src/components/InfoPanel.tsx | 196 + client/src/components/InlineNumber.module.css | 25 + client/src/components/InlineNumber.tsx | 66 + client/src/components/ItemPicker.module.css | 81 + client/src/components/ItemPicker.tsx | 83 + client/src/components/StatBlock.module.css | 75 + client/src/components/StatBlock.tsx | 53 + client/src/components/StatsPanel.module.css | 21 + client/src/components/StatsPanel.tsx | 34 + client/src/components/TalentList.module.css | 103 + client/src/components/TalentList.tsx | 131 + client/src/components/TalentPicker.module.css | 82 + client/src/components/TalentPicker.tsx | 84 + client/src/main.tsx | 9 + client/src/pages/CampaignList.module.css | 94 + client/src/pages/CampaignList.tsx | 81 + client/src/pages/CampaignView.module.css | 163 + client/src/pages/CampaignView.tsx | 383 ++ client/src/socket.ts | 8 + client/src/types.ts | 86 + client/src/utils/derived-ac.ts | 47 + client/src/utils/derived-attacks.ts | 48 + client/src/utils/modifiers.ts | 22 + client/src/vite-env.d.ts | 1 + client/tsconfig.json | 13 + client/vite.config.ts | 16 + ...2026-04-08-shadowdark-character-manager.md | 3214 +++++++++++++++++ ...26-04-08-v2-item-database-derived-stats.md | 2528 +++++++++++++ docs/plans/2026-04-09-view-edit-mode.md | 1412 ++++++++ ...-08-shadowdark-character-manager-design.md | 164 + ...8-v2-item-database-derived-stats-design.md | 235 ++ .../specs/2026-04-09-view-edit-mode-design.md | 161 + package-lock.json | 327 ++ package.json | 12 + server/dist/db.js | 58 + server/dist/index.js | 25 + server/dist/routes/campaigns.js | 48 + server/dist/routes/characters.js | 238 ++ server/dist/socket.js | 16 + server/package-lock.json | 2170 +++++++++++ server/package.json | 23 + server/src/db.ts | 137 + server/src/index.ts | 36 + server/src/routes/campaigns.ts | 54 + server/src/routes/characters.ts | 380 ++ server/src/routes/game-items.ts | 18 + server/src/routes/game-talents.ts | 17 + server/src/seed-items.ts | 268 ++ server/src/seed-talents.ts | 154 + server/src/socket.ts | 26 + server/tsconfig.json | 14 + 80 files changed, 17911 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 client/index.html create mode 100644 client/package-lock.json create mode 100644 client/package.json create mode 100644 client/src/App.module.css create mode 100644 client/src/App.tsx create mode 100644 client/src/api.ts create mode 100644 client/src/components/AcDisplay.module.css create mode 100644 client/src/components/AcDisplay.tsx create mode 100644 client/src/components/AttackBlock.module.css create mode 100644 client/src/components/AttackBlock.tsx create mode 100644 client/src/components/CharacterCard.module.css create mode 100644 client/src/components/CharacterCard.tsx create mode 100644 client/src/components/CharacterDetail.module.css create mode 100644 client/src/components/CharacterDetail.tsx create mode 100644 client/src/components/CharacterSheet.module.css create mode 100644 client/src/components/CharacterSheet.tsx create mode 100644 client/src/components/CurrencyRow.module.css create mode 100644 client/src/components/CurrencyRow.tsx create mode 100644 client/src/components/GearList.module.css create mode 100644 client/src/components/GearList.tsx create mode 100644 client/src/components/GearPanel.module.css create mode 100644 client/src/components/GearPanel.tsx create mode 100644 client/src/components/HpBar.module.css create mode 100644 client/src/components/HpBar.tsx create mode 100644 client/src/components/HpDisplay.module.css create mode 100644 client/src/components/HpDisplay.tsx create mode 100644 client/src/components/InfoPanel.module.css create mode 100644 client/src/components/InfoPanel.tsx create mode 100644 client/src/components/InlineNumber.module.css create mode 100644 client/src/components/InlineNumber.tsx create mode 100644 client/src/components/ItemPicker.module.css create mode 100644 client/src/components/ItemPicker.tsx create mode 100644 client/src/components/StatBlock.module.css create mode 100644 client/src/components/StatBlock.tsx create mode 100644 client/src/components/StatsPanel.module.css create mode 100644 client/src/components/StatsPanel.tsx create mode 100644 client/src/components/TalentList.module.css create mode 100644 client/src/components/TalentList.tsx create mode 100644 client/src/components/TalentPicker.module.css create mode 100644 client/src/components/TalentPicker.tsx create mode 100644 client/src/main.tsx create mode 100644 client/src/pages/CampaignList.module.css create mode 100644 client/src/pages/CampaignList.tsx create mode 100644 client/src/pages/CampaignView.module.css create mode 100644 client/src/pages/CampaignView.tsx create mode 100644 client/src/socket.ts create mode 100644 client/src/types.ts create mode 100644 client/src/utils/derived-ac.ts create mode 100644 client/src/utils/derived-attacks.ts create mode 100644 client/src/utils/modifiers.ts create mode 100644 client/src/vite-env.d.ts create mode 100644 client/tsconfig.json create mode 100644 client/vite.config.ts create mode 100644 docs/plans/2026-04-08-shadowdark-character-manager.md create mode 100644 docs/plans/2026-04-08-v2-item-database-derived-stats.md create mode 100644 docs/plans/2026-04-09-view-edit-mode.md create mode 100644 docs/specs/2026-04-08-shadowdark-character-manager-design.md create mode 100644 docs/specs/2026-04-08-v2-item-database-derived-stats-design.md create mode 100644 docs/specs/2026-04-09-view-edit-mode-design.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server/dist/db.js create mode 100644 server/dist/index.js create mode 100644 server/dist/routes/campaigns.js create mode 100644 server/dist/routes/characters.js create mode 100644 server/dist/socket.js create mode 100644 server/package-lock.json create mode 100644 server/package.json create mode 100644 server/src/db.ts create mode 100644 server/src/index.ts create mode 100644 server/src/routes/campaigns.ts create mode 100644 server/src/routes/characters.ts create mode 100644 server/src/routes/game-items.ts create mode 100644 server/src/routes/game-talents.ts create mode 100644 server/src/seed-items.ts create mode 100644 server/src/seed-talents.ts create mode 100644 server/src/socket.ts create mode 100644 server/tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3013d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +server/data/ +.superpowers/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ba8560a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,74 @@ +{ + "workbench.colorCustomizations": { + "editor.selectionBackground": "#5b9e8d", + "editor.selectionHighlightBackground": "#845f96", + "terminal.selectionBackground": "#4444aa", + "textMateRules": [ + { + "scope": "storage.type.class.jsdoc,entity.name.type.instance.jsdoc,variable.other.jsdoc", + "settings": { + "foreground": "#485c6c" + } + }, + { + "scope": "storage.type.class.jsdoc punctuation.definition.block.tag.jsdoc", + "settings": { + "foreground": "#485c6c" + } + }, + { + "scope": "comment.block.documentation.phpdoc.php", + "settings": { + "foreground": "#5c6370" + } + }, + { + "scope": "keyword.other.phpdoc.php,comment.block.documentation.phpdoc.php", + "settings": { + "foreground": "#76687d" + } + }, + { + "scope": "keyword.other.type.php,meta.other.type.phpdoc.php,comment.block.documentation.phpdoc.php", + "settings": { + "foreground": "#76687d" + } + }, + { + "scope": "support.class.php", + "settings": { + "foreground": "#E5C07B" + } + }, + { + "scope": "meta.other.type.phpdoc.php,comment.block.documentation.phpdoc.php", + "settings": { + "foreground": "#5c6370" + } + }, + { + "scope": "meta.other.type.phpdoc.php support.class.php", + "settings": { + "foreground": "#5c6370" + } + }, + { + "scope": "meta.other.type.phpdoc.php support.class.builtin.php", + "settings": { + "foreground": "#5c6370" + } + } + ], + "activityBar.background": "#875000", + "titleBar.activeBackground": "#BD7000", + "titleBar.activeForeground": "#FFFAF2", + "titleBar.inactiveBackground": "#875000", + "titleBar.inactiveForeground": "#FFFAF2", + "statusBar.background": "#875000", + "statusBar.foreground": "#FFFAF2", + "statusBar.debuggingBackground": "#875000", + "statusBar.debuggingForeground": "#FFFAF2", + "statusBar.noFolderBackground": "#875000", + "statusBar.noFolderForeground": "#FFFAF2" + } +} \ No newline at end of file diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..7677652 --- /dev/null +++ b/client/index.html @@ -0,0 +1,12 @@ + + + + + + Shadowdark Character Manager + + +
+ + + diff --git a/client/package-lock.json b/client/package-lock.json new file mode 100644 index 0000000..681b62e --- /dev/null +++ b/client/package-lock.json @@ -0,0 +1,1986 @@ +{ + "name": "shadowdark-client", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "shadowdark-client", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.1.1", + "socket.io-client": "^4.8.1" + }, + "devDependencies": { + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", + "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.334", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", + "dev": true, + "license": "ISC" + }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz", + "integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz", + "integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..8edc8cf --- /dev/null +++ b/client/package.json @@ -0,0 +1,22 @@ +{ + "name": "shadowdark-client", + "private": true, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.1.1", + "socket.io-client": "^4.8.1" + }, + "devDependencies": { + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} diff --git a/client/src/App.module.css b/client/src/App.module.css new file mode 100644 index 0000000..f7cf62c --- /dev/null +++ b/client/src/App.module.css @@ -0,0 +1,32 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: "Segoe UI", system-ui, -apple-system, sans-serif; + background: #1a1a2e; + color: #e0e0e0; + min-height: 100vh; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 1rem; +} + +.header { + text-align: center; + padding: 1rem 0; + border-bottom: 1px solid #333; + margin-bottom: 1.5rem; +} + +.header h1 { + font-size: 1.8rem; + color: #c9a84c; + font-variant: small-caps; + letter-spacing: 0.05em; +} diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..c040e3f --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,20 @@ +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import CampaignList from "./pages/CampaignList"; +import CampaignView from "./pages/CampaignView"; +import styles from "./App.module.css"; + +export default function App() { + return ( + +
+
+

Shadowdark

+
+ + } /> + } /> + +
+
+ ); +} diff --git a/client/src/api.ts b/client/src/api.ts new file mode 100644 index 0000000..7aed0f2 --- /dev/null +++ b/client/src/api.ts @@ -0,0 +1,109 @@ +import type { + Campaign, + Character, + Gear, + Talent, + GameItem, + GameTalent, +} from "./types"; + +const BASE = "/api"; + +async function request(path: string, options?: RequestInit): Promise { + const res = await fetch(`${BASE}${path}`, { + headers: { "Content-Type": "application/json" }, + ...options, + }); + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })); + throw new Error(err.error || res.statusText); + } + if (res.status === 204) return undefined as T; + return res.json(); +} + +// Campaigns +export const getCampaigns = () => request("/campaigns"); +export const createCampaign = (name: string) => + request("/campaigns", { + method: "POST", + body: JSON.stringify({ name }), + }); +export const deleteCampaign = (id: number) => + request(`/campaigns/${id}`, { method: "DELETE" }); + +// Characters +export const getCharacters = (campaignId: number) => + request(`/campaigns/${campaignId}/characters`); +export const createCharacter = ( + campaignId: number, + data: { name: string; class?: string; ancestry?: string; hp_max?: number }, +) => + request(`/campaigns/${campaignId}/characters`, { + method: "POST", + body: JSON.stringify(data), + }); +export const updateCharacter = (id: number, data: Partial) => + request(`/characters/${id}`, { + method: "PATCH", + body: JSON.stringify(data), + }); +export const deleteCharacter = (id: number) => + request(`/characters/${id}`, { method: "DELETE" }); + +// Stats +export const updateStat = ( + characterId: number, + statName: string, + value: number, +) => + request<{ characterId: number; statName: string; value: number }>( + `/characters/${characterId}/stats/${statName}`, + { method: "PATCH", body: JSON.stringify({ value }) }, + ); + +// Gear +export const addGear = ( + characterId: number, + data: { + name: string; + type?: string; + slot_count?: number; + properties?: Record; + effects?: Record; + game_item_id?: number | null; + }, +) => + request(`/characters/${characterId}/gear`, { + method: "POST", + body: JSON.stringify(data), + }); +export const removeGear = (characterId: number, gearId: number) => + request(`/characters/${characterId}/gear/${gearId}`, { + method: "DELETE", + }); + +// Talents +export const addTalent = ( + characterId: number, + data: { + name: string; + description?: string; + effect?: Record; + game_talent_id?: number | null; + }, +) => + request(`/characters/${characterId}/talents`, { + method: "POST", + body: JSON.stringify(data), + }); +export const removeTalent = (characterId: number, talentId: number) => + request(`/characters/${characterId}/talents/${talentId}`, { + method: "DELETE", + }); + +// Game Items +export const getGameItems = () => request("/game-items"); + +// Game Talents +export const getGameTalents = () => request("/game-talents"); diff --git a/client/src/components/AcDisplay.module.css b/client/src/components/AcDisplay.module.css new file mode 100644 index 0000000..c6b5304 --- /dev/null +++ b/client/src/components/AcDisplay.module.css @@ -0,0 +1,67 @@ +.container { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.label { + font-size: 0.75rem; + color: #888; + text-transform: uppercase; + font-weight: 600; +} + +.value { + font-size: 1.4rem; + font-weight: 700; + color: #5dade2; + cursor: pointer; + min-width: 2rem; + text-align: center; +} + +.value.overridden { + color: #c9a84c; +} + +.source { + font-size: 0.7rem; + color: #666; +} + +.override { + display: flex; + align-items: center; + gap: 0.3rem; +} + +.overrideIndicator { + font-size: 0.65rem; + color: #c9a84c; + cursor: pointer; + background: rgba(201, 168, 76, 0.15); + border: none; + border-radius: 3px; + padding: 0.1rem 0.3rem; +} + +.overrideIndicator:hover { + background: rgba(201, 168, 76, 0.3); +} + +.calculatedHint { + font-size: 0.65rem; + color: #555; +} + +.editInput { + width: 3rem; + padding: 0.2rem 0.3rem; + background: #0f1a30; + border: 1px solid #c9a84c; + border-radius: 4px; + color: #e0e0e0; + font-size: 1.2rem; + font-weight: 700; + text-align: center; +} diff --git a/client/src/components/AcDisplay.tsx b/client/src/components/AcDisplay.tsx new file mode 100644 index 0000000..22419c4 --- /dev/null +++ b/client/src/components/AcDisplay.tsx @@ -0,0 +1,92 @@ +import { useState } from "react"; +import type { AcBreakdown } from "../utils/derived-ac"; +import styles from "./AcDisplay.module.css"; + +interface AcDisplayProps { + breakdown: AcBreakdown; + onOverride: (value: number | null) => void; + mode?: "view" | "edit"; +} + +export default function AcDisplay({ + breakdown, + onOverride, + mode = "view", +}: AcDisplayProps) { + const [editing, setEditing] = useState(false); + const [editValue, setEditValue] = useState(""); + + const isOverridden = breakdown.override !== null; + + function startEdit() { + setEditValue(String(breakdown.effective)); + setEditing(true); + } + + function commitEdit() { + setEditing(false); + const num = parseInt(editValue, 10); + if (!isNaN(num) && num !== breakdown.calculated) { + onOverride(num); + } else if (num === breakdown.calculated) { + onOverride(null); + } + } + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === "Enter") commitEdit(); + if (e.key === "Escape") setEditing(false); + } + + return ( +
+ AC + {mode === "edit" ? ( + editing ? ( + setEditValue(e.target.value)} + onBlur={commitEdit} + onKeyDown={handleKeyDown} + autoFocus + /> + ) : ( + + {breakdown.effective} + + ) + ) : ( + + {breakdown.effective} + + )} + {mode === "edit" && ( +
+
{breakdown.source}
+ {isOverridden && ( +
+ + auto: {breakdown.calculated} + + +
+ )} +
+ )} +
+ ); +} diff --git a/client/src/components/AttackBlock.module.css b/client/src/components/AttackBlock.module.css new file mode 100644 index 0000000..1369fc1 --- /dev/null +++ b/client/src/components/AttackBlock.module.css @@ -0,0 +1,73 @@ +.section { + margin-top: 1rem; +} + +.title { + font-size: 0.9rem; + font-weight: 700; + color: #c9a84c; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 0.5rem; +} + +.list { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.line { + display: flex; + justify-content: space-between; + align-items: center; + background: #0f1a30; + border-radius: 6px; + padding: 0.35rem 0.6rem; + font-size: 0.85rem; +} + +.weaponName { + font-weight: 700; + text-transform: uppercase; + color: #e0e0e0; +} + +.stats { + color: #888; +} + +.modifier { + color: #c9a84c; + font-weight: 600; +} + +.damage { + color: #e0e0e0; +} + +.tags { + font-size: 0.7rem; + color: #666; + margin-left: 0.3rem; +} + +.talentLine { + font-style: italic; + color: #888; + font-size: 0.8rem; + padding: 0.25rem 0.6rem; +} + +.rollSpace { + width: 2.5rem; + text-align: center; + color: #444; + font-size: 0.75rem; +} + +.empty { + font-size: 0.8rem; + color: #555; + font-style: italic; +} diff --git a/client/src/components/AttackBlock.tsx b/client/src/components/AttackBlock.tsx new file mode 100644 index 0000000..501147d --- /dev/null +++ b/client/src/components/AttackBlock.tsx @@ -0,0 +1,43 @@ +import type { AttackLine } from "../types"; +import styles from "./AttackBlock.module.css"; + +interface AttackBlockProps { + attacks: AttackLine[]; +} + +export default function AttackBlock({ attacks }: AttackBlockProps) { + const weapons = attacks.filter((a) => !a.isTalent); + const talents = attacks.filter((a) => a.isTalent); + + return ( +
+
Attacks
+
+ {weapons.length === 0 && talents.length === 0 && ( + No weapons equipped + )} + {weapons.map((atk) => ( +
+ + {atk.name} + {atk.tags.length > 0 && ( + ({atk.tags.join(", ")}) + )} + + + {atk.modifierStr} + {", "} + {atk.damage} + + +
+ ))} + {talents.map((atk) => ( +
+ {atk.name}: {atk.description} +
+ ))} +
+
+ ); +} diff --git a/client/src/components/CharacterCard.module.css b/client/src/components/CharacterCard.module.css new file mode 100644 index 0000000..ded79fd --- /dev/null +++ b/client/src/components/CharacterCard.module.css @@ -0,0 +1,78 @@ +.card { + background: #16213e; + border: 1px solid #333; + border-radius: 10px; + padding: 1rem; + cursor: pointer; + transition: + border-color 0.15s, + transform 0.1s; +} + +.card:hover { + border-color: #c9a84c; + transform: translateY(-2px); +} + +.cardHeader { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 0.5rem; +} + +.name { + font-size: 1.1rem; + font-weight: 700; + color: #e0e0e0; +} + +.level { + font-size: 0.8rem; + color: #888; +} + +.meta { + font-size: 0.8rem; + color: #666; + margin-bottom: 0.75rem; +} + +.hpAcRow { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.ac { + display: flex; + align-items: center; + gap: 0.3rem; + font-weight: 700; +} + +.acLabel { + font-size: 0.75rem; + color: #888; + text-transform: uppercase; + font-weight: 600; +} + +.acValue { + font-size: 1.1rem; + color: #5dade2; +} + +.gearSummary { + font-size: 0.75rem; + color: #666; + text-align: right; + margin-top: 0.5rem; +} + +.xp { + font-size: 0.75rem; + color: #888; + text-align: right; +} diff --git a/client/src/components/CharacterCard.tsx b/client/src/components/CharacterCard.tsx new file mode 100644 index 0000000..2257c91 --- /dev/null +++ b/client/src/components/CharacterCard.tsx @@ -0,0 +1,63 @@ +import type { Character } from "../types"; +import HpBar from "./HpBar"; +import StatBlock from "./StatBlock"; +import styles from "./CharacterCard.module.css"; +import { calculateAC } from "../utils/derived-ac"; + +interface CharacterCardProps { + character: Character; + onHpChange: (characterId: number, hp: number) => void; + onStatChange: (characterId: number, statName: string, value: number) => void; + onClick: (characterId: number) => void; +} + +export default function CharacterCard({ + character, + onHpChange, + onStatChange, + onClick, +}: CharacterCardProps) { + const totalSlots = character.gear.reduce((sum, g) => sum + g.slot_count, 0); + + return ( +
onClick(character.id)}> +
+ + {character.name} + {character.title ? ` ${character.title}` : ""} + + Lvl {character.level} +
+
+ {character.ancestry} {character.class} +
+
e.stopPropagation()}> + onHpChange(character.id, hp)} + /> +
+ AC + + {calculateAC(character).effective} + +
+
+
e.stopPropagation()}> + + onStatChange(character.id, statName, value) + } + /> +
+
+ {totalSlots} gear slot{totalSlots !== 1 ? "s" : ""} used +
+
+ XP: {character.xp} / {character.level * 10} +
+
+ ); +} diff --git a/client/src/components/CharacterDetail.module.css b/client/src/components/CharacterDetail.module.css new file mode 100644 index 0000000..57482ee --- /dev/null +++ b/client/src/components/CharacterDetail.module.css @@ -0,0 +1,82 @@ +.overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + padding: 1rem; +} + +.modal { + background: #1a1a2e; + border: 1px solid #333; + border-radius: 12px; + width: 100%; + max-width: 1100px; + max-height: 95vh; + overflow-y: auto; + overflow-x: hidden; + padding: 1.5rem; + scrollbar-width: thin; + scrollbar-color: #333 transparent; +} + +.modal::-webkit-scrollbar { + width: 6px; +} + +.modal::-webkit-scrollbar-track { + background: transparent; +} + +.modal::-webkit-scrollbar-thumb { + background: #333; + border-radius: 3px; +} + +.modal::-webkit-scrollbar-thumb:hover { + background: #555; +} + +.topBar { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.75rem; +} + +.editBtn { + padding: 0.35rem 0.75rem; + background: transparent; + border: 1px solid #c9a84c; + border-radius: 5px; + color: #c9a84c; + cursor: pointer; + font-size: 0.8rem; + font-weight: 600; +} + +.editBtn:hover { + background: rgba(201, 168, 76, 0.15); +} + +.editBtn.active { + background: #c9a84c; + color: #1a1a2e; +} + +.closeBtn { + background: none; + border: none; + color: #888; + font-size: 1.5rem; + cursor: pointer; + padding: 0.25rem 0.5rem; +} + +.closeBtn:hover { + color: #e0e0e0; +} diff --git a/client/src/components/CharacterDetail.tsx b/client/src/components/CharacterDetail.tsx new file mode 100644 index 0000000..c1ba6e3 --- /dev/null +++ b/client/src/components/CharacterDetail.tsx @@ -0,0 +1,74 @@ +import { useState } from "react"; +import type { Character, GameItem } from "../types"; +import CharacterSheet from "./CharacterSheet"; +import styles from "./CharacterDetail.module.css"; + +interface CharacterDetailProps { + character: Character; + onUpdate: (id: number, data: Partial) => void; + onStatChange: (characterId: number, statName: string, value: number) => void; + onAddGearFromItem: (characterId: number, item: GameItem) => void; + onAddGearCustom: ( + characterId: number, + data: { name: string; type: string; slot_count: number }, + ) => void; + onRemoveGear: (characterId: number, gearId: number) => void; + onAddTalent: ( + characterId: number, + data: { + name: string; + description: string; + effect?: Record; + game_talent_id?: number | null; + }, + ) => void; + onRemoveTalent: (characterId: number, talentId: number) => void; + onDelete: (id: number) => void; + onClose: () => void; +} + +export default function CharacterDetail({ + character, + onUpdate, + onStatChange, + onAddGearFromItem, + onAddGearCustom, + onRemoveGear, + onAddTalent, + onRemoveTalent, + onDelete, + onClose, +}: CharacterDetailProps) { + const [mode, setMode] = useState<"view" | "edit">("view"); + + return ( +
+
e.stopPropagation()}> +
+ + +
+ + +
+
+ ); +} diff --git a/client/src/components/CharacterSheet.module.css b/client/src/components/CharacterSheet.module.css new file mode 100644 index 0000000..d3bb2ad --- /dev/null +++ b/client/src/components/CharacterSheet.module.css @@ -0,0 +1,157 @@ +.banner { + display: flex; + justify-content: space-between; + align-items: center; + background: linear-gradient(135deg, #16213e, #0f1a30); + border: 1px solid #333; + border-radius: 8px; + padding: 0.75rem 1rem; + margin-bottom: 1rem; +} + +.identity { + flex: 1; +} + +.name { + font-size: 1.4rem; + font-weight: 700; + color: #c9a84c; +} + +.title { + color: #888; + font-size: 0.9rem; +} + +.subtitle { + color: #666; + font-size: 0.8rem; + margin-top: 0.15rem; +} + +.vitals { + display: flex; + gap: 1.25rem; + align-items: center; +} + +.vital { + text-align: center; +} + +.vitalValue { + font-size: 1.3rem; + font-weight: 700; +} + +.hp { + color: #4caf50; +} + +.ac { + color: #5dade2; +} + +.vitalLabel { + font-size: 0.75rem; + color: #888; + text-transform: uppercase; + font-weight: 600; +} + +.hpValues { + display: flex; + align-items: center; + gap: 0.3rem; +} + +.hpSlash { + color: #666; + font-size: 0.9rem; +} + +.hpMax { + color: #888; + font-weight: 600; +} + +.xpThreshold { + font-size: 0.75rem; + color: #666; +} + +.xpCurrent { + color: #c9a84c; + font-weight: 600; +} + +.nameInput { + font-size: 1.3rem; + font-weight: 700; + color: #c9a84c; + background: #0f1a30; + border: 1px solid #333; + border-radius: 5px; + padding: 0.2rem 0.4rem; + width: 10rem; +} + +.nameInput:focus { + outline: none; + border-color: #c9a84c; +} + +.titleInput { + font-size: 0.85rem; + color: #888; + background: #0f1a30; + border: 1px solid #333; + border-radius: 5px; + padding: 0.15rem 0.4rem; + width: 8rem; + margin-left: 0.3rem; +} + +.titleInput:focus { + outline: none; + border-color: #c9a84c; +} + +.panels { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 0.75rem; +} + +@media (max-width: 1100px) { + .panels { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 768px) { + .panels { + grid-template-columns: 1fr; + } +} + +.deleteSection { + margin-top: 1rem; + padding-top: 0.75rem; + border-top: 1px solid #333; +} + +.deleteBtn { + padding: 0.4rem 0.75rem; + background: transparent; + border: 1px solid #e74c3c; + border-radius: 5px; + color: #e74c3c; + cursor: pointer; + font-size: 0.8rem; +} + +.deleteBtn:hover { + background: rgba(231, 76, 60, 0.1); +} diff --git a/client/src/components/CharacterSheet.tsx b/client/src/components/CharacterSheet.tsx new file mode 100644 index 0000000..4807b70 --- /dev/null +++ b/client/src/components/CharacterSheet.tsx @@ -0,0 +1,212 @@ +import { useState, useRef, useEffect } from "react"; +import type { Character, GameItem } from "../types"; +import { calculateAC } from "../utils/derived-ac"; +import AcDisplay from "./AcDisplay"; +import InlineNumber from "./InlineNumber"; +import StatsPanel from "./StatsPanel"; +import InfoPanel from "./InfoPanel"; +import GearPanel from "./GearPanel"; +import styles from "./CharacterSheet.module.css"; + +interface CharacterSheetProps { + character: Character; + mode: "view" | "edit"; + onUpdate: (id: number, data: Partial) => void; + onStatChange: (characterId: number, statName: string, value: number) => void; + onAddGearFromItem: (characterId: number, item: GameItem) => void; + onAddGearCustom: ( + characterId: number, + data: { name: string; type: string; slot_count: number }, + ) => void; + onRemoveGear: (characterId: number, gearId: number) => void; + onAddTalent: ( + characterId: number, + data: { + name: string; + description: string; + effect?: Record; + game_talent_id?: number | null; + }, + ) => void; + onRemoveTalent: (characterId: number, talentId: number) => void; + onDelete: (id: number) => void; +} + +export default function CharacterSheet({ + character, + mode, + onUpdate, + onStatChange, + onAddGearFromItem, + onAddGearCustom, + onRemoveGear, + onAddTalent, + onRemoveTalent, + onDelete, +}: CharacterSheetProps) { + const [confirmDelete, setConfirmDelete] = useState(false); + const debounceRef = useRef>(); + const acBreakdown = calculateAC(character); + const xpThreshold = character.level * 10; + + useEffect(() => { + return () => { + if (debounceRef.current) clearTimeout(debounceRef.current); + }; + }, []); + + function handleNameField(field: string, value: string) { + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => { + onUpdate(character.id, { [field]: value }); + }, 400); + } + + function handleAcOverride(value: number | null) { + const overrides = { ...(character.overrides || {}) }; + if (value === null) { + delete overrides.ac; + } else { + overrides.ac = value; + } + onUpdate(character.id, { overrides } as Partial); + } + + return ( + <> + {/* HEADER BANNER */} +
+
+ {mode === "edit" ? ( +
+ handleNameField("name", e.target.value)} + /> + handleNameField("title", e.target.value)} + /> +
+ ) : ( +
+ {character.name} + {character.title && ( + {character.title} + )} +
+ )} +
+ Level {character.level} {character.ancestry} {character.class} +
+
+
+ {/* HP — click to edit */} +
+
+ HP + onUpdate(character.id, { hp_current: hp })} + className={`${styles.vitalValue} ${styles.hp}`} + /> + / + {mode === "edit" ? ( + onUpdate(character.id, { hp_max: hp })} + className={styles.hpMax} + min={0} + /> + ) : ( + {character.hp_max} + )} +
+
+ + {/* AC — display only in view, override in edit */} +
+ +
+ + {/* XP — click to edit */} +
+
+ XP + onUpdate(character.id, { xp })} + className={`${styles.vitalValue} ${styles.xpCurrent}`} + min={0} + /> + / {xpThreshold} +
+
+
+
+ + {/* THREE PANELS */} +
+ + + + onUpdate(charId, { [field]: value }) + } + /> +
+ + {/* DELETE — edit mode only */} + {mode === "edit" && ( +
+ {confirmDelete ? ( +
+ Delete {character.name}? + {" "} + +
+ ) : ( + + )} +
+ )} + + ); +} diff --git a/client/src/components/CurrencyRow.module.css b/client/src/components/CurrencyRow.module.css new file mode 100644 index 0000000..0810c9b --- /dev/null +++ b/client/src/components/CurrencyRow.module.css @@ -0,0 +1,70 @@ +.row { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.5rem 0; +} + +.coin { + display: flex; + align-items: center; + gap: 0.3rem; +} + +.coinLabel { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.gp { + color: #c9a84c; +} +.sp { + color: #a0a0a0; +} +.cp { + color: #b87333; +} + +.coinInput { + width: 3.5rem; + padding: 0.25rem 0.4rem; + background: #0f1a30; + border: 1px solid #333; + border-radius: 4px; + color: #e0e0e0; + font-size: 0.85rem; + text-align: center; +} + +.coinInput:focus { + outline: none; + border-color: #c9a84c; +} + +.coinBtn { + width: 20px; + height: 20px; + border-radius: 50%; + border: 1px solid #444; + background: #16213e; + color: #e0e0e0; + cursor: pointer; + font-size: 0.75rem; + display: flex; + align-items: center; + justify-content: center; +} + +.coinBtn:hover { + border-color: #c9a84c; + color: #c9a84c; +} + +.coinValue { + min-width: 1.5rem; + text-align: center; + font-size: 0.85rem; + font-weight: 600; +} diff --git a/client/src/components/CurrencyRow.tsx b/client/src/components/CurrencyRow.tsx new file mode 100644 index 0000000..659d08a --- /dev/null +++ b/client/src/components/CurrencyRow.tsx @@ -0,0 +1,80 @@ +import InlineNumber from "./InlineNumber"; +import styles from "./CurrencyRow.module.css"; + +interface CurrencyRowProps { + gp: number; + sp: number; + cp: number; + onChange: (field: "gp" | "sp" | "cp", value: number) => void; + mode?: "view" | "edit"; +} + +export default function CurrencyRow({ + gp, + sp, + cp, + onChange, + mode = "view", +}: CurrencyRowProps) { + return ( +
+
+ GP + {mode === "edit" ? ( + onChange("gp", Number(e.target.value))} + /> + ) : ( + onChange("gp", v)} + className={styles.coinValue} + min={0} + /> + )} +
+
+ SP + {mode === "edit" ? ( + onChange("sp", Number(e.target.value))} + /> + ) : ( + onChange("sp", v)} + className={styles.coinValue} + min={0} + /> + )} +
+
+ CP + {mode === "edit" ? ( + onChange("cp", Number(e.target.value))} + /> + ) : ( + onChange("cp", v)} + className={styles.coinValue} + min={0} + /> + )} +
+
+ ); +} diff --git a/client/src/components/GearList.module.css b/client/src/components/GearList.module.css new file mode 100644 index 0000000..0ffdfe8 --- /dev/null +++ b/client/src/components/GearList.module.css @@ -0,0 +1,153 @@ +.section { + margin-top: 0.75rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.title { + font-size: 0.9rem; + font-weight: 700; + color: #c9a84c; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.slotCounter { + font-size: 0.8rem; + font-weight: 600; +} + +.slotCounter.normal { + color: #4caf50; +} +.slotCounter.warning { + color: #ff9800; +} +.slotCounter.over { + color: #e74c3c; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.tableHeader { + font-size: 0.7rem; + color: #666; + text-transform: uppercase; + font-weight: 600; + text-align: left; + padding: 0.25rem 0.5rem; + border-bottom: 1px solid #333; +} + +.right { + text-align: right; +} +.center { + text-align: center; +} + +.row { + border-bottom: 1px solid #222; +} + +.row:hover { + background: rgba(201, 168, 76, 0.05); +} + +.cell { + padding: 0.35rem 0.5rem; + font-size: 0.85rem; + vertical-align: middle; +} + +.itemName { + font-weight: 600; +} + +.removeBtn { + background: none; + border: none; + color: #555; + cursor: pointer; + font-size: 0.9rem; + padding: 0.1rem 0.3rem; +} + +.removeBtn:hover { + color: #e74c3c; +} + +.addArea { + margin-top: 0.5rem; +} + +.addBtn { + padding: 0.4rem 0.75rem; + background: #c9a84c; + color: #1a1a2e; + border: none; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + font-size: 0.8rem; +} + +.addBtn:hover { + background: #d4b65a; +} + +.cancelBtn { + padding: 0.4rem 0.75rem; + background: #333; + color: #888; + border: none; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + font-size: 0.8rem; +} + +.customForm { + display: flex; + gap: 0.4rem; + margin-top: 0.5rem; +} + +.customInput { + flex: 1; + padding: 0.4rem 0.6rem; + background: #0f1a30; + border: 1px solid #333; + border-radius: 6px; + color: #e0e0e0; + font-size: 0.85rem; +} + +.customInput:focus { + outline: none; + border-color: #c9a84c; +} + +.customSelect { + padding: 0.4rem 0.6rem; + background: #0f1a30; + border: 1px solid #333; + border-radius: 6px; + color: #e0e0e0; + font-size: 0.85rem; +} + +.empty { + font-size: 0.8rem; + color: #555; + font-style: italic; + padding: 0.5rem; +} diff --git a/client/src/components/GearList.tsx b/client/src/components/GearList.tsx new file mode 100644 index 0000000..886921c --- /dev/null +++ b/client/src/components/GearList.tsx @@ -0,0 +1,184 @@ +import { useState } from "react"; +import type { Gear, GameItem } from "../types"; +import ItemPicker from "./ItemPicker"; +import CurrencyRow from "./CurrencyRow"; +import styles from "./GearList.module.css"; + +interface GearListProps { + gear: Gear[]; + gp: number; + sp: number; + cp: number; + slotsUsed: number; + slotsMax: number; + onAddFromItem: (item: GameItem) => void; + onAddCustom: (data: { + name: string; + type: string; + slot_count: number; + }) => void; + onRemove: (gearId: number) => void; + onCurrencyChange: (field: "gp" | "sp" | "cp", value: number) => void; + mode?: "view" | "edit"; +} + +function gearIcon(type: string): string { + switch (type) { + case "weapon": + return "⚔"; + case "armor": + return "🛡"; + case "spell": + return "✨"; + default: + return "⚙"; + } +} + +export default function GearList({ + gear, + gp, + sp, + cp, + slotsUsed, + slotsMax, + onAddFromItem, + onAddCustom, + onRemove, + onCurrencyChange, + mode = "view", +}: GearListProps) { + const [showPicker, setShowPicker] = useState(false); + const [showCustom, setShowCustom] = useState(false); + const [customName, setCustomName] = useState(""); + const [customType, setCustomType] = useState("gear"); + + function handleCustomAdd(e: React.FormEvent) { + e.preventDefault(); + if (!customName.trim()) return; + onAddCustom({ name: customName.trim(), type: customType, slot_count: 1 }); + setCustomName(""); + setShowCustom(false); + } + + function handleItemSelect(item: GameItem) { + onAddFromItem(item); + setShowPicker(false); + } + + const slotClass = + slotsUsed >= slotsMax + ? styles.over + : slotsUsed >= slotsMax - 2 + ? styles.warning + : styles.normal; + + return ( +
+
+ Gear & Inventory + + Slots: {slotsUsed} / {slotsMax} + +
+ + {gear.length === 0 ? ( +

No gear yet

+ ) : ( + + + + + + + + + + + {gear.map((item) => ( + + + + + + + ))} + +
ItemType + Slots +
+ {item.name} + + {gearIcon(item.type)} + + {item.slot_count > 0 ? item.slot_count : "—"} + + {mode === "edit" && ( + + )} +
+ )} + + + + {mode === "edit" && ( +
+ {showPicker ? ( + { + setShowPicker(false); + setShowCustom(true); + }} + onClose={() => setShowPicker(false)} + /> + ) : showCustom ? ( +
+ setCustomName(e.target.value)} + autoFocus + /> + + + +
+ ) : ( + + )} +
+ )} +
+ ); +} diff --git a/client/src/components/GearPanel.module.css b/client/src/components/GearPanel.module.css new file mode 100644 index 0000000..2f78a99 --- /dev/null +++ b/client/src/components/GearPanel.module.css @@ -0,0 +1,6 @@ +.panel { + background: #16213e; + border: 1px solid #333; + border-radius: 8px; + padding: 0.75rem; +} diff --git a/client/src/components/GearPanel.tsx b/client/src/components/GearPanel.tsx new file mode 100644 index 0000000..f967d23 --- /dev/null +++ b/client/src/components/GearPanel.tsx @@ -0,0 +1,50 @@ +import type { Character, GameItem } from "../types"; +import GearList from "./GearList"; +import styles from "./GearPanel.module.css"; + +interface GearPanelProps { + character: Character; + mode: "view" | "edit"; + onAddGearFromItem: (characterId: number, item: GameItem) => void; + onAddGearCustom: ( + characterId: number, + data: { name: string; type: string; slot_count: number }, + ) => void; + onRemoveGear: (characterId: number, gearId: number) => void; + onCurrencyChange: ( + characterId: number, + field: "gp" | "sp" | "cp", + value: number, + ) => void; +} + +export default function GearPanel({ + character, + mode, + onAddGearFromItem, + onAddGearCustom, + onRemoveGear, + onCurrencyChange, +}: GearPanelProps) { + const slotsUsed = character.gear.reduce((sum, g) => sum + g.slot_count, 0); + + return ( +
+ onAddGearFromItem(character.id, item)} + onAddCustom={(data) => onAddGearCustom(character.id, data)} + onRemove={(gearId) => onRemoveGear(character.id, gearId)} + onCurrencyChange={(field, value) => + onCurrencyChange(character.id, field, value) + } + mode={mode} + /> +
+ ); +} diff --git a/client/src/components/HpBar.module.css b/client/src/components/HpBar.module.css new file mode 100644 index 0000000..d4618ad --- /dev/null +++ b/client/src/components/HpBar.module.css @@ -0,0 +1,60 @@ +.hpBar { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.label { + font-size: 0.75rem; + color: #888; + text-transform: uppercase; + font-weight: 600; +} + +.values { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 1.1rem; + font-weight: 700; +} + +.current { + color: #4caf50; +} + +.current.hurt { + color: #ff9800; +} + +.current.critical { + color: #e74c3c; +} + +.slash { + color: #666; +} + +.max { + color: #888; +} + +.btn { + width: 24px; + height: 24px; + border-radius: 50%; + border: 1px solid #444; + background: #16213e; + color: #e0e0e0; + cursor: pointer; + font-size: 0.9rem; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; +} + +.btn:hover { + border-color: #c9a84c; + color: #c9a84c; +} diff --git a/client/src/components/HpBar.tsx b/client/src/components/HpBar.tsx new file mode 100644 index 0000000..7cf130b --- /dev/null +++ b/client/src/components/HpBar.tsx @@ -0,0 +1,30 @@ +import styles from "./HpBar.module.css"; + +interface HpBarProps { + current: number; + max: number; + onChange: (current: number) => void; +} + +export default function HpBar({ current, max, onChange }: HpBarProps) { + const ratio = max > 0 ? current / max : 1; + const colorClass = + ratio <= 0.25 ? styles.critical : ratio <= 0.5 ? styles.hurt : ""; + + return ( +
+ HP + + + {current} + / + {max} + + +
+ ); +} diff --git a/client/src/components/HpDisplay.module.css b/client/src/components/HpDisplay.module.css new file mode 100644 index 0000000..5c11c5a --- /dev/null +++ b/client/src/components/HpDisplay.module.css @@ -0,0 +1,143 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.2rem; +} + +.values { + display: flex; + align-items: baseline; + gap: 0.2rem; +} + +.current { + font-size: 1.4rem; + font-weight: 700; + color: #4caf50; +} + +.current.hurt { + color: #ff9800; +} + +.current.critical { + color: #e74c3c; +} + +.slash { + color: #666; + font-size: 0.9rem; +} + +.max { + color: #888; + font-weight: 600; + font-size: 1rem; +} + +.label { + font-size: 0.65rem; + color: #888; + text-transform: uppercase; + font-weight: 600; +} + +.buttons { + display: flex; + gap: 0.3rem; +} + +.healBtn { + padding: 0.15rem 0.5rem; + background: rgba(76, 175, 80, 0.15); + border: 1px solid #4caf50; + border-radius: 4px; + color: #4caf50; + cursor: pointer; + font-size: 0.65rem; + font-weight: 600; + text-transform: uppercase; +} + +.healBtn:hover { + background: rgba(76, 175, 80, 0.3); +} + +.dmgBtn { + padding: 0.15rem 0.5rem; + background: rgba(231, 76, 60, 0.15); + border: 1px solid #e74c3c; + border-radius: 4px; + color: #e74c3c; + cursor: pointer; + font-size: 0.65rem; + font-weight: 600; + text-transform: uppercase; +} + +.dmgBtn:hover { + background: rgba(231, 76, 60, 0.3); +} + +.inputRow { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.amountInput { + width: 2.5rem; + padding: 0.2rem 0.3rem; + background: #0f1a30; + border: 1px solid #444; + border-radius: 4px; + color: #e0e0e0; + font-size: 0.85rem; + text-align: center; + font-weight: 600; +} + +.amountInput:focus { + outline: none; +} + +.amountInput.healing { + border-color: #4caf50; +} + +.amountInput.damage { + border-color: #e74c3c; +} + +.applyBtn { + padding: 0.2rem 0.4rem; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.7rem; + font-weight: 600; +} + +.applyBtn.healing { + background: #4caf50; + color: #1a1a2e; +} + +.applyBtn.damage { + background: #e74c3c; + color: #fff; +} + +.cancelBtn { + background: none; + border: none; + color: #666; + cursor: pointer; + font-size: 0.85rem; + padding: 0 0.2rem; +} + +.cancelBtn:hover { + color: #e0e0e0; +} diff --git a/client/src/components/HpDisplay.tsx b/client/src/components/HpDisplay.tsx new file mode 100644 index 0000000..f337d11 --- /dev/null +++ b/client/src/components/HpDisplay.tsx @@ -0,0 +1,84 @@ +import { useState } from "react"; +import styles from "./HpDisplay.module.css"; + +interface HpDisplayProps { + current: number; + max: number; + onChange: (newCurrent: number) => void; +} + +export default function HpDisplay({ current, max, onChange }: HpDisplayProps) { + const [mode, setMode] = useState<"idle" | "heal" | "damage">("idle"); + const [amount, setAmount] = useState(""); + + const ratio = max > 0 ? current / max : 1; + const colorClass = + ratio <= 0.25 ? styles.critical : ratio <= 0.5 ? styles.hurt : ""; + + function apply() { + const num = parseInt(amount, 10); + if (isNaN(num) || num <= 0) { + reset(); + return; + } + if (mode === "heal") { + onChange(Math.min(current + num, max)); + } else if (mode === "damage") { + onChange(current - num); + } + reset(); + } + + function reset() { + setMode("idle"); + setAmount(""); + } + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === "Enter") apply(); + if (e.key === "Escape") reset(); + } + + return ( +
+
+ {current} + / + {max} +
+
HP
+ {mode === "idle" ? ( +
+ + +
+ ) : ( +
+ setAmount(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="#" + autoFocus + /> + + +
+ )} +
+ ); +} diff --git a/client/src/components/InfoPanel.module.css b/client/src/components/InfoPanel.module.css new file mode 100644 index 0000000..733510c --- /dev/null +++ b/client/src/components/InfoPanel.module.css @@ -0,0 +1,112 @@ +.panel { + background: #16213e; + border: 1px solid #333; + border-radius: 8px; + padding: 0.75rem; +} + +.sectionTitle { + font-size: 0.8rem; + font-weight: 700; + color: #c9a84c; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 0.5rem; +} + +.infoGrid { + display: flex; + flex-direction: column; + gap: 0.4rem; + margin-top: 0.75rem; +} + +.infoRow { + display: flex; + gap: 0.3rem; + font-size: 0.85rem; +} + +.infoLabel { + color: #666; + font-size: 0.7rem; + text-transform: uppercase; + font-weight: 600; + min-width: 5rem; +} + +.infoValue { + color: #e0e0e0; +} + +.notes { + font-size: 0.85rem; + color: #aaa; + white-space: pre-wrap; + margin-top: 0.5rem; +} + +.editField { + padding: 0.3rem 0.5rem; + background: #0f1a30; + border: 1px solid #333; + border-radius: 5px; + color: #e0e0e0; + font-size: 0.85rem; + width: 100%; +} + +.editField:focus { + outline: none; + border-color: #c9a84c; +} + +.editSelect { + padding: 0.3rem 0.5rem; + background: #0f1a30; + border: 1px solid #333; + border-radius: 5px; + color: #e0e0e0; + font-size: 0.85rem; +} + +.editRow { + display: flex; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.editRow > * { + flex: 1; +} + +.field { + display: flex; + flex-direction: column; + gap: 0.15rem; +} + +.fieldLabel { + font-size: 0.65rem; + color: #666; + text-transform: uppercase; + font-weight: 600; +} + +.notesEdit { + width: 100%; + min-height: 50px; + padding: 0.4rem; + background: #0f1a30; + border: 1px solid #333; + border-radius: 5px; + color: #e0e0e0; + font-size: 0.85rem; + font-family: inherit; + resize: vertical; +} + +.notesEdit:focus { + outline: none; + border-color: #c9a84c; +} diff --git a/client/src/components/InfoPanel.tsx b/client/src/components/InfoPanel.tsx new file mode 100644 index 0000000..8579e78 --- /dev/null +++ b/client/src/components/InfoPanel.tsx @@ -0,0 +1,196 @@ +import { useRef, useEffect } from "react"; +import type { Character } from "../types"; +import TalentList from "./TalentList"; +import styles from "./InfoPanel.module.css"; + +const CLASSES = ["Fighter", "Priest", "Thief", "Wizard"]; +const ANCESTRIES = ["Human", "Elf", "Dwarf", "Halfling", "Goblin", "Half-Orc"]; +const ALIGNMENTS = ["Lawful", "Neutral", "Chaotic"]; + +interface InfoPanelProps { + character: Character; + mode: "view" | "edit"; + onUpdate: (id: number, data: Partial) => void; + onAddTalent: ( + characterId: number, + data: { + name: string; + description: string; + effect?: Record; + game_talent_id?: number | null; + }, + ) => void; + onRemoveTalent: (characterId: number, talentId: number) => void; +} + +export default function InfoPanel({ + character, + mode, + onUpdate, + onAddTalent, + onRemoveTalent, +}: InfoPanelProps) { + const debounceRef = useRef>(); + + useEffect(() => { + return () => { + if (debounceRef.current) clearTimeout(debounceRef.current); + }; + }, []); + + function handleField(field: string, value: string | number) { + if (typeof value === "string") { + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => { + onUpdate(character.id, { [field]: value }); + }, 400); + } else { + onUpdate(character.id, { [field]: value }); + } + } + + return ( +
+ onAddTalent(character.id, data)} + onRemove={(id) => onRemoveTalent(character.id, id)} + mode={mode} + /> + +
+ Info +
+ + {mode === "view" ? ( +
+ {character.background && ( +
+ Background + {character.background} +
+ )} + {character.deity && ( +
+ Deity + {character.deity} +
+ )} + {character.languages && ( +
+ Languages + {character.languages} +
+ )} +
+ Alignment + {character.alignment} +
+ {character.notes && ( + <> +
+ Notes +
+
{character.notes}
+ + )} +
+ ) : ( +
+
+
+ + +
+
+ + +
+
+
+
+ + handleField("level", Number(e.target.value))} + /> +
+
+ + +
+
+
+ + handleField("background", e.target.value)} + /> +
+
+ + handleField("deity", e.target.value)} + /> +
+
+ + handleField("languages", e.target.value)} + /> +
+
+ +