Initialize League GUI prototype
This commit is contained in:
79
.codex/project.md
Normal file
79
.codex/project.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Codex Project Notes
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
`League of Legends GUI Overhaul` is a React/Vite prototype for a modern, dark, MOBA-/fantasy-inspired client interface.
|
||||||
|
|
||||||
|
Repository:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Toxic/league-of-legends-gui-overhaul
|
||||||
|
```
|
||||||
|
|
||||||
|
Remote:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://git.wilkensxl.de/Toxic/league-of-legends-gui-overhaul.git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Use these commands as the source of truth:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
npm run build
|
||||||
|
npm test
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no separate lint script yet. `npm run build` runs `tsc --noEmit` before the Vite build. There is no audit or release-check script yet.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
```text
|
||||||
|
React, Vite, TypeScript, React Router, Vitest, Testing Library, CSS custom properties.
|
||||||
|
```
|
||||||
|
|
||||||
|
Package manager or build tool:
|
||||||
|
|
||||||
|
```text
|
||||||
|
npm
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Artifacts
|
||||||
|
|
||||||
|
Release artifacts are produced in:
|
||||||
|
|
||||||
|
Expected files:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Rules
|
||||||
|
|
||||||
|
- Do not commit secrets, tokens, `.env` files, certificates, or private keys.
|
||||||
|
- Treat generated credentials as sensitive.
|
||||||
|
- Prefer local generation and local processing for user data.
|
||||||
|
- Keep dependency audit results visible in CI once dependencies exist.
|
||||||
|
- Do not add external network calls unless the feature explicitly requires them.
|
||||||
|
- Document any external assets, API calls, telemetry, or package publishing behavior when implementation begins.
|
||||||
|
|
||||||
|
## Release Rules
|
||||||
|
|
||||||
|
No published release process exists yet.
|
||||||
|
|
||||||
|
Before a release:
|
||||||
|
|
||||||
|
1. run `npm run build`,
|
||||||
|
2. run `npm test`,
|
||||||
|
3. run the release checklist,
|
||||||
|
4. verify CI is green,
|
||||||
|
5. verify download links when publishing artifacts,
|
||||||
|
6. update README and changelog,
|
||||||
|
7. create a tag,
|
||||||
|
8. create the release.
|
||||||
|
|
||||||
|
Do not create releases unless the user explicitly asks for a release.
|
||||||
114
.gitea/workflows/dependency-check.yml
Normal file
114
.gitea/workflows/dependency-check.yml
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
name: Scheduled Dependency Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "29 3 * * 2"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dependency-check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Detect project stack
|
||||||
|
id: detect
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
stacks=""
|
||||||
|
|
||||||
|
[ -f package.json ] && stacks="${stacks} node"
|
||||||
|
{ [ -f pyproject.toml ] || [ -f requirements.txt ]; } && stacks="${stacks} python"
|
||||||
|
[ -f Cargo.toml ] && stacks="${stacks} rust"
|
||||||
|
[ -f go.mod ] && stacks="${stacks} go"
|
||||||
|
{ [ -f Dockerfile ] || [ -f compose.yml ] || [ -f docker-compose.yml ]; } && stacks="${stacks} docker"
|
||||||
|
|
||||||
|
echo "stacks=${stacks:-generic}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Detected stacks:${stacks:- generic}"
|
||||||
|
|
||||||
|
- name: Node dependency report
|
||||||
|
if: contains(steps.detect.outputs.stacks, 'node')
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then
|
||||||
|
npm ci
|
||||||
|
else
|
||||||
|
npm install --package-lock-only --ignore-scripts
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Security audit:"
|
||||||
|
npm audit --omit=dev --audit-level=high
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Outdated dependencies:"
|
||||||
|
npm outdated || true
|
||||||
|
|
||||||
|
- name: Python dependency report
|
||||||
|
if: contains(steps.detect.outputs.stacks, 'python')
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip pip-audit
|
||||||
|
|
||||||
|
echo "Security audit:"
|
||||||
|
if [ -f requirements.txt ]; then
|
||||||
|
pip-audit -r requirements.txt
|
||||||
|
else
|
||||||
|
pip-audit
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Outdated packages:"
|
||||||
|
python -m pip list --outdated || true
|
||||||
|
|
||||||
|
- name: Rust dependency report
|
||||||
|
if: contains(steps.detect.outputs.stacks, 'rust')
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cargo install cargo-audit cargo-outdated --locked
|
||||||
|
|
||||||
|
echo "Security audit:"
|
||||||
|
cargo audit
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Outdated crates:"
|
||||||
|
cargo outdated || true
|
||||||
|
|
||||||
|
- name: Go dependency report
|
||||||
|
if: contains(steps.detect.outputs.stacks, 'go')
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
|
|
||||||
|
echo "Security audit:"
|
||||||
|
govulncheck ./...
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Available dependency updates:"
|
||||||
|
go list -u -m all || true
|
||||||
|
|
||||||
|
- name: Docker base image report
|
||||||
|
if: contains(steps.detect.outputs.stacks, 'docker')
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "Docker image references:"
|
||||||
|
grep -RInE --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build '^\s*FROM\s+' Dockerfile* . 2>/dev/null || true
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Review Docker base images manually for pinned versions, official sources, and current security status."
|
||||||
|
|
||||||
|
- name: Dependency guidance
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cat <<'EOF'
|
||||||
|
Dependency check completed.
|
||||||
|
|
||||||
|
This workflow reports vulnerabilities and available updates. It does
|
||||||
|
not modify dependency files, create pull requests, or publish packages.
|
||||||
|
|
||||||
|
Recommended manual follow-up:
|
||||||
|
- update dependencies in a focused branch,
|
||||||
|
- run the project test/build commands,
|
||||||
|
- review lockfile diffs carefully,
|
||||||
|
- document intentionally held versions.
|
||||||
|
EOF
|
||||||
133
.gitea/workflows/release-dry-run.yml
Normal file
133
.gitea/workflows/release-dry-run.yml
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
name: Release Dry Run
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-dry-run:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Inspect release metadata
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
missing=0
|
||||||
|
|
||||||
|
required_docs=(
|
||||||
|
"README.md"
|
||||||
|
"CHANGELOG.md"
|
||||||
|
"SECURITY.md"
|
||||||
|
"docs/release-checklist.md"
|
||||||
|
)
|
||||||
|
|
||||||
|
for file in "${required_docs[@]}"; do
|
||||||
|
if [ ! -f "$file" ]; then
|
||||||
|
echo "Missing release document: $file"
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
placeholder_paths=(README.md AGENTS.md .codex docs)
|
||||||
|
placeholder_pattern='PROJECT_NAME|PROJECT_DESCRIPTION|REPOSITORY_OWNER|REPOSITORY_NAME|PACKAGE_NAME|ARTIFACT_NAME|ARTIFACT_OUTPUT_DIRECTORY|DOWNLOAD_URL|BUILD_COMMAND|TEST_COMMAND|LINT_COMMAND|AUDIT_COMMAND'
|
||||||
|
|
||||||
|
for path in "${placeholder_paths[@]}"; do
|
||||||
|
[ -e "$path" ] || continue
|
||||||
|
if grep -RInE --exclude-dir=.git "$placeholder_pattern" "$path"; then
|
||||||
|
echo "Unresolved template placeholders found."
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$missing" -eq 1 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Detect project stack
|
||||||
|
id: detect
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
stacks=""
|
||||||
|
|
||||||
|
[ -f package.json ] && stacks="${stacks} node"
|
||||||
|
{ [ -f pyproject.toml ] || [ -f requirements.txt ]; } && stacks="${stacks} python"
|
||||||
|
[ -f Cargo.toml ] && stacks="${stacks} rust"
|
||||||
|
[ -f go.mod ] && stacks="${stacks} go"
|
||||||
|
|
||||||
|
echo "stacks=${stacks:-generic}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Detected stacks:${stacks:- generic}"
|
||||||
|
|
||||||
|
- name: Node release checks
|
||||||
|
if: contains(steps.detect.outputs.stacks, 'node')
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then
|
||||||
|
npm ci
|
||||||
|
else
|
||||||
|
npm install
|
||||||
|
fi
|
||||||
|
|
||||||
|
node -e "const p=require('./package.json'); if(!p.name||!p.version){throw new Error('package.json needs name and version')}; console.log(p.name+'@'+p.version)"
|
||||||
|
|
||||||
|
npm run lint --if-present
|
||||||
|
npm test --if-present
|
||||||
|
npm run build --if-present
|
||||||
|
npm run release:check --if-present
|
||||||
|
|
||||||
|
- name: Python release checks
|
||||||
|
if: contains(steps.detect.outputs.stacks, 'python')
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
|
||||||
|
if [ -f requirements.txt ]; then
|
||||||
|
python -m pip install -r requirements.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f pyproject.toml ]; then
|
||||||
|
python -m pip install build
|
||||||
|
python -m build
|
||||||
|
else
|
||||||
|
echo "No pyproject.toml found; skipped Python package build."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Rust release checks
|
||||||
|
if: contains(steps.detect.outputs.stacks, 'rust')
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cargo test
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
- name: Go release checks
|
||||||
|
if: contains(steps.detect.outputs.stacks, 'go')
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
go test ./...
|
||||||
|
go build ./...
|
||||||
|
|
||||||
|
- name: Artifact report
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "Potential release artifacts:"
|
||||||
|
find . \
|
||||||
|
-path ./.git -prune -o \
|
||||||
|
-path ./node_modules -prune -o \
|
||||||
|
-path './dist/*' -type f -print -o \
|
||||||
|
-path './build/*' -type f -print -o \
|
||||||
|
-path './release/*' -type f -print -o \
|
||||||
|
-path './target/release/*' -type f -print \
|
||||||
|
| sed 's#^\./##' \
|
||||||
|
| head -200
|
||||||
|
|
||||||
|
cat <<'EOF'
|
||||||
|
|
||||||
|
Release dry run completed.
|
||||||
|
|
||||||
|
This workflow verifies release readiness. It does not create tags,
|
||||||
|
releases, packages, or upload artifacts.
|
||||||
|
EOF
|
||||||
139
.gitea/workflows/repo-cleanup.yml
Normal file
139
.gitea/workflows/repo-cleanup.yml
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
name: Scheduled Repository Cleanup Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "43 3 * * 1"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cleanup-check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Check ignored and untracked generated files
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "Ignored files that would be skipped by git:"
|
||||||
|
git status --ignored --short || true
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Tracked generated files check:"
|
||||||
|
generated_patterns=(
|
||||||
|
'(^|/)node_modules/'
|
||||||
|
'(^|/)dist/'
|
||||||
|
'(^|/)build/'
|
||||||
|
'(^|/)out/'
|
||||||
|
'(^|/)release/'
|
||||||
|
'(^|/)target/'
|
||||||
|
'(^|/)coverage/'
|
||||||
|
'\.log$'
|
||||||
|
'\.tmp$'
|
||||||
|
'\.temp$'
|
||||||
|
)
|
||||||
|
|
||||||
|
found=0
|
||||||
|
tracked_files="$(git ls-files)"
|
||||||
|
for pattern in "${generated_patterns[@]}"; do
|
||||||
|
if echo "$tracked_files" | grep -Ei "$pattern"; then
|
||||||
|
found=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$found" -eq 1 ]; then
|
||||||
|
echo "Generated files appear to be tracked. Review .gitignore and remove generated outputs from version control if appropriate."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check large tracked files
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
limit_bytes="${LARGE_FILE_LIMIT_BYTES:-5242880}"
|
||||||
|
found=0
|
||||||
|
|
||||||
|
while IFS= read -r file; do
|
||||||
|
[ -f "$file" ] || continue
|
||||||
|
size="$(wc -c < "$file")"
|
||||||
|
if [ "$size" -gt "$limit_bytes" ]; then
|
||||||
|
echo "${file} is ${size} bytes, above limit ${limit_bytes}."
|
||||||
|
found=1
|
||||||
|
fi
|
||||||
|
done < <(git ls-files)
|
||||||
|
|
||||||
|
if [ "$found" -eq 1 ]; then
|
||||||
|
echo "Large tracked files found. Move release artifacts to packages/releases or document why they belong in git."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check local config and secret-prone files
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
found=0
|
||||||
|
|
||||||
|
risky_patterns=(
|
||||||
|
'^\.env$'
|
||||||
|
'^\.env\.'
|
||||||
|
'\.pfx$'
|
||||||
|
'\.p12$'
|
||||||
|
'\.pem$'
|
||||||
|
'\.key$'
|
||||||
|
'\.token$'
|
||||||
|
'(^|/)secrets/'
|
||||||
|
)
|
||||||
|
|
||||||
|
tracked_files="$(git ls-files)"
|
||||||
|
for pattern in "${risky_patterns[@]}"; do
|
||||||
|
if echo "$tracked_files" | grep -Ei "$pattern" | grep -vE '^\.env\.example$'; then
|
||||||
|
found=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$found" -eq 1 ]; then
|
||||||
|
echo "Secret-prone local config files are tracked. Review immediately."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check stale branches
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
git fetch --all --prune
|
||||||
|
|
||||||
|
protected='^(main|master|develop|dev|release|staging|production)$'
|
||||||
|
cutoff="$(date -u -d '90 days ago' +%s)"
|
||||||
|
found=0
|
||||||
|
|
||||||
|
while IFS='|' read -r branch timestamp; do
|
||||||
|
branch="${branch#origin/}"
|
||||||
|
[ "$branch" = "HEAD" ] && continue
|
||||||
|
echo "$branch" | grep -Eq "$protected" && continue
|
||||||
|
|
||||||
|
if [ "$timestamp" -lt "$cutoff" ]; then
|
||||||
|
echo "Stale remote branch candidate: ${branch}"
|
||||||
|
found=1
|
||||||
|
fi
|
||||||
|
done < <(git for-each-ref refs/remotes/origin --format='%(refname:short)|%(committerdate:unix)')
|
||||||
|
|
||||||
|
if [ "$found" -eq 1 ]; then
|
||||||
|
echo "Stale branch candidates found. Review manually before deleting anything."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Cleanup guidance
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cat <<'EOF'
|
||||||
|
Repository cleanup check completed.
|
||||||
|
|
||||||
|
This workflow reports cleanup candidates. It does not delete branches,
|
||||||
|
packages, releases, or files automatically.
|
||||||
|
|
||||||
|
Recommended manual follow-up:
|
||||||
|
- remove generated files from git,
|
||||||
|
- update .gitignore,
|
||||||
|
- move large artifacts to releases or package registry,
|
||||||
|
- review stale branches,
|
||||||
|
- document intentional exceptions.
|
||||||
|
EOF
|
||||||
173
.gitea/workflows/security-scan.yml
Normal file
173
.gitea/workflows/security-scan.yml
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
name: Scheduled Security Scan
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "17 3 * * 1"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
security-scan:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Detect project stack
|
||||||
|
id: detect
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
stacks=""
|
||||||
|
|
||||||
|
[ -f package.json ] && stacks="${stacks} node"
|
||||||
|
{ [ -f pyproject.toml ] || [ -f requirements.txt ]; } && stacks="${stacks} python"
|
||||||
|
[ -f Cargo.toml ] && stacks="${stacks} rust"
|
||||||
|
[ -f go.mod ] && stacks="${stacks} go"
|
||||||
|
{ [ -f Dockerfile ] || [ -f compose.yml ] || [ -f docker-compose.yml ]; } && stacks="${stacks} docker"
|
||||||
|
|
||||||
|
echo "stacks=${stacks:-generic}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Detected stacks:${stacks:- generic}"
|
||||||
|
|
||||||
|
- name: Node production dependency audit
|
||||||
|
if: contains(steps.detect.outputs.stacks, 'node')
|
||||||
|
run: npm audit --omit=dev --audit-level=high
|
||||||
|
|
||||||
|
- name: Python dependency audit
|
||||||
|
if: contains(steps.detect.outputs.stacks, 'python')
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip pip-audit
|
||||||
|
if [ -f requirements.txt ]; then
|
||||||
|
pip-audit -r requirements.txt
|
||||||
|
else
|
||||||
|
pip-audit
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Rust dependency audit
|
||||||
|
if: contains(steps.detect.outputs.stacks, 'rust')
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cargo install cargo-audit --locked
|
||||||
|
cargo audit
|
||||||
|
|
||||||
|
- name: Go vulnerability scan
|
||||||
|
if: contains(steps.detect.outputs.stacks, 'go')
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
|
govulncheck ./...
|
||||||
|
|
||||||
|
- name: Suspicious code pattern scan
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
grep_excludes=(
|
||||||
|
--exclude-dir=.git
|
||||||
|
--exclude-dir=node_modules
|
||||||
|
--exclude-dir=dist
|
||||||
|
--exclude-dir=build
|
||||||
|
--exclude-dir=release
|
||||||
|
--exclude=security-scan.yml
|
||||||
|
)
|
||||||
|
|
||||||
|
patterns=(
|
||||||
|
'eval\s*\('
|
||||||
|
'new Function\s*\('
|
||||||
|
'dangerouslySetInnerHTML'
|
||||||
|
'innerHTML\s*='
|
||||||
|
'child_process'
|
||||||
|
'exec\s*\('
|
||||||
|
'spawn\s*\('
|
||||||
|
'shell\.openExternal'
|
||||||
|
'nodeIntegration:\s*true'
|
||||||
|
'webSecurity:\s*false'
|
||||||
|
'allowRunningInsecureContent:\s*true'
|
||||||
|
'curl .*sh'
|
||||||
|
'wget .*sh'
|
||||||
|
)
|
||||||
|
|
||||||
|
found=0
|
||||||
|
for pattern in "${patterns[@]}"; do
|
||||||
|
if grep -RInE "${grep_excludes[@]}" "$pattern" .; then
|
||||||
|
found=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$found" -eq 1 ]; then
|
||||||
|
echo "Suspicious code patterns were found. Review the matches above."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Secret and config leak scan
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
grep_excludes=(
|
||||||
|
--exclude-dir=.git
|
||||||
|
--exclude-dir=node_modules
|
||||||
|
--exclude-dir=dist
|
||||||
|
--exclude-dir=build
|
||||||
|
--exclude-dir=release
|
||||||
|
--exclude=security-scan.yml
|
||||||
|
)
|
||||||
|
|
||||||
|
patterns=(
|
||||||
|
'BEGIN (RSA |EC |OPENSSH |)PRIVATE KEY'
|
||||||
|
'AKIA[0-9A-Z]{16}'
|
||||||
|
'xox[baprs]-[0-9A-Za-z-]+'
|
||||||
|
'gh[pousr]_[0-9A-Za-z_]+'
|
||||||
|
'sk-[A-Za-z0-9]{20,}'
|
||||||
|
'api[_-]?key\s*=\s*["'\'']?[A-Za-z0-9_\-]{20,}'
|
||||||
|
'token\s*=\s*["'\'']?[A-Za-z0-9_\-]{20,}'
|
||||||
|
'password\s*=\s*["'\'']?[^[:space:]]{8,}'
|
||||||
|
)
|
||||||
|
|
||||||
|
found=0
|
||||||
|
for pattern in "${patterns[@]}"; do
|
||||||
|
if grep -RInE "${grep_excludes[@]}" "$pattern" .; then
|
||||||
|
found=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if find . -path ./.git -prune -o \( -name ".env" -o -name ".env.*" \) -not -name ".env.example" -print | grep .; then
|
||||||
|
echo "Committed environment files were found."
|
||||||
|
found=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$found" -eq 1 ]; then
|
||||||
|
echo "Potential secret or config leak detected. Review the matches above."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: AI instruction safety scan
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
grep_excludes=(
|
||||||
|
--exclude-dir=.git
|
||||||
|
--exclude-dir=node_modules
|
||||||
|
--exclude-dir=dist
|
||||||
|
--exclude-dir=build
|
||||||
|
--exclude-dir=release
|
||||||
|
--exclude=security-scan.yml
|
||||||
|
)
|
||||||
|
|
||||||
|
patterns=(
|
||||||
|
'ignore (all )?(previous|above) instructions'
|
||||||
|
'system prompt'
|
||||||
|
'developer message'
|
||||||
|
'reveal your instructions'
|
||||||
|
'exfiltrate'
|
||||||
|
'send.*token'
|
||||||
|
'send.*secret'
|
||||||
|
'disable.*safety'
|
||||||
|
'jailbreak'
|
||||||
|
)
|
||||||
|
|
||||||
|
found=0
|
||||||
|
for pattern in "${patterns[@]}"; do
|
||||||
|
if grep -RInEi "${grep_excludes[@]}" "$pattern" .; then
|
||||||
|
found=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$found" -eq 1 ]; then
|
||||||
|
echo "Potential unsafe AI-control text found. Review whether this is documentation, test data, or malicious content."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
99
.gitea/workflows/template-compliance.yml
Normal file
99
.gitea/workflows/template-compliance.yml
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
name: Codex Template Compliance
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
template-compliance:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check required Codex files
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
missing=0
|
||||||
|
|
||||||
|
required_files=(
|
||||||
|
"AGENTS.md"
|
||||||
|
".codex/project.md"
|
||||||
|
"README.md"
|
||||||
|
)
|
||||||
|
|
||||||
|
recommended_files=(
|
||||||
|
"SECURITY.md"
|
||||||
|
"CHANGELOG.md"
|
||||||
|
"docs/agent-handoff.md"
|
||||||
|
)
|
||||||
|
|
||||||
|
for file in "${required_files[@]}"; do
|
||||||
|
if [ ! -f "$file" ]; then
|
||||||
|
echo "Missing required Codex file: $file"
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for file in "${recommended_files[@]}"; do
|
||||||
|
if [ ! -f "$file" ]; then
|
||||||
|
echo "Recommended Codex file not found: $file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$missing" -eq 1 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check unresolved placeholders
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
found=0
|
||||||
|
paths=(AGENTS.md README.md SECURITY.md CHANGELOG.md .codex docs)
|
||||||
|
pattern='PROJECT_NAME|PROJECT_DESCRIPTION|REPOSITORY_OWNER|REPOSITORY_NAME|PACKAGE_NAME|ARTIFACT_NAME|ARTIFACT_OUTPUT_DIRECTORY|AUTHOR_NAME|PROJECT_STACK|DOWNLOAD_URL|BUILD_COMMAND|TEST_COMMAND|LINT_COMMAND|AUDIT_COMMAND|README_COMMAND|INSTALL_COMMAND|DEV_COMMAND|PACKAGE_MANAGER|PROJECT_VERSION'
|
||||||
|
|
||||||
|
for path in "${paths[@]}"; do
|
||||||
|
[ -e "$path" ] || continue
|
||||||
|
if grep -RInE --exclude-dir=.git "$pattern" "$path"; then
|
||||||
|
found=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$found" -eq 1 ]; then
|
||||||
|
echo "Unresolved template placeholders found. Replace real values or mark genuinely unknown values as PENDING."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check workflow baseline
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "Detected Gitea workflows:"
|
||||||
|
find .gitea/workflows -maxdepth 1 -type f -name '*.yml' -print 2>/dev/null || true
|
||||||
|
|
||||||
|
if [ ! -f ".gitea/workflows/security-scan.yml" ]; then
|
||||||
|
echo "Recommended workflow missing: .gitea/workflows/security-scan.yml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f ".gitea/workflows/repo-cleanup.yml" ]; then
|
||||||
|
echo "Recommended workflow missing: .gitea/workflows/repo-cleanup.yml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Compliance guidance
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cat <<'EOF'
|
||||||
|
Codex template compliance check completed.
|
||||||
|
|
||||||
|
This workflow verifies agent context and template hygiene. It does
|
||||||
|
not change files automatically.
|
||||||
|
|
||||||
|
Recommended manual follow-up:
|
||||||
|
- add missing required Codex context files,
|
||||||
|
- replace unresolved placeholders,
|
||||||
|
- keep README and project context aligned,
|
||||||
|
- document intentional exceptions in .codex/project.md.
|
||||||
|
EOF
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
coverage/
|
||||||
|
.vite/
|
||||||
|
.codex-agent-repository-kit/
|
||||||
66
AGENTS.md
Normal file
66
AGENTS.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Agent Instructions
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
League of Legends GUI Overhaul: React/Vite prototype for a modern, dark, MOBA-/fantasy-inspired client interface.
|
||||||
|
|
||||||
|
## Repository Rules
|
||||||
|
|
||||||
|
- Prefer existing project patterns over new abstractions.
|
||||||
|
- Keep changes scoped to the user's request.
|
||||||
|
- Do not commit secrets, `.env` files, private keys, certificates, or tokens.
|
||||||
|
- Do not rewrite history or run destructive git commands unless explicitly requested.
|
||||||
|
- Do not create a release unless explicitly requested.
|
||||||
|
- Check repository state before editing and before finishing. Preserve unrelated user changes.
|
||||||
|
- Replace applicable placeholders in copied templates. Remove non-applicable placeholder sections instead of leaving fake values.
|
||||||
|
- The current stack is Node, React, Vite, TypeScript, React Router, Vitest, Testing Library, and CSS custom properties.
|
||||||
|
- If `GITEA_TOKEN` is available locally, use it only for read-only Gitea API checks such as private repository metadata, package-read visibility, and Actions run status. Never print, commit, or store the token.
|
||||||
|
- After pushing commits that trigger a Gitea workflow, poll the workflow run until it succeeds or a concrete blocker is known.
|
||||||
|
- Repository cleanup automation must be non-destructive. Do not delete branches, packages, releases, or tracked files without explicit user approval.
|
||||||
|
- Dependency, compliance, and release dry-run automation must report findings only. Do not auto-update dependencies, auto-open PRs, create tags, publish packages, or create releases without explicit user approval.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Use these commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
npm run build
|
||||||
|
npm test
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no separate lint script yet. `npm run build` runs `tsc --noEmit` before the Vite build.
|
||||||
|
|
||||||
|
Keep `.codex/project.md` and this `AGENTS.md` aligned when commands, artifact paths, or release rules change.
|
||||||
|
|
||||||
|
## Artifacts
|
||||||
|
|
||||||
|
Build output is produced in:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
No release package naming or download verification process exists yet.
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- Review `docs/security-review.md` before release work.
|
||||||
|
- Fill `docs/security-review.md` with actual checked commands and results when performing release-readiness work.
|
||||||
|
- Review scheduled security workflow failures before changing code. Treat matches as leads: they may be true positives, documentation examples, or test fixtures.
|
||||||
|
- Review repository cleanup workflow failures as maintenance leads. Document intentional exceptions instead of blindly deleting files.
|
||||||
|
- Review dependency and template compliance workflow failures as maintenance leads. Preserve project-specific conventions when they are documented.
|
||||||
|
- Treat generated credentials and config files as sensitive.
|
||||||
|
- Keep external network calls documented.
|
||||||
|
- Prefer local processing for user data.
|
||||||
|
- Keep CI publishing secrets in repository or organization secrets, not in tracked files.
|
||||||
|
|
||||||
|
## Finish Checklist
|
||||||
|
|
||||||
|
- `git diff --check` passes.
|
||||||
|
- The cheapest reliable verification command has been run, or the reason it could not run is documented.
|
||||||
|
- README, changelog, security review, and release checklist are updated when the change touches release behavior.
|
||||||
|
- `docs/agent-handoff.md` is updated when work is interrupted, risky, or spans multiple sessions.
|
||||||
|
- Any pushed Gitea workflow has been polled to success or a concrete blocker has been reported.
|
||||||
8
CHANGELOG.md
Normal file
8
CHANGELOG.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project are documented here.
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- Added the Codex Agent Repository Kit baseline.
|
||||||
|
- Documented that the project currently has no selected implementation stack or runnable build commands.
|
||||||
41
CONTRIBUTING.md
Normal file
41
CONTRIBUTING.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
## Working Rules
|
||||||
|
|
||||||
|
- Keep changes scoped to the issue or user request.
|
||||||
|
- Prefer existing project patterns.
|
||||||
|
- Do not commit secrets, generated credentials, local `.env` files, or private keys.
|
||||||
|
- Do not create releases unless explicitly requested.
|
||||||
|
- Preserve unrelated user changes.
|
||||||
|
|
||||||
|
## Before Committing
|
||||||
|
|
||||||
|
Run the cheapest reliable verification commands for this project.
|
||||||
|
|
||||||
|
Current available check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Once a real stack exists, add and document lint, test, build, and audit commands.
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
|
Pull requests should include:
|
||||||
|
|
||||||
|
- summary of changes,
|
||||||
|
- verification performed,
|
||||||
|
- known risks or skipped checks,
|
||||||
|
- artifact or download notes when relevant.
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
Before release work, update:
|
||||||
|
|
||||||
|
```text
|
||||||
|
CHANGELOG.md
|
||||||
|
docs/release-checklist.md
|
||||||
|
docs/security-review.md
|
||||||
|
README.md
|
||||||
|
```
|
||||||
119
INSTALL.md
Normal file
119
INSTALL.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# Installation
|
||||||
|
|
||||||
|
This guide explains how to install and run the League of Legends GUI Overhaul prototype locally.
|
||||||
|
|
||||||
|
The project is a React/Vite/TypeScript app. It does not install a game client, connect to Riot services, or include official Riot assets.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Node.js with npm available in your terminal.
|
||||||
|
- Recommended npm version: 10 or newer.
|
||||||
|
- A local clone of this repository.
|
||||||
|
|
||||||
|
Check your tools:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install Dependencies
|
||||||
|
|
||||||
|
Open a terminal in this project folder:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd D:\Codex\Main\CodexTest\projects\league-of-legends-gui-overhaul
|
||||||
|
```
|
||||||
|
|
||||||
|
Install dependencies:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates `node_modules/` locally. The folder is ignored by Git.
|
||||||
|
|
||||||
|
## Start The GUI
|
||||||
|
|
||||||
|
Start the local development server:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Vite prints a local URL, usually:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
Open that URL in your browser.
|
||||||
|
|
||||||
|
## Verify The Installation
|
||||||
|
|
||||||
|
Run the production build:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the test suite:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional dependency audit:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm audit --audit-level=moderate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Preview A Production Build
|
||||||
|
|
||||||
|
After `npm run build`, preview the built app:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Problems
|
||||||
|
|
||||||
|
### `npm` is not recognized
|
||||||
|
|
||||||
|
Install Node.js from the official Node.js distribution or add an existing Node/npm installation to your `PATH`.
|
||||||
|
|
||||||
|
After updating `PATH`, open a new terminal and check again:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm --version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port 5173 is already in use
|
||||||
|
|
||||||
|
Start Vite on another port:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run dev -- --port 5174
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependencies changed
|
||||||
|
|
||||||
|
If `package-lock.json` changed or dependencies were updated, reinstall:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## What This Installs
|
||||||
|
|
||||||
|
The install step only installs frontend development dependencies for this prototype:
|
||||||
|
|
||||||
|
- React
|
||||||
|
- Vite
|
||||||
|
- TypeScript
|
||||||
|
- React Router
|
||||||
|
- Vitest
|
||||||
|
- Testing Library
|
||||||
|
|
||||||
|
It does not install any Riot software, game files, launchers, or protected assets.
|
||||||
125
README.md
Normal file
125
README.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# League of Legends GUI Overhaul
|
||||||
|
|
||||||
|
A React/Vite prototype for a modern, dark, MOBA-/fantasy-inspired client interface. The goal of iteration 1 is a stable base GUI: routing, layout, reusable components, mock data, centralized theme tokens, tests, and documentation.
|
||||||
|
|
||||||
|
This project does not use Riot assets and is not a 1:1 clone of the official League of Legends client.
|
||||||
|
|
||||||
|
Repository:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://git.wilkensxl.de/Toxic/league-of-legends-gui-overhaul.git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- React
|
||||||
|
- Vite
|
||||||
|
- TypeScript
|
||||||
|
- React Router
|
||||||
|
- Vitest
|
||||||
|
- Testing Library
|
||||||
|
- CSS Custom Properties
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
For detailed installation steps, see [INSTALL.md](INSTALL.md).
|
||||||
|
|
||||||
|
Install dependencies:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the dev server:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Build:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/
|
||||||
|
|-- app/
|
||||||
|
|-- components/
|
||||||
|
| `-- ui/
|
||||||
|
|-- config/
|
||||||
|
|-- data/
|
||||||
|
|-- features/
|
||||||
|
|-- layouts/
|
||||||
|
|-- pages/
|
||||||
|
|-- styles/
|
||||||
|
|-- test/
|
||||||
|
`-- types/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extending The App
|
||||||
|
|
||||||
|
Add a page:
|
||||||
|
|
||||||
|
1. Create a component in `src/pages/`.
|
||||||
|
2. Register it in `src/config/routes.tsx`.
|
||||||
|
3. Add a navigation item in `src/config/navigation.ts` only if it should appear in the sidebar.
|
||||||
|
|
||||||
|
Add a navigation item:
|
||||||
|
|
||||||
|
1. Add an entry with `id`, `label`, `path`, and optional `iconName`, `section`, `enabled`.
|
||||||
|
2. Keep sidebar rendering generic; do not hardcode menu entries in `AppShell`.
|
||||||
|
|
||||||
|
Adjust theme tokens:
|
||||||
|
|
||||||
|
1. Edit `src/styles/theme.css`.
|
||||||
|
2. Add preset overrides with `[data-theme="preset-name"]`.
|
||||||
|
3. Prefer tokens over page-level hardcoded colors, spacing, shadows, or radii.
|
||||||
|
|
||||||
|
Extend mock data:
|
||||||
|
|
||||||
|
1. Add or edit records in `src/data/`.
|
||||||
|
2. Keep display logic in pages or feature modules, not inside mock files.
|
||||||
|
|
||||||
|
## Iteration 1 Includes
|
||||||
|
|
||||||
|
- App Shell with header, sidebar, content area, and status footer
|
||||||
|
- Routes for Home, Play, Champions, Profile, and Settings
|
||||||
|
- A mock champion detail route at `/champions/:championId`
|
||||||
|
- A right-side Social Sidebar powered by friend mock data
|
||||||
|
- Session-only theme presets from Settings
|
||||||
|
- Prototype Toast flows from header, settings, and social actions
|
||||||
|
- Base UI components: Button, Panel, SearchInput, Dropdown, Modal, Tabs, Tooltip, Toast
|
||||||
|
- Mock data for champions, friends, news, and match history
|
||||||
|
- Central CSS theme tokens
|
||||||
|
- Smoke tests for rendering, navigation, routing, and core UI components
|
||||||
|
|
||||||
|
## Iteration 1 Does Not Include
|
||||||
|
|
||||||
|
- Login
|
||||||
|
- Riot API integration
|
||||||
|
- Matchmaking
|
||||||
|
- Persistence
|
||||||
|
- Full champion detail content beyond the first placeholder route
|
||||||
|
- Plugin engine
|
||||||
|
- Final visual polish
|
||||||
|
- Official Riot or League assets
|
||||||
|
|
||||||
|
## Next Iterations
|
||||||
|
|
||||||
|
- Polish the App Shell visuals
|
||||||
|
- Add ability, skin, and progression modules to champion detail pages
|
||||||
|
- Expand the social sidebar with parties, invites, and richer presence
|
||||||
|
- Add persisted theme preferences
|
||||||
|
- Use Modal flows in more page-level actions
|
||||||
|
- Improve responsive behavior
|
||||||
|
- Add subtle transitions and animation states
|
||||||
|
- Plan a lightweight plugin/module extension layer
|
||||||
22
SECURITY.md
Normal file
22
SECURITY.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| --- | --- |
|
||||||
|
| Latest project state | Yes |
|
||||||
|
|
||||||
|
## Reporting A Vulnerability
|
||||||
|
|
||||||
|
Report security issues privately to the project owner.
|
||||||
|
|
||||||
|
Do not include secrets, production data, private credentials, or unreleased exploit details in public issues.
|
||||||
|
|
||||||
|
## Project Security Principles
|
||||||
|
|
||||||
|
- Keep secrets out of the repository.
|
||||||
|
- Do not commit `.env` files, tokens, certificates, private keys, or generated credentials.
|
||||||
|
- Prefer local processing for user data.
|
||||||
|
- Document external network calls when implementation begins.
|
||||||
|
- Keep release artifacts reproducible through CI once release artifacts exist.
|
||||||
|
- Run dependency audits before releases once dependencies exist.
|
||||||
38
docs/agent-handoff.md
Normal file
38
docs/agent-handoff.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Agent Handoff
|
||||||
|
|
||||||
|
Use this file when a task spans multiple sessions, has unresolved follow-up work, or changes release behavior.
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
```text
|
||||||
|
Codex Agent Repository Kit baseline applied. The project is a React/Vite/TypeScript prototype with npm scripts for development, build, and tests.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
- Added agent instructions and project notes.
|
||||||
|
- Added security, contribution, changelog, release checklist, security review, and release notes documents.
|
||||||
|
- Added non-destructive Gitea maintenance workflows.
|
||||||
|
- Project remote set to `https://git.wilkensxl.de/Toxic/league-of-legends-gui-overhaul.git`.
|
||||||
|
- Documented current npm build and test commands.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
| --- | --- |
|
||||||
|
| `git diff --check` | PENDING |
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- Separate lint, audit, and release-check scripts.
|
||||||
|
- Release packaging and download target.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Add separate lint, audit, and release-check scripts if needed.
|
||||||
|
- Add or update build workflow around `npm run build` and `npm test`.
|
||||||
|
- Update README, `AGENTS.md`, `.codex/project.md`, and release docs when implementation begins.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Release packaging is intentionally limited until download targets are defined.
|
||||||
38
docs/release-checklist.md
Normal file
38
docs/release-checklist.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Release Checklist
|
||||||
|
|
||||||
|
No release process exists yet. Complete this checklist only after the project has real build, test, audit, and artifact commands.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
- [ ] Version number updated.
|
||||||
|
- [ ] Changelog updated.
|
||||||
|
- [ ] README updated.
|
||||||
|
|
||||||
|
## Quality
|
||||||
|
|
||||||
|
- [ ] Working tree is clean.
|
||||||
|
- [ ] Lint or type checks pass.
|
||||||
|
- [ ] Tests pass or missing tests are documented.
|
||||||
|
- [ ] Build succeeds in CI.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- [ ] Security review is current.
|
||||||
|
- [ ] Dependency audit is clean or documented.
|
||||||
|
- [ ] No secrets are committed.
|
||||||
|
- [ ] Release artifacts do not contain local config files.
|
||||||
|
|
||||||
|
## Artifacts
|
||||||
|
|
||||||
|
- [ ] Artifacts are produced by documented commands.
|
||||||
|
- [ ] Artifacts are uploaded.
|
||||||
|
- [ ] Download links work.
|
||||||
|
- [ ] Package registry links work if used.
|
||||||
|
- [ ] Installer, portable, or archive naming is clear.
|
||||||
|
|
||||||
|
## Release
|
||||||
|
|
||||||
|
- [ ] Git tag created.
|
||||||
|
- [ ] Release notes written.
|
||||||
|
- [ ] Release published.
|
||||||
|
- [ ] Post-release download smoke test completed.
|
||||||
25
docs/release-notes.md
Normal file
25
docs/release-notes.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# League of Legends GUI Overhaul PENDING
|
||||||
|
|
||||||
|
## Downloads
|
||||||
|
|
||||||
|
No release artifacts exist yet.
|
||||||
|
|
||||||
|
## Highlights
|
||||||
|
|
||||||
|
- Codex Agent Repository Kit baseline applied.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- Dependency audit: pending until dependencies exist.
|
||||||
|
- Secret handling: secrets must not be committed.
|
||||||
|
- External network calls: pending until implementation exists.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
| --- | --- |
|
||||||
|
| `git diff --check` | PENDING |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Release notes are a placeholder until the project has a releasable artifact.
|
||||||
54
docs/security-review.md
Normal file
54
docs/security-review.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Security Review
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Project:
|
||||||
|
|
||||||
|
```text
|
||||||
|
League of Legends GUI Overhaul
|
||||||
|
```
|
||||||
|
|
||||||
|
Reviewed version or commit:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PENDING
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Patterns Checked
|
||||||
|
|
||||||
|
- [ ] No `eval`.
|
||||||
|
- [ ] No dynamic `Function` constructor.
|
||||||
|
- [ ] No unsafe HTML injection.
|
||||||
|
- [ ] No unexpected shell execution.
|
||||||
|
- [ ] No unexpected external network calls.
|
||||||
|
- [ ] No secrets committed.
|
||||||
|
- [ ] No unsafe file writes outside expected user-selected paths.
|
||||||
|
|
||||||
|
## Dependency Review
|
||||||
|
|
||||||
|
Command:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PENDING
|
||||||
|
```
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
```text
|
||||||
|
No dependency audit exists yet because no implementation stack or dependency manifest exists.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Runtime Review
|
||||||
|
|
||||||
|
- [ ] Least-privilege runtime configuration.
|
||||||
|
- [ ] External URLs documented.
|
||||||
|
- [ ] Local data storage documented.
|
||||||
|
- [ ] Sensitive data is not persisted unless explicitly required.
|
||||||
|
|
||||||
|
## Release Notes
|
||||||
|
|
||||||
|
Known residual risks:
|
||||||
|
|
||||||
|
```text
|
||||||
|
The implementation stack, runtime behavior, and artifact process are not defined yet.
|
||||||
|
```
|
||||||
12
index.html
Normal file
12
index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>League GUI Overhaul</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2534
package-lock.json
generated
Normal file
2534
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
Normal file
29
package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "league-of-legends-gui-overhaul",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc --noEmit && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"test": "vitest run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^6.30.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
"@testing-library/react": "^16.1.0",
|
||||||
|
"@testing-library/user-event": "^14.5.2",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
|
"jsdom": "^25.0.1",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"vite": "^8.0.11",
|
||||||
|
"vitest": "^4.1.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/app/App.test.tsx
Normal file
78
src/app/App.test.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { screen, within } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { renderAppAt } from "../test/renderWithRouter";
|
||||||
|
|
||||||
|
describe("App shell", () => {
|
||||||
|
it("renders the main layout and navigation", () => {
|
||||||
|
renderAppAt("/");
|
||||||
|
|
||||||
|
const navigation = screen.getByRole("navigation", { name: /primary navigation/i });
|
||||||
|
|
||||||
|
expect(screen.getByText("Nexus Overhaul")).toBeInTheDocument();
|
||||||
|
expect(navigation).toBeInTheDocument();
|
||||||
|
expect(within(navigation).getByRole("link", { name: /home/i })).toBeInTheDocument();
|
||||||
|
expect(within(navigation).getByRole("link", { name: /play/i })).toBeInTheDocument();
|
||||||
|
expect(within(navigation).getByRole("link", { name: /champions/i })).toBeInTheDocument();
|
||||||
|
expect(within(navigation).getByRole("link", { name: /profile/i })).toBeInTheDocument();
|
||||||
|
expect(within(navigation).getByRole("link", { name: /settings/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("complementary", { name: /friends and social status/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Status: local prototype/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Theme: dusk-gold/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["/", "Dashboard"],
|
||||||
|
["/play", "Queue prototype"],
|
||||||
|
["/champions", "Collection prototype"],
|
||||||
|
["/champions/nyra", "Combat Profile"],
|
||||||
|
["/profile", "Player profile"],
|
||||||
|
["/settings", "Configuration prototype"]
|
||||||
|
])("renders route %s", (path, expectedText) => {
|
||||||
|
renderAppAt(path);
|
||||||
|
|
||||||
|
expect(screen.getByText(expectedText)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("switches views through sidebar navigation", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderAppAt("/");
|
||||||
|
const navigation = screen.getByRole("navigation", { name: /primary navigation/i });
|
||||||
|
|
||||||
|
await user.click(within(navigation).getByRole("link", { name: /champions/i }));
|
||||||
|
|
||||||
|
expect(screen.getByRole("heading", { name: "Champions" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens a champion detail page from the champion grid", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderAppAt("/champions");
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("link", { name: /nyra/i }));
|
||||||
|
|
||||||
|
expect(screen.getByRole("heading", { name: "Nyra" })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Combat Profile")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies a theme preset from settings", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const { container } = renderAppAt("/settings");
|
||||||
|
|
||||||
|
await user.selectOptions(screen.getByLabelText(/theme preset/i), "arcane-teal");
|
||||||
|
|
||||||
|
expect(container.querySelector(".app-shell")).toHaveAttribute("data-theme", "arcane-teal");
|
||||||
|
expect(screen.getByText(/Theme: arcane-teal/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Theme preview applied")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a toast when sending a social invite", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderAppAt("/");
|
||||||
|
const socialSidebar = screen.getByRole("complementary", { name: /friends and social status/i });
|
||||||
|
|
||||||
|
await user.click(within(socialSidebar).getAllByRole("button", { name: /invite/i })[0]);
|
||||||
|
|
||||||
|
expect(screen.getByText("Invite sent")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/received a prototype party invite/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
16
src/app/App.tsx
Normal file
16
src/app/App.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import { AppProvider } from "./AppContext";
|
||||||
|
import { AppRoutes } from "./router";
|
||||||
|
import { AppShell } from "../layouts/AppShell";
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
return (
|
||||||
|
<AppProvider>
|
||||||
|
<BrowserRouter future={{ v7_relativeSplatPath: true, v7_startTransition: true }}>
|
||||||
|
<AppShell>
|
||||||
|
<AppRoutes />
|
||||||
|
</AppShell>
|
||||||
|
</BrowserRouter>
|
||||||
|
</AppProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
src/app/AppContext.tsx
Normal file
64
src/app/AppContext.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { createContext, useContext, useMemo, useState } from "react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import type { ToastVariant } from "../components/ui";
|
||||||
|
|
||||||
|
export type ThemePreset = "dusk-gold" | "arcane-teal" | "obsidian";
|
||||||
|
|
||||||
|
export type AppNotification = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
variant?: ToastVariant;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AppContextValue = {
|
||||||
|
theme: ThemePreset;
|
||||||
|
setTheme: (theme: ThemePreset) => void;
|
||||||
|
notifications: AppNotification[];
|
||||||
|
notify: (notification: Omit<AppNotification, "id">) => void;
|
||||||
|
dismissNotification: (id: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppContext = createContext<AppContextValue | undefined>(undefined);
|
||||||
|
|
||||||
|
type AppProviderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AppProvider({ children }: AppProviderProps) {
|
||||||
|
const [theme, setTheme] = useState<ThemePreset>("dusk-gold");
|
||||||
|
const [notifications, setNotifications] = useState<AppNotification[]>([]);
|
||||||
|
|
||||||
|
const value = useMemo<AppContextValue>(
|
||||||
|
() => ({
|
||||||
|
theme,
|
||||||
|
setTheme,
|
||||||
|
notifications,
|
||||||
|
notify: (notification) => {
|
||||||
|
setNotifications((current) => [
|
||||||
|
...current.slice(-2),
|
||||||
|
{
|
||||||
|
...notification,
|
||||||
|
id: crypto.randomUUID()
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
dismissNotification: (id) => {
|
||||||
|
setNotifications((current) => current.filter((notification) => notification.id !== id));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[notifications, theme]
|
||||||
|
);
|
||||||
|
|
||||||
|
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAppContext() {
|
||||||
|
const context = useContext(AppContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useAppContext must be used inside AppProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
13
src/app/router.tsx
Normal file
13
src/app/router.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Navigate, Route, Routes } from "react-router-dom";
|
||||||
|
import { appRoutes } from "../config/routes";
|
||||||
|
|
||||||
|
export function AppRoutes() {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
{appRoutes.map((route) => (
|
||||||
|
<Route key={route.id} path={route.path} element={<route.component />} />
|
||||||
|
))}
|
||||||
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/components/ui/Button.tsx
Normal file
30
src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
||||||
|
|
||||||
|
type ButtonVariant = "primary" | "secondary" | "ghost";
|
||||||
|
type ButtonSize = "sm" | "md" | "lg";
|
||||||
|
|
||||||
|
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||||
|
children: ReactNode;
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
active?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Button({
|
||||||
|
children,
|
||||||
|
variant = "primary",
|
||||||
|
size = "md",
|
||||||
|
active = false,
|
||||||
|
className = "",
|
||||||
|
...props
|
||||||
|
}: ButtonProps) {
|
||||||
|
const classes = ["ui-button", `ui-button-${variant}`, `ui-button-${size}`, active ? "is-active" : "", className]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={classes} {...props}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
src/components/ui/Dropdown.tsx
Normal file
26
src/components/ui/Dropdown.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { SelectHTMLAttributes } from "react";
|
||||||
|
|
||||||
|
export type DropdownOption = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DropdownProps = SelectHTMLAttributes<HTMLSelectElement> & {
|
||||||
|
label: string;
|
||||||
|
options: DropdownOption[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Dropdown({ label, options, className = "", ...props }: DropdownProps) {
|
||||||
|
return (
|
||||||
|
<label className={["ui-field", className].filter(Boolean).join(" ")}>
|
||||||
|
<span>{label}</span>
|
||||||
|
<select className="ui-select" {...props}>
|
||||||
|
{options.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
src/components/ui/Modal.tsx
Normal file
29
src/components/ui/Modal.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { Button } from "./Button";
|
||||||
|
|
||||||
|
type ModalProps = {
|
||||||
|
open: boolean;
|
||||||
|
title: string;
|
||||||
|
children: ReactNode;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Modal({ open, title, children, onClose }: ModalProps) {
|
||||||
|
if (!open) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ui-modal-backdrop" role="presentation">
|
||||||
|
<section className="ui-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
||||||
|
<header className="ui-modal-header">
|
||||||
|
<h2 id="modal-title">{title}</h2>
|
||||||
|
<Button variant="ghost" size="sm" onClick={onClose} aria-label="Close modal">
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</header>
|
||||||
|
<div>{children}</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/components/ui/Panel.tsx
Normal file
21
src/components/ui/Panel.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { HTMLAttributes, ReactNode } from "react";
|
||||||
|
|
||||||
|
type PanelProps = HTMLAttributes<HTMLElement> & {
|
||||||
|
children: ReactNode;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Panel({ children, title, subtitle, className = "", ...props }: PanelProps) {
|
||||||
|
return (
|
||||||
|
<section className={["ui-panel", className].filter(Boolean).join(" ")} {...props}>
|
||||||
|
{(title || subtitle) && (
|
||||||
|
<header className="ui-panel-header">
|
||||||
|
{title && <h2>{title}</h2>}
|
||||||
|
{subtitle && <p>{subtitle}</p>}
|
||||||
|
</header>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
src/components/ui/SearchInput.tsx
Normal file
14
src/components/ui/SearchInput.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { InputHTMLAttributes } from "react";
|
||||||
|
|
||||||
|
type SearchInputProps = InputHTMLAttributes<HTMLInputElement> & {
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SearchInput({ label = "Search", className = "", ...props }: SearchInputProps) {
|
||||||
|
return (
|
||||||
|
<label className={["ui-field", className].filter(Boolean).join(" ")}>
|
||||||
|
<span>{label}</span>
|
||||||
|
<input className="ui-input" type="search" {...props} />
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
src/components/ui/Tabs.tsx
Normal file
42
src/components/ui/Tabs.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { Button } from "./Button";
|
||||||
|
|
||||||
|
export type TabItem = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
content: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TabsProps = {
|
||||||
|
tabs: TabItem[];
|
||||||
|
defaultTabId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Tabs({ tabs, defaultTabId }: TabsProps) {
|
||||||
|
const [activeId, setActiveId] = useState(defaultTabId ?? tabs[0]?.id);
|
||||||
|
const activeTab = tabs.find((tab) => tab.id === activeId) ?? tabs[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ui-tabs">
|
||||||
|
<div className="ui-tab-list" role="tablist">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<Button
|
||||||
|
key={tab.id}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
role="tab"
|
||||||
|
active={tab.id === activeTab.id}
|
||||||
|
aria-selected={tab.id === activeTab.id}
|
||||||
|
onClick={() => setActiveId(tab.id)}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="ui-tab-panel" role="tabpanel">
|
||||||
|
{activeTab.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/components/ui/Toast.tsx
Normal file
24
src/components/ui/Toast.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export type ToastVariant = "info" | "success" | "warning";
|
||||||
|
|
||||||
|
type ToastProps = {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
variant?: ToastVariant;
|
||||||
|
onDismiss?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Toast({ title, message, variant = "info", onDismiss }: ToastProps) {
|
||||||
|
return (
|
||||||
|
<aside className={`ui-toast ui-toast-${variant}`} role="status">
|
||||||
|
<div>
|
||||||
|
<strong>{title}</strong>
|
||||||
|
<span>{message}</span>
|
||||||
|
</div>
|
||||||
|
{onDismiss && (
|
||||||
|
<button className="toast-dismiss" type="button" onClick={onDismiss} aria-label={`Dismiss ${title}`}>
|
||||||
|
x
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
src/components/ui/Tooltip.tsx
Normal file
14
src/components/ui/Tooltip.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
type TooltipProps = {
|
||||||
|
content: string;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Tooltip({ content, children }: TooltipProps) {
|
||||||
|
return (
|
||||||
|
<span className="ui-tooltip" data-tooltip={content}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/components/ui/index.ts
Normal file
11
src/components/ui/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export { Button } from "./Button";
|
||||||
|
export { Dropdown } from "./Dropdown";
|
||||||
|
export type { DropdownOption } from "./Dropdown";
|
||||||
|
export { Modal } from "./Modal";
|
||||||
|
export { Panel } from "./Panel";
|
||||||
|
export { SearchInput } from "./SearchInput";
|
||||||
|
export { Tabs } from "./Tabs";
|
||||||
|
export type { TabItem } from "./Tabs";
|
||||||
|
export { Toast } from "./Toast";
|
||||||
|
export type { ToastVariant } from "./Toast";
|
||||||
|
export { Tooltip } from "./Tooltip";
|
||||||
44
src/components/ui/ui.test.tsx
Normal file
44
src/components/ui/ui.test.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { screen } from "@testing-library/react";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { Button, Dropdown, Panel, SearchInput } from ".";
|
||||||
|
import { renderWithRouter } from "../../test/renderWithRouter";
|
||||||
|
|
||||||
|
describe("base UI components", () => {
|
||||||
|
it("renders a button", () => {
|
||||||
|
renderWithRouter(<Button>Confirm</Button>);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: /confirm/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a panel with title and content", () => {
|
||||||
|
renderWithRouter(
|
||||||
|
<Panel title="Panel title">
|
||||||
|
<p>Panel body</p>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByRole("heading", { name: /panel title/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Panel body")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a search input", () => {
|
||||||
|
renderWithRouter(<SearchInput label="Champion search" placeholder="Search" />);
|
||||||
|
|
||||||
|
expect(screen.getByLabelText(/champion search/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a dropdown", () => {
|
||||||
|
renderWithRouter(
|
||||||
|
<Dropdown
|
||||||
|
label="Role"
|
||||||
|
options={[
|
||||||
|
{ label: "All roles", value: "all" },
|
||||||
|
{ label: "Duelist", value: "duelist" }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByLabelText(/role/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("option", { name: /duelist/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
44
src/config/navigation.ts
Normal file
44
src/config/navigation.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type { NavigationItem } from "../types/navigation";
|
||||||
|
|
||||||
|
export const navigationItems: NavigationItem[] = [
|
||||||
|
{
|
||||||
|
id: "home",
|
||||||
|
label: "Home",
|
||||||
|
path: "/",
|
||||||
|
iconName: "H",
|
||||||
|
section: "primary",
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "play",
|
||||||
|
label: "Play",
|
||||||
|
path: "/play",
|
||||||
|
iconName: "P",
|
||||||
|
section: "primary",
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "champions",
|
||||||
|
label: "Champions",
|
||||||
|
path: "/champions",
|
||||||
|
iconName: "C",
|
||||||
|
section: "collection",
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "profile",
|
||||||
|
label: "Profile",
|
||||||
|
path: "/profile",
|
||||||
|
iconName: "R",
|
||||||
|
section: "account",
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "settings",
|
||||||
|
label: "Settings",
|
||||||
|
path: "/settings",
|
||||||
|
iconName: "S",
|
||||||
|
section: "system",
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
];
|
||||||
49
src/config/routes.tsx
Normal file
49
src/config/routes.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { ComponentType } from "react";
|
||||||
|
import { ChampionDetailPage } from "../pages/ChampionDetailPage";
|
||||||
|
import { ChampionsPage } from "../pages/ChampionsPage";
|
||||||
|
import { HomePage } from "../pages/HomePage";
|
||||||
|
import { PlayPage } from "../pages/PlayPage";
|
||||||
|
import { ProfilePage } from "../pages/ProfilePage";
|
||||||
|
import { SettingsPage } from "../pages/SettingsPage";
|
||||||
|
import type { AppRoute } from "../types/navigation";
|
||||||
|
|
||||||
|
type RouteComponent = ComponentType;
|
||||||
|
|
||||||
|
export const appRoutes: Array<AppRoute & { component: RouteComponent }> = [
|
||||||
|
{
|
||||||
|
id: "home",
|
||||||
|
title: "Home",
|
||||||
|
path: "/",
|
||||||
|
component: HomePage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "play",
|
||||||
|
title: "Play",
|
||||||
|
path: "/play",
|
||||||
|
component: PlayPage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "champions",
|
||||||
|
title: "Champions",
|
||||||
|
path: "/champions",
|
||||||
|
component: ChampionsPage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "champion-detail",
|
||||||
|
title: "Champion Detail",
|
||||||
|
path: "/champions/:championId",
|
||||||
|
component: ChampionDetailPage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "profile",
|
||||||
|
title: "Profile",
|
||||||
|
path: "/profile",
|
||||||
|
component: ProfilePage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "settings",
|
||||||
|
title: "Settings",
|
||||||
|
path: "/settings",
|
||||||
|
component: SettingsPage
|
||||||
|
}
|
||||||
|
];
|
||||||
40
src/data/champions.mock.ts
Normal file
40
src/data/champions.mock.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { Champion } from "../types/domain";
|
||||||
|
|
||||||
|
export const champions: Champion[] = [
|
||||||
|
{
|
||||||
|
id: "nyra",
|
||||||
|
name: "Nyra",
|
||||||
|
role: "Invoker",
|
||||||
|
difficulty: "Medium",
|
||||||
|
tags: ["arcane", "control", "mid"],
|
||||||
|
shortDescription: "A stormbound tactician who shapes fights with precise zones.",
|
||||||
|
accentColor: "#36d7d0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "kael",
|
||||||
|
name: "Kael",
|
||||||
|
role: "Duelist",
|
||||||
|
difficulty: "High",
|
||||||
|
tags: ["melee", "burst", "solo"],
|
||||||
|
shortDescription: "A blade-focused skirmisher built around timing and pressure.",
|
||||||
|
accentColor: "#c89b3c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "maera",
|
||||||
|
name: "Maera",
|
||||||
|
role: "Guardian",
|
||||||
|
difficulty: "Low",
|
||||||
|
tags: ["support", "shield", "teamfight"],
|
||||||
|
shortDescription: "A steady frontliner who protects allies and anchors engages.",
|
||||||
|
accentColor: "#5b8cff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "oren",
|
||||||
|
name: "Oren",
|
||||||
|
role: "Striker",
|
||||||
|
difficulty: "Medium",
|
||||||
|
tags: ["ranged", "tempo", "objective"],
|
||||||
|
shortDescription: "A marksman prototype focused on objective windows and spacing.",
|
||||||
|
accentColor: "#e56b6f"
|
||||||
|
}
|
||||||
|
];
|
||||||
25
src/data/friends.mock.ts
Normal file
25
src/data/friends.mock.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { Friend } from "../types/domain";
|
||||||
|
|
||||||
|
export const friends: Friend[] = [
|
||||||
|
{
|
||||||
|
id: "friend-1",
|
||||||
|
displayName: "Astra",
|
||||||
|
status: "online",
|
||||||
|
activity: "Browsing champions",
|
||||||
|
level: 42
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "friend-2",
|
||||||
|
displayName: "Vey",
|
||||||
|
status: "in-game",
|
||||||
|
activity: "Draft lobby",
|
||||||
|
level: 57
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "friend-3",
|
||||||
|
displayName: "Solin",
|
||||||
|
status: "away",
|
||||||
|
activity: "Idle",
|
||||||
|
level: 31
|
||||||
|
}
|
||||||
|
];
|
||||||
22
src/data/matchHistory.mock.ts
Normal file
22
src/data/matchHistory.mock.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { MatchHistoryEntry } from "../types/domain";
|
||||||
|
|
||||||
|
export const matchHistory: MatchHistoryEntry[] = [
|
||||||
|
{
|
||||||
|
id: "match-1",
|
||||||
|
championName: "Nyra",
|
||||||
|
mode: "Summoner Rift",
|
||||||
|
result: "Victory",
|
||||||
|
kda: "8 / 2 / 11",
|
||||||
|
duration: "31m 12s",
|
||||||
|
date: "Today"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "match-2",
|
||||||
|
championName: "Maera",
|
||||||
|
mode: "ARAM",
|
||||||
|
result: "Defeat",
|
||||||
|
kda: "2 / 6 / 19",
|
||||||
|
duration: "18m 44s",
|
||||||
|
date: "Yesterday"
|
||||||
|
}
|
||||||
|
];
|
||||||
18
src/data/news.mock.ts
Normal file
18
src/data/news.mock.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { NewsItem } from "../types/domain";
|
||||||
|
|
||||||
|
export const newsItems: NewsItem[] = [
|
||||||
|
{
|
||||||
|
id: "news-1",
|
||||||
|
title: "Base Shell Prototype Ready",
|
||||||
|
category: "Development",
|
||||||
|
date: "2026-05-10",
|
||||||
|
summary: "The first interface foundation focuses on layout, routing, reusable UI, and mock data."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "news-2",
|
||||||
|
title: "Champion Grid Planning",
|
||||||
|
category: "Collection",
|
||||||
|
date: "2026-05-10",
|
||||||
|
summary: "The champion overview starts with searchable mock cards and can later gain filters."
|
||||||
|
}
|
||||||
|
];
|
||||||
9
src/features/champions/championLookup.ts
Normal file
9
src/features/champions/championLookup.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { champions } from "../../data/champions.mock";
|
||||||
|
|
||||||
|
export function getChampionById(championId: string | undefined) {
|
||||||
|
if (!championId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return champions.find((champion) => champion.id === championId);
|
||||||
|
}
|
||||||
15
src/features/champions/filterChampions.ts
Normal file
15
src/features/champions/filterChampions.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Champion } from "../../types/domain";
|
||||||
|
|
||||||
|
export function filterChampions(champions: Champion[], query: string, role: string): Champion[] {
|
||||||
|
const normalizedQuery = query.trim().toLowerCase();
|
||||||
|
|
||||||
|
return champions.filter((champion) => {
|
||||||
|
const matchesRole = role === "all" || champion.role === role;
|
||||||
|
const matchesQuery =
|
||||||
|
normalizedQuery.length === 0 ||
|
||||||
|
champion.name.toLowerCase().includes(normalizedQuery) ||
|
||||||
|
champion.tags.some((tag) => tag.includes(normalizedQuery));
|
||||||
|
|
||||||
|
return matchesRole && matchesQuery;
|
||||||
|
});
|
||||||
|
}
|
||||||
6
src/features/profile/profileSummary.ts
Normal file
6
src/features/profile/profileSummary.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export const profileSummary = {
|
||||||
|
displayName: "Toxic",
|
||||||
|
level: 42,
|
||||||
|
rank: "Gold prototype rank",
|
||||||
|
note: "No persistence in iteration 1."
|
||||||
|
};
|
||||||
44
src/features/social/SocialSidebar.tsx
Normal file
44
src/features/social/SocialSidebar.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { useAppContext } from "../../app/AppContext";
|
||||||
|
import { Button } from "../../components/ui";
|
||||||
|
import { friends } from "../../data/friends.mock";
|
||||||
|
import { getFriendStatusLabel } from "./friendStatus";
|
||||||
|
|
||||||
|
export function SocialSidebar() {
|
||||||
|
const { notify } = useAppContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="social-sidebar" aria-label="Friends and social status">
|
||||||
|
<header>
|
||||||
|
<span className="eyebrow">Social</span>
|
||||||
|
<h2>Friends</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="social-list">
|
||||||
|
{friends.map((friend) => (
|
||||||
|
<article key={friend.id} className="social-card">
|
||||||
|
<span className={`status-dot status-${friend.status}`} />
|
||||||
|
<div>
|
||||||
|
<h3>{friend.displayName}</h3>
|
||||||
|
<p>{getFriendStatusLabel(friend.status)}</p>
|
||||||
|
<span>{friend.activity}</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
disabled={friend.status === "offline"}
|
||||||
|
onClick={() =>
|
||||||
|
notify({
|
||||||
|
title: "Invite sent",
|
||||||
|
message: `${friend.displayName} received a prototype party invite.`,
|
||||||
|
variant: "success"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Invite
|
||||||
|
</Button>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/features/social/friendStatus.ts
Normal file
12
src/features/social/friendStatus.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { FriendStatus } from "../../types/domain";
|
||||||
|
|
||||||
|
export function getFriendStatusLabel(status: FriendStatus): string {
|
||||||
|
const labels: Record<FriendStatus, string> = {
|
||||||
|
online: "Online",
|
||||||
|
away: "Away",
|
||||||
|
"in-game": "In game",
|
||||||
|
offline: "Offline"
|
||||||
|
};
|
||||||
|
|
||||||
|
return labels[status];
|
||||||
|
}
|
||||||
106
src/layouts/AppShell.tsx
Normal file
106
src/layouts/AppShell.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
import { useAppContext } from "../app/AppContext";
|
||||||
|
import { navigationItems } from "../config/navigation";
|
||||||
|
import { Button } from "../components/ui/Button";
|
||||||
|
import { Toast } from "../components/ui/Toast";
|
||||||
|
import { SocialSidebar } from "../features/social/SocialSidebar";
|
||||||
|
|
||||||
|
type AppShellProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AppShell({ children }: AppShellProps) {
|
||||||
|
const enabledItems = navigationItems.filter((item) => item.enabled !== false);
|
||||||
|
const { dismissNotification, notifications, notify, theme } = useAppContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-shell" data-theme={theme}>
|
||||||
|
<aside className="sidebar" aria-label="Primary navigation">
|
||||||
|
<div className="brand">
|
||||||
|
<span className="brand-mark">N</span>
|
||||||
|
<div>
|
||||||
|
<strong>Nexus Overhaul</strong>
|
||||||
|
<span>Base GUI</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="nav-list" aria-label="Primary navigation">
|
||||||
|
{enabledItems.map((item) => (
|
||||||
|
<NavLink
|
||||||
|
key={item.id}
|
||||||
|
to={item.path}
|
||||||
|
end={item.path === "/"}
|
||||||
|
className={({ isActive }) => `nav-item${isActive ? " nav-item-active" : ""}`}
|
||||||
|
>
|
||||||
|
<span className="nav-icon" aria-hidden="true">
|
||||||
|
{item.iconName ?? item.label.charAt(0)}
|
||||||
|
</span>
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div className="app-frame">
|
||||||
|
<header className="topbar">
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">Iteration 1</span>
|
||||||
|
<h1>MOBA Interface Foundation</h1>
|
||||||
|
</div>
|
||||||
|
<div className="topbar-actions">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
notify({
|
||||||
|
title: "Patch notes",
|
||||||
|
message: "News and patch notes are still powered by mock data.",
|
||||||
|
variant: "info"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Patch Notes
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
notify({
|
||||||
|
title: "Session queued",
|
||||||
|
message: "This is a prototype action. No matchmaking is started.",
|
||||||
|
variant: "success"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Start Session
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="content-area">{children}</main>
|
||||||
|
|
||||||
|
<footer className="status-bar">
|
||||||
|
<span>Status: local prototype</span>
|
||||||
|
<span>Theme: {theme}</span>
|
||||||
|
<span>Modules: base shell</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SocialSidebar />
|
||||||
|
|
||||||
|
{notifications.length > 0 && (
|
||||||
|
<div className="toast-stack" aria-label="Notifications">
|
||||||
|
{notifications.map((notification) => (
|
||||||
|
<Toast
|
||||||
|
key={notification.id}
|
||||||
|
title={notification.title}
|
||||||
|
message={notification.message}
|
||||||
|
variant={notification.variant}
|
||||||
|
onDismiss={() => dismissNotification(notification.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/main.tsx
Normal file
11
src/main.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { App } from "./app/App";
|
||||||
|
import "./styles/theme.css";
|
||||||
|
import "./styles/global.css";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
67
src/pages/ChampionDetailPage.tsx
Normal file
67
src/pages/ChampionDetailPage.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import type { CSSProperties } from "react";
|
||||||
|
import { Link, useParams } from "react-router-dom";
|
||||||
|
import { Panel } from "../components/ui";
|
||||||
|
import { getChampionById } from "../features/champions/championLookup";
|
||||||
|
|
||||||
|
export function ChampionDetailPage() {
|
||||||
|
const { championId } = useParams();
|
||||||
|
const champion = getChampionById(championId);
|
||||||
|
|
||||||
|
if (!champion) {
|
||||||
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
|
<Panel title="Champion not found" subtitle="The requested mock champion does not exist.">
|
||||||
|
<Link className="text-link" to="/champions">
|
||||||
|
Return to champions
|
||||||
|
</Link>
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
|
<section className="champion-detail-hero">
|
||||||
|
<div className="champion-portrait" style={{ "--champion-accent": champion.accentColor } as CSSProperties}>
|
||||||
|
{champion.name.charAt(0)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">{champion.role}</span>
|
||||||
|
<h2>{champion.name}</h2>
|
||||||
|
<p>{champion.shortDescription}</p>
|
||||||
|
<div className="tag-row">
|
||||||
|
{champion.tags.map((tag) => (
|
||||||
|
<span key={tag}>{tag}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="dashboard-grid">
|
||||||
|
<Panel title="Combat Profile" subtitle="Placeholder values for future tuning">
|
||||||
|
<dl className="stat-list">
|
||||||
|
<div>
|
||||||
|
<dt>Difficulty</dt>
|
||||||
|
<dd>{champion.difficulty}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Primary Role</dt>
|
||||||
|
<dd>{champion.role}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Theme Hint</dt>
|
||||||
|
<dd>{champion.accentColor ?? "Default accent"}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel title="Future Detail Modules" subtitle="Prepared but not implemented">
|
||||||
|
<p>Abilities, skins, progression, lore, and build recommendations can be added as independent modules.</p>
|
||||||
|
<Link className="action-link action-link-inline" to="/champions">
|
||||||
|
Back to overview
|
||||||
|
</Link>
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
src/pages/ChampionsPage.tsx
Normal file
72
src/pages/ChampionsPage.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import type { CSSProperties } from "react";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { Dropdown, Panel, SearchInput } from "../components/ui";
|
||||||
|
import { champions } from "../data/champions.mock";
|
||||||
|
import { filterChampions } from "../features/champions/filterChampions";
|
||||||
|
|
||||||
|
const roleOptions = [
|
||||||
|
{ label: "All roles", value: "all" },
|
||||||
|
{ label: "Controller", value: "Controller" },
|
||||||
|
{ label: "Duelist", value: "Duelist" },
|
||||||
|
{ label: "Guardian", value: "Guardian" },
|
||||||
|
{ label: "Invoker", value: "Invoker" },
|
||||||
|
{ label: "Striker", value: "Striker" }
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ChampionsPage() {
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const [role, setRole] = useState("all");
|
||||||
|
|
||||||
|
const filteredChampions = useMemo(() => {
|
||||||
|
return filterChampions(champions, query, role);
|
||||||
|
}, [query, role]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
|
<section className="page-heading">
|
||||||
|
<span className="eyebrow">Collection prototype</span>
|
||||||
|
<h2>Champions</h2>
|
||||||
|
<p>Searchable mock champion cards with filtering hooks for later collection features.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="toolbar">
|
||||||
|
<SearchInput
|
||||||
|
label="Champion search"
|
||||||
|
placeholder="Search by name or tag"
|
||||||
|
value={query}
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
/>
|
||||||
|
<Dropdown label="Role" options={roleOptions} value={role} onChange={(event) => setRole(event.target.value)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="champion-grid">
|
||||||
|
{filteredChampions.map((champion) => (
|
||||||
|
<Link key={champion.id} to={`/champions/${champion.id}`} className="champion-card">
|
||||||
|
<div className="champion-sigil" style={{ "--champion-accent": champion.accentColor } as CSSProperties}>
|
||||||
|
{champion.name.charAt(0)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="champion-card-header">
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">{champion.role}</span>
|
||||||
|
<h3>{champion.name}</h3>
|
||||||
|
</div>
|
||||||
|
<span className={`difficulty-badge difficulty-${champion.difficulty.toLowerCase()}`}>
|
||||||
|
{champion.difficulty}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p>{champion.shortDescription}</p>
|
||||||
|
<div className="tag-row">
|
||||||
|
{champion.tags.map((tag) => (
|
||||||
|
<span key={tag}>{tag}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className="card-action">View details</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
68
src/pages/HomePage.tsx
Normal file
68
src/pages/HomePage.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { newsItems } from "../data/news.mock";
|
||||||
|
import { Button, Panel, Tabs, Toast } from "../components/ui";
|
||||||
|
|
||||||
|
export function HomePage() {
|
||||||
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
|
<section className="hero-panel">
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">Dashboard</span>
|
||||||
|
<h2>Build the foundation before the spectacle.</h2>
|
||||||
|
<p>
|
||||||
|
A modular shell for future MOBA-inspired views, themes, social panels, and champion experiences.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="hero-actions">
|
||||||
|
<Link className="action-link" to="/play">
|
||||||
|
Play Prototype
|
||||||
|
</Link>
|
||||||
|
<Link className="text-link" to="/champions">
|
||||||
|
Browse champions
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="dashboard-grid">
|
||||||
|
<Panel title="Quick Actions" subtitle="Entry points for iteration 1">
|
||||||
|
<div className="button-row">
|
||||||
|
<Button>Open Play</Button>
|
||||||
|
<Button variant="secondary">Review Collection</Button>
|
||||||
|
<Button variant="ghost">Read Notes</Button>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel title="Patch Notes" subtitle="Mock news from separated data">
|
||||||
|
<div className="list-stack">
|
||||||
|
{newsItems.map((item) => (
|
||||||
|
<article key={item.id} className="compact-card">
|
||||||
|
<span className="eyebrow">{item.category}</span>
|
||||||
|
<h3>{item.title}</h3>
|
||||||
|
<p>{item.summary}</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel title="System Status" subtitle="Local prototype state">
|
||||||
|
<Tabs
|
||||||
|
tabs={[
|
||||||
|
{
|
||||||
|
id: "layout",
|
||||||
|
label: "Layout",
|
||||||
|
content: <p>Shell, navigation, status bar, and content regions are wired.</p>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "data",
|
||||||
|
label: "Data",
|
||||||
|
content: <p>Mock data is isolated from page and UI components.</p>
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Toast title="Base GUI" message="Iteration 1 focuses on structure and testability." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
src/pages/PlayPage.tsx
Normal file
49
src/pages/PlayPage.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Button, Modal, Panel, Tooltip } from "../components/ui";
|
||||||
|
|
||||||
|
const playModes = [
|
||||||
|
{
|
||||||
|
id: "rift",
|
||||||
|
name: "Summoner Rift",
|
||||||
|
description: "Structured 5v5 queue placeholder for the main arena experience."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "aram",
|
||||||
|
name: "ARAM",
|
||||||
|
description: "Compact teamfight mode placeholder for faster sessions."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "custom",
|
||||||
|
name: "Custom",
|
||||||
|
description: "Private lobby placeholder for future party and settings flows."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function PlayPage() {
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
|
<section className="page-heading">
|
||||||
|
<span className="eyebrow">Queue prototype</span>
|
||||||
|
<h2>Play</h2>
|
||||||
|
<p>Select a mock mode and open the lobby dialog. No matchmaking exists in iteration 1.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="mode-grid">
|
||||||
|
{playModes.map((mode) => (
|
||||||
|
<Panel key={mode.id} title={mode.name} subtitle="Mode placeholder">
|
||||||
|
<p>{mode.description}</p>
|
||||||
|
<Tooltip content="Opens a non-persistent lobby preview">
|
||||||
|
<Button onClick={() => setModalOpen(true)}>Create Lobby</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Panel>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal open={modalOpen} title="Lobby Preview" onClose={() => setModalOpen(false)}>
|
||||||
|
<p>This dialog proves that modal composition is available for later queue and party flows.</p>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
59
src/pages/ProfilePage.tsx
Normal file
59
src/pages/ProfilePage.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Panel } from "../components/ui";
|
||||||
|
import { friends } from "../data/friends.mock";
|
||||||
|
import { matchHistory } from "../data/matchHistory.mock";
|
||||||
|
import { profileSummary } from "../features/profile/profileSummary";
|
||||||
|
import { getFriendStatusLabel } from "../features/social/friendStatus";
|
||||||
|
|
||||||
|
export function ProfilePage() {
|
||||||
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
|
<section className="profile-header">
|
||||||
|
<div className="profile-avatar" aria-hidden="true">
|
||||||
|
T
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="eyebrow">Player profile</span>
|
||||||
|
<h2>{profileSummary.displayName}</h2>
|
||||||
|
<p>
|
||||||
|
Level {profileSummary.level} - {profileSummary.rank} - {profileSummary.note}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="dashboard-grid">
|
||||||
|
<Panel title="Match History" subtitle="Mock entries">
|
||||||
|
<div className="list-stack">
|
||||||
|
{matchHistory.map((match) => (
|
||||||
|
<article key={match.id} className="compact-card compact-card-row">
|
||||||
|
<div>
|
||||||
|
<h3>{match.championName}</h3>
|
||||||
|
<p>
|
||||||
|
{match.mode} - {match.duration} - {match.date}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<strong className={match.result === "Victory" ? "result-win" : "result-loss"}>{match.result}</strong>
|
||||||
|
<span>{match.kda}</span>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel title="Friends" subtitle="Social sidebar source data">
|
||||||
|
<div className="list-stack">
|
||||||
|
{friends.map((friend) => (
|
||||||
|
<article key={friend.id} className="friend-row">
|
||||||
|
<span className={`status-dot status-${friend.status}`} />
|
||||||
|
<div>
|
||||||
|
<h3>{friend.displayName}</h3>
|
||||||
|
<p>
|
||||||
|
{getFriendStatusLabel(friend.status)} - {friend.activity} {friend.level ? `- Level ${friend.level}` : ""}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
76
src/pages/SettingsPage.tsx
Normal file
76
src/pages/SettingsPage.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import type { ThemePreset } from "../app/AppContext";
|
||||||
|
import { useAppContext } from "../app/AppContext";
|
||||||
|
import { Button, Dropdown, Panel, SearchInput } from "../components/ui";
|
||||||
|
|
||||||
|
const densityOptions = [
|
||||||
|
{ label: "Comfortable", value: "comfortable" },
|
||||||
|
{ label: "Compact", value: "compact" },
|
||||||
|
{ label: "Dense", value: "dense" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const themeOptions = [
|
||||||
|
{ label: "Dusk Gold", value: "dusk-gold" },
|
||||||
|
{ label: "Arcane Teal", value: "arcane-teal" },
|
||||||
|
{ label: "Obsidian", value: "obsidian" }
|
||||||
|
];
|
||||||
|
|
||||||
|
export function SettingsPage() {
|
||||||
|
const { notify, setTheme, theme } = useAppContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
|
<section className="page-heading">
|
||||||
|
<span className="eyebrow">Configuration prototype</span>
|
||||||
|
<h2>Settings</h2>
|
||||||
|
<p>Demonstrates form controls for future theme and interface preferences.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Panel title="Interface" subtitle="Non-persistent controls for iteration 1">
|
||||||
|
<div className="settings-grid">
|
||||||
|
<Dropdown
|
||||||
|
label="Theme preset"
|
||||||
|
options={themeOptions}
|
||||||
|
value={theme}
|
||||||
|
onChange={(event) => {
|
||||||
|
const nextTheme = event.target.value as ThemePreset;
|
||||||
|
setTheme(nextTheme);
|
||||||
|
notify({
|
||||||
|
title: "Theme preview applied",
|
||||||
|
message: `${nextTheme} is active for this session.`,
|
||||||
|
variant: "success"
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Dropdown label="Layout density" options={densityOptions} defaultValue="comfortable" />
|
||||||
|
<SearchInput label="Search settings" placeholder="Find a future setting" />
|
||||||
|
</div>
|
||||||
|
<div className="button-row">
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
notify({
|
||||||
|
title: "Preview active",
|
||||||
|
message: "Settings are session-only until persistence is added.",
|
||||||
|
variant: "info"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Apply Preview
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setTheme("dusk-gold");
|
||||||
|
notify({
|
||||||
|
title: "Settings reset",
|
||||||
|
message: "Theme preview returned to dusk-gold.",
|
||||||
|
variant: "warning"
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
681
src/styles/global.css
Normal file
681
src/styles/global.css
Normal file
@@ -0,0 +1,681 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgb(54 215 208 / 0.14), transparent 28rem),
|
||||||
|
linear-gradient(135deg, #05070d 0%, var(--color-bg) 46%, #0b1220 100%);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 17rem minmax(0, 1fr) 18rem;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
border-right: 1px solid var(--color-border);
|
||||||
|
background: rgb(7 10 18 / 0.92);
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-sidebar {
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
background: linear-gradient(180deg, rgb(13 20 34 / 0.94), rgb(7 10 18 / 0.92));
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-sidebar h2 {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-list {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: rgb(17 27 45 / 0.76);
|
||||||
|
padding: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-card h3 {
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-card span:not(.status-dot) {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand span:last-child {
|
||||||
|
display: block;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-mark,
|
||||||
|
.nav-icon,
|
||||||
|
.profile-avatar {
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid var(--color-border-strong);
|
||||||
|
background: linear-gradient(135deg, rgb(200 155 60 / 0.28), rgb(54 215 208 / 0.1));
|
||||||
|
color: var(--color-gold-bright);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-mark {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
min-height: 2.75rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 0 var(--space-3);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover,
|
||||||
|
.nav-item-active {
|
||||||
|
border-color: var(--color-border-strong);
|
||||||
|
background: rgb(200 155 60 / 0.1);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-frame {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar,
|
||||||
|
.status-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-4);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
padding: var(--space-4) var(--space-5);
|
||||||
|
background: rgb(13 20 34 / 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar h1 {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-actions,
|
||||||
|
.button-row,
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
min-width: 0;
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
border-bottom: 0;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-stack {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-heading,
|
||||||
|
.hero-panel,
|
||||||
|
.profile-header {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background: linear-gradient(135deg, rgb(22 36 58 / 0.95), rgb(13 20 34 / 0.88));
|
||||||
|
box-shadow: var(--shadow-panel);
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-panel {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-panel h2,
|
||||||
|
.page-heading h2,
|
||||||
|
.profile-header h2 {
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
color: var(--color-gold-bright);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-grid,
|
||||||
|
.mode-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.champion-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-panel,
|
||||||
|
.champion-card,
|
||||||
|
.compact-card,
|
||||||
|
.ui-toast {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: rgb(17 27 45 / 0.9);
|
||||||
|
box-shadow: var(--shadow-panel);
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-panel-header {
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-panel-header h2 {
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button,
|
||||||
|
.action-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
border: 1px solid var(--color-border-strong);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 0 var(--space-4);
|
||||||
|
color: var(--color-text);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform var(--transition-fast), border-color var(--transition-fast), background var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button:hover,
|
||||||
|
.action-link:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: var(--color-gold-bright);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button-primary,
|
||||||
|
.action-link {
|
||||||
|
background: linear-gradient(135deg, #9b752b, #312613);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button-secondary {
|
||||||
|
background: rgb(54 215 208 / 0.12);
|
||||||
|
border-color: rgb(54 215 208 / 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button-ghost {
|
||||||
|
background: transparent;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button-sm {
|
||||||
|
min-height: 2rem;
|
||||||
|
padding: 0 var(--space-3);
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button-lg {
|
||||||
|
min-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button.is-active {
|
||||||
|
border-color: var(--color-teal);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-field {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-2);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-input,
|
||||||
|
.ui-select {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 2.75rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--color-bg-elevated);
|
||||||
|
color: var(--color-text);
|
||||||
|
padding: 0 var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar,
|
||||||
|
.settings-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 14rem;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-grid {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-stack {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-card h3,
|
||||||
|
.friend-row h3,
|
||||||
|
.champion-card h3 {
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-card-row,
|
||||||
|
.friend-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.champion-card {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-4);
|
||||||
|
transition: border-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.champion-card:hover {
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
box-shadow: var(--shadow-glow), var(--shadow-panel);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.champion-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-badge,
|
||||||
|
.card-action {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-low {
|
||||||
|
border-color: var(--color-success);
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-medium {
|
||||||
|
border-color: var(--color-warning);
|
||||||
|
color: var(--color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-high {
|
||||||
|
border-color: var(--color-danger);
|
||||||
|
color: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
color: var(--color-gold-bright);
|
||||||
|
}
|
||||||
|
|
||||||
|
.champion-sigil {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid var(--champion-accent, var(--color-gold));
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: color-mix(in srgb, var(--champion-accent, var(--color-gold)) 20%, transparent);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-top: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-row span {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.champion-detail-hero {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
gap: var(--space-5);
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--color-border-strong);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, color-mix(in srgb, var(--champion-accent, var(--color-gold)) 24%, transparent), transparent 24rem),
|
||||||
|
linear-gradient(135deg, rgb(22 36 58 / 0.96), rgb(7 10 18 / 0.94));
|
||||||
|
box-shadow: var(--shadow-glow), var(--shadow-panel);
|
||||||
|
padding: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.champion-detail-hero h2 {
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.champion-portrait {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 8rem;
|
||||||
|
height: 8rem;
|
||||||
|
border: 1px solid var(--champion-accent, var(--color-gold));
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, color-mix(in srgb, var(--champion-accent, var(--color-gold)) 28%, transparent), transparent),
|
||||||
|
var(--color-surface-strong);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-list {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-list div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-4);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
padding-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-list dt {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-list dd {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-link-inline {
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar {
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 0.75rem;
|
||||||
|
height: 0.75rem;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-online,
|
||||||
|
.status-in-game {
|
||||||
|
background: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-away {
|
||||||
|
background: var(--color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-win {
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-loss {
|
||||||
|
color: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-link {
|
||||||
|
color: var(--color-teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab-list {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab-panel {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tooltip {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tooltip:hover::after {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: calc(100% + var(--space-2));
|
||||||
|
z-index: 10;
|
||||||
|
min-width: 12rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--color-bg-elevated);
|
||||||
|
color: var(--color-text);
|
||||||
|
content: attr(data-tooltip);
|
||||||
|
padding: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-toast {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--space-1);
|
||||||
|
border-color: rgb(54 215 208 / 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-toast div {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-stack {
|
||||||
|
position: fixed;
|
||||||
|
right: var(--space-5);
|
||||||
|
bottom: var(--space-5);
|
||||||
|
z-index: 20;
|
||||||
|
display: grid;
|
||||||
|
width: min(24rem, calc(100vw - 2rem));
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-dismiss {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-toast-success {
|
||||||
|
border-color: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-toast-warning {
|
||||||
|
border-color: var(--color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: rgb(0 0 0 / 0.62);
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-modal {
|
||||||
|
width: min(34rem, 100%);
|
||||||
|
border: 1px solid var(--color-border-strong);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background: var(--color-surface);
|
||||||
|
box-shadow: var(--shadow-glow), var(--shadow-panel);
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-4);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.app-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-sidebar {
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list,
|
||||||
|
.dashboard-grid,
|
||||||
|
.mode-grid,
|
||||||
|
.toolbar,
|
||||||
|
.settings-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-panel,
|
||||||
|
.champion-detail-hero,
|
||||||
|
.topbar,
|
||||||
|
.status-bar {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.champion-detail-hero {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/styles/theme.css
Normal file
60
src/styles/theme.css
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--color-bg: #070a12;
|
||||||
|
--color-bg-elevated: #0d1422;
|
||||||
|
--color-surface: #111b2d;
|
||||||
|
--color-surface-strong: #16243a;
|
||||||
|
--color-border: #2b3a52;
|
||||||
|
--color-border-strong: #6f5622;
|
||||||
|
--color-text: #f2f0e8;
|
||||||
|
--color-text-muted: #aeb8c8;
|
||||||
|
--color-gold: #c89b3c;
|
||||||
|
--color-gold-bright: #f0c66c;
|
||||||
|
--color-teal: #36d7d0;
|
||||||
|
--color-blue: #5b8cff;
|
||||||
|
--color-danger: #e56b6f;
|
||||||
|
--color-warning: #f0b35f;
|
||||||
|
--color-success: #61d394;
|
||||||
|
--space-1: 0.25rem;
|
||||||
|
--space-2: 0.5rem;
|
||||||
|
--space-3: 0.75rem;
|
||||||
|
--space-4: 1rem;
|
||||||
|
--space-5: 1.5rem;
|
||||||
|
--space-6: 2rem;
|
||||||
|
--radius-sm: 4px;
|
||||||
|
--radius-md: 8px;
|
||||||
|
--radius-lg: 12px;
|
||||||
|
--shadow-panel: 0 18px 48px rgb(0 0 0 / 0.32);
|
||||||
|
--shadow-glow: 0 0 28px rgb(54 215 208 / 0.14);
|
||||||
|
--font-body: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
--font-display: Georgia, "Times New Roman", serif;
|
||||||
|
--transition-fast: 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="arcane-teal"] {
|
||||||
|
--color-bg: #041216;
|
||||||
|
--color-bg-elevated: #092029;
|
||||||
|
--color-surface: #0c2a33;
|
||||||
|
--color-surface-strong: #123945;
|
||||||
|
--color-border: #1d5561;
|
||||||
|
--color-border-strong: #36d7d0;
|
||||||
|
--color-gold: #b7a46a;
|
||||||
|
--color-gold-bright: #ead58a;
|
||||||
|
--color-teal: #55f2e8;
|
||||||
|
--color-blue: #61a4ff;
|
||||||
|
--shadow-glow: 0 0 30px rgb(85 242 232 / 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="obsidian"] {
|
||||||
|
--color-bg: #050507;
|
||||||
|
--color-bg-elevated: #0d0d12;
|
||||||
|
--color-surface: #15151c;
|
||||||
|
--color-surface-strong: #20202a;
|
||||||
|
--color-border: #343444;
|
||||||
|
--color-border-strong: #8d6d2e;
|
||||||
|
--color-gold: #b98a32;
|
||||||
|
--color-gold-bright: #e4bc65;
|
||||||
|
--color-teal: #6796a7;
|
||||||
|
--color-blue: #7f8fbf;
|
||||||
|
--shadow-glow: 0 0 24px rgb(185 138 50 / 0.14);
|
||||||
|
}
|
||||||
24
src/test/renderWithRouter.tsx
Normal file
24
src/test/renderWithRouter.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { MemoryRouter } from "react-router-dom";
|
||||||
|
import { render } from "@testing-library/react";
|
||||||
|
import { AppProvider } from "../app/AppContext";
|
||||||
|
import { AppRoutes } from "../app/router";
|
||||||
|
import { AppShell } from "../layouts/AppShell";
|
||||||
|
|
||||||
|
export function renderAppAt(path = "/") {
|
||||||
|
return render(
|
||||||
|
<AppProvider>
|
||||||
|
<MemoryRouter initialEntries={[path]} future={{ v7_relativeSplatPath: true, v7_startTransition: true }}>
|
||||||
|
<AppShell>
|
||||||
|
<AppRoutes />
|
||||||
|
</AppShell>
|
||||||
|
</MemoryRouter>
|
||||||
|
</AppProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderWithRouter(children: ReactNode) {
|
||||||
|
return render(
|
||||||
|
<MemoryRouter future={{ v7_relativeSplatPath: true, v7_startTransition: true }}>{children}</MemoryRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/test/setup.ts
Normal file
1
src/test/setup.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
37
src/types/domain.ts
Normal file
37
src/types/domain.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
export type Champion = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role: "Controller" | "Duelist" | "Guardian" | "Invoker" | "Striker";
|
||||||
|
difficulty: "Low" | "Medium" | "High";
|
||||||
|
tags: string[];
|
||||||
|
shortDescription: string;
|
||||||
|
accentColor?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FriendStatus = "online" | "away" | "in-game" | "offline";
|
||||||
|
|
||||||
|
export type Friend = {
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
status: FriendStatus;
|
||||||
|
activity: string;
|
||||||
|
level?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NewsItem = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
category: string;
|
||||||
|
date: string;
|
||||||
|
summary: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MatchHistoryEntry = {
|
||||||
|
id: string;
|
||||||
|
championName: string;
|
||||||
|
mode: string;
|
||||||
|
result: "Victory" | "Defeat";
|
||||||
|
kda: string;
|
||||||
|
duration: string;
|
||||||
|
date: string;
|
||||||
|
};
|
||||||
16
src/types/navigation.ts
Normal file
16
src/types/navigation.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export type NavigationSection = "primary" | "collection" | "account" | "system";
|
||||||
|
|
||||||
|
export type NavigationItem = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
path: string;
|
||||||
|
iconName?: string;
|
||||||
|
section?: NavigationSection;
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AppRoute = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src", "vite.config.ts"]
|
||||||
|
}
|
||||||
13
vite.config.ts
Normal file
13
vite.config.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/// <reference types="vitest" />
|
||||||
|
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
environment: "jsdom",
|
||||||
|
globals: true,
|
||||||
|
setupFiles: "./src/test/setup.ts"
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user