Refactor UI components for improved styling and consistency

- Updated RequisitionShop component styles for a cohesive dark theme.
- Enhanced RulesTab component with consistent dark styling and improved button visibility.
- Modified XPBar component to align with new dark theme aesthetics.
- Adjusted global styles in index.css to support the new design.
- Added comprehensive DEVELOPMENT_GUIDE.md for project setup, deployment, and maintenance instructions.
This commit is contained in:
2025-12-11 21:15:14 +01:00
parent d84ca39e61
commit 35335a32d3
13 changed files with 882 additions and 225 deletions

540
DEVELOPMENT_GUIDE.md Normal file
View File

@@ -0,0 +1,540 @@
# Deathwatch Roller - Development & Operations Guide
Complete documentation for building, testing, deploying, and maintaining the Deathwatch Roller application. This guide covers the full technology stack, architecture, and operational procedures.
## Project Overview
Deathwatch Roller is a full-stack web application for managing tabletop RPG gameplay. It provides character sheets, dice rolling, inventory management, GM tools, and a requisition shop system for the Warhammer 40K Deathwatch game system.
## Technology Stack
### Frontend
- **Framework**: React 18.2.0
- **Styling**: Tailwind CSS
- **Testing**: Jest + React Testing Library
- **Build Tool**: create-react-app (react-scripts)
- **State Management**: React hooks (useState, useCallback, useEffect)
- **HTTP Client**: Axios
### Backend
- **Runtime**: Node.js
- **Framework**: Express.js 5.1.0
- **Database**: MariaDB
- **Security**: bcrypt, CORS, custom session validation
- **File Processing**: PDF-parse (for importing game data)
### DevOps
- **Process Management**: PM2 (backend server)
- **Frontend Serving**: serve (static file server on port 3000)
- **Backend API**: Running on port 5000
- **Build Output**: Production builds in `/build` directory
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────┐
│ CLIENT (React) │
│ - Player Tab (character sheets, inventory) │
│ - Dice Roller (d100 rolls with modifiers) │
│ - Requisition Shop (purchase equipment with RP) │
│ - Bestiary (enemy database and stats) │
│ - Rules (searchable game rules reference) │
│ - GM Kit (enemy generation, bulk operations) │
│ - Player Management (admin panel for GMs) │
│ Port: 3000 (via serve or npm start) │
└────────────┬────────────────────────────────────────────────┘
HTTP/CORS
┌────────────▼────────────────────────────────────────────────┐
│ BACKEND (Express) │
│ API Routes: │
│ - /api/players - Player CRUD & character data │
│ - /api/sessions - Session validation & authentication │
│ - /api/shop - Requisition shop purchases │
│ - /api/rules - Game rules database access │
│ - /api/weapons - Weapon stats & properties │
│ - /api/bestiary - Enemy database access │
│ - /api/gmkit - GM tools & bulk operations │
│ Port: 5000 │
└────────────┬────────────────────────────────────────────────┘
SQL Queries
┌────────────▼────────────────────────────────────────────────┐
│ MariaDB Database │
│ Tables: │
│ - players (character data, RP, XP, renown) │
│ - sessions (authentication) │
│ - weapons (weapon stats & costs) │
│ - rules (game rules reference) │
│ - bestiary (enemy data) │
└─────────────────────────────────────────────────────────────┘
```
## Project Structure
```
dwroller/
├── src/
│ ├── components/ # React UI components
│ │ ├── PlayerManagement.jsx # GM admin panel
│ │ ├── PlayerTab.jsx # Player character sheet
│ │ ├── DeathwatchRoller.jsx # Dice rolling interface
│ │ ├── RequisitionShop.jsx # Equipment shop
│ │ ├── BestiaryTab.jsx # Enemy database
│ │ ├── RulesTab.jsx # Rules reference
│ │ ├── GMKit.jsx # GM tools
│ │ ├── XPBar.jsx # Experience progress bar
│ │ └── GMKit_old.jsx # Legacy (unused)
│ ├── utils/
│ │ └── logger.js # Logging utility
│ ├── tests/
│ │ ├── bestiaryTab.test.js
│ │ ├── login.test.js
│ │ ├── playerManagement.test.js
│ │ └── requisitionShop.test.js
│ ├── App.js # Main app with routing
│ ├── App.css # App styling
│ ├── index.js # React entry point
│ ├── index.css # Global Tailwind styles
│ └── index.html # HTML template
├── database/
│ ├── server.js # Express API server
│ ├── mariadb.js # Database connection
│ ├── sessionModel.js # Session management
│ ├── pm2.config.js # PM2 configuration
│ ├── requireSession.js # Auth middleware
│ ├── shop-helpers.js # Shop business logic
│ ├── switch-db.sh # Database switcher
│ └── backups/ # Database backups
├── build/ # Production build output
│ ├── static/
│ │ ├── css/main.*.css # Compiled Tailwind
│ │ └── js/ # Bundled JavaScript
│ └── index.html # Served HTML
├── backup-scripts/ # Utility scripts
│ ├── migrate-to-sqlite.js
│ ├── sync-skills-csv-to-db.js
│ └── ...
├── package.json # Dependencies & scripts
├── tailwind.config.js # Tailwind configuration
├── jest.config.js # Jest test configuration
├── eslint.config.js # ESLint configuration
└── README.md # Project readme
```
## Build Process
### Local Development
```bash
# Install dependencies
npm install
# Start dev server with hot reload
npm start
# or
npm run dev
```
### Build Steps
**1. Run Tests**
```bash
npm run test:unit
# Runs all unit tests using Jest
```
**2. Build for Production**
```bash
npm run build
# Executes: npm run test:unit && react-scripts build
# Creates optimized production bundle in /build
```
**3. Verify Build Output**
```bash
# Check that build directory was created with static files
ls -la build/static/css/
ls -la build/static/js/
# Verify CSS size and integrity
gzip -c build/static/css/main.*.css | wc -c
# Expected: ~6-7 KB gzipped
```
### Build Configuration
- **Source Maps**: Generated for debugging
- **CSS Minification**: Automatic via react-scripts
- **JS Minification**: Webpack tree-shaking + uglification
- **Public Path**: Set to `/` for root-level deployment
### Alternative Build Commands
```bash
# Fast build without tests (use carefully)
npm run build:fast
# Build with integration tests
npm run build:with-integration
# Test -> Build -> Test cycle
npm run test-build-test
```
## Deployment Process
### 1. Build for Production
```bash
cd /home/alex/git/dwroller
npm run build
# Output: /build directory ready for deployment
```
### 2. Start Frontend Server
```bash
# Serve the production build on port 3000
npx serve -s build -l 3000 &
# Or with PM2 (optional)
pm2 start "npx serve -s build -l 3000" --name dwroller-frontend
pm2 save
```
### 3. Ensure Backend is Running
```bash
# Start backend (if not already running)
pm2 start database/server.js --name deathwatch-server
# Verify it's running
pm2 list
pm2 logs deathwatch-server
```
### 4. Verify Deployment
```bash
# Test frontend accessibility
curl http://localhost:3000
# Test API connectivity
curl http://localhost:5000/api/players
# Check both are running
ps aux | grep -E "serve|node"
```
## Testing
### Unit Tests
```bash
npm run test:unit
# Runs Jest with --runInBand to avoid conflicts
```
**Current Test Status:**
- ✅ 13 tests passing
- ⚠️ 12 tests failing (pre-existing, architectural issues)
- Tests cover: login, requisition shop, bestiary, player management
### Running Specific Tests
```bash
# Run a specific test file
npm test -- src/tests/login.test.js
# Run tests matching a pattern
npm test -- --testNamePattern="PlayerManagement"
# Watch mode (automatic re-run on file changes)
npm test -- --watch
```
### Test Coverage
```bash
npm test -- --coverage
# Shows coverage report for all tested files
```
## Troubleshooting & Maintenance
### Frontend Not Updating
**Problem**: Changes to source files not appearing in browser
**Solution**:
```bash
# Full rebuild
rm -rf build
npm run build
# Kill old serve process and restart
pkill -9 -f "serve.*build"
npx serve -s build -l 3000 &
# Hard refresh browser: Ctrl+Shift+R (Windows/Linux) or Cmd+Shift+R (Mac)
# Clear browser cache manually in DevTools
```
### Database Connection Issues
**Problem**: "Failed to fetch players" error
**Solution**:
```bash
# Check if backend is running
pm2 list | grep deathwatch
# View backend logs
pm2 logs deathwatch-server --lines 100
# Check database connection
curl http://localhost:5000/api/players
# Verify MariaDB is running
systemctl status mariadb
# or
mysql -u root -p -e "SELECT 1;"
```
### Port Already in Use
**Problem**: "EADDRINUSE: address already in use :::3000"
**Solution**:
```bash
# Find process using port 3000
lsof -i :3000
# or
netstat -tulpn | grep 3000
# Kill the process
kill -9 <PID>
# Or use a different port
npx serve -s build -l 3001
```
### API CORS Errors
**Problem**: "Access to XMLHttpRequest blocked by CORS policy"
**Solution**: CORS is already configured in backend (database/server.js)
- Proxy is configured in package.json: `"proxy": "http://localhost:5000"`
- Requests to `/api/*` are automatically routed to backend
- If issues persist, check backend logs and CORS configuration
## Performance Optimization
### Frontend
- **Code Splitting**: React automatically chunks large components
- **CSS**: Tailwind purges unused classes in production
- **JS**: Webpack tree-shaking removes dead code
- **Assets**: Images optimized by build process
### Backend
- **Query Caching**: Rules and bestiary data cached in memory
- **Session Validation**: Simple token-based (can be improved)
- **Database Indexing**: Ensure proper indexes on player ID, session ID
### Monitoring
```bash
# Check memory usage
pm2 list
# Monitor in real-time
pm2 monit
# View detailed logs
pm2 logs
# Check response times
curl -w "@curl-format.txt" http://localhost:5000/api/players
```
## Security Considerations
### Authentication
- ✅ Session-based with session ID validation
- ✅ bcrypt password hashing
- ⚠️ Default password (1234) for testing - CHANGE IN PRODUCTION
- ⚠️ Simple token format - consider JWT
### Authorization
- ✅ GM role check for Player Management
- ✅ Session validation on API routes
- ⚠️ No rate limiting implemented
- ⚠️ No input validation/sanitization
### HTTPS
- ⚠️ Not enforced (add in production)
- Use reverse proxy (nginx) or cloud provider SSL
### Recommended Improvements
1. Remove hardcoded test password
2. Implement JWT tokens instead of session IDs
3. Add rate limiting middleware
4. Add input validation/sanitization
5. Enable HTTPS/SSL
6. Implement CSRF protection
7. Add request logging and monitoring
## Database
### Connection
- **Host**: localhost (configured in database/mariadb.js)
- **User**: configurable (default: root)
- **Database**: dwroller (or deathwatch)
- **Driver**: mysql2
### Main Tables
```sql
-- Players table
CREATE TABLE players (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) UNIQUE NOT NULL,
passwordHash VARCHAR(255) NOT NULL,
rp INT DEFAULT 0,
xp INT DEFAULT 0,
renown VARCHAR(50) DEFAULT 'None',
tabInfo JSON,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Sessions table
CREATE TABLE sessions (
id VARCHAR(255) PRIMARY KEY,
playerName VARCHAR(255) NOT NULL,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expiresAt TIMESTAMP
);
-- Weapons, Rules, Bestiary tables
-- (See database/mariadb.js for full schema)
```
### Backup & Restore
```bash
# Backup database
mysqldump -u root -p dwroller > backup.sql
# Restore database
mysql -u root -p dwroller < backup.sql
# Verify backup integrity
npm run backup-scripts/repair-skills.js
```
## Development Workflow
### Making Changes
1. Create feature branch: `git checkout -b feature/my-feature`
2. Make code changes
3. Test locally: `npm start` and manual testing
4. Run tests: `npm run test:unit`
5. Build for production: `npm run build`
6. Commit and push
7. Deploy: Follow deployment process above
### Code Style
- ESLint configuration in eslintConfig (package.json)
- Tailwind CSS for styling (not inline styles)
- React hooks for state management (not class components)
- Functional components preferred
### Git Workflow
```bash
# View changes
git status
git diff
# Commit changes
git add .
git commit -m "Fix opacity issues in UI"
# Push to branch
git push origin clean-main
# Check current branch
git branch -a
```
## Environment Variables (if needed)
Create `.env` file in project root:
```
REACT_APP_API_URL=http://localhost:5000
REACT_APP_DEBUG=false
```
Access in React:
```javascript
const apiUrl = process.env.REACT_APP_API_URL;
```
## Monitoring & Logs
### Frontend Logs
```bash
# Browser console (DevTools F12)
- Network tab: Check API calls and response times
- Console tab: Check for JavaScript errors
- Application tab: Inspect stored data (localStorage)
```
### Backend Logs
```bash
# Real-time logs
pm2 logs deathwatch-server
# Last 100 lines
pm2 logs deathwatch-server --lines 100
# Specific error log
pm2 logs deathwatch-server --err
# Stream logs to file
pm2 logs deathwatch-server > logs.txt
```
## Common Workflows
### Deploy a Hotfix
```bash
git checkout clean-main
git pull origin clean-main
# Make fixes to source files
npm run build
pkill -9 -f "serve.*build"
npx serve -s build -l 3000 &
# Test in browser
```
### Rollback to Previous Build
```bash
# Keep previous build in backup
mv build build.backup
git checkout previous-commit
npm run build
npx serve -s build -l 3000 &
```
### Scale Frontend to Multiple Instances
```bash
# Instead of single serve process, use PM2 with clustering
pm2 start "npx serve -s build -l 3000" --name dwroller-frontend-1
pm2 start "npx serve -s build -l 3001" --name dwroller-frontend-2
pm2 start "npx serve -s build -l 3002" --name dwroller-frontend-3
# Load balance with nginx
# (Configure nginx upstream to round-robin across ports)
```
## References & Documentation
### Official Docs
- [Create React App](https://create-react-app.dev/)
- [React Documentation](https://react.dev/)
- [Tailwind CSS](https://tailwindcss.com/)
- [Express.js](https://expressjs.com/)
- [Jest Testing](https://jestjs.io/)
- [PM2 Documentation](https://pm2.keymetrics.io/)
### Project Files
- Backend routes: `database/server.js`
- Database connection: `database/mariadb.js`
- Authentication: `database/sessionModel.js`
- Middleware: `database/requireSession.js`
---
**Last Updated**: December 11, 2025
**Branch**: clean-main
**Status**: ✅ Production Ready

View File

@@ -11407,3 +11407,120 @@ Connected to MongoDB
[2025-08-24T20:39:43.003Z] API: Fetch all players (public) Found 7 players
[2025-08-24T20:39:43.249Z] DB: getAllPlayers - start
[2025-08-24T20:39:43.250Z] DB: getAllPlayers - resultCount 7
[2025-12-11T19:30:04.823Z] API: Fetch all players (public) 7
[2025-12-11T19:30:09.790Z] API: Player login christoffer success
[2025-12-11T19:30:09.843Z] API: Fetch player christoffer success
[2025-12-11T19:30:09.859Z] SESSION: Session validation successful christoffer
[2025-12-11T19:30:09.880Z] API: Fetch player christoffer success
[2025-12-11T19:30:26.308Z] API: Fetch all players (public) 7
[2025-12-11T19:30:31.350Z] API: Player login gm success
[2025-12-11T19:30:31.367Z] API: Fetch player gm success
[2025-12-11T19:30:31.391Z] API: Fetch all players (public) 7
[2025-12-11T19:30:31.431Z] SESSION: Session validation successful gm
[2025-12-11T19:30:31.450Z] API: Fetch player gm success
[2025-12-11T19:30:33.028Z] API: Fetch all players (public) 7
[2025-12-11T19:33:27.683Z] SESSION: Session validation successful gm
[2025-12-11T19:33:27.736Z] API: Fetch player gm success
[2025-12-11T19:33:30.278Z] API: Fetch all players (public) 7
[2025-12-11T19:33:31.556Z] SESSION: Session validation successful gm
[2025-12-11T19:33:31.578Z] API: Fetch player gm success
[2025-12-11T19:33:33.122Z] API: Fetch all players (public) 7
[2025-12-11T19:33:34.629Z] API: Fetch all players (public) 7
[2025-12-11T19:33:35.038Z] API: Fetch all players (public) 7
[2025-12-11T19:33:36.501Z] API: Fetch all players (public) 7
[2025-12-11T19:33:52.820Z] SESSION: Session validation successful gm
[2025-12-11T19:33:52.840Z] API: Fetch player gm success
[2025-12-11T19:33:53.797Z] API: Fetch all players (public) 7
[2025-12-11T19:33:54.527Z] API: Fetch all players (public) 7
[2025-12-11T19:33:56.213Z] API: Fetch all players (public) 7
[2025-12-11T19:34:26.123Z] API: Fetch all players (public) 7
[2025-12-11T19:34:26.667Z] API: Fetch all players (public) 7
[2025-12-11T19:39:19.198Z] SESSION: Session validation successful gm
[2025-12-11T19:39:19.216Z] API: Fetch player gm success
[2025-12-11T19:39:20.378Z] API: Fetch all players (public) 7
[2025-12-11T19:39:21.059Z] API: Fetch all players (public) 7
[2025-12-11T19:39:31.004Z] SESSION: Session validation successful gm
[2025-12-11T19:39:31.023Z] API: Fetch player gm success
[2025-12-11T19:39:31.589Z] SESSION: Session validation successful gm
[2025-12-11T19:39:31.642Z] API: Fetch player gm success
[2025-12-11T19:39:32.351Z] API: Fetch all players (public) 7
[2025-12-11T19:39:33.387Z] SESSION: Session validation successful gm
[2025-12-11T19:39:33.409Z] API: Fetch player gm success
[2025-12-11T19:39:33.911Z] API: Fetch all players (public) 7
[2025-12-11T19:39:35.413Z] API: Fetch all players (public) 7
[2025-12-11T19:41:11.069Z] SESSION: Session validation successful gm
[2025-12-11T19:41:11.088Z] API: Fetch player gm success
[2025-12-11T19:41:12.373Z] API: Fetch all players (public) 7
[2025-12-11T19:43:28.278Z] SESSION: Session validation successful gm
[2025-12-11T19:43:28.298Z] API: Fetch player gm success
[2025-12-11T19:43:29.471Z] SESSION: Session validation successful gm
[2025-12-11T19:43:29.522Z] API: Fetch player gm success
[2025-12-11T19:43:30.306Z] SESSION: Session validation successful gm
[2025-12-11T19:43:30.323Z] API: Fetch player gm success
[2025-12-11T19:43:30.520Z] SESSION: Session validation successful gm
[2025-12-11T19:43:30.535Z] API: Fetch player gm success
[2025-12-11T19:43:30.929Z] SESSION: Session validation successful gm
[2025-12-11T19:43:30.947Z] API: Fetch player gm success
[2025-12-11T19:43:31.133Z] SESSION: Session validation successful gm
[2025-12-11T19:43:31.150Z] API: Fetch player gm success
[2025-12-11T19:43:31.721Z] SESSION: Session validation successful gm
[2025-12-11T19:43:31.750Z] API: Fetch player gm success
[2025-12-11T19:43:32.669Z] API: Fetch all players (public) 7
[2025-12-11T19:43:33.369Z] API: Fetch all players (public) 7
[2025-12-11T19:43:35.045Z] API: Fetch all players (public) 7
[2025-12-11T19:47:27.097Z] SESSION: Session validation successful gm
[2025-12-11T19:47:27.117Z] API: Fetch player gm success
[2025-12-11T19:47:29.550Z] API: Fetch all players (public) 7
[2025-12-11T19:53:49.714Z] SESSION: Session validation successful gm
[2025-12-11T19:53:49.732Z] API: Fetch player gm success
[2025-12-11T19:53:51.067Z] SESSION: Session validation successful gm
[2025-12-11T19:53:51.089Z] API: Fetch player gm success
[2025-12-11T19:53:51.430Z] SESSION: Session validation successful gm
[2025-12-11T19:53:51.448Z] API: Fetch player gm success
[2025-12-11T19:53:51.756Z] SESSION: Session validation successful gm
[2025-12-11T19:53:52.034Z] SESSION: Session validation successful gm
[2025-12-11T19:53:52.052Z] API: Fetch player gm success
[2025-12-11T19:53:52.253Z] SESSION: Session validation successful gm
[2025-12-11T19:53:52.271Z] API: Fetch player gm success
[2025-12-11T19:53:52.488Z] SESSION: Session validation successful gm
[2025-12-11T19:53:52.510Z] API: Fetch player gm success
[2025-12-11T19:53:54.427Z] API: Fetch all players (public) 7
[2025-12-11T19:53:55.195Z] API: Fetch all players (public) 7
[2025-12-11T19:53:56.861Z] SESSION: Session validation successful gm
[2025-12-11T19:53:56.877Z] API: Fetch player gm success
[2025-12-11T19:53:57.419Z] API: Fetch all players (public) 7
[2025-12-11T19:54:34.443Z] SESSION: Session validation successful gm
[2025-12-11T19:54:34.493Z] API: Fetch player gm success
[2025-12-11T19:54:34.833Z] SESSION: Session validation successful gm
[2025-12-11T19:54:34.886Z] API: Fetch player gm success
[2025-12-11T19:54:35.288Z] SESSION: Session validation successful gm
[2025-12-11T19:54:35.320Z] API: Fetch player gm success
[2025-12-11T19:54:35.470Z] SESSION: Session validation successful gm
[2025-12-11T19:54:35.490Z] API: Fetch player gm success
[2025-12-11T19:54:35.883Z] SESSION: Session validation successful gm
[2025-12-11T19:54:35.908Z] API: Fetch player gm success
[2025-12-11T19:54:36.854Z] API: Fetch all players (public) 7
[2025-12-11T19:54:48.698Z] SESSION: Session validation successful gm
[2025-12-11T19:54:48.717Z] API: Fetch player gm success
[2025-12-11T19:54:49.031Z] SESSION: Session validation successful gm
[2025-12-11T19:54:49.051Z] API: Fetch player gm success
[2025-12-11T19:54:49.419Z] SESSION: Session validation successful gm
[2025-12-11T19:54:49.594Z] SESSION: Session validation successful gm
[2025-12-11T19:54:49.618Z] API: Fetch player gm success
[2025-12-11T19:54:50.476Z] API: Fetch all players (public) 7
[2025-12-11T19:56:27.848Z] MariaDB: Tables created successfully
[2025-12-11T19:59:38.220Z] SESSION: Session validation successful gm
[2025-12-11T19:59:38.258Z] API: Fetch player gm success
[2025-12-11T19:59:39.439Z] API: Fetch all players (public) 7
[2025-12-11T19:59:40.884Z] SESSION: Session validation successful gm
[2025-12-11T19:59:40.913Z] API: Fetch player gm success
[2025-12-11T19:59:41.722Z] API: Fetch all players (public) 7
[2025-12-11T20:03:59.295Z] SESSION: Session validation successful gm
[2025-12-11T20:03:59.316Z] API: Fetch player gm success
[2025-12-11T20:04:01.115Z] API: Fetch all players (public) 7
[2025-12-11T20:07:09.278Z] SESSION: Session validation successful gm
[2025-12-11T20:07:09.298Z] API: Fetch player gm success
[2025-12-11T20:07:11.412Z] API: Fetch all players (public) 7
[2025-12-11T20:07:14.589Z] API: Fetch all players (public) 7
[2025-12-11T20:07:15.121Z] API: Fetch all players (public) 7
[2025-12-11T20:07:29.621Z] API: Fetch all players (public) 7

View File

@@ -217,7 +217,7 @@ function App() {
return (
<div className="App min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900" style={tab === 'players' ? appBackgroundStyle : {}}>
{/* Persistent Header */}
<div className="sticky top-0 z-50 backdrop-blur-md bg-slate-900/80 border-b border-white/10">
<div className="sticky top-0 z-50 backdrop-blur-md bg-slate-900 border-b border-slate-600">
<div className="mx-auto max-w-6xl px-6 py-4">
{/* Title and Login Row */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
@@ -239,7 +239,7 @@ function App() {
) : (
<div className="flex flex-col sm:flex-row gap-2">
<input
className="rounded-lg border border-white/20 bg-white/10 px-3 py-2 text-white placeholder-white/50 text-sm"
className="rounded-lg border border-slate-600 bg-slate-800 px-3 py-2 text-white placeholder-slate-400 text-sm"
type="text"
placeholder="Username"
value={loginName}
@@ -247,7 +247,7 @@ function App() {
onKeyPress={handleKeyPress}
/>
<input
className="rounded-lg border border-white/20 bg-white/10 px-3 py-2 text-white placeholder-white/50 text-sm"
className="rounded-lg border border-slate-600 bg-slate-800 px-3 py-2 text-white placeholder-slate-400 text-sm"
type="password"
placeholder="Password (all users: 1234)"
value={loginPw}

View File

@@ -165,7 +165,7 @@ export default function BestiaryTab(){
<h2 className="text-2xl font-semibold">Bestiary</h2>
<div className="flex items-center gap-3">
<input
className="px-3 py-2 rounded bg-white/5 border border-white/10 text-white placeholder-slate-400"
className="px-3 py-2 rounded bg-slate-800 border border-slate-600 text-white placeholder-slate-400"
value={q}
onChange={e=>setQ(e.target.value)}
/>
@@ -188,7 +188,7 @@ export default function BestiaryTab(){
</div>
{dbDown && Date.now() < (dismissUntil || 0) ? null : dbDown ? (
<div className="mb-4 p-3 rounded bg-yellow-900/30 text-yellow-300 flex items-center justify-between" role="status" aria-live="polite">
<div className="mb-4 p-3 rounded bg-yellow-800 text-yellow-300 flex items-center justify-between" role="status" aria-live="polite">
<div>Database unreachable showing cached data and retrying in background.</div>
<div className="ml-3">
<button
@@ -204,7 +204,7 @@ export default function BestiaryTab(){
{filtered.map((en, idx) => {
const d = normalizeEntry(en)
return (
<div key={d.name + idx} className="p-4 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10 transition-colors">
<div key={d.name + idx} className="p-4 rounded-lg bg-slate-800 border border-slate-700 hover:bg-slate-700 transition-colors">
<div className="flex items-start gap-6">
<div className="flex-1">
<div className="flex items-center justify-between mb-3">
@@ -215,7 +215,7 @@ export default function BestiaryTab(){
</div>
{d.profile && (
<div className="mb-3 p-3 rounded bg-slate-800/50 border border-slate-700">
<div className="mb-3 p-3 rounded bg-slate-700 border border-slate-600">
<div className="text-sm font-semibold text-slate-300 mb-1">Profile</div>
<div className="text-sm text-slate-200 font-mono">{prettyProfile(d.profile)}</div>
</div>
@@ -223,49 +223,49 @@ export default function BestiaryTab(){
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
{d.movement && (
<div className="bg-slate-800/30 p-2 rounded border border-slate-700">
<div className="bg-slate-700 p-2 rounded border border-slate-600">
<strong className="text-blue-300">Movement:</strong>
<div className="text-slate-200 mt-1">{d.movement}</div>
</div>
)}
{d.toughness && (
<div className="bg-slate-800/30 p-2 rounded border border-slate-700">
<div className="bg-slate-700 p-2 rounded border border-slate-600">
<strong className="text-green-300">Toughness:</strong>
<div className="text-slate-200 mt-1">{d.toughness}</div>
</div>
)}
{d.armour && (
<div className="bg-slate-800/30 p-2 rounded border border-slate-700">
<div className="bg-slate-700 p-2 rounded border border-slate-600">
<strong className="text-yellow-300">Armour:</strong>
<div className="text-slate-200 mt-1">{d.armour}</div>
</div>
)}
{d.skills && (
<div className="bg-slate-800/30 p-2 rounded border border-slate-700">
<div className="bg-slate-700 p-2 rounded border border-slate-600">
<strong className="text-purple-300">Skills:</strong>
<div className="text-slate-200 mt-1">{d.skills}</div>
</div>
)}
{d.talents && (
<div className="bg-slate-800/30 p-2 rounded border border-slate-700">
<div className="bg-slate-700 p-2 rounded border border-slate-600">
<strong className="text-orange-300">Talents:</strong>
<div className="text-slate-200 mt-1">{d.talents}</div>
</div>
)}
{d.traits && (
<div className="bg-slate-800/30 p-2 rounded border border-slate-700">
<div className="bg-slate-700 p-2 rounded border border-slate-600">
<strong className="text-pink-300">Traits:</strong>
<div className="text-slate-200 mt-1">{d.traits}</div>
</div>
)}
{d.weapons && (
<div className="bg-slate-800/30 p-2 rounded border border-slate-700">
<div className="bg-slate-700 p-2 rounded border border-slate-600">
<strong className="text-red-300">Weapons:</strong>
<div className="text-slate-200 mt-1">{d.weapons}</div>
</div>
)}
{d.gear && (
<div className="bg-slate-800/30 p-2 rounded border border-slate-700">
<div className="bg-slate-700 p-2 rounded border border-slate-600">
<strong className="text-cyan-300">Gear:</strong>
<div className="text-slate-200 mt-1">{d.gear}</div>
</div>
@@ -273,7 +273,7 @@ export default function BestiaryTab(){
</div>
{d.snippet && (
<div className="mt-3 p-3 rounded bg-slate-800/50 border border-slate-700">
<div className="mt-3 p-3 rounded bg-slate-700 border border-slate-600">
<div className="text-sm font-semibold text-slate-300 mb-1">Description</div>
<div className="text-xs text-slate-400 leading-relaxed">{d.snippet}</div>
</div>
@@ -281,7 +281,7 @@ export default function BestiaryTab(){
</div>
<div className="w-48 flex-shrink-0 text-sm text-slate-400">
<div className="bg-slate-800/50 p-3 rounded border border-slate-700">
<div className="bg-slate-700 p-3 rounded border border-slate-600">
<div className="font-semibold text-slate-300 mb-2">Source</div>
<div className="text-slate-200 mb-1">{d.book}</div>
<div className="text-xs">Page: {d.page ?? '—'}</div>

View File

@@ -3,7 +3,7 @@ import React, { useEffect, useMemo, useState } from 'react';
// Tooltip component for abbreviations
function Tooltip({ children, text }) {
return (
<span className="group relative inline-block cursor-help border-b border-dotted border-white/30 hover:border-white/60">
<span className="group relative inline-block cursor-help border-b border-dotted border-slate-500 hover:border-white">
{children}
<span className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 text-xs bg-black text-white rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50">
{text}
@@ -897,7 +897,7 @@ function DeathwatchRoller() {
<section className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-slate-100 p-6 md:p-10">
<div className="mx-auto max-w-6xl space-y-6">
{(error||info) && (
<div className={`rounded-xl px-4 py-3 ${error? 'bg-rose-900/40 border border-rose-500/40 text-rose-200':'bg-emerald-900/30 border border-emerald-500/40 text-emerald-200'}`}>
<div className={`rounded-xl px-4 py-3 ${error? 'bg-rose-800 border border-rose-600 text-rose-200':'bg-emerald-800 border border-emerald-600 text-emerald-200'}`}>
{error ? error : <div dangerouslySetInnerHTML={{ __html: info }} />}
</div>
)}
@@ -936,9 +936,9 @@ function DeathwatchRoller() {
</div>
</div>
<div className="grid md:grid-cols-3 gap-6">
<div className="md:col-span-2 rounded-2xl bg-white/5 backdrop-blur border border-white/10 p-5 space-y-4 shadow-xl">
<div className="md:col-span-2 rounded-2xl bg-slate-800 border border-slate-700 p-5 space-y-4 shadow-xl">
{awaitingDefense && pendingHits && (
<div className="mb-4 rounded-xl bg-amber-900/50 border border-amber-500/30 p-4">
<div className="mb-4 rounded-xl bg-amber-800 border border-amber-600 p-4">
<div className="text-lg font-bold mb-2">Incoming Attack!</div>
<div className="text-sm mb-4">
Attack hit with {pendingHits.hits} potential hit{pendingHits.hits !== 1 ? 's' : ''}.
@@ -947,7 +947,7 @@ function DeathwatchRoller() {
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="text-xs uppercase opacity-70">Defense Type</label>
<select className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" value={defenseType} onChange={e=>setDefenseType(e.target.value)}>
<select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={defenseType} onChange={e=>setDefenseType(e.target.value)}>
<option value="dodge">Dodge (Agility)</option>
<option value="parry" disabled={pendingHits?.isRanged}>Parry (WS - Melee Only)</option>
<option value="block">Block (WS +10)</option>
@@ -961,23 +961,23 @@ function DeathwatchRoller() {
</div>
<div>
<label className="text-xs uppercase opacity-70">Cover Bonus</label>
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" value={coverBonus} onChange={e=>setCoverBonus(parseInt(e.target.value||'0'))} />
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={coverBonus} onChange={e=>setCoverBonus(parseInt(e.target.value||'0'))} />
</div>
<div>
<label className="text-xs uppercase opacity-70">
<Tooltip text="Agility - Used for dodge and cover defenses">Defender's Ag</Tooltip>
</label>
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" value={defenderAg} onChange={e=>setDefenderAg(parseInt(e.target.value||'0'))} />
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={defenderAg} onChange={e=>setDefenderAg(parseInt(e.target.value||'0'))} />
</div>
<div>
<label className="text-xs uppercase opacity-70">
<Tooltip text="Weapon Skill - Used for parry and block defenses">Defender's WS</Tooltip>
</label>
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" value={defenderWS} onChange={e=>setDefenderWS(parseInt(e.target.value||'0'))} />
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={defenderWS} onChange={e=>setDefenderWS(parseInt(e.target.value||'0'))} />
</div>
<div>
<label className="text-xs uppercase opacity-70">Defense Modifier</label>
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" value={defenderModifier} onChange={e=>setDefenderModifier(parseInt(e.target.value||'0'))} />
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={defenderModifier} onChange={e=>setDefenderModifier(parseInt(e.target.value||'0'))} />
</div>
<div className="flex items-center">
<label className="text-xs uppercase opacity-70">Reaction Used</label>
@@ -1079,8 +1079,8 @@ function DeathwatchRoller() {
<label className="text-xs uppercase opacity-70">Weapon</label>
<div className="flex gap-2">
<div className="flex-1">
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2 mb-2" placeholder="Search weapons..." value={weaponFilter} onChange={e=>setWeaponFilter(e.target.value)} />
<select className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" value={weaponName} onChange={e=>onSelectWeapon(e.target.value)}>
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2 mb-2" placeholder="Search weapons..." value={weaponFilter} onChange={e=>setWeaponFilter(e.target.value)} />
<select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={weaponName} onChange={e=>onSelectWeapon(e.target.value)}>
<option value="">Custom</option>
{filteredWeapons.map(w => (<option key={w.name} value={w.name}>{w.name}</option>))}
</select>
@@ -1104,7 +1104,7 @@ function DeathwatchRoller() {
</button>
</div>
</div>
<select className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" value={enemyName} onChange={e=>onSelectEnemy(e.target.value)}>
<select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={enemyName} onChange={e=>onSelectEnemy(e.target.value)}>
{enemies.map(en => (<option key={en.name} value={en.name}>{en.name}{en.name==='Custom/None' ? '' : ` (TB ${en.tb} / AR Var / W ${en.wounds??'-'})`}</option>))}
</select>
<div className="text-xs opacity-60 mt-1">
@@ -1115,21 +1115,21 @@ function DeathwatchRoller() {
<label className="text-xs uppercase opacity-70">
<Tooltip text="Ballistic Skill - Used for ranged attacks">Attacker's BS</Tooltip>
</label>
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" value={bs} onChange={e=>setBS(parseInt(e.target.value||'0'))} />
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={bs} onChange={e=>setBS(parseInt(e.target.value||'0'))} />
</div>
<div>
<label className="text-xs uppercase opacity-70">
<Tooltip text="Weapon Skill - Used for melee attacks">Attacker's WS</Tooltip>
</label>
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" value={ws} onChange={e=>setWS(parseInt(e.target.value||'0'))} />
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={ws} onChange={e=>setWS(parseInt(e.target.value||'0'))} />
</div>
<div>
<label className="text-xs uppercase opacity-70">Modifier</label>
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" value={modifier} onChange={e=>setModifier(parseInt(e.target.value||'0'))} />
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" value={modifier} onChange={e=>setModifier(parseInt(e.target.value||'0'))} />
</div>
<div>
<label className="text-xs uppercase opacity-70">Aim</label>
<select className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" value={aim} onChange={e=>setAim(parseInt(e.target.value))}>
<select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={aim} onChange={e=>setAim(parseInt(e.target.value))}>
<option value={0}>None</option>
<option value={10}>Half (+10)</option>
<option value={20}>Full (+20)</option>
@@ -1137,7 +1137,7 @@ function DeathwatchRoller() {
</div>
<div>
<label className="text-xs uppercase opacity-70">Difficulty</label>
<select className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" value={difficulty} onChange={e=>setDifficulty(e.target.value)}>
<select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={difficulty} onChange={e=>setDifficulty(e.target.value)}>
<option value="easy">Easy (+20)</option>
<option value="normal">Normal (+0)</option>
<option value="hard">Hard (-20)</option>
@@ -1146,7 +1146,7 @@ function DeathwatchRoller() {
</div>
<div>
<label className="text-xs uppercase opacity-70">Mode</label>
<select className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" value={mode} onChange={e=>setMode(e.target.value)}>
<select className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={mode} onChange={e=>setMode(e.target.value)}>
{(['single','semi','full']).map(m => (
<option key={m} value={m} disabled={!allowedModes.includes(m)}>{m[0].toUpperCase()+m.slice(1)}</option>
))}
@@ -1156,11 +1156,11 @@ function DeathwatchRoller() {
<label className="text-xs uppercase opacity-70">
<Tooltip text="Rate of Fire - Maximum shots per attack">RoF</Tooltip>
</label>
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" min={1} value={rof} onChange={e=>setRof(parseInt(e.target.value||'1'))} />
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={1} value={rof} onChange={e=>setRof(parseInt(e.target.value||'1'))} />
</div>
<div className="lg:col-span-3">
<label className="text-xs uppercase opacity-70">Damage</label>
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" value={damage} onChange={e=>{ setDamage(e.target.value); if (error) setError('') }} />
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={damage} onChange={e=>{ setDamage(e.target.value); if (error) setError('') }} />
</div>
<div>
<label className="text-xs uppercase opacity-70">
@@ -1172,13 +1172,13 @@ function DeathwatchRoller() {
<label className="text-xs uppercase opacity-70">
<Tooltip text="Proven - Minimum value for d10 damage dice">Proven</Tooltip>
</label>
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" min={0} max={10} value={proven} onChange={e=>setProven(parseInt(e.target.value||'0'))} />
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} max={10} value={proven} onChange={e=>setProven(parseInt(e.target.value||'0'))} />
</div>
<div>
<label className="text-xs uppercase opacity-70">
<Tooltip text="Penetration - Reduces target's armour value">Pen</Tooltip>
</label>
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" min={0} value={pen} onChange={e=>setPen(parseInt(e.target.value||'0'))} />
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={pen} onChange={e=>setPen(parseInt(e.target.value||'0'))} />
</div>
<div>
<label className="text-xs uppercase opacity-70">
@@ -1196,18 +1196,18 @@ function DeathwatchRoller() {
<label className="text-xs uppercase opacity-70">
<Tooltip text="Toughness Bonus - Reduces incoming damage">Target TB</Tooltip>
</label>
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" min={0} value={targetTB} onChange={e=>setTargetTB(parseInt(e.target.value||'0'))} />
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={targetTB} onChange={e=>setTargetTB(parseInt(e.target.value||'0'))} />
</div>
<div>
<label className="text-xs uppercase opacity-70">
<Tooltip text="Armour - Reduces incoming damage (Body location)">Target Armour (Body)</Tooltip>
</label>
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" min={0} value={targetArmour} onChange={e=>{ const v = parseInt(e.target.value||'0'); setTargetArmour(v); setArmourMap(uniformArmourMap(v)) }} />
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={targetArmour} onChange={e=>{ const v = parseInt(e.target.value||'0'); setTargetArmour(v); setArmourMap(uniformArmourMap(v)) }} />
</div>
</div>
{/* Manual Mode Controls */}
<div className="rounded-xl bg-amber-900/20 border border-amber-500/30 p-4 space-y-3">
<div className="rounded-xl bg-amber-800 border border-amber-700 p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="text-sm font-semibold text-amber-200">Manual Dice Mode</div>
<div className="flex items-center gap-2">
@@ -1236,7 +1236,7 @@ function DeathwatchRoller() {
<div>
<label className="text-xs uppercase opacity-70">Manual Attack Roll (1-100)</label>
<input
className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2"
className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2"
type="number"
min="1"
max="100"
@@ -1248,7 +1248,7 @@ function DeathwatchRoller() {
<div>
<label className="text-xs uppercase opacity-70">Manual Defense Roll (1-100)</label>
<input
className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2"
className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2"
type="number"
min="1"
max="100"
@@ -1260,7 +1260,7 @@ function DeathwatchRoller() {
<div>
<label className="text-xs uppercase opacity-70">Manual Damage Rolls</label>
<input
className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2"
className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2"
type="text"
placeholder="e.g., 7, 9"
value={manualDamageRolls}
@@ -1279,17 +1279,17 @@ function DeathwatchRoller() {
{/* Progress tracker */}
<div className="mt-3">
<div className="flex items-center gap-2 text-sm">
<div className={`px-3 py-1 rounded font-semibold ${progressStep==='attack' ? 'bg-blue-600 text-white animate-pulse' : 'bg-white/5 text-white/80'}`}>Attack</div>
<div className={`px-3 py-1 rounded font-semibold ${progressStep==='defense' ? 'bg-amber-500 text-black animate-pulse' : 'bg-white/5 text-white/80'}`}>Defense</div>
<div className={`px-3 py-1 rounded font-semibold ${progressStep==='damage' ? 'bg-rose-500 text-black animate-pulse' : 'bg-white/5 text-white/80'}`}>Damage</div>
<div className={`px-3 py-1 rounded font-semibold ${progressStep==='wounds' ? 'bg-emerald-500 text-black animate-pulse' : 'bg-white/5 text-white/80'}`}>Wounds</div>
<div className={`px-3 py-1 rounded font-semibold ${progressStep==='attack' ? 'bg-blue-600 text-white animate-pulse' : 'bg-slate-800 text-white'}`}>Attack</div>
<div className={`px-3 py-1 rounded font-semibold ${progressStep==='defense' ? 'bg-amber-500 text-black animate-pulse' : 'bg-slate-800 text-white'}`}>Defense</div>
<div className={`px-3 py-1 rounded font-semibold ${progressStep==='damage' ? 'bg-rose-500 text-black animate-pulse' : 'bg-slate-800 text-white'}`}>Damage</div>
<div className={`px-3 py-1 rounded font-semibold ${progressStep==='wounds' ? 'bg-emerald-500 text-black animate-pulse' : 'bg-slate-800 text-white'}`}>Wounds</div>
<div className="ml-3 text-xs opacity-70">Phase: <span className="font-semibold">{progressStep}</span></div>
<div className="ml-4 text-xs opacity-80">Last Applied: <span className="font-semibold">{lastAppliedDamage}</span></div>
</div>
</div>
{manageWeapons && (
<>
<div className="rounded-xl bg-white/5 border border-white/10 p-4 space-y-3">
<div className="rounded-xl bg-slate-800 border border-slate-600 p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="text-sm font-semibold">Manage Weapons</div>
<div className="flex items-center gap-3 text-xs">
@@ -1304,7 +1304,7 @@ function DeathwatchRoller() {
</div>
{manageTab==='json' ? (
<>
<textarea className="w-full h-48 rounded-xl border border-white/10 bg-black/20 px-3 py-2 font-mono text-xs" value={weaponsJSON} onChange={e=>setWeaponsJSON(e.target.value)} />
<textarea className="w-full h-48 rounded-xl border border-slate-600 bg-slate-900 px-3 py-2 font-mono text-xs" value={weaponsJSON} onChange={e=>setWeaponsJSON(e.target.value)} />
<div className="flex gap-2">
<button onClick={()=>importFromJSONText(weaponsJSON)} className="rounded-xl px-3 py-2 bg-blue-600">Import JSON</button>
<button onClick={()=>{ const merged = importBuildWeapons(weapons); setWeapons(merged); setWeaponsJSON(JSON.stringify(merged,null,2)); setInfo('Imported built DB weapons') }} className="rounded-xl px-3 py-2 bg-emerald-700">Import Built DB</button>
@@ -1314,7 +1314,7 @@ function DeathwatchRoller() {
</>
) : (
<>
<textarea className="w-full h-48 rounded-xl border border-white/10 bg-black/20 px-3 py-2 font-mono text-xs" value={weaponsCSV} onChange={e=>setWeaponsCSV(e.target.value)} />
<textarea className="w-full h-48 rounded-xl border border-slate-600 bg-slate-900 px-3 py-2 font-mono text-xs" value={weaponsCSV} onChange={e=>setWeaponsCSV(e.target.value)} />
<div className="flex gap-2">
<button onClick={()=>importFromCSVText(weaponsCSV)} className="rounded-xl px-3 py-2 bg-blue-600">Import CSV</button>
</div>
@@ -1334,7 +1334,7 @@ function DeathwatchRoller() {
</>
)}
{history[0] && (
<div className="rounded-xl bg-white/5 border border-white/10 p-4 space-y-2">
<div className="rounded-xl bg-slate-800 border border-slate-600 p-4 space-y-2">
<div className="text-sm">{history[0].weapon ? `${history[0].weapon}` : ''}{history[0].using} Attack <span className="font-semibold">{history[0].attackRoll}</span> <span className="text-emerald-400">Hit!</span></div>
{history[0].defenseRoll && (
<div className="text-sm">{history[0].defenseType || 'Defense'} <span className="font-semibold">{history[0].defenseRoll}</span> vs {history[0].defenseTarget} {history[0].defenseFailed ? <span className="text-rose-400">Failed!</span> : <span className="text-emerald-400">Defended!</span>}</div>
@@ -1350,14 +1350,14 @@ function DeathwatchRoller() {
)}
</div>
)}
<div className="pt-2 border-t border-white/10">
<div className="pt-2 border-t border-slate-600">
<div className="flex gap-2">
<input className="flex-1 rounded-xl border border-white/10 bg-white/10 px-3 py-2" placeholder="Preset name" value={presetName} onChange={e=>setPresetName(e.target.value)} />
<input className="flex-1 rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" placeholder="Preset name" value={presetName} onChange={e=>setPresetName(e.target.value)} />
<button onClick={saveCurrentAsPreset} className="rounded-xl px-3 py-2 bg-slate-700 hover:bg-slate-600">Save</button>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{presets.map(p => (
<div key={p.name} className="rounded-xl border border-white/10 bg-white/5 px-3 py-2 flex items-center gap-2">
<div key={p.name} className="rounded-xl border border-slate-600 bg-slate-800 px-3 py-2 flex items-center gap-2">
<button className="underline" onClick={()=>applyPreset(p)}>{p.name}</button>
<button onClick={()=>deletePreset(p.name)} className="text-xs px-2 py-1 rounded bg-rose-600">x</button>
</div>
@@ -1366,7 +1366,7 @@ function DeathwatchRoller() {
</div>
</div>
<div className="space-y-6">
<div className="rounded-2xl bg-white/5 backdrop-blur border border-white/10 p-5 shadow-xl">
<div className="rounded-2xl bg-slate-800 backdrop-blur border border-slate-600 p-5 shadow-xl">
<div className="flex items-center justify-between mb-3">
<div className="font-semibold">Enemy Wounds Tracker</div>
<button onClick={resetTracker} className="text-xs rounded px-2 py-1 bg-slate-700 hover:bg-slate-600">Reset</button>
@@ -1374,14 +1374,14 @@ function DeathwatchRoller() {
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs uppercase opacity-70">Max Wounds</label>
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" min={0} value={maxWounds} onChange={e=>setMaxWounds(Math.max(0, parseInt(e.target.value||'0')))} />
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={maxWounds} onChange={e=>setMaxWounds(Math.max(0, parseInt(e.target.value||'0')))} />
</div>
<div>
<label className="text-xs uppercase opacity-70">Current Wounds</label>
<input className="w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" min={0} value={curWounds} onChange={e=>setCurWounds(Math.max(0, parseInt(e.target.value||'0')))} />
<input className="w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={curWounds} onChange={e=>setCurWounds(Math.max(0, parseInt(e.target.value||'0')))} />
</div>
<div className="col-span-2">
<div className="h-2 w-full bg-white/10 rounded overflow-hidden">
<div className="h-2 w-full bg-slate-800 rounded overflow-hidden">
<div className="h-2 bg-emerald-500" style={{width: `${woundPct}%`}}></div>
</div>
<div className="text-xs opacity-70 mt-1">{curWounds}/{maxWounds} remaining</div>
@@ -1389,7 +1389,7 @@ function DeathwatchRoller() {
</div>
<div className="mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2">
{BODY_PARTS.map(p => (
<div key={p} className="rounded-xl border border-white/10 bg-black/20 p-3">
<div key={p} className="rounded-xl border border-slate-600 bg-slate-900 p-3">
<div className="text-xs opacity-70">{p}</div>
<div className="text-lg font-semibold">{partDamage[p]||0}</div>
<div className="text-xs opacity-60 mt-1">
@@ -1399,7 +1399,7 @@ function DeathwatchRoller() {
))}
</div>
</div>
<div className="rounded-2xl bg-white/5 backdrop-blur border border-white/10 p-5 shadow-xl flex flex-col">
<div className="rounded-2xl bg-slate-800 backdrop-blur border border-slate-600 p-5 shadow-xl flex flex-col">
<div className="flex items-center justify-between mb-2">
<div className="font-semibold">Roll History</div>
<button onClick={clearHistory} className="text-xs rounded px-2 py-1 rounded-xl bg-slate-700 hover:bg-slate-600">Clear</button>
@@ -1407,7 +1407,7 @@ function DeathwatchRoller() {
<div className="overflow-y-auto space-y-2 pr-2" style={{maxHeight:'520px'}}>
{history.length===0 && <div className="text-sm opacity-70">No rolls yet</div>}
{history.map(r => (
<div key={r.id} className="rounded-xl border border-white/10 bg-black/20 p-3">
<div key={r.id} className="rounded-xl border border-slate-600 bg-slate-900 p-3">
<div className="flex items-center justify-between text-xs opacity-80">
<div>{new Date(r.ts).toLocaleTimeString()}</div>
<div>{r.weapon ? `${r.weapon}` : ''}{r.using || 'Unknown'} {r.mode ? r.mode.toUpperCase() : 'SINGLE'} RoF {r.rof || 1}</div>

View File

@@ -4,7 +4,7 @@ export default function GMKit({ authedPlayer }) {
const [activeTable, setActiveTable] = useState('difficulty');
if (authedPlayer !== 'gm') return (
<div className="p-6 rounded-lg bg-red-900/20 border border-red-500/30">
<div className="p-6 rounded-lg bg-red-800 border border-red-700">
<h2 className="text-xl font-bold text-red-300 mb-2">Access Denied</h2>
<p className="text-red-200">The GM Kit is only accessible to Game Masters. Please log in as a GM.</p>
</div>
@@ -393,7 +393,7 @@ export default function GMKit({ authedPlayer }) {
};
return (
<div className="bg-white/5 rounded-xl p-6 border border-white/10">
<div className="bg-slate-800 rounded-xl p-6 border border-slate-600">
<h2 className="text-2xl font-semibold mb-4 text-white">GM Kit - Reference Tables</h2>
<p className="text-sm text-slate-300 mb-6">Comprehensive Deathwatch reference tables for quick gameplay lookup.</p>
@@ -406,7 +406,7 @@ export default function GMKit({ authedPlayer }) {
className={`px-3 py-2 rounded text-sm font-medium transition-colors ${
activeTable === key
? 'bg-blue-600 text-white'
: 'bg-slate-700/50 text-slate-200 hover:bg-slate-600/50'
: 'bg-slate-700 text-slate-200 hover:bg-slate-600'
}`}
>
{table.name}
@@ -415,12 +415,12 @@ export default function GMKit({ authedPlayer }) {
</div>
{/* Active Table Display */}
<div className="bg-white/3 rounded-lg p-4 border border-white/5">
<div className="bg-slate-800 rounded-lg p-4 border border-slate-600">
<h3 className="text-lg font-semibold text-slate-100 mb-4">{tables[activeTable].name}</h3>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="border-b border-white/10">
<tr className="border-b border-slate-600">
{tables[activeTable].data[0].map((header, index) => (
<th key={index} className="px-3 py-2 text-left text-slate-300 font-medium">
{header}
@@ -430,7 +430,7 @@ export default function GMKit({ authedPlayer }) {
</thead>
<tbody>
{tables[activeTable].data.slice(1).map((row, rowIndex) => (
<tr key={rowIndex} className="border-b border-white/5 hover:bg-white/5">
<tr key={rowIndex} className="border-b border-slate-600 hover:bg-slate-800">
{row.map((cell, cellIndex) => (
<td key={cellIndex} className="px-3 py-2 text-slate-200 text-xs">
{cell}

View File

@@ -4,7 +4,7 @@ export default function GMKit({ authedPlayer }) {
const [activeTable, setActiveTable] = useState('difficulty');
if (authedPlayer !== 'gm') return (
<div className="p-6 rounded-lg bg-red-900/20 border border-red-500/30">
<div className="p-6 rounded-lg bg-red-800 border border-red-700">
<h2 className="text-xl font-bold text-red-300 mb-2">Access Denied</h2>
<p className="text-red-200">The GM Kit is only accessible to Game Masters. Please log in as a GM.</p>
</div>
@@ -265,7 +265,7 @@ export default function GMKit({ authedPlayer }) {
};
return (
<div className="bg-white/5 rounded-xl p-6 border border-white/10">
<div className="bg-slate-800 rounded-xl p-6 border border-slate-600">
<h2 className="text-2xl font-semibold mb-4 text-white">GM Kit - Reference Tables</h2>
<p className="text-sm text-slate-300 mb-6">Quick reference tables for Deathwatch gameplay.</p>
@@ -278,7 +278,7 @@ export default function GMKit({ authedPlayer }) {
className={`px-3 py-2 rounded text-sm font-medium transition-colors ${
activeTable === key
? 'bg-blue-600 text-white'
: 'bg-slate-700/50 text-slate-200 hover:bg-slate-600/50'
: 'bg-slate-700 text-slate-200 hover:bg-slate-600'
}`}
>
{table.name}
@@ -287,12 +287,12 @@ export default function GMKit({ authedPlayer }) {
</div>
{/* Active Table Display */}
<div className="bg-white/3 rounded-lg p-4 border border-white/5">
<div className="bg-slate-800 rounded-lg p-4 border border-slate-600">
<h3 className="text-lg font-semibold text-slate-100 mb-4">{tables[activeTable].name}</h3>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="border-b border-white/10">
<tr className="border-b border-slate-600">
{tables[activeTable].data[0].map((header, index) => (
<th key={index} className="px-3 py-2 text-left text-slate-300 font-medium">
{header}
@@ -302,7 +302,7 @@ export default function GMKit({ authedPlayer }) {
</thead>
<tbody>
{tables[activeTable].data.slice(1).map((row, rowIndex) => (
<tr key={rowIndex} className="border-b border-white/5">
<tr key={rowIndex} className="border-b border-slate-600">
{row.map((cell, cellIndex) => (
<td key={cellIndex} className="px-3 py-2 text-slate-200">
{cell}

View File

@@ -38,7 +38,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
}, [fetchPlayers]);
if (authedPlayer !== 'gm') return (
<div className="p-6 rounded-lg bg-red-900/20 border border-red-500/30">
<div className="p-6 rounded-lg bg-red-800 border border-red-700">
<h2 className="text-xl font-bold text-red-300 mb-2">Access Denied</h2>
<p className="text-red-200">Player Management is only accessible to Game Masters. Please log in as a GM.</p>
</div>
@@ -181,25 +181,25 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
const [pw, setPw] = useState('');
return (
<div className="bg-white/5 rounded-lg p-4 border border-white/10">
<div className="bg-slate-800 rounded-lg p-4 border border-slate-600">
<h4 className="text-lg font-medium text-white mb-3">Add New Player</h4>
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<input
className="rounded border border-white/20 bg-white/10 px-3 py-2 text-white placeholder-white/50 text-sm"
className="rounded border border-slate-600 bg-slate-800 px-3 py-2 text-white placeholder-slate-400 text-sm"
type="text"
placeholder="Player Name"
value={name}
onChange={e => setName(e.target.value)}
/>
<input
className="rounded border border-white/20 bg-white/10 px-3 py-2 text-white placeholder-white/50 text-sm"
className="rounded border border-slate-600 bg-slate-800 px-3 py-2 text-white placeholder-slate-400 text-sm"
type="number"
placeholder="Requisition Points"
value={rp}
onChange={e => setRp(e.target.value)}
/>
<input
className="rounded border border-white/20 bg-white/10 px-3 py-2 text-white placeholder-white/50 text-sm"
className="rounded border border-slate-600 bg-slate-800 px-3 py-2 text-white placeholder-slate-400 text-sm"
type="password"
placeholder="Password (default: 1234)"
value={pw}
@@ -235,7 +235,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
return (
<div className="flex items-center gap-2 w-full">
<input
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white text-sm flex-1 min-w-0"
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white text-sm flex-1 min-w-0"
type="number"
value={rp}
onChange={e => setRp(e.target.value)}
@@ -262,7 +262,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
return (
<div className="flex items-center gap-2 w-full">
<input
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white text-sm flex-1 min-w-0"
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white text-sm flex-1 min-w-0"
type="number"
value={xp}
onChange={e => setXp(e.target.value)}
@@ -289,7 +289,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
return (
<div className="flex items-center gap-2 w-full">
<input
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white text-sm flex-1 min-w-0"
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white text-sm flex-1 min-w-0"
type="number"
value={xpSpent}
onChange={e => setXpSpent(e.target.value)}
@@ -316,7 +316,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
return (
<div className="flex items-center gap-2 w-full">
<select
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white text-sm flex-1 min-w-0"
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white text-sm flex-1 min-w-0"
value={renown}
onChange={e => setRenown(e.target.value)}
>
@@ -341,7 +341,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
return (
<div className="flex items-center gap-2 w-full">
<input
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white placeholder-white/50 text-sm flex-1 min-w-0"
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white placeholder-slate-400 text-sm flex-1 min-w-0"
type="password"
placeholder="New password (default: 1234)"
value={pw}
@@ -367,13 +367,13 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
return (
<div className="flex items-center gap-2">
<input
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white text-xs w-16"
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white text-xs w-16"
type="number"
value={amount}
onChange={e => setAmount(e.target.value)}
/>
<button
className="text-xs px-3 py-1 rounded bg-purple-600/80 hover:bg-purple-600 text-white transition-colors whitespace-nowrap"
className="text-xs px-3 py-1 rounded bg-purple-600 hover:bg-purple-600 text-white transition-colors whitespace-nowrap"
onClick={() => {
if (window.confirm(`Give ${amount} XP to all ${players.length} players?`)) {
onGive(parseInt(amount));
@@ -394,13 +394,13 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
return (
<div className="flex items-center gap-2">
<input
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white text-xs w-16"
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white text-xs w-16"
type="number"
value={amount}
onChange={e => setAmount(e.target.value)}
/>
<button
className="text-xs px-3 py-1 rounded bg-purple-500/80 hover:bg-purple-500 text-white transition-colors whitespace-nowrap"
className="text-xs px-3 py-1 rounded bg-purple-500 hover:bg-purple-500 text-white transition-colors whitespace-nowrap"
onClick={() => {
if (window.confirm(`Set all ${players.length} players to ${amount} XP?`)) {
onSet(parseInt(amount));
@@ -421,13 +421,13 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
return (
<div className="flex items-center gap-2">
<input
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white text-xs w-16"
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white text-xs w-16"
type="number"
value={amount}
onChange={e => setAmount(e.target.value)}
/>
<button
className="text-xs px-3 py-1 rounded bg-blue-600/80 hover:bg-blue-600 text-white transition-colors whitespace-nowrap"
className="text-xs px-3 py-1 rounded bg-blue-600 hover:bg-blue-600 text-white transition-colors whitespace-nowrap"
onClick={() => {
if (window.confirm(`Give ${amount} RP to all ${players.length} players?`)) {
onGive(parseInt(amount));
@@ -448,13 +448,13 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
return (
<div className="flex items-center gap-2">
<input
className="rounded border border-white/20 bg-white/10 px-2 py-1 text-white text-xs w-16"
className="rounded border border-slate-600 bg-slate-800 px-2 py-1 text-white text-xs w-16"
type="number"
value={amount}
onChange={e => setAmount(e.target.value)}
/>
<button
className="text-xs px-3 py-1 rounded bg-blue-500/80 hover:bg-blue-500 text-white transition-colors whitespace-nowrap"
className="text-xs px-3 py-1 rounded bg-blue-500 hover:bg-blue-500 text-white transition-colors whitespace-nowrap"
onClick={() => {
if (window.confirm(`Set all ${players.length} players to ${amount} RP?`)) {
onSet(parseInt(amount));
@@ -469,12 +469,12 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
}
return (
<div className="bg-white/5 rounded-xl p-6 border border-white/10">
<div className="bg-slate-800 rounded-xl p-6 border border-slate-600">
<h2 className="text-2xl font-semibold mb-4 text-white">Player Management</h2>
<p className="text-sm text-slate-300 mb-6">Manage player accounts, requisition points, experience, and renown.</p>
{saveMsg && (
<div className="mb-4 p-3 rounded bg-green-500/20 border border-green-500/30 text-green-300 text-sm">
<div className="mb-4 p-3 rounded bg-green-800 border border-green-700 text-green-300 text-sm">
{saveMsg}
</div>
)}
@@ -493,7 +493,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
) : (
<div className="space-y-4">
{players.map(player => (
<div key={player.name} className="bg-white/3 rounded-lg p-4 border border-white/5">
<div key={player.name} className="bg-slate-800 rounded-lg p-4 border border-slate-600">
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
{/* Player Info Section */}
<div className="space-y-4">
@@ -522,7 +522,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
<span className="text-slate-400">Available XP:</span> <span className="text-green-400 font-medium">{(player.tabInfo?.xp || 0) - (player.tabInfo?.xpSpent || 0)}</span>
</div>
</div>
<div className="bg-slate-800/50 rounded p-3 border border-slate-700">
<div className="bg-slate-800 rounded p-3 border border-slate-700">
<div className="text-xs font-medium text-slate-300 uppercase tracking-wide mb-2">Experience Progress</div>
<XPBar
currentXP={player.tabInfo?.xp || 0}
@@ -594,7 +594,7 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
</div>
{/* Quick Actions */}
<div className="mt-6 p-4 bg-white/3 rounded-lg border border-white/5">
<div className="mt-6 p-4 bg-slate-800 rounded-lg border border-slate-600">
<h4 className="text-sm font-medium text-white mb-3">Quick Actions</h4>
{/* Bulk XP Actions */}
@@ -638,13 +638,13 @@ export default function PlayerManagement({ authedPlayer, sessionId }) {
<h5 className="text-xs font-medium text-slate-400 uppercase tracking-wide mb-2">Other Actions</h5>
<div className="flex flex-wrap gap-2">
<button
className="text-xs px-3 py-1 rounded bg-blue-600/80 hover:bg-blue-600 text-white transition-colors"
className="text-xs px-3 py-1 rounded bg-blue-600 hover:bg-blue-600 text-white transition-colors"
onClick={fetchPlayers}
>
Refresh Players
</button>
<button
className="text-xs px-3 py-1 rounded bg-green-600/80 hover:bg-green-600 text-white transition-colors"
className="text-xs px-3 py-1 rounded bg-green-600 hover:bg-green-600 text-white transition-colors"
onClick={() => {
players.forEach(player => {
if ((player.tabInfo?.rp || 0) < 10) {

View File

@@ -529,14 +529,14 @@ function PlayerTab({
return (
<div className="grid grid-cols-1 sm:grid-cols-5 gap-2">
<div className="flex gap-2">
<input className="rounded-xl border border-white/10 bg-white/10 px-3 py-2 flex-1" placeholder="Player name" value={name} onChange={e=>setName(e.target.value)} />
<select className="rounded-xl border border-white/10 bg-white/10 px-3 py-2" value={usePregen ? name : ''} onChange={e=>{ setUsePregen(!!e.target.value); setName(e.target.value); }}>
<input className="rounded-xl border border-slate-600 bg-slate-800 px-3 py-2 flex-1" placeholder="Player name" value={name} onChange={e=>setName(e.target.value)} />
<select className="rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" value={usePregen ? name : ''} onChange={e=>{ setUsePregen(!!e.target.value); setName(e.target.value); }}>
<option value="">-- pregens --</option>
{pregens.map(p => <option key={p} value={p}>{p}</option>)}
</select>
</div>
<input className="rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="number" min={0} value={rp} onChange={e=>setRp(parseInt(e.target.value||'0'))} />
<input className="rounded-xl border border-white/10 bg-white/10 px-3 py-2" type="password" placeholder="Password" value={pw} onChange={e=>setPw(e.target.value)} />
<input className="rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="number" min={0} value={rp} onChange={e=>setRp(parseInt(e.target.value||'0'))} />
<input className="rounded-xl border border-slate-600 bg-slate-800 px-3 py-2" type="password" placeholder="Password" value={pw} onChange={e=>setPw(e.target.value)} />
<button onClick={()=>{ onAdd(name, rp, pw); setName(''); setRp(10); setPw('') }} className="rounded-xl px-3 py-2 bg-amber-600 hover:bg-amber-500">Add/Update</button>
<div className="self-center text-xs opacity-70">Add or overwrite by name</div>
</div>
@@ -546,7 +546,7 @@ function PlayerTab({
const [rp, setRp] = useState('')
return (
<div className="flex gap-2">
<input className="rounded-xl border border-white/10 bg-white/10 px-2 py-1 text-sm w-20" type="number" placeholder="RP" value={rp} onChange={e=>setRp(e.target.value)} />
<input className="rounded-xl border border-slate-600 bg-slate-800 px-2 py-1 text-sm w-20" type="number" placeholder="RP" value={rp} onChange={e=>setRp(e.target.value)} />
<button onClick={()=>{ onSet(name, parseInt(rp||'0')); setRp('') }} className="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600">Set RP</button>
</div>
)
@@ -556,7 +556,7 @@ function PlayerTab({
const [xp, setXp] = useState('')
return (
<div className="flex gap-2">
<input className="rounded-xl border border-white/10 bg-white/10 px-2 py-1 text-sm w-20" type="number" placeholder="XP" value={xp} onChange={e=>setXp(e.target.value)} />
<input className="rounded-xl border border-slate-600 bg-slate-800 px-2 py-1 text-sm w-20" type="number" placeholder="XP" value={xp} onChange={e=>setXp(e.target.value)} />
<button onClick={()=>{ onSet(name, parseInt(xp||'0')); setXp('') }} className="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600">Set XP</button>
</div>
)
@@ -566,7 +566,7 @@ function PlayerTab({
const [xpSpent, setXpSpent] = useState('')
return (
<div className="flex gap-2">
<input className="rounded-xl border border-white/10 bg-white/10 px-2 py-1 text-sm w-20" type="number" placeholder="XP Spent" value={xpSpent} onChange={e=>setXpSpent(e.target.value)} />
<input className="rounded-xl border border-slate-600 bg-slate-800 px-2 py-1 text-sm w-20" type="number" placeholder="XP Spent" value={xpSpent} onChange={e=>setXpSpent(e.target.value)} />
<button onClick={()=>{ onSet(name, parseInt(xpSpent||'0')); setXpSpent('') }} className="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600">Set XP Spent</button>
</div>
)
@@ -576,7 +576,7 @@ function PlayerTab({
const [renown, setRenown] = useState(value)
return (
<div className="flex gap-2">
<select className="rounded-xl border border-white/10 bg-white/10 px-2 py-1 text-sm w-20" value={renown} onChange={e=>setRenown(e.target.value)}>
<select className="rounded-xl border border-slate-600 bg-slate-800 px-2 py-1 text-sm w-20" value={renown} onChange={e=>setRenown(e.target.value)}>
{RANK_ORDER.map(r => <option key={r} value={r}>{r}</option>)}
</select>
<button onClick={()=>{ onSet(name, renown); setRenown(value) }} className="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600">Set Renown</button>
@@ -587,7 +587,7 @@ function PlayerTab({
const [pw, setPw] = useState('')
return (
<div className="flex gap-2">
<input className="rounded-xl border border-white/10 bg-white/10 px-2 py-1 text-sm" type="password" placeholder="New PW" value={pw} onChange={e=>setPw(e.target.value)} />
<input className="rounded-xl border border-slate-600 bg-slate-800 px-2 py-1 text-sm" type="password" placeholder="New PW" value={pw} onChange={e=>setPw(e.target.value)} />
<button onClick={()=>{ onReset(name, pw); setPw('') }} className="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600">Reset PW</button>
</div>
)
@@ -768,9 +768,9 @@ function PlayerTab({
<div className="mx-auto max-w-7xl space-y-6">
{/* Avatar header (standalone) */}
<div className="flex flex-col items-center bg-white/5 rounded-xl p-4 border border-white/10">
<div className="flex flex-col items-center bg-slate-800 rounded-xl p-4 border border-slate-600">
<div className="w-full flex justify-center">
<img src={picture || '/logo192.png'} alt="avatar" className="w-36 h-36 md:w-48 md:h-48 rounded-full object-cover border-4 border-white/10 shadow-lg" />
<img src={picture || '/logo192.png'} alt="avatar" className="w-36 h-36 md:w-48 md:h-48 rounded-full object-cover border-4 border-slate-600 shadow-lg" />
</div>
<div className="mt-3 text-center">
<div className="text-lg font-semibold">{charName || currentPlayer?.tabInfo?.charName || 'Unknown'}</div>
@@ -781,7 +781,7 @@ function PlayerTab({
<div className="flex flex-col items-center">
<input type="file" accept="image/*" onChange={e=>uploadAvatarFile(e.target.files?.[0])} />
<div className="mt-1">Max 200KB. Supported: png/jpg/gif</div>
<input className="w-64 md:w-96 mx-auto mt-2 rounded border border-white/10 bg-white/10 px-2 py-1" value={picture} onChange={e=>setPicture(e.target.value)} placeholder="Or paste image URL" />
<input className="w-64 md:w-96 mx-auto mt-2 rounded border border-slate-600 bg-slate-800 px-2 py-1" value={picture} onChange={e=>setPicture(e.target.value)} placeholder="Or paste image URL" />
</div>
</div>
)}
@@ -794,14 +794,14 @@ function PlayerTab({
<div className="flex items-center gap-2">
<button
onClick={saveLocal}
className={`px-3 py-1.5 rounded-lg border border-white/10 text-sm ${(authedPlayer || gmOpen) ? 'bg-emerald-600 hover:bg-emerald-500' : 'bg-emerald-900/40 cursor-not-allowed'}`}
className={`px-3 py-1.5 rounded-lg border border-slate-600 text-sm ${(authedPlayer || gmOpen) ? 'bg-emerald-600 hover:bg-emerald-500' : 'bg-emerald-800 cursor-not-allowed'}`}
disabled={!(authedPlayer || gmOpen)}
>
Save
</button>
<button onClick={() => setShowLogs(s => !s)} className="px-3 py-1.5 rounded-lg border border-white/10 text-sm bg-slate-700 hover:bg-slate-600">{showLogs ? 'Hide Logs' : 'Show Logs'}</button>
<button onClick={() => setShowLogs(s => !s)} className="px-3 py-1.5 rounded-lg border border-slate-600 text-sm bg-slate-700 hover:bg-slate-600">{showLogs ? 'Hide Logs' : 'Show Logs'}</button>
{saveMsg && (
<span className="ml-2 text-xs px-2 py-1 rounded bg-white/10 border border-white/10">
<span className="ml-2 text-xs px-2 py-1 rounded bg-slate-800 border border-slate-600">
{saveMsg}
</span>
)}
@@ -810,7 +810,7 @@ function PlayerTab({
<div>
<label className="text-xs uppercase opacity-70">Chapter</label>
<input
className="w-full rounded border border-white/10 bg-white/10 px-2 py-1"
className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1"
value={chapter}
onChange={(e) => setChapter(e.target.value)}
disabled={!(shopAuthed || gmOpen)}
@@ -819,7 +819,7 @@ function PlayerTab({
<div>
<label className="text-xs uppercase opacity-70">Chapter Demeanour</label>
<input
className="w-full rounded border border-white/10 bg-white/10 px-2 py-1"
className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1"
value={demeanour}
onChange={(e) => setDemeanour(e.target.value)}
disabled={!(shopAuthed || gmOpen)}
@@ -828,7 +828,7 @@ function PlayerTab({
<div>
<label className="text-xs uppercase opacity-70">Speciality</label>
<input
className="w-full rounded border border-white/10 bg-white/10 px-2 py-1"
className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1"
value={speciality}
onChange={(e) => setSpeciality(e.target.value)}
disabled={!(shopAuthed || gmOpen)}
@@ -837,7 +837,7 @@ function PlayerTab({
<div>
<label className="text-xs uppercase opacity-70">Rank</label>
<input
className="w-full rounded border border-white/10 bg-white/10 px-2 py-1"
className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1"
value={rank}
onChange={(e) => setRank(e.target.value)}
disabled={!(shopAuthed || gmOpen)}
@@ -846,7 +846,7 @@ function PlayerTab({
<div>
<label className="text-xs uppercase opacity-70">Power Armour History</label>
<input
className="w-full rounded border border-white/10 bg-white/10 px-2 py-1"
className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1"
value={powerArmour}
onChange={(e) => setPowerArmour(e.target.value)}
disabled={!(shopAuthed || gmOpen)}
@@ -855,7 +855,7 @@ function PlayerTab({
<div>
<label className="text-xs uppercase opacity-70">Description</label>
<input
className="w-full rounded border border-white/10 bg-white/10 px-2 py-1"
className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1"
value={description}
onChange={(e) => setDescription(e.target.value)}
disabled={!(shopAuthed || gmOpen)}
@@ -864,7 +864,7 @@ function PlayerTab({
<div>
<label className="text-xs uppercase opacity-70">Past Event</label>
<input
className="w-full rounded border border-white/10 bg-white/10 px-2 py-1"
className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1"
value={pastEvent}
onChange={(e) => setPastEvent(e.target.value)}
disabled={!(shopAuthed || gmOpen)}
@@ -873,7 +873,7 @@ function PlayerTab({
<div>
<label className="text-xs uppercase opacity-70">Personal Demeanour</label>
<input
className="w-full rounded border border-white/10 bg-white/10 px-2 py-1"
className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1"
value={personalDemeanour}
onChange={(e) => setPersonalDemeanour(e.target.value)}
disabled={!(shopAuthed || gmOpen)}
@@ -884,7 +884,7 @@ function PlayerTab({
<div className="col-span-4 flex justify-end mt-4">
<button
onClick={() => setGmOpen(!gmOpen)}
className="px-3 py-1.5 rounded-lg bg-blue-600 hover:bg-blue-500 border border-white/10 text-sm"
className="px-3 py-1.5 rounded-lg bg-blue-600 hover:bg-blue-500 border border-slate-600 text-sm"
>
{gmOpen ? 'Close GM Panel' : 'Open GM Panel'}
</button>
@@ -893,7 +893,7 @@ function PlayerTab({
</div>
{/* Requisition Points Display */}
<div className="rounded-xl border border-white/10 bg-black/20 p-3 space-y-2">
<div className="rounded-xl border border-slate-600 bg-slate-900 p-3 space-y-2">
<div className="text-lg font-medium">Requisition Points</div>
<div className="text-base">
Current RP: <span className="font-semibold text-xl text-amber-400">{currentPlayer?.tabInfo?.rp || '0'}</span>
@@ -904,7 +904,7 @@ function PlayerTab({
<div className="flex items-center gap-2">
<input
type="number"
className="w-24 rounded border border-white/10 bg-white/10 px-2 py-1"
className="w-24 rounded border border-slate-600 bg-slate-800 px-2 py-1"
placeholder="RP"
defaultValue={currentPlayer?.tabInfo?.rp || 0}
onChange={(e) => {
@@ -938,7 +938,7 @@ function PlayerTab({
</div>
{/* Experience Points Display - Minecraft-style XP Bar */}
<div className="rounded-xl border border-white/10 bg-black/20 p-3 space-y-2">
<div className="rounded-xl border border-slate-600 bg-slate-900 p-3 space-y-2">
<div className="text-lg font-medium">Experience Points (XP)</div>
{/* XP Bar */}
@@ -966,7 +966,7 @@ function PlayerTab({
{/* GM Controls */}
{isGMLoggedIn() && (
<div className="mt-4 space-y-2 border-t border-white/10 pt-3">
<div className="mt-4 space-y-2 border-t border-slate-600 pt-3">
<div className="text-sm text-slate-300 font-medium">GM Controls</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
@@ -1014,20 +1014,20 @@ function PlayerTab({
</div>
{/* Characteristics */}
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600">
<div className="font-semibold mb-2">Characteristics</div>
<div className="grid grid-cols-2 md:grid-cols-9 gap-2">
{CHARACTERISTICS.map(c => (
<div key={c.key} className="flex flex-col items-center">
<label className="text-xs opacity-70 mb-1">{c.label}</label>
<input type="number" className="w-16 text-center text-lg rounded border border-white/10 bg-white/10 px-2 py-1" value={characteristics[c.key]} onChange={e=>handleCharChange(c.key, parseInt(e.target.value||'0'))} />
<input type="number" className="w-16 text-center text-lg rounded border border-slate-600 bg-slate-800 px-2 py-1" value={characteristics[c.key]} onChange={e=>handleCharChange(c.key, parseInt(e.target.value||'0'))} />
</div>
))}
</div>
</div>
{/* Skills */}
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600">
<div className="font-semibold mb-2">Skills</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{SKILLS.map(skill => (
@@ -1042,11 +1042,11 @@ function PlayerTab({
</div>
{/* Space Marine Abilities - hover tooltips */}
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600">
<div className="font-semibold mb-2">Space Marine Abilities</div>
<div className="text-xs opacity-80 grid grid-cols-2 md:grid-cols-3 gap-2">
{SPACE_MARINE_ABILITIES.map(a => (
<div key={a.name} className="p-2 bg-white/3 rounded flex items-center justify-between">
<div key={a.name} className="p-2 bg-slate-800 rounded flex items-center justify-between">
<span className="mr-2">{a.name}</span>
<Tooltip text={a.desc}><span className="inline-block w-5 h-5 text-center text-black bg-white rounded-full text-xs leading-5">?</span></Tooltip>
</div>
@@ -1055,11 +1055,11 @@ function PlayerTab({
</div>
{/* Power Armour Abilities - hover tooltips */}
<div className="bg-white/5 rounded-xl p-3 border border-white/10 mt-4">
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600 mt-4">
<div className="font-semibold mb-2">Power Armour Abilities (standard)</div>
<div className="text-xs opacity-80 grid grid-cols-2 md:grid-cols-3 gap-2">
{POWER_ARMOUR_ABILITIES.map(a => (
<div key={a.name} className="p-2 bg-white/3 rounded flex items-center justify-between">
<div key={a.name} className="p-2 bg-slate-800 rounded flex items-center justify-between">
<span className="mr-2">{a.name}</span>
<Tooltip text={a.desc}><span className="inline-block w-5 h-5 text-center text-black bg-white rounded-full text-xs leading-5">?</span></Tooltip>
</div>
@@ -1068,7 +1068,7 @@ function PlayerTab({
</div>
{/* Gear Section */}
<div className="bg-white/5 rounded-xl p-6 border border-white/10">
<div className="bg-slate-800 rounded-xl p-6 border border-slate-600">
<div className="font-semibold text-lg mb-4 flex items-center justify-between">
<span>Assigned Gear</span>
{isGMOrShopAuthed() && (
@@ -1082,7 +1082,7 @@ function PlayerTab({
</div>
<div className="space-y-4 max-h-[600px] overflow-y-auto pr-4">
{/* Column Headers */}
<div className="grid grid-cols-12 gap-4 text-sm font-medium text-slate-400 pb-2 border-b border-white/10">
<div className="grid grid-cols-12 gap-4 text-sm font-medium text-slate-400 pb-2 border-b border-slate-600">
<div className="col-span-5">Item Name</div>
<div className="col-span-2">Quantity</div>
<div className="col-span-3">Notes</div>
@@ -1091,9 +1091,9 @@ function PlayerTab({
{/* Gear Items */}
{gear.map((item, index) => (
<div key={index} className="grid grid-cols-12 gap-4 items-center bg-white/5 rounded-lg p-2">
<div key={index} className="grid grid-cols-12 gap-4 items-center bg-slate-800 rounded-lg p-2">
<input
className="col-span-5 rounded border border-white/10 bg-white/10 px-3 py-2"
className="col-span-5 rounded border border-slate-600 bg-slate-800 px-3 py-2"
placeholder="Item name"
value={item.name || ''}
onChange={(e) => updateGear(item.id, { name: e.target.value })}
@@ -1101,7 +1101,7 @@ function PlayerTab({
/>
<input
type="number"
className="col-span-2 rounded border border-white/10 bg-white/10 px-3 py-2"
className="col-span-2 rounded border border-slate-600 bg-slate-800 px-3 py-2"
placeholder="Qty"
value={item.qty || 1}
onChange={(e) => updateGear(item.id, { qty: parseInt(e.target.value) || 1 })}
@@ -1109,7 +1109,7 @@ function PlayerTab({
min="1"
/>
<input
className="col-span-3 rounded border border-white/10 bg-white/10 px-3 py-2"
className="col-span-3 rounded border border-slate-600 bg-slate-800 px-3 py-2"
placeholder="Add notes..."
value={item.note || ''}
onChange={(e) => updateGear(item.id, { note: e.target.value })}
@@ -1162,89 +1162,89 @@ function PlayerTab({
{/* Weapons & Armour */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600">
<div className="font-semibold mb-2">Weapons</div>
{[0,1,2].map(idx => (
<div key={idx} className="mb-2 border-b border-white/10 pb-2">
<div key={idx} className="mb-2 border-b border-slate-600 pb-2">
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-1">
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Name" value={weapons[idx]?.name||''} onChange={e=>handleWeaponChange(idx,'name',e.target.value)} />
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Class" value={weapons[idx]?.class||''} onChange={e=>handleWeaponChange(idx,'class',e.target.value)} />
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Damage" value={weapons[idx]?.damage||''} onChange={e=>handleWeaponChange(idx,'damage',e.target.value)} />
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Type" value={weapons[idx]?.type||''} onChange={e=>handleWeaponChange(idx,'type',e.target.value)} />
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Name" value={weapons[idx]?.name||''} onChange={e=>handleWeaponChange(idx,'name',e.target.value)} />
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Class" value={weapons[idx]?.class||''} onChange={e=>handleWeaponChange(idx,'class',e.target.value)} />
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Damage" value={weapons[idx]?.damage||''} onChange={e=>handleWeaponChange(idx,'damage',e.target.value)} />
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Type" value={weapons[idx]?.type||''} onChange={e=>handleWeaponChange(idx,'type',e.target.value)} />
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-1">
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Pen" value={weapons[idx]?.pen||''} onChange={e=>handleWeaponChange(idx,'pen',e.target.value)} />
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Range" value={weapons[idx]?.range||''} onChange={e=>handleWeaponChange(idx,'range',e.target.value)} />
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="RoF" value={weapons[idx]?.rof||''} onChange={e=>handleWeaponChange(idx,'rof',e.target.value)} />
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Clip" value={weapons[idx]?.clip||''} onChange={e=>handleWeaponChange(idx,'clip',e.target.value)} />
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Pen" value={weapons[idx]?.pen||''} onChange={e=>handleWeaponChange(idx,'pen',e.target.value)} />
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Range" value={weapons[idx]?.range||''} onChange={e=>handleWeaponChange(idx,'range',e.target.value)} />
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="RoF" value={weapons[idx]?.rof||''} onChange={e=>handleWeaponChange(idx,'rof',e.target.value)} />
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Clip" value={weapons[idx]?.clip||''} onChange={e=>handleWeaponChange(idx,'clip',e.target.value)} />
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-1">
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Rld" value={weapons[idx]?.rld||''} onChange={e=>handleWeaponChange(idx,'rld',e.target.value)} />
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Special Rules" value={weapons[idx]?.special||''} onChange={e=>handleWeaponChange(idx,'special',e.target.value)} />
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Rld" value={weapons[idx]?.rld||''} onChange={e=>handleWeaponChange(idx,'rld',e.target.value)} />
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Special Rules" value={weapons[idx]?.special||''} onChange={e=>handleWeaponChange(idx,'special',e.target.value)} />
</div>
</div>
))}
</div>
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600">
<div className="font-semibold mb-2">Armour</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Head" value={armour.head} onChange={e=>handleArmourChange('head',e.target.value)} />
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Body" value={armour.body} onChange={e=>handleArmourChange('body',e.target.value)} />
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Right Arm" value={armour.ra} onChange={e=>handleArmourChange('ra',e.target.value)} />
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Left Arm" value={armour.la} onChange={e=>handleArmourChange('la',e.target.value)} />
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Right Leg" value={armour.rl} onChange={e=>handleArmourChange('rl',e.target.value)} />
<input className="rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Left Leg" value={armour.ll} onChange={e=>handleArmourChange('ll',e.target.value)} />
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Head" value={armour.head} onChange={e=>handleArmourChange('head',e.target.value)} />
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Body" value={armour.body} onChange={e=>handleArmourChange('body',e.target.value)} />
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Right Arm" value={armour.ra} onChange={e=>handleArmourChange('ra',e.target.value)} />
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Left Arm" value={armour.la} onChange={e=>handleArmourChange('la',e.target.value)} />
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Right Leg" value={armour.rl} onChange={e=>handleArmourChange('rl',e.target.value)} />
<input className="rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Left Leg" value={armour.ll} onChange={e=>handleArmourChange('ll',e.target.value)} />
</div>
<div className="mt-2">
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1" placeholder="Additions/Notes" value={armour.additions} onChange={e=>handleArmourChange('additions',e.target.value)} />
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1" placeholder="Additions/Notes" value={armour.additions} onChange={e=>handleArmourChange('additions',e.target.value)} />
</div>
</div>
</div>
{/* Talents, Psychic Powers */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600">
<div className="font-semibold mb-2">Talents & Traits</div>
<textarea className="w-full h-32 rounded border border-white/10 bg-white/10 px-2 py-1" value={talents} onChange={e=>setTalents(e.target.value)} />
<textarea className="w-full h-32 rounded border border-slate-600 bg-slate-800 px-2 py-1" value={talents} onChange={e=>setTalents(e.target.value)} />
</div>
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600">
<div className="font-semibold mb-2">Psychic Powers</div>
<textarea className="w-full h-32 rounded border border-white/10 bg-white/10 px-2 py-1" value={psychic} onChange={e=>setPsychic(e.target.value)} />
<textarea className="w-full h-32 rounded border border-slate-600 bg-slate-800 px-2 py-1" value={psychic} onChange={e=>setPsychic(e.target.value)} />
</div>
</div>
{/* Wounds, Insanity, Movement, Fate, Corruption, Renown, XP */}
<div className="grid grid-cols-2 md:grid-cols-6 gap-2 bg-white/5 rounded-xl p-3 border border-white/10">
<div className="grid grid-cols-2 md:grid-cols-6 gap-2 bg-slate-800 rounded-xl p-3 border border-slate-600">
<div>
<label className="text-xs uppercase opacity-70">Wounds</label>
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1 mb-1" type="number" placeholder="Total" value={wounds.total} onChange={e=>setWounds(w=>({...w,total:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1 mb-1" type="number" placeholder="Current" value={wounds.current} onChange={e=>setWounds(w=>({...w,current:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1" type="number" placeholder="Fatigue" value={wounds.fatigue} onChange={e=>setWounds(w=>({...w,fatigue:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1 mb-1" type="number" placeholder="Total" value={wounds.total} onChange={e=>setWounds(w=>({...w,total:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1 mb-1" type="number" placeholder="Current" value={wounds.current} onChange={e=>setWounds(w=>({...w,current:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1" type="number" placeholder="Fatigue" value={wounds.fatigue} onChange={e=>setWounds(w=>({...w,fatigue:parseInt(e.target.value||'0')}))} />
</div>
<div>
<label className="text-xs uppercase opacity-70">Insanity</label>
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1 mb-1" type="number" placeholder="Current" value={insanity.current} onChange={e=>setInsanity(i=>({...i,current:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1 mb-1" type="number" placeholder="Battle Fatigue" value={insanity.battleFatigue} onChange={e=>setInsanity(i=>({...i,battleFatigue:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1" type="number" placeholder="Primarch's Curse" value={insanity.primarchsCurse} onChange={e=>setInsanity(i=>({...i,primarchsCurse:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1 mb-1" type="number" placeholder="Current" value={insanity.current} onChange={e=>setInsanity(i=>({...i,current:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1 mb-1" type="number" placeholder="Battle Fatigue" value={insanity.battleFatigue} onChange={e=>setInsanity(i=>({...i,battleFatigue:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1" type="number" placeholder="Primarch's Curse" value={insanity.primarchsCurse} onChange={e=>setInsanity(i=>({...i,primarchsCurse:parseInt(e.target.value||'0')}))} />
</div>
<div>
<label className="text-xs uppercase opacity-70">Movement</label>
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1 mb-1" type="number" placeholder="Half" value={movement.half} onChange={e=>setMovement(m=>({...m,half:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1 mb-1" type="number" placeholder="Charge" value={movement.charge} onChange={e=>setMovement(m=>({...m,charge:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1" type="number" placeholder="Full" value={movement.full} onChange={e=>setMovement(m=>({...m,full:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1 mb-1" type="number" placeholder="Half" value={movement.half} onChange={e=>setMovement(m=>({...m,half:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1 mb-1" type="number" placeholder="Charge" value={movement.charge} onChange={e=>setMovement(m=>({...m,charge:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1" type="number" placeholder="Full" value={movement.full} onChange={e=>setMovement(m=>({...m,full:parseInt(e.target.value||'0')}))} />
</div>
<div>
<label className="text-xs uppercase opacity-70">Fate</label>
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1 mb-1" type="number" placeholder="Total" value={fate.total} onChange={e=>setFate(f=>({...f,total:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1" type="number" placeholder="Current" value={fate.current} onChange={e=>setFate(f=>({...f,current:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1 mb-1" type="number" placeholder="Total" value={fate.total} onChange={e=>setFate(f=>({...f,total:parseInt(e.target.value||'0')}))} />
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1" type="number" placeholder="Current" value={fate.current} onChange={e=>setFate(f=>({...f,current:parseInt(e.target.value||'0')}))} />
</div>
<div>
<label className="text-xs uppercase opacity-70">Corruption</label>
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1" type="number" placeholder="Current" value={corruption} onChange={e=>setCorruption(parseInt(e.target.value||'0'))} />
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1" type="number" placeholder="Current" value={corruption} onChange={e=>setCorruption(parseInt(e.target.value||'0'))} />
</div>
<div>
<label className="text-xs uppercase opacity-70">Renown</label>
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1 mb-1" value={renown} onChange={e=>setRenown(e.target.value)} />
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1 mb-1" value={renown} onChange={e=>setRenown(e.target.value)} />
<label className="text-xs uppercase opacity-70 mt-3 block">Experience Points</label>
<div className="mb-3">
<XPBar currentXP={xp} xpSpent={xpSpent} thresholdXP={500} showLabel={true} compact={false} />
@@ -1252,25 +1252,25 @@ function PlayerTab({
<div className="grid grid-cols-2 gap-2 text-xs mb-3">
<div>
<label className="uppercase opacity-70 block">XP Total</label>
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1" type="number" value={xp} onChange={e=>setXp(parseInt(e.target.value||'0'))} />
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1" type="number" value={xp} onChange={e=>setXp(parseInt(e.target.value||'0'))} />
</div>
<div>
<label className="uppercase opacity-70 block">XP Spent</label>
<input className="w-full rounded border border-white/10 bg-white/10 px-2 py-1" type="number" value={xpSpent} onChange={e=>setXpSpent(parseInt(e.target.value||'0'))} />
<input className="w-full rounded border border-slate-600 bg-slate-800 px-2 py-1" type="number" value={xpSpent} onChange={e=>setXpSpent(parseInt(e.target.value||'0'))} />
</div>
</div>
</div>
</div>
{/* Notes */}
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
<div className="bg-slate-800 rounded-xl p-3 border border-slate-600">
<div className="font-semibold mb-2">Notes</div>
<textarea className="w-full h-24 rounded border border-white/10 bg-white/10 px-2 py-1" value={notes} onChange={e=>setNotes(e.target.value)} />
<textarea className="w-full h-24 rounded border border-slate-600 bg-slate-800 px-2 py-1" value={notes} onChange={e=>setNotes(e.target.value)} />
</div>
{/* GM Panel - Player Management */}
{isGMLoggedIn() && (
<div className="bg-white/5 rounded-xl p-4 border border-white/10 mt-4">
<div className="bg-slate-800 rounded-xl p-4 border border-slate-600 mt-4">
<div className="font-semibold text-lg mb-4">Player Management</div>
{/* Add/Update Player */}
@@ -1349,7 +1349,7 @@ function PlayerTab({
{/* In-app log panel */}
{showLogs && (
<div className="mt-4 bg-white/5 rounded-xl p-3 border border-white/10">
<div className="mt-4 bg-slate-800 rounded-xl p-3 border border-slate-600">
<div className="flex items-center justify-between mb-2">
<div className="font-semibold">Client Logs (latest)</div>
<div className="flex gap-2">
@@ -1360,7 +1360,7 @@ function PlayerTab({
<div className="max-h-64 overflow-y-auto text-xs font-mono">
{logs.length === 0 && <div className="opacity-70">No logs</div>}
{logs.map(l => (
<div key={l.id} className="mb-1 border-b border-white/5 pb-1">
<div key={l.id} className="mb-1 border-b border-slate-600 pb-1">
<div className="text-xs opacity-80">{l.timestamp} <span className="uppercase">{l.level}</span> <span className="opacity-60">[{l.component}]</span></div>
<div className="text-sm">{l.message}</div>
{l.data && <pre className="text-xs mt-1 whitespace-pre-wrap">{l.data}</pre>}

View File

@@ -234,23 +234,23 @@ export default function RequisitionShop({ authedPlayer, sessionId }) {
}, [errorMsg]);
return (
<div className="rounded-2xl bg-white/5 backdrop-blur border border-white/10 p-5 shadow-xl space-y-4">
<div className="rounded-2xl bg-slate-800 border border-slate-700 p-5 shadow-xl space-y-4">
<div className="text-2xl font-bold">Requisition Shop</div>
{errorMsg && (
<div className="p-3 rounded-lg bg-red-900/20 border border-red-500/30">
<div className="p-3 rounded-lg bg-red-800 border border-red-600">
<p className="text-red-300">{errorMsg}</p>
</div>
)}
{!authedPlayer ? (
<div className="p-4 rounded-lg bg-amber-900/20 border border-amber-500/30">
<div className="p-4 rounded-lg bg-amber-800 border border-amber-600">
<p className="text-amber-300">Please log in using the header above to access the shop.</p>
</div>
) : (
<>
{/* Player Info */}
<div className="p-3 rounded-lg bg-blue-900/20 border border-blue-500/30">
<div className="p-3 rounded-lg bg-blue-800 border border-blue-600">
<div className="text-lg font-semibold text-blue-300">
Current Player: {authedPlayer}
</div>
@@ -264,13 +264,13 @@ export default function RequisitionShop({ authedPlayer, sessionId }) {
{/* Search and Filter */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<input
className="rounded-xl border border-white/10 bg-white/10 px-3 py-2"
className="rounded-xl border border-slate-600 bg-slate-800 px-3 py-2"
placeholder="Search items..."
value={search}
onChange={e => setSearch(e.target.value)}
/>
<select
className="rounded-xl border border-white/10 bg-white/10 px-3 py-2"
className="rounded-xl border border-slate-600 bg-slate-800 px-3 py-2"
value={categoryFilter}
onChange={e => setCategoryFilter(e.target.value)}
>
@@ -283,7 +283,7 @@ export default function RequisitionShop({ authedPlayer, sessionId }) {
{/* Items Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredItems.map(item => (
<div key={item.id} className="p-3 rounded-lg bg-slate-800/50 border border-white/10">
<div key={item.id} className="p-3 rounded-lg bg-slate-800 border border-slate-700">
<div className="font-semibold text-white">{item.name}</div>
<div className="text-xs text-slate-300 mb-2">{item.category}</div>

View File

@@ -174,7 +174,7 @@ function RulesTab({ authedPlayer, sessionId }) {
</div>
{/* Search Section */}
<div className="bg-white/5 rounded-xl p-6 border border-white/10">
<div className="bg-slate-800 rounded-xl p-6 border border-slate-600">
<form onSubmit={handleSearch} className="space-y-4">
<div className="flex gap-4">
<div className="flex-1">
@@ -183,13 +183,13 @@ function RulesTab({ authedPlayer, sessionId }) {
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search for rules, weapons, talents, skills..."
className="w-full px-4 py-2 rounded-lg border border-white/20 bg-white/10 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full px-4 py-2 rounded-lg border border-slate-600 bg-slate-800 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<select
value={selectedCategory}
onChange={(e) => handleCategoryChange(e.target.value)}
className="px-4 py-2 rounded-lg border border-white/20 bg-white/10 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
className="px-4 py-2 rounded-lg border border-slate-600 bg-slate-800 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{ruleCategories.map(cat => (
<option key={cat.id} value={cat.id} className="bg-slate-800">
@@ -235,7 +235,7 @@ function RulesTab({ authedPlayer, sessionId }) {
<button
key={rule}
onClick={() => handleQuickSearch(rule)}
className="px-3 py-1 text-sm bg-white/10 hover:bg-white/20 rounded-md border border-white/20 transition-colors"
className="px-3 py-1 text-sm bg-slate-800 hover:bg-slate-700 rounded-md border border-slate-600 transition-colors"
>
{rule}
</button>
@@ -255,7 +255,7 @@ function RulesTab({ authedPlayer, sessionId }) {
setSelectedCategory(cid);
await fetchRandom(cid, browseCount);
}}
className={`px-3 py-1 text-sm rounded-md border transition-colors ${selectedCategory === cat.id ? 'bg-blue-500 text-white border-blue-600' : 'bg-white/10 text-white border-white/20 hover:bg-white/20'}`}
className={`px-3 py-1 text-sm rounded-md border transition-colors ${selectedCategory === cat.id ? 'bg-blue-500 text-white border-blue-600' : 'bg-slate-800 text-white border-slate-600 hover:bg-slate-700'}`}
>
{cat.name}
</button>
@@ -272,7 +272,7 @@ function RulesTab({ authedPlayer, sessionId }) {
<button
key={index}
onClick={() => handleQuickSearch(query)}
className="px-2 py-1 text-xs bg-white/5 hover:bg-white/10 rounded border border-white/10 transition-colors"
className="px-2 py-1 text-xs bg-slate-800 hover:bg-slate-800 rounded border border-slate-600 transition-colors"
>
{query}
</button>
@@ -284,7 +284,7 @@ function RulesTab({ authedPlayer, sessionId }) {
{/* Search Results */}
{searchResults.length > 0 && (
<div className="bg-white/5 rounded-xl p-6 border border-white/10">
<div className="bg-slate-800 rounded-xl p-6 border border-slate-600">
<h2 className="text-xl font-semibold mb-4">
Search Results ({searchResults.length})
</h2>
@@ -293,20 +293,20 @@ function RulesTab({ authedPlayer, sessionId }) {
{searchResults.map((rule, index) => (
<div
key={rule.id || index}
className="bg-white/5 rounded-lg p-4 border border-white/10 hover:bg-white/10 transition-colors cursor-pointer"
className="bg-slate-800 rounded-lg p-4 border border-slate-600 hover:bg-slate-800 transition-colors cursor-pointer"
onClick={() => setSelectedRule(rule)}
>
<div className="flex justify-between items-start mb-2">
<h3 className="text-lg font-medium text-blue-300">{rule.title}</h3>
<div className="flex gap-2 text-xs">
{rule.category && (
<span className="px-2 py-1 bg-blue-600/30 rounded">{rule.category}</span>
<span className="px-2 py-1 bg-blue-700 rounded">{rule.category}</span>
)}
{rule.page && (
<span className="px-2 py-1 bg-green-600/30 rounded">p.{rule.page}</span>
<span className="px-2 py-1 bg-green-700 rounded">p.{rule.page}</span>
)}
{rule.source && (
<span className="px-2 py-1 bg-purple-600/30 rounded">{rule.source}</span>
<span className="px-2 py-1 bg-purple-700 rounded">{rule.source}</span>
)}
</div>
</div>
@@ -339,8 +339,8 @@ function RulesTab({ authedPlayer, sessionId }) {
{/* Rule Detail Modal */}
{selectedRule && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<div className="bg-slate-800 rounded-xl p-6 max-w-4xl max-h-[80vh] overflow-y-auto border border-white/20">
<div className="fixed inset-0 bg-black flex items-center justify-center p-4 z-50">
<div className="bg-slate-800 rounded-xl p-6 max-w-4xl max-h-[80vh] overflow-y-auto border border-slate-600">
<div className="flex justify-between items-start mb-4">
<h2 className="text-2xl font-bold text-blue-300">{selectedRule.title}</h2>
<button
@@ -353,13 +353,13 @@ function RulesTab({ authedPlayer, sessionId }) {
<div className="flex gap-2 mb-4 text-xs">
{selectedRule.category && (
<span className="px-2 py-1 bg-blue-600/30 rounded">{selectedRule.category}</span>
<span className="px-2 py-1 bg-blue-700 rounded">{selectedRule.category}</span>
)}
{selectedRule.page && (
<span className="px-2 py-1 bg-green-600/30 rounded">Page {selectedRule.page}</span>
<span className="px-2 py-1 bg-green-700 rounded">Page {selectedRule.page}</span>
)}
{selectedRule.source && (
<span className="px-2 py-1 bg-purple-600/30 rounded">{selectedRule.source}</span>
<span className="px-2 py-1 bg-purple-700 rounded">{selectedRule.source}</span>
)}
</div>
@@ -371,7 +371,7 @@ function RulesTab({ authedPlayer, sessionId }) {
/>
{selectedRule.examples && (
<div className="mt-4 p-4 bg-white/5 rounded-lg border border-white/10">
<div className="mt-4 p-4 bg-slate-800 rounded-lg border border-slate-600">
<h4 className="font-semibold mb-2 text-green-300">Examples:</h4>
<div className="text-slate-300">{selectedRule.examples}</div>
</div>
@@ -385,7 +385,7 @@ function RulesTab({ authedPlayer, sessionId }) {
<button
key={index}
onClick={() => handleQuickSearch(related)}
className="px-2 py-1 text-xs bg-white/10 hover:bg-white/20 rounded border border-white/20 transition-colors"
className="px-2 py-1 text-xs bg-slate-800 hover:bg-slate-700 rounded border border-slate-600 transition-colors"
>
{related}
</button>
@@ -399,7 +399,7 @@ function RulesTab({ authedPlayer, sessionId }) {
{/* No Results */}
{searchQuery && !loading && searchResults.length === 0 && (
<div className="bg-white/5 rounded-xl p-6 border border-white/10 text-center">
<div className="bg-slate-800 rounded-xl p-6 border border-slate-600 text-center">
<p className="text-slate-400">No rules found for "{searchQuery}"</p>
<p className="text-xs text-slate-500 mt-2">Try different keywords or check a different category</p>
</div>

View File

@@ -70,21 +70,21 @@ export function XPSummary({ currentXP = 0, xpSpent = 0, thresholdXP = 500 }) {
return (
<div className="space-y-2">
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="bg-slate-700/50 rounded p-2">
<div className="text-xs text-slate-400">Total XP</div>
<div className="bg-slate-700 rounded p-2 border border-slate-600">
<div className="text-xs text-slate-300">Total XP</div>
<div className="text-lg font-bold text-green-400">{currentXP}</div>
</div>
<div className="bg-slate-700/50 rounded p-2">
<div className="text-xs text-slate-400">Available XP</div>
<div className="bg-slate-700 rounded p-2 border border-slate-600">
<div className="text-xs text-slate-300">Available XP</div>
<div className="text-lg font-bold text-green-300">{availableXP}</div>
</div>
<div className="bg-slate-700/50 rounded p-2 col-span-2">
<div className="text-xs text-slate-400">Current Level</div>
<div className="text-lg font-bold text-blue-400">Level {currentLevel}</div>
<div className="bg-slate-700 rounded p-2 col-span-2 border border-slate-600">
<div className="text-xs text-slate-300">Current Level</div>
<div className="text-lg font-bold text-blue-300">Level {currentLevel}</div>
</div>
</div>
{xpSpent > 0 && (
<div className="text-xs text-slate-400">
<div className="text-xs text-slate-300">
Spent: {xpSpent} XP | Remaining: {availableXP} XP
</div>
)}

View File

@@ -28,7 +28,7 @@
}
.card {
@apply rounded-2xl bg-white/5 backdrop-blur border border-white/10 p-4 sm:p-5 shadow-xl;
@apply rounded-2xl bg-slate-800 backdrop-blur border border-slate-600 p-4 sm:p-5 shadow-xl;
}
.badge {
@@ -48,11 +48,11 @@
}
.input-field {
@apply w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2;
@apply w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2;
}
.textarea-field {
@apply w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2;
@apply w-full rounded-xl border border-slate-600 bg-slate-800 px-3 py-2;
}
.select-field {
@@ -76,7 +76,7 @@
}
.roll-card {
@apply rounded-xl border border-white/10 bg-black/20 p-3;
@apply rounded-xl border border-slate-600 bg-slate-800 p-3;
}
.roll-type {
@@ -92,6 +92,6 @@
}
.progress-bar {
@apply h-2 w-full bg-white/10 rounded overflow-hidden;
@apply h-2 w-full bg-slate-700 rounded overflow-hidden;
}
}