From 2adea0ce07d817a7371359b6db07aa870ca19c5b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 8 Sep 2025 20:31:01 +0000 Subject: [PATCH 001/147] Refactor minion damage and improve UI feedback Co-authored-by: jwbram --- src/App.css | 1 + src/App.jsx | 63 +++++++++++++++---- src/components/Buttons.jsx | 3 +- src/components/Cards.jsx | 124 +++++++++++++++++++++++-------------- src/index.css | 3 + 5 files changed, 133 insertions(+), 61 deletions(-) diff --git a/src/App.css b/src/App.css index f455070..9537cd2 100644 --- a/src/App.css +++ b/src/App.css @@ -474,6 +474,7 @@ background: var(--primary); color: var(--text-primary); border-color: var(--primary); + font-weight: 600; /* Make filled symbols bolder to match visual weight */ } .unified-game-piece .countdown-symbol.empty { diff --git a/src/App.jsx b/src/App.jsx index a8f4f12..e4c5ecf 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -302,22 +302,59 @@ const AppContent = () => { // Adversary handlers const handleAdversaryDamage = (id, damage, currentHp, maxHp) => { - // In Daggerheart: HP = damage taken, so damage increases HP - const newHp = Math.min(currentHp + damage, maxHp) // Can't exceed max damage - console.log('Applying damage:', { id, damage, currentHp, maxHp, newHp }) + const targetAdversary = adversaries.find(adv => adv.id === id) + if (!targetAdversary) return - // Optimistic update - update local state immediately - const updatedAdversaries = adversaries.map(adv => - adv.id === id ? { ...adv, hp: newHp } : adv - ) + // Check if this is a minion + const isMinion = targetAdversary.type === 'Minion' + const minionFeature = targetAdversary.features?.find(f => f.name?.startsWith('Minion (')) + const minionThreshold = minionFeature ? parseInt(minionFeature.name.match(/\((\d+)\)/)?.[1] || '1') : 1 - // Update the selected item if it's the one being modified - if (selectedItem && selectedItem.id === id) { - setSelectedItem(prev => ({ ...prev, hp: newHp })) + if (isMinion) { + // Minion mechanics: any damage defeats the minion, and excess damage can defeat additional minions + console.log('Applying minion damage:', { id, damage, minionThreshold }) + + // First, defeat the target minion + deleteAdversary(id) + + // Calculate how many additional minions can be defeated + const additionalMinions = Math.floor(damage / minionThreshold) + console.log(`Damage ${damage} with threshold ${minionThreshold} can defeat ${additionalMinions} additional minions`) + + if (additionalMinions > 0) { + // Find other minions of the same type that can be defeated + const sameTypeMinions = adversaries.filter(adv => + adv.type === 'Minion' && + adv.id !== id && + adv.name === targetAdversary.name + ) + + // Defeat up to the calculated number of additional minions + const minionsToDefeat = Math.min(additionalMinions, sameTypeMinions.length) + console.log(`Defeating ${minionsToDefeat} additional minions of type ${targetAdversary.name}`) + + for (let i = 0; i < minionsToDefeat; i++) { + deleteAdversary(sameTypeMinions[i].id) + } + } + } else { + // Regular adversary damage mechanics + const newHp = Math.min(currentHp + damage, maxHp) // Can't exceed max damage + console.log('Applying damage:', { id, damage, currentHp, maxHp, newHp }) + + // Optimistic update - update local state immediately + const updatedAdversaries = adversaries.map(adv => + adv.id === id ? { ...adv, hp: newHp } : adv + ) + + // Update the selected item if it's the one being modified + if (selectedItem && selectedItem.id === id) { + setSelectedItem(prev => ({ ...prev, hp: newHp })) + } + + // Send update to server + updateAdversary(id, { hp: newHp }) } - - // Send update to server - updateAdversary(id, { hp: newHp }) } const handleAdversaryHealing = (id, healing, currentHp) => { diff --git a/src/components/Buttons.jsx b/src/components/Buttons.jsx index 50701e0..3036396 100644 --- a/src/components/Buttons.jsx +++ b/src/components/Buttons.jsx @@ -87,7 +87,8 @@ const Button = ({ // Otherwise, map actions to semantic button types if (action === 'delete' || action === 'remove' || action === 'bulk-clear') { // Delete buttons start neutral, turn red only during confirmation - variantClass = isConfirming ? 'btn-danger' : 'btn-secondary' + // Exception: immediate delete actions (like filled countdowns) are always red + variantClass = (isConfirming || immediate) ? 'btn-danger' : 'btn-secondary' } else if (action === 'close') { variantClass = 'btn-secondary' // Close actions (non-destructive) } else if (action === 'edit' || action === 'view' || action === 'filter' || action === 'increment' || action === 'decrement') { diff --git a/src/components/Cards.jsx b/src/components/Cards.jsx index 970c1b3..0fc3e6d 100644 --- a/src/components/Cards.jsx +++ b/src/components/Cards.jsx @@ -234,7 +234,7 @@ const Cards = ({ {/* Damage Input Popup */} - {showDamageInput && item.thresholds && item.thresholds.major && item.thresholds.severe && ( + {showDamageInput && ((item.thresholds && item.thresholds.major && item.thresholds.severe) || item.type === 'Minion') && (
{ @@ -256,16 +256,21 @@ const Cards = ({ if (e.key === 'Enter') { const damage = parseInt(damageValue) if (damage > 0) { - // Calculate HP damage based on damage thresholds - let hpDamage = 0 - if (damage >= item.thresholds.severe) { - hpDamage = 3 // Severe damage - } else if (damage >= item.thresholds.major) { - hpDamage = 2 // Major damage - } else if (damage >= 1) { - hpDamage = 1 // Minor damage + if (item.type === 'Minion') { + // For minions, apply the raw damage amount (minion mechanics handle the rest) + onApplyDamage && onApplyDamage(item.id, damage, item.hp, item.hpMax) + } else { + // Calculate HP damage based on damage thresholds for regular adversaries + let hpDamage = 0 + if (damage >= item.thresholds.severe) { + hpDamage = 3 // Severe damage + } else if (damage >= item.thresholds.major) { + hpDamage = 2 // Major damage + } else if (damage >= 1) { + hpDamage = 1 // Minor damage + } + onApplyDamage && onApplyDamage(item.id, hpDamage, item.hp, item.hpMax) } - onApplyDamage && onApplyDamage(item.id, hpDamage, item.hp, item.hpMax) setShowDamageInput(false) setDamageValue('') } @@ -277,50 +282,75 @@ const Cards = ({ autoFocus />
- {[1, 2, 3].map((level) => { - const damage = parseInt(damageValue) || 0 - let isActive = false - if (level === 1 && damage >= 1) isActive = true - if (level === 2 && damage >= item.thresholds.major) isActive = true - if (level === 3 && damage >= item.thresholds.severe) isActive = true - - return ( - { - e.stopPropagation() - // Set the input value to the threshold amount for this level - if (level === 1) { - setDamageValue('1') - } else if (level === 2) { - setDamageValue(item.thresholds.major.toString()) - } else if (level === 3) { - setDamageValue(item.thresholds.severe.toString()) - } - }} - title={`Click to set damage to ${level === 1 ? '1' : level === 2 ? item.thresholds.major : item.thresholds.severe}`} - > - - - ) - })} + {item.type === 'Minion' ? ( + // Minion damage indicator - show how many additional minions can be defeated + (() => { + const damage = parseInt(damageValue) || 0 + const minionFeature = item.features?.find(f => f.name?.startsWith('Minion (')) + const minionThreshold = minionFeature ? parseInt(minionFeature.name.match(/\((\d+)\)/)?.[1] || '1') : 1 + const additionalMinions = Math.floor(damage / minionThreshold) + + return ( + 0 ? 'active' : ''}`} + title={`${damage} damage can defeat ${additionalMinions + 1} minion${additionalMinions + 1 !== 1 ? 's' : ''} (1 + ${additionalMinions} additional)`} + > + {damage > 0 ? `+${additionalMinions}` : '0'} + + ) + })() + ) : ( + // Regular adversary damage indicators + [1, 2, 3].map((level) => { + const damage = parseInt(damageValue) || 0 + let isActive = false + if (level === 1 && damage >= 1) isActive = true + if (level === 2 && damage >= item.thresholds.major) isActive = true + if (level === 3 && damage >= item.thresholds.severe) isActive = true + + return ( + { + e.stopPropagation() + // Set the input value to the threshold amount for this level + if (level === 1) { + setDamageValue('1') + } else if (level === 2) { + setDamageValue(item.thresholds.major.toString()) + } else if (level === 3) { + setDamageValue(item.thresholds.severe.toString()) + } + }} + title={`Click to set damage to ${level === 1 ? '1' : level === 2 ? item.thresholds.major : item.thresholds.severe}`} + > + + + ) + }) + )} diff --git a/src/index.css b/src/index.css index a9f3710..10bc8cd 100644 --- a/src/index.css +++ b/src/index.css @@ -1819,10 +1819,12 @@ textarea, color: var(--purple); text-shadow: 0 0 4px rgba(59, 130, 246, 0.5); font-weight: 600; /* Make filled symbols bolder to match visual weight */ + font-size: 0.875rem; /* Ensure consistent font size */ } .countdown-symbol.empty { color: var(--grey); + font-size: 0.875rem; /* Ensure consistent font size */ } .countdown-symbol:hover { @@ -2727,11 +2729,13 @@ textarea, color: var(--purple); text-shadow: 0 0 4px rgba(59, 130, 246, 0.5); font-weight: 600; /* Make filled symbols bolder to match visual weight */ + font-size: 0.875rem; /* Ensure consistent font size */ } .simple-list-row.compact.countdown .countdown-symbol.empty { color: var(--text-secondary); opacity: 0.6; + font-size: 0.875rem; /* Ensure consistent font size */ } .simple-list-row.compact.countdown .countdown-symbol:hover { @@ -2772,11 +2776,13 @@ textarea, .simple-list-row.compact.adversary .hp-symbols .countdown-symbol.filled { color: var(--red); text-shadow: 0 0 4px rgba(220, 38, 38, 0.5); + font-size: 0.875rem; /* Ensure consistent font size */ } .simple-list-row.compact.adversary .stress-symbols .countdown-symbol.filled { color: #f59e0b; text-shadow: 0 0 4px rgba(245, 158, 11, 0.5); + font-size: 0.875rem; /* Ensure consistent font size */ } /* Overflow icon styling */ @@ -3975,6 +3981,33 @@ textarea, color: #dc2626; } +.damage-drop.minion-additional { + color: var(--text-primary); + background: var(--bg-secondary); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 500; + text-shadow: none; + border: 1px solid var(--border); + cursor: default; + font-variant-numeric: tabular-nums; +} + +.damage-drop.minion-placeholder { + color: var(--text-secondary); + background: var(--bg-secondary); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 500; + text-shadow: none; + border: 1px solid var(--border); + cursor: default; + opacity: 0.7; + font-variant-numeric: tabular-nums; +} + .damage-submit-btn { display: flex; align-items: center; @@ -4066,6 +4099,7 @@ textarea, .countdown-symbol.filled[data-countdown-type="simple-hope"] { color: #f59e0b !important; /* Gold for hope/progress */ text-shadow: 0 0 4px rgba(245, 158, 11, 0.5) !important; + font-size: 0.875rem !important; /* Ensure consistent font size */ } .countdown-symbol.filled[data-countdown-type="consequence"], @@ -4073,12 +4107,14 @@ textarea, .countdown-symbol.filled[data-countdown-type="simple-fear"] { color: var(--purple) !important; /* Purple for fear/consequence */ text-shadow: 0 0 4px rgba(59, 130, 246, 0.5) !important; + font-size: 0.875rem !important; /* Ensure consistent font size */ } .countdown-symbol.filled[data-countdown-type="long-term"], .countdown-symbol.filled[data-countdown-type="standard"] { color: var(--text-secondary) !important; /* Neutral for simple/long-term */ text-shadow: 0 0 4px rgba(163, 163, 163, 0.5) !important; /* Subtle grey glow */ + font-size: 0.875rem !important; /* Ensure consistent font size */ } /* Styling for countdowns at max value - color based on type */ @@ -5008,7 +5044,7 @@ textarea, } .countdown-symbol { - font-size: 1.25rem; + font-size: 0.875rem; line-height: 1; transition: all 0.2s ease; } @@ -5016,11 +5052,13 @@ textarea, .countdown-symbol.filled { color: var(--purple); font-weight: 600; /* Make filled symbols bolder to match visual weight */ + font-size: 0.875rem; /* Ensure consistent font size */ } .countdown-symbol.empty { color: var(--text-secondary); opacity: 0.6; + font-size: 0.875rem; /* Ensure consistent font size */ } .countdown-symbols-section { @@ -5537,11 +5575,13 @@ textarea, .countdown-symbol.filled { color: var(--purple); + font-size: 0.875rem; /* Ensure consistent font size */ } .countdown-symbol.empty { color: var(--text-secondary); opacity: 0.6; + font-size: 0.875rem; /* Ensure consistent font size */ } .description-badge { From 250e00bf5bc001f7688ef5295044b81345ffb5a5 Mon Sep 17 00:00:00 2001 From: Jackson Brammer Date: Mon, 8 Sep 2025 15:50:07 -0700 Subject: [PATCH 005/147] Re-enable GitHub Actions auto-deploy to GitHub Pages - Restored deploy.yml workflow from .disabled state - Auto-deploy will now trigger on every push to main branch - Uses peaceiris/actions-gh-pages@v3 for reliable deployment - Builds project and deploys dist/ folder to GitHub Pages --- .github/workflows/deploy.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..e5e6505 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,34 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + if: github.ref == 'refs/heads/main' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist From d87019cfc78c8113987bb5db64cbd34768a584bf Mon Sep 17 00:00:00 2001 From: Jackson Brammer Date: Mon, 8 Sep 2025 15:52:29 -0700 Subject: [PATCH 006/147] Revert to proper GitHub Actions deployment workflow - Using peaceiris/actions-gh-pages@v3 as intended - Need to configure GitHub Pages source to GitHub Actions --- .github/workflows/deploy.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e5e6505..3adb8f7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -27,8 +27,9 @@ jobs: run: npm run build - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 + run: | + npm install -g gh-pages + gh-pages -d dist if: github.ref == 'refs/heads/main' - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From e9c3c93d8f97903eab2e885d4c2517daa223030f Mon Sep 17 00:00:00 2001 From: Jackson Brammer Date: Mon, 8 Sep 2025 15:58:32 -0700 Subject: [PATCH 007/147] Try personal_token instead of github_token for Pages deployment --- .github/workflows/deploy.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3adb8f7..06201b8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -27,9 +27,8 @@ jobs: run: npm run build - name: Deploy to GitHub Pages - run: | - npm install -g gh-pages - gh-pages -d dist + uses: peaceiris/actions-gh-pages@v3 if: github.ref == 'refs/heads/main' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + personal_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist From b6e4a61327cdf8479e407efbcad483254627dddf Mon Sep 17 00:00:00 2001 From: Jackson Brammer Date: Mon, 8 Sep 2025 15:59:47 -0700 Subject: [PATCH 008/147] Use official GitHub Pages deployment workflow - Switched to actions/configure-pages and actions/deploy-pages - Uses proper permissions and environment setup - Modern approach that works directly with GitHub Pages - No need for gh-pages branch or external tokens --- .github/workflows/deploy.yml | 66 +++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 06201b8..0783c28 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,32 +3,50 @@ name: Deploy to GitHub Pages on: push: branches: [ main ] - pull_request: - branches: [ main ] + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false jobs: - build-and-deploy: + build: runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - cache: 'npm' + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build - - name: Install dependencies - run: npm ci - - - name: Build - run: npm run build - - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 - if: github.ref == 'refs/heads/main' - with: - personal_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./dist + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./dist + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 From a3d2c0ed1cf7431fb1c1e9c1f44117c0f4ba9557 Mon Sep 17 00:00:00 2001 From: Splinter714 <65372425+Splinter714@users.noreply.github.com> Date: Mon, 8 Sep 2025 16:16:17 -0700 Subject: [PATCH 009/147] Create static.yml --- .github/workflows/static.yml | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/static.yml diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000..f2c9e97 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,43 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: '.' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 From c935bc9988b87d193f19060ed90c984321009b14 Mon Sep 17 00:00:00 2001 From: Jackson Brammer Date: Mon, 8 Sep 2025 16:18:39 -0700 Subject: [PATCH 010/147] Simplify to use working npm run deploy method - Uses the same deployment method that works manually - Runs npm run deploy automatically on every push to main - Should work since manual deployment works perfectly --- .github/workflows/deploy.yml | 34 ++++------------------------------ 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0783c28..81f72da 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,17 +4,8 @@ on: push: branches: [ main ] -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: "pages" - cancel-in-progress: false - jobs: - build: + deploy: runs-on: ubuntu-latest steps: - name: Checkout @@ -29,24 +20,7 @@ jobs: - name: Install dependencies run: npm ci - - name: Build - run: npm run build - - - name: Setup Pages - uses: actions/configure-pages@v4 - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./dist - - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 + run: npm run deploy + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 3fca9627eeb3d54767c340aeff56b5a465e126f6 Mon Sep 17 00:00:00 2001 From: Jackson Brammer Date: Mon, 8 Sep 2025 16:23:04 -0700 Subject: [PATCH 011/147] Clean up auto-deployment files and update todo - Removed GitHub Actions workflow files (deploy.yml, static.yml) - Manual deployment is working perfectly - Updated TODO.md to reflect cleanup - No need for paid GitHub Actions when manual works great --- .github/workflows/deploy.yml | 26 ---------------- .github/workflows/deploy.yml.disabled | 34 --------------------- .github/workflows/static.yml | 43 --------------------------- TODO.md | 30 +++++++++++++++++++ 4 files changed, 30 insertions(+), 103 deletions(-) delete mode 100644 .github/workflows/deploy.yml delete mode 100644 .github/workflows/deploy.yml.disabled delete mode 100644 .github/workflows/static.yml create mode 100644 TODO.md diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 81f72da..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Deploy to GitHub Pages - -on: - push: - branches: [ main ] - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Deploy to GitHub Pages - run: npm run deploy - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/deploy.yml.disabled b/.github/workflows/deploy.yml.disabled deleted file mode 100644 index e5e6505..0000000 --- a/.github/workflows/deploy.yml.disabled +++ /dev/null @@ -1,34 +0,0 @@ -name: Deploy to GitHub Pages - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build - run: npm run build - - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 - if: github.ref == 'refs/heads/main' - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./dist diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml deleted file mode 100644 index f2c9e97..0000000 --- a/.github/workflows/static.yml +++ /dev/null @@ -1,43 +0,0 @@ -# Simple workflow for deploying static content to GitHub Pages -name: Deploy static content to Pages - -on: - # Runs on pushes targeting the default branch - push: - branches: ["main"] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - # Single deploy job since we're just deploying - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Pages - uses: actions/configure-pages@v5 - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - # Upload entire repository - path: '.' - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..346b585 --- /dev/null +++ b/TODO.md @@ -0,0 +1,30 @@ +# Daggerheart App - Todo List + +## 🚀 **Deployment** +- [x] Manual deployment working (`npm run deploy`) +- [x] GitHub Actions requires paid plan - sticking with manual +- [x] Manual deployment is sufficient for development +- [x] Cleaned up unnecessary auto-deployment workflow files + +## 🐛 **Issues to Address** +- [ ] Issue #1: Filled countdown pips smaller than empty (FIXED) +- [ ] Issue #2: Implement minion damage mechanics (REFINED) + +## 🎨 **UI/UX Improvements** +- [ ] Countdown trigger controls positioning +- [ ] Card layout consistency +- [ ] Color scheme refinements + +## 🔧 **Technical Debt** +- [ ] Code cleanup and optimization +- [ ] Component refactoring +- [ ] Performance improvements + +## 📝 **Notes** +- Manual deployment works perfectly +- GitHub Actions configured but blocked by billing +- Minion damage mechanics implemented and refined +- Countdown system fully functional + +--- +*Last updated: $(date)* From 02945a7d8f76f2e3b036739eba17e42e61c08089 Mon Sep 17 00:00:00 2001 From: Jackson Brammer Date: Tue, 9 Sep 2025 00:20:30 -0700 Subject: [PATCH 012/147] UI improvements: remove section dividers, fix alignment, add padding - Remove horizontal rules between count/env/adv sections - Add top padding to section headers for better spacing - Fix section title alignment to be left-aligned - Revert browser table changes that caused column visibility issues - Update TODO.md with completed tasks --- TODO.md | 45 ++-- src/App.css | 1 + src/components/Cards.jsx | 39 ++-- src/components/GameBoard.jsx | 261 +++++++++++----------- src/components/InlineCountdownCreator.jsx | 4 +- src/index.css | 201 ++++++++++++----- 6 files changed, 322 insertions(+), 229 deletions(-) diff --git a/TODO.md b/TODO.md index 346b585..a60c811 100644 --- a/TODO.md +++ b/TODO.md @@ -1,30 +1,31 @@ -# Daggerheart App - Todo List +# Todo List -## 🚀 **Deployment** -- [x] Manual deployment working (`npm run deploy`) -- [x] GitHub Actions requires paid plan - sticking with manual -- [x] Manual deployment is sufficient for development -- [x] Cleaned up unnecessary auto-deployment workflow files +- in mobile view when tapping an adv or env card and it opens the expanded view, we need some way to get back to the list, either by having the expanded view open in-line to begin with, OR by having a back button of sorts +- countdown trigger buttons - I want them in a single clean tight row in the countdown header, but a row below the countdown title and countdown add -## 🐛 **Issues to Address** -- [ ] Issue #1: Filled countdown pips smaller than empty (FIXED) -- [ ] Issue #2: Implement minion damage mechanics (REFINED) +# Completed Items -## 🎨 **UI/UX Improvements** -- [ ] Countdown trigger controls positioning -- [ ] Card layout consistency -- [ ] Color scheme refinements +## Deployment +- Manual deployment working (npm run deploy) +- GitHub Actions requires paid plan - sticking with manual +- Manual deployment is sufficient for development +- Cleaned up unnecessary auto-deployment workflow files +- Cleaned up old test branches -## 🔧 **Technical Debt** -- [ ] Code cleanup and optimization -- [ ] Component refactoring -- [ ] Performance improvements +## Issues Fixed +- Issue #1: Filled countdown pips smaller than empty - FIXED +- Issue #2: Implement minion damage mechanics - FIXED +- Issue #3: Delete button for filled countdowns - FIXED +- Fix difficulty badge click functionality - FIXED -## 📝 **Notes** +## UI Improvements +- move plus buttons for adv/env/count to be just to the right of those words in their headings instead of on the far right of the left column; those buttons should be vertically aligned between those three sections, even though those 3 words aren't the exact same length - COMPLETED +- make countdown creator appear in the header row just to the right of the plus button and all fit there at once - COMPLETED +- countdowns - change simple fear and simple hope to just hope and fear - COMPLETED +pa- countdown cards - make delete button that appears when countdown is full not take place of increment button, but instead appear separately - COMPLETED + +## Notes - Manual deployment works perfectly - GitHub Actions configured but blocked by billing - Minion damage mechanics implemented and refined -- Countdown system fully functional - ---- -*Last updated: $(date)* +- Countdown system fully functional \ No newline at end of file diff --git a/src/App.css b/src/App.css index 9537cd2..5b7fa26 100644 --- a/src/App.css +++ b/src/App.css @@ -37,6 +37,7 @@ min-width: 0; background: var(--bg-dark); overflow-y: auto; + overflow-x: hidden; /* Prevent horizontal spill outside column */ display: flex; flex-direction: column; height: 100%; diff --git a/src/components/Cards.jsx b/src/components/Cards.jsx index b8f2537..f9d994d 100644 --- a/src/components/Cards.jsx +++ b/src/components/Cards.jsx @@ -1080,7 +1080,25 @@ const Cards = ({ > − - {item.value >= item.max && (!item.loop || item.loop === 'none') ? ( + + {item.value >= item.max && (!item.loop || item.loop === 'none') && ( - ) : ( - )}
diff --git a/src/components/GameBoard.jsx b/src/components/GameBoard.jsx index d15c96a..e8d9cb1 100644 --- a/src/components/GameBoard.jsx +++ b/src/components/GameBoard.jsx @@ -365,47 +365,107 @@ const GameBoard = ({ {/* Countdowns Section */}
-

Countdowns

-
- {/* Countdown Trigger Controls - only show if there are countdowns */} - {countdowns && countdowns.length > 0 && ( -
- {(() => { - const triggers = getNeededTriggers() - return ( - <> - {/* Show/Hide Long-term Countdowns Button */} - {countdowns && countdowns.some(c => c.type === 'long-term') && ( +
+

Countdowns

+ + {/* Inline Countdown Creator */} + {showInlineCreator.campaign && ( + + )} +
+ + {/* Countdown Trigger Controls - only show if there are countdowns */} + {countdowns && countdowns.length > 0 && ( +
+ {(() => { + const triggers = getNeededTriggers() + return ( + <> + {/* Basic Roll Triggers - only show if there are standard countdowns */} + {triggers.basicRollTriggers && ( + + )} + + {/* Simple Fear/Hope Triggers - only show if there are simple fear/hope countdowns AND no dynamic countdowns */} + {triggers.simpleFearTriggers && ( + + )} + {triggers.simpleHopeTriggers && ( + + )} + + {/* Complex Roll Outcome Triggers - only show if there are dynamic countdowns */} + {triggers.complexRollTriggers && ( + <> - )} - - {/* Basic Roll Triggers - only show if there are standard countdowns */} - {triggers.basicRollTriggers && ( - )} - - {/* Simple Fear/Hope Triggers - only show if there are simple fear/hope countdowns AND no dynamic countdowns */} - {triggers.simpleFearTriggers && ( - )} - {triggers.simpleHopeTriggers && ( - )} - - {/* Complex Roll Outcome Triggers - only show if there are dynamic countdowns */} - {triggers.complexRollTriggers && ( - <> - - - - - - - )} - - ) - })()} -
- )} - - -
+ + + )} + + {/* Show/Hide Long-term Countdowns Button - moved to end */} + {countdowns && countdowns.some(c => c.type === 'long-term') && ( + + )} + + ) + })()} +
+ )}
- {/* Inline Countdown Creator */} - {showInlineCreator.campaign && ( - - )} - {/* Non-long-term countdowns - always visible */} {countdowns && countdowns.filter(c => c.type !== 'long-term').length > 0 && (
-

Environment

-
+
+

Environment

{drawerItem && ( diff --git a/src/index.css b/src/index.css index 2d80206..0e6ca5f 100644 --- a/src/index.css +++ b/src/index.css @@ -37,6 +37,19 @@ box-sizing: border-box; } +html, body { + height: 100%; + height: 100dvh; /* Use dynamic viewport height for better mobile support */ + overflow: auto; /* Allow scrolling when content exceeds viewport */ +} + +/* Safari mobile viewport fixes */ +@supports (height: 100dvh) { + html, body { + height: 100dvh; + } +} + body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; background: var(--bg-dark); @@ -1378,6 +1391,7 @@ textarea, /* Mobile drawer styles */ .mobile-drawer { position: fixed; + top: 49px; /* Start below fear header */ bottom: 0; left: 0; right: 0; @@ -1390,18 +1404,22 @@ textarea, } .drawer-backdrop { - position: fixed; - top: 49px; /* Start below the fear header including its border */ + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); opacity: 0; transition: opacity 0.3s ease; + pointer-events: none; /* Prevent backdrop from receiving events when hidden */ + visibility: hidden; /* Ensure backdrop is completely hidden when not open */ } .mobile-drawer.open .drawer-backdrop { opacity: 1; + pointer-events: auto; /* Allow backdrop events only when drawer is open */ + visibility: visible; /* Make backdrop visible when drawer is open */ } .drawer-content { @@ -1412,14 +1430,30 @@ textarea, background: var(--bg-dark); border-top: 1px solid var(--border); border-radius: 16px 16px 0 0; - max-height: calc(100vh - 49px); /* Account for fear header height + border */ - transform: translateY(100%); - transition: transform 0.3s ease; + height: 100%; /* Use full height of container */ overflow: hidden; + pointer-events: none; /* Prevent content from receiving events when hidden */ + visibility: hidden; /* Ensure content is completely hidden when not open */ + overscroll-behavior: none !important; /* Prevent pull-to-refresh */ + overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ + touch-action: none !important; /* Prevent all touch gestures to stop pull-to-refresh */ + } + + .drawer-body { + padding: 1rem; + height: calc(100% - 80px); /* Use 100% instead of viewport units for consistency */ + display: flex; + flex-direction: column; + overflow-y: auto; /* Allow scrolling on drawer body */ + -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */ + overscroll-behavior: none !important; /* Prevent pull-to-refresh */ + overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ + touch-action: manipulation !important; /* Allow scrolling but prevent zoom and pull-to-refresh */ } .mobile-drawer.open .drawer-content { - transform: translateY(0); + pointer-events: auto; /* Allow content events only when drawer is open */ + visibility: visible; /* Make content visible when drawer is open */ } .drawer-header { @@ -1429,6 +1463,10 @@ textarea, padding: 1rem; border-bottom: 1px solid var(--border); position: relative; + overscroll-behavior: none !important; /* Prevent pull-to-refresh */ + overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ + touch-action: none !important; /* Prevent all touch gestures to stop pull-to-refresh */ + cursor: pointer; /* Make it clear the header is clickable */ } .drawer-handle { @@ -1437,28 +1475,23 @@ textarea, background: var(--border); border-radius: 2px; cursor: pointer; + overscroll-behavior: none; /* Prevent pull-to-refresh */ + overscroll-behavior-y: none; /* Prevent vertical pull-to-refresh */ + touch-action: none; /* Prevent all touch actions except our custom handling */ } - .drawer-close { - position: absolute; - right: 1rem; - top: 50%; - transform: translateY(-50%); - background: none; - border: none; - color: var(--text-primary); - font-size: 1.5rem; - cursor: pointer; - padding: 0.5rem; - line-height: 1; - } + /* Removed drawer-close styles since header is now clickable */ .drawer-body { padding: 1rem; - height: calc(100vh - 49px - 80px); /* Account for fear header + border and drawer header */ + height: calc(100% - 80px); /* Use 100% instead of viewport units for consistency */ display: flex; flex-direction: column; - overflow: hidden; /* No scrolling on drawer body */ + overflow-y: auto; /* Allow scrolling on drawer body */ + -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */ + overscroll-behavior: none !important; /* Prevent pull-to-refresh */ + overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ + touch-action: manipulation !important; /* Allow scrolling but prevent zoom and pull-to-refresh */ } /* Mobile drawer browser specific styles */ @@ -1473,14 +1506,55 @@ textarea, flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; + overscroll-behavior: none !important; /* Prevent pull-to-refresh */ + overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ + touch-action: pan-y !important; /* Allow vertical scrolling only */ } + - /* Prevent background scrolling when mobile drawer is open */ + /* Prevent background scrolling when any mobile drawer is open */ body.mobile-drawer-open { overflow: hidden; position: fixed; width: 100%; + height: 100%; + touch-action: none; /* Prevent touch scrolling */ + overscroll-behavior: none; /* Prevent pull-to-refresh */ + overscroll-behavior-y: none; /* Prevent vertical pull-to-refresh */ + } + + /* Additional aggressive pull-to-refresh prevention for mobile drawers */ + .mobile-drawer { + overscroll-behavior: none !important; + overscroll-behavior-y: none !important; + touch-action: none !important; + } + + .mobile-drawer .drawer-content { + overscroll-behavior: none !important; + overscroll-behavior-y: none !important; + touch-action: none !important; + } + + .mobile-drawer .drawer-header { + overscroll-behavior: none !important; + overscroll-behavior-y: none !important; + touch-action: none !important; + } + + .mobile-drawer .drawer-handle { + overscroll-behavior: none !important; + overscroll-behavior-y: none !important; + touch-action: none !important; + } + + /* Allow scrolling in expanded card content areas */ + .drawer-body .expanded-card { + overscroll-behavior: none !important; /* Prevent pull-to-refresh */ + overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ + touch-action: none !important; /* Prevent all touch gestures on card container */ } + } /* Browser condensed card styles for both desktop and mobile */ @@ -1533,10 +1607,10 @@ textarea, /* Sidebar Styles */ .sidebar { - position: fixed; - bottom: 0; /* Bottom bar for both desktop and mobile */ - left: 0; - right: 0; + position: relative; /* Changed from fixed to relative */ + bottom: auto; /* Remove fixed positioning */ + left: auto; + right: auto; width: 100%; /* Full width */ height: 60px; /* Fixed height */ background: var(--bg-dark); @@ -1776,6 +1850,8 @@ textarea, padding: 0.125rem; width: 100%; height: 60%; + touch-action: manipulation; /* Prevent zooming */ + user-select: none; /* Prevent text selection */ } .fear-controls { @@ -2802,7 +2878,7 @@ textarea, gap: 0.25rem; margin-bottom: 0.25rem; position: relative; - padding-left: 1rem; /* Add space from left border */ + padding-left: 0.25rem; /* Reduced from 1rem to 0.25rem */ padding-top: 0.75rem; /* Add top padding to section headers */ } @@ -5453,6 +5529,11 @@ textarea, gap: 0.25rem; } +/* Damage input popup - prevent mobile auto-zoom */ +.damage-input-popup input { + font-size: 1rem; /* 16px to prevent mobile auto-zoom */ +} + /* Experience form specific sizing */ .experience-form .form-row .form-group:first-child { flex: 3; @@ -5477,7 +5558,7 @@ textarea, border-radius: 0.25rem; background: var(--bg-primary); color: var(--text-primary); - font-size: 0.875rem; + font-size: 1rem; /* 16px to prevent mobile auto-zoom */ } .form-group input:focus, @@ -5852,7 +5933,7 @@ textarea, } .countdown-row .form-input { - font-size: 0.875rem; + font-size: 1rem; /* 16px to prevent mobile auto-zoom */ padding: 0.375rem 0.5rem; } @@ -5870,7 +5951,7 @@ textarea, } .countdown-stack .form-input { - font-size: 0.875rem; + font-size: 1rem; /* 16px to prevent mobile auto-zoom */ padding: 0.5rem; } @@ -6296,7 +6377,7 @@ textarea, border-radius: 0.25rem; background: var(--bg-dark); color: var(--text-primary); - font-size: 0.875rem; + font-size: 1rem; /* 16px to prevent mobile auto-zoom */ min-width: 8rem; } From ffa996e0079ce26ff27fa8f08909e6e4b2b87c9d Mon Sep 17 00:00:00 2001 From: Jackson Brammer Date: Tue, 9 Sep 2025 16:29:34 -0700 Subject: [PATCH 017/147] Fix remaining mobile drawer issues - Fixed pull-to-refresh still happening below drawer handle by making CSS more aggressive - Set drawer-body touch-action to 'none' to prevent all touch gestures - Added specific touch-action: pan-y only for scrollable areas (browser table, expanded content) - Fixed errant scroll up when releasing partial swipe-to-close gesture - Always prevent default and stop propagation in handleTouchEnd - Clear touch state after snap-back to prevent unwanted scroll behavior - Enhanced touch state management to prevent scroll conflicts This should eliminate both pull-to-refresh and unwanted scroll behaviors. --- src/components/List.jsx | 19 +++++++++++-------- src/index.css | 21 +++++++++++++++++++-- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/components/List.jsx b/src/components/List.jsx index 7e4152d..f743ff3 100644 --- a/src/components/List.jsx +++ b/src/components/List.jsx @@ -154,11 +154,9 @@ const List = ({ const distance = touchCurrent - touchStart - // Only prevent default if this was a significant downward swipe - if (distance > 30) { - e.preventDefault() - e.stopPropagation() - } + // Always prevent default to avoid any scroll behavior + e.preventDefault() + e.stopPropagation() // If swipe was far enough, smoothly animate downward to close if (distance > 100) { @@ -173,14 +171,19 @@ const List = ({ // If swipe was significant but not enough to close, snap back smoothly else if (distance > 30) { setDrawerOffset(0) + // Clear touch state to prevent any scroll behavior + setTimeout(() => { + setTouchStart(null) + setTouchCurrent(null) + }, 50) } // If swipe was small, just reset else if (distance <= 30) { setDrawerOffset(0) + // Clear touch state immediately for small swipes + setTouchStart(null) + setTouchCurrent(null) } - - setTouchStart(null) - setTouchCurrent(null) } const sensors = useSensors( useSensor(PointerSensor, { diff --git a/src/index.css b/src/index.css index 0e6ca5f..85254ce 100644 --- a/src/index.css +++ b/src/index.css @@ -1448,7 +1448,7 @@ textarea, -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */ overscroll-behavior: none !important; /* Prevent pull-to-refresh */ overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ - touch-action: manipulation !important; /* Allow scrolling but prevent zoom and pull-to-refresh */ + touch-action: none !important; /* Prevent all touch gestures to stop pull-to-refresh */ } .mobile-drawer.open .drawer-content { @@ -1491,7 +1491,7 @@ textarea, -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */ overscroll-behavior: none !important; /* Prevent pull-to-refresh */ overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ - touch-action: manipulation !important; /* Allow scrolling but prevent zoom and pull-to-refresh */ + touch-action: none !important; /* Prevent all touch gestures to stop pull-to-refresh */ } /* Mobile drawer browser specific styles */ @@ -1511,6 +1511,23 @@ textarea, touch-action: pan-y !important; /* Allow vertical scrolling only */ } + /* Allow scrolling in expanded card content areas */ + .drawer-body .expanded-card { + overscroll-behavior: none !important; /* Prevent pull-to-refresh */ + overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ + touch-action: none !important; /* Prevent all touch gestures on card container */ + } + + .drawer-body .expanded-content { + overflow-y: auto !important; /* Allow vertical scrolling */ + -webkit-overflow-scrolling: touch !important; /* Smooth scrolling on iOS Safari */ + overscroll-behavior: none !important; /* Prevent pull-to-refresh */ + overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ + touch-action: pan-y !important; /* Allow vertical scrolling only */ + height: calc(100vh - 200px) !important; /* Force a specific height to enable scrolling */ + max-height: calc(100vh - 200px) !important; /* Don't exceed this height */ + } + /* Prevent background scrolling when any mobile drawer is open */ body.mobile-drawer-open { From 894a15ed41cdf9e2395ad28a9100a4ff3df15749 Mon Sep 17 00:00:00 2001 From: Jackson Brammer Date: Tue, 9 Sep 2025 16:32:58 -0700 Subject: [PATCH 018/147] Add version display with git commit hash - Added automatic version numbering using git commit hash - Version displays in top-right corner with semi-transparent background - Updates automatically with each deployment - Responsive design for mobile and desktop - Non-interactive overlay that doesn't interfere with UI - Uses monospace font for clear readability - Includes backdrop blur and subtle border for visibility Files added: - src/components/Version.jsx - Version display component - config/vite.config.js - Updated to inject git commit hash - src/App.jsx - Added Version component to main app - src/index.css - Added version display styles This allows easy verification when GitHub Pages has updated. --- config/vite.config.js | 13 +++++++++++++ src/App.jsx | 4 ++++ src/components/Version.jsx | 13 +++++++++++++ src/index.css | 27 +++++++++++++++++++++++++++ 4 files changed, 57 insertions(+) create mode 100644 src/components/Version.jsx diff --git a/config/vite.config.js b/config/vite.config.js index c67d2c3..6e5d8be 100644 --- a/config/vite.config.js +++ b/config/vite.config.js @@ -1,11 +1,24 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import path from 'path' +import { execSync } from 'child_process' + +// Get git commit hash for version +function getGitCommitHash() { + try { + return execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim() + } catch (error) { + return 'dev' + } +} export default defineConfig({ plugins: [react()], root: path.resolve(__dirname, '../src'), base: '/Daggerheart/', // GitHub Pages subdirectory + define: { + __APP_VERSION__: JSON.stringify(getGitCommitHash()) + }, server: { host: '0.0.0.0', // Bind to all network interfaces port: 5173, // Default Vite port diff --git a/src/App.jsx b/src/App.jsx index 73e6cc7..74eae4a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,6 +4,7 @@ import { useGameState } from './useGameState' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faSkull, faFire, faMoon, faStar, faDice } from '@fortawesome/free-solid-svg-icons' import { Swords, TreePine, Pencil, Clock, Plus, Trash2, Menu, Wrench } from 'lucide-react' +import Version from './components/Version' import './App.css' // Import all the UI components @@ -644,6 +645,9 @@ const AppContent = () => { } }} > + {/* Version Display */} + + {/* Top Bar: Fear Tracker */}
{ + const version = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : 'dev' + + return ( +
+ v{version} +
+ ) +} + +export default Version diff --git a/src/index.css b/src/index.css index 85254ce..72e0384 100644 --- a/src/index.css +++ b/src/index.css @@ -6520,3 +6520,30 @@ textarea, font-size: 0.85rem; white-space: nowrap; } + +/* Version Display */ +.version-display { + position: fixed; + top: 0.5rem; + right: 0.5rem; + background: rgba(0, 0, 0, 0.7); + color: var(--text-light); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + font-family: monospace; + z-index: 1000; + pointer-events: none; + user-select: none; + backdrop-filter: blur(4px); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +@media (max-width: 800px) { + .version-display { + top: 0.25rem; + right: 0.25rem; + font-size: 0.7rem; + padding: 0.2rem 0.4rem; + } +} From ebc4f0f12d7576b71b1305d9970d8c3b582b2e6e Mon Sep 17 00:00:00 2001 From: Jackson Brammer Date: Tue, 9 Sep 2025 16:39:20 -0700 Subject: [PATCH 019/147] Implement robust JavaScript-based touch handling solution - Replaced conflicting CSS touch-action rules with JavaScript-controlled touch events - Set all drawer elements to touch-action: auto to let JavaScript handle events - Implemented intelligent touch event handling: * Only prevents default for actual swipe-to-dismiss gestures * Allows normal scrolling when not handling swipe gestures * Prevents pull-to-refresh only when needed * Clears touch state properly to prevent unwanted scroll behavior Touch handling logic: - handleTouchStart: Only prevents default for swipe-to-dismiss gestures - handleTouchMove: Only prevents default for downward swipes > 0px - handleTouchEnd: Only prevents default for significant swipes > 30px - Proper touch state clearing prevents errant scroll after snap-back This should finally resolve the back-and-forth between broken scrolling and broken pull-to-refresh prevention by giving precise control over when touch events are intercepted vs allowed to pass through for normal scrolling. --- src/App.jsx | 45 ++++++++++++++++++++++------------------- src/components/List.jsx | 26 +++++++++++++----------- src/index.css | 10 ++++----- 3 files changed, 43 insertions(+), 38 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 74eae4a..e007cf0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -72,11 +72,13 @@ const AppContent = () => { if (!isTableAtTop) { // Don't handle swipe-to-dismiss for table touches when not at top + // Allow normal scrolling return } } - e.preventDefault() // Prevent default scroll behavior + // For swipe-to-dismiss gestures, prevent default to avoid pull-to-refresh + e.preventDefault() setTouchStart(e.targetTouches[0].clientY) setTouchCurrent(e.targetTouches[0].clientY) setDrawerOffset(0) @@ -85,37 +87,33 @@ const AppContent = () => { const handleTouchMove = (e) => { if (!touchStart) return - // Check if touch is on the browser table - const isTableTouch = e.target.closest('.browser-table-container') || - e.target.closest('.browser-table') || - e.target.closest('table') - - if (isTableTouch) { - // Don't handle swipe-to-dismiss for table touches - return - } - - e.preventDefault() // Prevent default scroll behavior - const currentY = e.targetTouches[0].clientY const deltaY = currentY - touchStart - // Only allow downward swipes (positive deltaY) + // Only prevent default if this is a downward swipe-to-dismiss gesture if (deltaY > 0) { + e.preventDefault() setTouchCurrent(currentY) setDrawerOffset(deltaY) - - // Don't close immediately - let user complete the gesture + } else if (deltaY < -10) { + // If swiping up more than 10px, reset the swipe state + setTouchStart(null) + setTouchCurrent(null) + setDrawerOffset(0) } + // For upward swipes less than 10px, allow normal scrolling } const handleTouchEnd = (e) => { if (!touchStart || !touchCurrent) return - e.preventDefault() // Prevent default scroll behavior - const distance = touchCurrent - touchStart + // Only prevent default if this was a significant downward swipe + if (distance > 30) { + e.preventDefault() + } + // If swipe was far enough, smoothly animate downward to close if (distance > 100) { // Animate drawer offset to full height (smooth downward close) @@ -129,14 +127,19 @@ const AppContent = () => { // If swipe was significant but not enough to close, snap back smoothly else if (distance > 30) { setDrawerOffset(0) + // Clear touch state to prevent any scroll behavior + setTimeout(() => { + setTouchStart(null) + setTouchCurrent(null) + }, 50) } // If swipe was small, just reset else if (distance <= 30) { setDrawerOffset(0) + // Clear touch state immediately for small swipes + setTouchStart(null) + setTouchCurrent(null) } - - setTouchStart(null) - setTouchCurrent(null) } const [creatorFormData, setCreatorFormData] = useState({ diff --git a/src/components/List.jsx b/src/components/List.jsx index f743ff3..70695d7 100644 --- a/src/components/List.jsx +++ b/src/components/List.jsx @@ -101,10 +101,6 @@ const List = ({ const [drawerOffset, setDrawerOffset] = useState(0) const handleTouchStart = (e) => { - // Always prevent default to avoid pull-to-refresh - e.preventDefault() - e.stopPropagation() - // Check if touch is on header OR on content when scrolled to top const isHeaderTouch = e.target.closest('.drawer-header') const drawerBody = e.target.closest('.drawer-body') @@ -118,9 +114,14 @@ const List = ({ // Only handle swipe-to-dismiss for header touches OR content touches when at top if (!isHeaderTouch && !isContentAtTop) { // Don't handle swipe-to-dismiss for content touches when not at top + // Allow normal scrolling return } + // For swipe-to-dismiss gestures, prevent default to avoid pull-to-refresh + e.preventDefault() + e.stopPropagation() + const touchY = e.targetTouches[0].clientY setTouchStart(touchY) setTouchCurrent(touchY) @@ -128,17 +129,15 @@ const List = ({ } const handleTouchMove = (e) => { - // Always prevent default to avoid pull-to-refresh - e.preventDefault() - e.stopPropagation() - if (!touchStart) return const currentY = e.targetTouches[0].clientY const deltaY = currentY - touchStart - // Only handle swipe-to-dismiss for downward swipes (positive deltaY) + // Only prevent default if this is a downward swipe-to-dismiss gesture if (deltaY > 0) { + e.preventDefault() + e.stopPropagation() setTouchCurrent(currentY) setDrawerOffset(deltaY) } else if (deltaY < -10) { @@ -147,6 +146,7 @@ const List = ({ setTouchCurrent(null) setDrawerOffset(0) } + // For upward swipes less than 10px, allow normal scrolling } const handleTouchEnd = (e) => { @@ -154,9 +154,11 @@ const List = ({ const distance = touchCurrent - touchStart - // Always prevent default to avoid any scroll behavior - e.preventDefault() - e.stopPropagation() + // Only prevent default if this was a significant downward swipe + if (distance > 30) { + e.preventDefault() + e.stopPropagation() + } // If swipe was far enough, smoothly animate downward to close if (distance > 100) { diff --git a/src/index.css b/src/index.css index 72e0384..50788c6 100644 --- a/src/index.css +++ b/src/index.css @@ -1448,7 +1448,7 @@ textarea, -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */ overscroll-behavior: none !important; /* Prevent pull-to-refresh */ overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ - touch-action: none !important; /* Prevent all touch gestures to stop pull-to-refresh */ + touch-action: auto !important; /* Let JavaScript handle touch events */ } .mobile-drawer.open .drawer-content { @@ -1491,7 +1491,7 @@ textarea, -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */ overscroll-behavior: none !important; /* Prevent pull-to-refresh */ overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ - touch-action: none !important; /* Prevent all touch gestures to stop pull-to-refresh */ + touch-action: auto !important; /* Let JavaScript handle touch events */ } /* Mobile drawer browser specific styles */ @@ -1508,14 +1508,14 @@ textarea, -webkit-overflow-scrolling: touch; overscroll-behavior: none !important; /* Prevent pull-to-refresh */ overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ - touch-action: pan-y !important; /* Allow vertical scrolling only */ + touch-action: auto !important; /* Let JavaScript handle touch events */ } /* Allow scrolling in expanded card content areas */ .drawer-body .expanded-card { overscroll-behavior: none !important; /* Prevent pull-to-refresh */ overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ - touch-action: none !important; /* Prevent all touch gestures on card container */ + touch-action: auto !important; /* Let JavaScript handle touch events */ } .drawer-body .expanded-content { @@ -1523,7 +1523,7 @@ textarea, -webkit-overflow-scrolling: touch !important; /* Smooth scrolling on iOS Safari */ overscroll-behavior: none !important; /* Prevent pull-to-refresh */ overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ - touch-action: pan-y !important; /* Allow vertical scrolling only */ + touch-action: auto !important; /* Let JavaScript handle touch events */ height: calc(100vh - 200px) !important; /* Force a specific height to enable scrolling */ max-height: calc(100vh - 200px) !important; /* Don't exceed this height */ } From 337693146eefc364ca0520e7aba844856c0d619e Mon Sep 17 00:00:00 2001 From: Jackson Brammer Date: Tue, 9 Sep 2025 16:47:40 -0700 Subject: [PATCH 020/147] Separate touch handling strategies for different drawer types EXPANDED CARD DRAWER (List.jsx) - Scroll-First Strategy: - Only handles swipe-to-dismiss on header touches - All content touches allow normal scrolling (no preventDefault) - CSS: touch-action: pan-y for scrolling, JavaScript handles swipe-to-dismiss - Prioritizes scrolling for long adversary/environment content BROWSER/CREATOR DRAWER (App.jsx) - Table-Aware Strategy: - Intelligently handles table scrolling vs swipe-to-dismiss - Table touches: allow scrolling when scrolled down, swipe-to-dismiss when at top - Non-table touches: always handle swipe-to-dismiss - CSS: touch-action: pan-y for table scrolling, JavaScript handles swipe-to-dismiss Key Differences: - Expanded cards: Header-only swipe-to-dismiss, content always scrolls - Browser drawer: Context-aware (table scroll position determines behavior) - Both prevent pull-to-refresh via overscroll-behavior: none - Both use touch-action: pan-y to allow scrolling while JavaScript handles gestures This separation should end the back-and-forth by giving each drawer type the appropriate touch handling strategy for its specific use case. --- src/App.jsx | 38 ++++++++++++++++----------------- src/components/List.jsx | 47 ++++++++++++++++------------------------- src/index.css | 9 ++++---- 3 files changed, 41 insertions(+), 53 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index e007cf0..9cb9f67 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -60,24 +60,27 @@ const AppContent = () => { const [drawerOffset, setDrawerOffset] = useState(0) const handleTouchStart = (e) => { - // Check if touch is on the browser table (which should scroll normally) + // BROWSER/CREATOR DRAWER STRATEGY: Handle table scrolling intelligently + + // Check if touch is on the browser table const isTableTouch = e.target.closest('.browser-table-container') || e.target.closest('.browser-table') || e.target.closest('table') if (isTableTouch) { - // Check if table is scrolled to the top + // For table touches, check scroll position const tableContainer = e.target.closest('.browser-table-container') const isTableAtTop = tableContainer && tableContainer.scrollTop <= 10 if (!isTableAtTop) { - // Don't handle swipe-to-dismiss for table touches when not at top - // Allow normal scrolling + // Table is scrolled down - allow normal table scrolling + // Don't prevent default - let browser handle table scrolling return } + // Table is at top - allow swipe-to-dismiss } - // For swipe-to-dismiss gestures, prevent default to avoid pull-to-refresh + // For non-table touches OR table touches at top, handle swipe-to-dismiss e.preventDefault() setTouchStart(e.targetTouches[0].clientY) setTouchCurrent(e.targetTouches[0].clientY) @@ -87,56 +90,51 @@ const AppContent = () => { const handleTouchMove = (e) => { if (!touchStart) return + // BROWSER/CREATOR DRAWER STRATEGY: Handle swipe gestures with table awareness const currentY = e.targetTouches[0].clientY const deltaY = currentY - touchStart - // Only prevent default if this is a downward swipe-to-dismiss gesture + // Only handle downward swipes for swipe-to-dismiss if (deltaY > 0) { e.preventDefault() setTouchCurrent(currentY) setDrawerOffset(deltaY) - } else if (deltaY < -10) { - // If swiping up more than 10px, reset the swipe state + } + // For upward swipes, reset state to allow normal scrolling + else if (deltaY < 0) { setTouchStart(null) setTouchCurrent(null) setDrawerOffset(0) } - // For upward swipes less than 10px, allow normal scrolling } const handleTouchEnd = (e) => { if (!touchStart || !touchCurrent) return + // BROWSER/CREATOR DRAWER STRATEGY: Handle swipe gestures consistently const distance = touchCurrent - touchStart - // Only prevent default if this was a significant downward swipe - if (distance > 30) { - e.preventDefault() - } + // Always prevent default for swipe gestures + e.preventDefault() // If swipe was far enough, smoothly animate downward to close if (distance > 100) { - // Animate drawer offset to full height (smooth downward close) setDrawerOffset(window.innerHeight) - - // Then close the drawer after the transition completes setTimeout(() => { handleCloseRightColumn() - }, 300) // Match the CSS transition duration + }, 300) } // If swipe was significant but not enough to close, snap back smoothly else if (distance > 30) { setDrawerOffset(0) - // Clear touch state to prevent any scroll behavior setTimeout(() => { setTouchStart(null) setTouchCurrent(null) }, 50) } // If swipe was small, just reset - else if (distance <= 30) { + else { setDrawerOffset(0) - // Clear touch state immediately for small swipes setTouchStart(null) setTouchCurrent(null) } diff --git a/src/components/List.jsx b/src/components/List.jsx index 70695d7..a7de2da 100644 --- a/src/components/List.jsx +++ b/src/components/List.jsx @@ -101,24 +101,18 @@ const List = ({ const [drawerOffset, setDrawerOffset] = useState(0) const handleTouchStart = (e) => { - // Check if touch is on header OR on content when scrolled to top - const isHeaderTouch = e.target.closest('.drawer-header') - const drawerBody = e.target.closest('.drawer-body') + // EXPANDED CARD DRAWER STRATEGY: Prioritize scrolling, only handle swipe-to-dismiss on header - // If touching content, check if we're scrolled to the top - let isContentAtTop = false - if (drawerBody) { - isContentAtTop = drawerBody.scrollTop <= 10 // Allow small tolerance - } + // Only handle swipe-to-dismiss for header touches + const isHeaderTouch = e.target.closest('.drawer-header') - // Only handle swipe-to-dismiss for header touches OR content touches when at top - if (!isHeaderTouch && !isContentAtTop) { - // Don't handle swipe-to-dismiss for content touches when not at top - // Allow normal scrolling + if (!isHeaderTouch) { + // For all non-header touches, allow normal scrolling + // Don't prevent default - let the browser handle scrolling return } - // For swipe-to-dismiss gestures, prevent default to avoid pull-to-refresh + // Only for header touches, handle swipe-to-dismiss e.preventDefault() e.stopPropagation() @@ -131,58 +125,53 @@ const List = ({ const handleTouchMove = (e) => { if (!touchStart) return + // EXPANDED CARD DRAWER STRATEGY: Only handle header swipe gestures const currentY = e.targetTouches[0].clientY const deltaY = currentY - touchStart - // Only prevent default if this is a downward swipe-to-dismiss gesture + // Only handle downward swipes for swipe-to-dismiss if (deltaY > 0) { e.preventDefault() e.stopPropagation() setTouchCurrent(currentY) setDrawerOffset(deltaY) - } else if (deltaY < -10) { - // If swiping up more than 10px, reset the swipe state to prevent scroll + } + // For upward swipes, reset state to allow normal scrolling + else if (deltaY < 0) { setTouchStart(null) setTouchCurrent(null) setDrawerOffset(0) } - // For upward swipes less than 10px, allow normal scrolling } const handleTouchEnd = (e) => { if (!touchStart || !touchCurrent) return + // EXPANDED CARD DRAWER STRATEGY: Handle header swipe gestures only const distance = touchCurrent - touchStart - // Only prevent default if this was a significant downward swipe - if (distance > 30) { - e.preventDefault() - e.stopPropagation() - } + // Always prevent default for header swipe gestures + e.preventDefault() + e.stopPropagation() // If swipe was far enough, smoothly animate downward to close if (distance > 100) { - // Animate drawer offset to full height (smooth downward close) setDrawerOffset(window.innerHeight) - - // Then close the drawer after the transition completes setTimeout(() => { closeDrawer() - }, 300) // Match the CSS transition duration + }, 300) } // If swipe was significant but not enough to close, snap back smoothly else if (distance > 30) { setDrawerOffset(0) - // Clear touch state to prevent any scroll behavior setTimeout(() => { setTouchStart(null) setTouchCurrent(null) }, 50) } // If swipe was small, just reset - else if (distance <= 30) { + else { setDrawerOffset(0) - // Clear touch state immediately for small swipes setTouchStart(null) setTouchCurrent(null) } diff --git a/src/index.css b/src/index.css index 50788c6..75b4548 100644 --- a/src/index.css +++ b/src/index.css @@ -1502,20 +1502,21 @@ textarea, overflow: hidden; } + /* BROWSER/CREATOR DRAWER: Handle table scrolling intelligently */ .drawer-body .browser-table-container { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: none !important; /* Prevent pull-to-refresh */ overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ - touch-action: auto !important; /* Let JavaScript handle touch events */ + touch-action: pan-y !important; /* Allow vertical scrolling, JavaScript handles swipe-to-dismiss */ } - /* Allow scrolling in expanded card content areas */ + /* EXPANDED CARD DRAWER: Prioritize scrolling, only prevent pull-to-refresh */ .drawer-body .expanded-card { overscroll-behavior: none !important; /* Prevent pull-to-refresh */ overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ - touch-action: auto !important; /* Let JavaScript handle touch events */ + touch-action: pan-y !important; /* Allow vertical scrolling, JavaScript handles swipe-to-dismiss */ } .drawer-body .expanded-content { @@ -1523,7 +1524,7 @@ textarea, -webkit-overflow-scrolling: touch !important; /* Smooth scrolling on iOS Safari */ overscroll-behavior: none !important; /* Prevent pull-to-refresh */ overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ - touch-action: auto !important; /* Let JavaScript handle touch events */ + touch-action: pan-y !important; /* Allow vertical scrolling */ height: calc(100vh - 200px) !important; /* Force a specific height to enable scrolling */ max-height: calc(100vh - 200px) !important; /* Don't exceed this height */ } From a70d5c1b9ae107f20b86cf390a2e8479dc380890 Mon Sep 17 00:00:00 2001 From: Jackson Brammer Date: Tue, 9 Sep 2025 16:57:00 -0700 Subject: [PATCH 021/147] Fix expanded card drawer swipe-to-dismiss from scrollable content - Expanded card drawer now handles swipe-to-dismiss from both header AND scrollable content when at top - Added check for drawerBody.scrollTop <= 10 to detect when content is scrolled to top - Behavior: Header always swipe-to-dismiss, content swipe-to-dismiss when at top, content scroll when scrolled down - This matches the browser drawer behavior for consistency Touch handling logic: - isHeaderTouch: Always handle swipe-to-dismiss - isContentAtTop: Handle swipe-to-dismiss when content is at top - Content scrolled down: Allow normal scrolling (no preventDefault) Now both drawer types have consistent swipe-to-dismiss behavior: - Browser drawer: Table swipe-to-dismiss when table at top - Expanded card drawer: Content swipe-to-dismiss when content at top --- src/components/List.jsx | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/components/List.jsx b/src/components/List.jsx index a7de2da..e3c4a5f 100644 --- a/src/components/List.jsx +++ b/src/components/List.jsx @@ -101,18 +101,26 @@ const List = ({ const [drawerOffset, setDrawerOffset] = useState(0) const handleTouchStart = (e) => { - // EXPANDED CARD DRAWER STRATEGY: Prioritize scrolling, only handle swipe-to-dismiss on header + // EXPANDED CARD DRAWER STRATEGY: Handle swipe-to-dismiss on header OR content when at top - // Only handle swipe-to-dismiss for header touches + // Check if touch is on header const isHeaderTouch = e.target.closest('.drawer-header') - if (!isHeaderTouch) { - // For all non-header touches, allow normal scrolling + // Check if touch is on scrollable content + const drawerBody = e.target.closest('.drawer-body') + let isContentAtTop = false + if (drawerBody) { + isContentAtTop = drawerBody.scrollTop <= 10 // Allow small tolerance + } + + // Only handle swipe-to-dismiss for header touches OR content touches when at top + if (!isHeaderTouch && !isContentAtTop) { + // For content touches when scrolled down, allow normal scrolling // Don't prevent default - let the browser handle scrolling return } - // Only for header touches, handle swipe-to-dismiss + // For header touches OR content touches at top, handle swipe-to-dismiss e.preventDefault() e.stopPropagation() @@ -125,7 +133,7 @@ const List = ({ const handleTouchMove = (e) => { if (!touchStart) return - // EXPANDED CARD DRAWER STRATEGY: Only handle header swipe gestures + // EXPANDED CARD DRAWER STRATEGY: Handle swipe gestures from header or content at top const currentY = e.targetTouches[0].clientY const deltaY = currentY - touchStart @@ -147,10 +155,10 @@ const List = ({ const handleTouchEnd = (e) => { if (!touchStart || !touchCurrent) return - // EXPANDED CARD DRAWER STRATEGY: Handle header swipe gestures only + // EXPANDED CARD DRAWER STRATEGY: Handle swipe gestures from header or content at top const distance = touchCurrent - touchStart - // Always prevent default for header swipe gestures + // Always prevent default for swipe gestures e.preventDefault() e.stopPropagation() From 39aad299d400e9a21df4a436137d54ea5e2fa142 Mon Sep 17 00:00:00 2001 From: Jackson Brammer Date: Tue, 9 Sep 2025 18:04:41 -0700 Subject: [PATCH 022/147] Fix pull-to-refresh in browser drawer non-table areas - Added handle touch handling to prevent pull-to-refresh - Added CSS rules for browser wrapper/top-row/filters to prevent pull-to-refresh - Browser drawer now prevents pull-to-refresh in all areas except scrollable table --- src/App.jsx | 14 +++++++++++++- src/index.css | 9 +++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/App.jsx b/src/App.jsx index 9cb9f67..46b5e3c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -67,6 +67,9 @@ const AppContent = () => { e.target.closest('.browser-table') || e.target.closest('table') + // Check if touch is on drawer handle + const isHandleTouch = e.target.closest('.drawer-handle') + if (isTableTouch) { // For table touches, check scroll position const tableContainer = e.target.closest('.browser-table-container') @@ -78,9 +81,18 @@ const AppContent = () => { return } // Table is at top - allow swipe-to-dismiss + } else if (isHandleTouch) { + // For handle touches, always handle swipe-to-dismiss + // Prevent default to avoid pull-to-refresh + e.preventDefault() + setTouchStart(e.targetTouches[0].clientY) + setTouchCurrent(e.targetTouches[0].clientY) + setDrawerOffset(0) + return } - // For non-table touches OR table touches at top, handle swipe-to-dismiss + // For all other touches (non-table, non-handle), prevent pull-to-refresh + // and handle swipe-to-dismiss e.preventDefault() setTouchStart(e.targetTouches[0].clientY) setTouchCurrent(e.targetTouches[0].clientY) diff --git a/src/index.css b/src/index.css index 75b4548..e7b48ea 100644 --- a/src/index.css +++ b/src/index.css @@ -1512,6 +1512,15 @@ textarea, touch-action: pan-y !important; /* Allow vertical scrolling, JavaScript handles swipe-to-dismiss */ } + /* BROWSER/CREATOR DRAWER: Prevent pull-to-refresh in non-table areas */ + .drawer-body .browser-wrapper, + .drawer-body .browser-top-row, + .drawer-body .browser-filters { + overscroll-behavior: none !important; /* Prevent pull-to-refresh */ + overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ + touch-action: manipulation !important; /* Prevent pull-to-refresh, allow tap */ + } + /* EXPANDED CARD DRAWER: Prioritize scrolling, only prevent pull-to-refresh */ .drawer-body .expanded-card { overscroll-behavior: none !important; /* Prevent pull-to-refresh */ From 3f9c215c066feae9a77495916555680f22ec83be Mon Sep 17 00:00:00 2001 From: Jackson Brammer Date: Tue, 9 Sep 2025 18:10:03 -0700 Subject: [PATCH 023/147] Fix browser drawer pull-to-refresh logic - Always prevent pull-to-refresh everywhere in browser drawer - Handle table scrolling via CSS touch-action: pan-y - Simplified JavaScript to always preventDefault and handle swipe-to-dismiss --- src/App.jsx | 34 ++-------------------------------- src/index.css | 4 ++-- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 46b5e3c..2de6929 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -60,39 +60,9 @@ const AppContent = () => { const [drawerOffset, setDrawerOffset] = useState(0) const handleTouchStart = (e) => { - // BROWSER/CREATOR DRAWER STRATEGY: Handle table scrolling intelligently + // BROWSER/CREATOR DRAWER STRATEGY: Prevent pull-to-refresh everywhere, handle table scrolling via CSS - // Check if touch is on the browser table - const isTableTouch = e.target.closest('.browser-table-container') || - e.target.closest('.browser-table') || - e.target.closest('table') - - // Check if touch is on drawer handle - const isHandleTouch = e.target.closest('.drawer-handle') - - if (isTableTouch) { - // For table touches, check scroll position - const tableContainer = e.target.closest('.browser-table-container') - const isTableAtTop = tableContainer && tableContainer.scrollTop <= 10 - - if (!isTableAtTop) { - // Table is scrolled down - allow normal table scrolling - // Don't prevent default - let browser handle table scrolling - return - } - // Table is at top - allow swipe-to-dismiss - } else if (isHandleTouch) { - // For handle touches, always handle swipe-to-dismiss - // Prevent default to avoid pull-to-refresh - e.preventDefault() - setTouchStart(e.targetTouches[0].clientY) - setTouchCurrent(e.targetTouches[0].clientY) - setDrawerOffset(0) - return - } - - // For all other touches (non-table, non-handle), prevent pull-to-refresh - // and handle swipe-to-dismiss + // Always prevent pull-to-refresh and handle swipe-to-dismiss e.preventDefault() setTouchStart(e.targetTouches[0].clientY) setTouchCurrent(e.targetTouches[0].clientY) diff --git a/src/index.css b/src/index.css index e7b48ea..99c665b 100644 --- a/src/index.css +++ b/src/index.css @@ -1502,14 +1502,14 @@ textarea, overflow: hidden; } - /* BROWSER/CREATOR DRAWER: Handle table scrolling intelligently */ + /* BROWSER/CREATOR DRAWER: Handle table scrolling via CSS */ .drawer-body .browser-table-container { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: none !important; /* Prevent pull-to-refresh */ overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ - touch-action: pan-y !important; /* Allow vertical scrolling, JavaScript handles swipe-to-dismiss */ + touch-action: pan-y !important; /* Allow vertical scrolling only */ } /* BROWSER/CREATOR DRAWER: Prevent pull-to-refresh in non-table areas */ From fddefea24c19772feb56ce493e0876129498de53 Mon Sep 17 00:00:00 2001 From: Jackson Brammer Date: Tue, 9 Sep 2025 19:11:47 -0700 Subject: [PATCH 024/147] Browser filters and dropdowns - Persist selected filters per type (adv/env) in localStorage - Reconcile saved filters with available data on type switch to avoid blanks - Fix crash from effect using uninitialized variables - Keep dropdown open when selecting; close only on click-away - Position dropdowns under header cell; clamp to viewport - Size dropdowns to content with max-width, prevent overflow - Redesign dropdown options with checkboxes; remove purple flood and headers - Lighter separators, improved hover; tooltips show selection count - Minor CSS tweaks and cleanup --- src/App.jsx | 23 ++- src/components/Browser.jsx | 301 +++++++++++++++++++++++++------------ src/index.css | 197 +++++++++++++++++++----- 3 files changed, 384 insertions(+), 137 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 2de6929..fa5f045 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -60,9 +60,28 @@ const AppContent = () => { const [drawerOffset, setDrawerOffset] = useState(0) const handleTouchStart = (e) => { - // BROWSER/CREATOR DRAWER STRATEGY: Prevent pull-to-refresh everywhere, handle table scrolling via CSS + // BROWSER/CREATOR DRAWER STRATEGY: Handle header vs content differently - // Always prevent pull-to-refresh and handle swipe-to-dismiss + // Check if touch is on the browser header (search/create row) + const isHeaderTouch = e.target.closest('.browser-header') + + // Check if touch is on the browser content (scrollable area) + const isContentTouch = e.target.closest('.browser-content') + + if (isContentTouch) { + // For content touches, check scroll position + const contentContainer = e.target.closest('.browser-content') + const isContentAtTop = contentContainer && contentContainer.scrollTop <= 10 + + if (!isContentAtTop) { + // Content is scrolled down - allow normal scrolling + // Don't prevent default - let browser handle scrolling + return + } + // Content is at top - handle swipe-to-dismiss + } + + // For header touches OR content touches at top, prevent pull-to-refresh and handle swipe-to-dismiss e.preventDefault() setTouchStart(e.targetTouches[0].clientY) setTouchCurrent(e.targetTouches[0].clientY) diff --git a/src/components/Browser.jsx b/src/components/Browser.jsx index 0dd285c..955a18c 100644 --- a/src/components/Browser.jsx +++ b/src/components/Browser.jsx @@ -1,7 +1,7 @@ import React, { useState, useMemo, useEffect, useRef } from 'react' import Button from './Buttons' import Cards from './Cards' -import { Swords, TreePine, ArrowLeft, Plus, Star, Skull } from 'lucide-react' +import { Swords, TreePine, Plus, Star, Skull, Filter, Square, CheckSquare } from 'lucide-react' import adversariesData from '../data/adversaries.json' import environmentsData from '../data/environments.json' @@ -15,21 +15,26 @@ const Browser = ({ console.log('Browser component is being used, not List component') const [searchTerm, setSearchTerm] = useState('') - // Load filter state from localStorage - const getInitialFilterState = () => { - const savedFilters = localStorage.getItem(`browser-filters-${type}`) - if (savedFilters) { + // Load selected filter arrays from localStorage per type + const getInitialSelectedFilters = () => { + const saved = localStorage.getItem(`browser-filters-${type}`) + if (saved) { try { - return JSON.parse(savedFilters) + const parsed = JSON.parse(saved) + return { + tiers: Array.isArray(parsed.tiers) ? parsed.tiers : [], + types: Array.isArray(parsed.types) ? parsed.types : [] + } } catch (e) { - console.warn('Failed to parse saved filter state:', e) + console.warn('Failed to parse saved selected filters:', e) } } - return { tier: '', type: '' } + return { tiers: [], types: [] } } - const [filterTier, setFilterTier] = useState(getInitialFilterState().tier) - const [filterType, setFilterType] = useState(getInitialFilterState().type) + // Multi-select filter states + const [selectedTiers, setSelectedTiers] = useState(getInitialSelectedFilters().tiers) + const [selectedTypes, setSelectedTypes] = useState(getInitialSelectedFilters().types) // Load initial sort state synchronously to prevent jitter const getInitialSortState = () => { const savedSort = localStorage.getItem(`browser-sort-${type}`) @@ -52,6 +57,10 @@ const Browser = ({ const [showTierDropdown, setShowTierDropdown] = useState(false) const [showTypeDropdown, setShowTypeDropdown] = useState(false) + // Refs for filter buttons + const tierFilterRef = useRef(null) + const typeFilterRef = useRef(null) + // Load expanded card state from localStorage const getInitialExpandedCard = () => { const savedExpanded = localStorage.getItem(`browser-expanded-${type}`) @@ -129,13 +138,33 @@ const Browser = ({ } }, [expandedCard, type]) - // Save filter state to localStorage whenever it changes + // Persist selected filters per type (avoid writing on type change until values load) useEffect(() => { localStorage.setItem(`browser-filters-${type}`, JSON.stringify({ - tier: filterTier, - type: filterType + tiers: selectedTiers, + types: selectedTypes })) - }, [filterTier, filterType, type]) + }, [selectedTiers, selectedTypes]) + + // When switching data type, load its saved filters + useEffect(() => { + const saved = localStorage.getItem(`browser-filters-${type}`) + if (saved) { + try { + const parsed = JSON.parse(saved) + setSelectedTiers(Array.isArray(parsed.tiers) ? parsed.tiers : []) + setSelectedTypes(Array.isArray(parsed.types) ? parsed.types : []) + } catch (e) { + setSelectedTiers([]) + setSelectedTypes([]) + } + } else { + setSelectedTiers([]) + setSelectedTypes([]) + } + }, [type]) + + // (moved below itemTypes/itemTiers) // Use imported data directly - only the relevant type const adversaryData = adversariesData.adversaries || [] @@ -145,11 +174,12 @@ const Browser = ({ const currentData = type === 'adversary' ? adversaryData : environmentData const dataType = type === 'adversary' ? 'adversary' : 'environment' - // Close dropdowns when clicking outside + // Close dropdowns when clicking anywhere outside the dropdown or filter buttons useEffect(() => { const handleClickOutside = (event) => { - if (!event.target.closest('th')) { - setShowCategoryDropdown(false) + const clickedInsideDropdown = event.target.closest('.filter-dropdown') + const clickedFilterButton = event.target.closest('.header-filter-icon') + if (!clickedInsideDropdown && !clickedFilterButton) { setShowTierDropdown(false) setShowTypeDropdown(false) } @@ -185,6 +215,26 @@ const Browser = ({ return tiers.sort((a, b) => a - b) }, [unifiedData]) + // Reconcile saved filters with available data to avoid empty results + useEffect(() => { + // Types + if (selectedTypes.length > 0) { + const validTypes = new Set(itemTypes) + const nextTypes = selectedTypes.filter(t => validTypes.has(t)) + if (nextTypes.length !== selectedTypes.length) { + setSelectedTypes(nextTypes.length > 0 ? nextTypes : []) + } + } + // Tiers (compare as strings) + if (selectedTiers.length > 0) { + const validTiers = new Set(itemTiers.map(t => String(t))) + const nextTiers = selectedTiers.filter(t => validTiers.has(String(t))) + if (nextTiers.length !== selectedTiers.length) { + setSelectedTiers(nextTiers.length > 0 ? nextTiers : []) + } + } + }, [itemTypes, itemTiers]) + // Sort and filter items const filteredAndSortedItems = useMemo(() => { let filtered = unifiedData.filter(item => { @@ -192,8 +242,8 @@ const Browser = ({ item.name.toLowerCase().includes(searchTerm.toLowerCase()) || (item.description && item.description.toLowerCase().includes(searchTerm.toLowerCase())) - const matchesTier = filterTier === '' || item.tier.toString() === filterTier - const matchesType = filterType === '' || item.displayType === filterType + const matchesTier = selectedTiers.length === 0 || selectedTiers.includes(item.tier.toString()) + const matchesType = selectedTypes.length === 0 || selectedTypes.includes(item.displayType) return matchesSearch && matchesTier && matchesType }) @@ -252,7 +302,7 @@ const Browser = ({ }) return filtered - }, [unifiedData, searchTerm, filterTier, filterType, sortFields]) + }, [unifiedData, searchTerm, selectedTiers, selectedTypes, sortFields]) const handleSort = (field) => { setSortFields(prevSortFields => { @@ -278,6 +328,47 @@ const Browser = ({ } } + // Tooltip helpers for filter buttons + const tierTooltip = selectedTiers.length === 0 ? 'All' : `${selectedTiers.length} selected` + const typeTooltip = selectedTypes.length === 0 ? 'All' : `${selectedTypes.length} selected` + + // Multi-select handlers + const handleTierSelect = (tier) => { + setSelectedTiers(prev => { + if (prev.includes(tier)) { + return prev.filter(t => t !== tier) + } else { + return [...prev, tier] + } + }) + } + + const handleTypeSelect = (type) => { + setSelectedTypes(prev => { + if (prev.includes(type)) { + return prev.filter(t => t !== type) + } else { + return [...prev, type] + } + }) + } + + // Calculate dropdown position based on the header cell (th); let CSS size it to content + const getDropdownStyle = (buttonRef) => { + if (!buttonRef?.current) return {} + const thEl = buttonRef.current.closest('th') + if (!thEl) return {} + const thRect = thEl.getBoundingClientRect() + const gutter = 8 + const left = Math.max(gutter, thRect.left) + return { + position: 'fixed', + top: thRect.bottom + 4, + left, + zIndex: 99999 + } + } + const handleAddFromDatabase = (itemData) => { console.log('Browser handleAddFromDatabase called with:', itemData) @@ -305,20 +396,11 @@ const Browser = ({ onMouseDown={(e) => e.stopPropagation()} onMouseUp={(e) => e.stopPropagation()} > - {/* Back Arrow, Search, and Custom Button Row */} -
- - + {/* Fixed Header Row */} +
setSearchTerm(e.target.value)} className="browser-search-input" @@ -328,70 +410,15 @@ const Browser = ({ action="add" onClick={() => onCreateCustom && onCreateCustom(type)} size="sm" - className="browser-add-btn" + className="browser-create-btn" > - + Create
- {/* Filter Controls */} -
- - - + {/* Scrollable Content */} +
- {/* Filter Dropdowns */} - {showTierDropdown && ( -
-
{ setFilterTier(''); setShowTierDropdown(false) }}> - All Tiers -
- {itemTiers.map(tier => ( -
{ setFilterTier(tier.toString()); setShowTierDropdown(false) }} - > - Tier {tier} -
- ))} -
- )} - - {showTypeDropdown && ( -
-
{ setFilterType(''); setShowTypeDropdown(false) }}> - All Types -
- {itemTypes.map(type => ( -
{ setFilterType(type); setShowTypeDropdown(false) }} - > - {type} -
- ))} -
- )} -
- - {/* Items Table */} -
@@ -405,19 +432,105 @@ const Browser = ({ onClick={() => handleSort('tier')} className={`sortable ${sortFields[0]?.field === 'tier' ? 'active' : ''} ${sortFields[0]?.field === 'tier' ? sortFields[0].direction : ''}`} > - +
+ Tier + + {showTierDropdown && ( +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > +
setSelectedTiers([])} + > + {selectedTiers.length === 0 ? : } + All +
+ {itemTiers.map(tier => { + const isSelected = selectedTiers.includes(tier.toString()) + return ( +
handleTierSelect(tier.toString())} + > + {isSelected ? : } + {tier} +
+ ) + })} +
+ )} +
diff --git a/src/index.css b/src/index.css index 99c665b..5b53d40 100644 --- a/src/index.css +++ b/src/index.css @@ -1380,14 +1380,14 @@ textarea, /* Visual feedback for expandable cards on mobile */ .sortable-item .card.compact { - cursor: pointer; - } - + cursor: pointer; +} + .sortable-item .card.compact:hover { - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); - } - + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + /* Mobile drawer styles */ .mobile-drawer { position: fixed; @@ -1502,8 +1502,8 @@ textarea, overflow: hidden; } - /* BROWSER/CREATOR DRAWER: Handle table scrolling via CSS */ - .drawer-body .browser-table-container { + /* BROWSER/CREATOR DRAWER: Let entire browser content scroll */ + .drawer-body .browser-content { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; @@ -1512,10 +1512,8 @@ textarea, touch-action: pan-y !important; /* Allow vertical scrolling only */ } - /* BROWSER/CREATOR DRAWER: Prevent pull-to-refresh in non-table areas */ - .drawer-body .browser-wrapper, - .drawer-body .browser-top-row, - .drawer-body .browser-filters { + /* BROWSER/CREATOR DRAWER: Prevent pull-to-refresh in header */ + .drawer-body .browser-header { overscroll-behavior: none !important; /* Prevent pull-to-refresh */ overscroll-behavior-y: none !important; /* Prevent vertical pull-to-refresh */ touch-action: manipulation !important; /* Prevent pull-to-refresh, allow tap */ @@ -2199,7 +2197,7 @@ textarea, .bulk-clear-btn { width: 100%; justify-content: center; - } +} .countdown-creator-compact { @@ -6004,8 +6002,6 @@ textarea, .browser-wrapper { display: flex; flex-direction: column; - gap: 1rem; - padding: 0; /* Remove padding */ background: var(--bg-primary); border-radius: 0.5rem; height: 100%; @@ -6014,11 +6010,21 @@ textarea, overflow: hidden; } -.browser-top-row { +.browser-header { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} + +.browser-content { + flex: 1; + overflow-y: auto; + overflow-x: visible; /* Allow dropdowns to extend horizontally */ + padding: 0.75rem; } .browser-search-input { @@ -6029,12 +6035,91 @@ textarea, border-radius: 0.5rem; color: var(--text-primary); font-size: 1rem; + height: 2.5rem; /* Match button height */ + box-sizing: border-box; +} + +.browser-create-btn { + flex-shrink: 0; + background: var(--purple) !important; + color: white !important; + border: 1px solid var(--purple) !important; +} + +.browser-create-btn:hover { + background: var(--purple-dark) !important; + border-color: var(--purple-dark) !important; +} + +.header-with-filter { + display: flex; + align-items: center; + justify-content: center; /* Center the content */ + gap: 0.25rem; /* Reduced from 0.5rem */ + position: relative; /* Enable absolute positioning for dropdowns */ +} + +.header-filter-icon { + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: 0.25rem; + border-radius: 0.25rem; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + position: relative; /* Enable absolute positioning for dropdowns */ +} + +.header-filter-icon:hover { + background: var(--bg-secondary); + color: var(--text-primary); +} + +/* Sort arrows */ +.sort-arrow { + color: var(--purple); + font-size: 0.875rem; + margin-left: 0.25rem; + font-weight: bold; +} + +/* Ensure table headers have enough space */ +.browser-content th { + min-width: 80px; + padding: 0.75rem 0.5rem; + white-space: nowrap; + position: relative; /* Enable absolute positioning for dropdowns */ +} + +/* Specific column widths */ +.browser-content th:nth-child(1) { min-width: 150px; } /* Name */ +.browser-content th:nth-child(2) { min-width: 80px; } /* Tier */ +.browser-content th:nth-child(3) { min-width: 100px; } /* Type */ +.browser-content th:nth-child(4) { min-width: 90px; } /* Difficulty */ +.browser-content th:nth-child(5) { min-width: 60px; } /* Action */ + +.browser-content th.sortable.asc::after { + content: '↑'; + color: var(--purple); + opacity: 1; +} + +.browser-content th.sortable.desc::after { + content: '↓'; + color: var(--purple); + opacity: 1; +} + +.browser-content th.sortable:hover::after { + opacity: 0.8; } .browser-search-input:focus { outline: none; - border-color: var(--purple); - box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2); + border-color: var(--border); } .browser-top-row .btn { @@ -6102,7 +6187,7 @@ textarea, .browser-table-container { flex: 1; - overflow: hidden; /* Prevent horizontal overflow */ + overflow: visible; /* Allow dropdowns to show */ background: var(--bg-card); border-radius: 0.5rem; border: 1px solid var(--border); @@ -6161,14 +6246,14 @@ textarea, .browser-table th:nth-child(2), .browser-table td:nth-child(2) { - width: 50px; /* Tier column - wider for icon */ + width: 50px; /* Tier column - minimal width for numbers */ text-align: center; - overflow: visible; /* Don't hide icon */ - text-overflow: unset; /* No ellipsis for icons */ + overflow: visible; /* Don't hide text */ + text-overflow: unset; /* No ellipsis for text */ } .browser-table th:nth-child(2) { - width: 50px !important; /* Restore fixed width */ + width: 50px !important; /* Minimal width for "Tier" header */ text-align: center !important; } @@ -6179,25 +6264,28 @@ textarea, .browser-table th:nth-child(3), .browser-table td:nth-child(3) { - width: 100px; /* Type column - wider to fit "Exploration" */ + width: 100px; /* Type column - fit "Type" header */ overflow: hidden; text-overflow: ellipsis; + text-align: center; /* Center the type content */ } -.browser-table td:nth-child(3) { - text-align: center !important; /* Center the type content */ +.browser-table th:nth-child(3) { + text-align: center !important; /* Center the type header */ + padding-left: 0.5rem !important; /* Add left padding to balance the filter button */ + padding-right: 0.5rem !important; /* Add right padding to balance the filter button */ } .browser-table th:nth-child(4), .browser-table td:nth-child(4) { - width: 50px; /* Difficulty column - wider for icon */ + width: 40px; /* Diff column - minimal width for "Diff" header */ text-align: center; - overflow: visible; /* Don't hide icon */ - text-overflow: unset; /* No ellipsis for icons */ + overflow: visible; /* Don't hide text */ + text-overflow: unset; /* No ellipsis for text */ } .browser-table th:nth-child(4) { - width: 50px !important; /* Restore fixed width */ + width: 40px !important; /* Minimal width for "Diff" header */ text-align: center !important; } @@ -6292,29 +6380,56 @@ textarea, } .filter-dropdown { - position: absolute; - top: 100%; - left: 0; + position: fixed; background: var(--bg-card); border: 1px solid var(--border); border-radius: 0.5rem; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - z-index: 1000; - min-width: 150px; - margin-top: 0.25rem; /* Add small gap from filter buttons */ + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25); + z-index: 99999; + margin-top: 0.25rem; + overflow: hidden; + width: max-content; + max-width: 60vw; } + .filter-option { - padding: 0.75rem 1rem; + padding: 0.25rem 0.5rem; /* Match table row padding */ cursor: pointer; color: var(--text-primary); + font-size: 1rem; /* Explicitly match default font size */ + height: 35px; /* Match table row height */ + display: flex; + align-items: center; + justify-content: flex-start; + border-bottom: 1px solid rgba(255,255,255,0.06); transition: background-color 0.2s ease; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.filter-option:hover { - background: var(--bg-hover); +.filter-option:last-child { + border-bottom: none; } +.filter-option:hover { background: var(--bg-hover); } + +.filter-option.selected .check-icon { color: var(--text-primary); } +.filter-option.selected .filter-label { color: var(--text-primary); } + +.check-icon { + width: 20px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + margin-right: 0.5rem; + color: var(--text-secondary); +} + +.filter-label { flex: 1; } + .filter-option:first-child { border-radius: 0.5rem 0.5rem 0 0; } From 891d78b5d4aee9792e02fd673cf044409a50db76 Mon Sep 17 00:00:00 2001 From: Jackson Brammer Date: Tue, 9 Sep 2025 20:54:07 -0700 Subject: [PATCH 025/147] Browser improvements and drawer fixes - Add filter active indicators with purple icon and centered dot badge - Tighten browser header layout: compact search/create row, remove gap above table - Fix mobile/desktop transitions to prefer drawers over panel switching - Apply duplicate handling to environments (same as adversaries with numbering) - Remove drawer body padding, add handle margin for better touch targets --- src/App.jsx | 25 +++++--------- src/GameStateContext.jsx | 68 ++++++++++++++++++++++++++++++++------ src/components/Browser.jsx | 8 +++-- src/index.css | 45 ++++++++++++++++++++----- 4 files changed, 110 insertions(+), 36 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index fa5f045..80b7e30 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -329,24 +329,17 @@ const AppContent = () => { setIsMobile(nowMobile) - // When transitioning from desktop to mobile, preserve current panel focus + // Transition to mobile: prefer drawers instead of switching panels if (!wasMobile && nowMobile) { - // If browser is open, keep it visible in mobile - if (rightColumnMode === 'database') { - setMobileView('right') - } - // If item is selected, keep it visible in mobile - else if (rightColumnMode === 'item') { - setMobileView('right') - } - // If creator is open, keep it visible in mobile - else if (rightColumnMode === 'creator') { - setMobileView('right') - } - // Otherwise default to left panel - else { - setMobileView('left') + if (rightColumnMode === 'database' || rightColumnMode === 'creator') { + setMobileDrawerOpen(true) } + // Do not force `mobileView` to 'right'; underlying app remains on left + } + + // Transition to desktop: close mobile drawers, content remains in right panel + if (wasMobile && !nowMobile) { + if (mobileDrawerOpen) setMobileDrawerOpen(false) } } diff --git a/src/GameStateContext.jsx b/src/GameStateContext.jsx index 0fd20ab..755ae65 100644 --- a/src/GameStateContext.jsx +++ b/src/GameStateContext.jsx @@ -303,17 +303,65 @@ export const GameStateProvider = ({ children }) => { // Environment management const createEnvironment = (environmentData) => { + // Generate unique ID with timestamp and random component + const uniqueId = `env-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` + + // Check for existing environments with the same name to add numbering + const existingEnvironments = gameState.environments || [] + const sameNameEnvironments = existingEnvironments.filter(env => { + // Extract base name (without existing number suffix) + const baseName = env.name.replace(/\s+\(\d+\)$/, '') + return baseName === environmentData.name + }) + + let displayName = environmentData.name || 'Unknown' + + if (sameNameEnvironments.length === 0) { + // First environment of this type - no suffix needed + displayName = environmentData.name + } else if (sameNameEnvironments.length === 1) { + // Second environment - first one gets (1), new one gets (2) + const firstEnvironment = sameNameEnvironments[0] + const firstEnvironmentBaseName = firstEnvironment.name.replace(/\s+\(\d+\)$/, '') + + // Update the first environment to have (1) suffix + const updatedEnvironments = gameState.environments.map(env => + env.id === firstEnvironment.id + ? { ...env, name: `${firstEnvironmentBaseName} (1)` } + : env + ) + + // Update the game state with the renamed first environment + setGameState(prev => ({ + ...prev, + environments: updatedEnvironments + })) + + // New environment gets (2) + displayName = `${environmentData.name} (2)` + } else { + // Find the next available number + const usedNumbers = sameNameEnvironments.map(env => { + const match = env.name.match(/\((\d+)\)$/) + return match ? parseInt(match[1]) : null + }).filter(num => num !== null) + + // Find the first unused number starting from 1 + let nextNumber = 1 + while (usedNumbers.includes(nextNumber)) { + nextNumber++ + } + + displayName = `${environmentData.name} (${nextNumber})` + } + const newEnvironment = { - id: `env-${Date.now()}`, - name: environmentData.name || 'Unknown', - type: environmentData.type || 'Unknown', - tier: environmentData.tier || 1, - difficulty: environmentData.difficulty || 'Medium', - description: environmentData.description || '', - impulses: environmentData.impulses || [], - features: environmentData.features || [], - isVisible: true, - ...environmentData + // Start with all properties from environmentData + ...environmentData, + // Override with our specific values to ensure correct initialization + id: uniqueId, + name: displayName, + isVisible: true }; setGameState(prev => ({ diff --git a/src/components/Browser.jsx b/src/components/Browser.jsx index 955a18c..1adbbc5 100644 --- a/src/components/Browser.jsx +++ b/src/components/Browser.jsx @@ -331,6 +331,8 @@ const Browser = ({ // Tooltip helpers for filter buttons const tierTooltip = selectedTiers.length === 0 ? 'All' : `${selectedTiers.length} selected` const typeTooltip = selectedTypes.length === 0 ? 'All' : `${selectedTypes.length} selected` + const isTierFiltered = selectedTiers.length > 0 + const isTypeFiltered = selectedTypes.length > 0 // Multi-select handlers const handleTierSelect = (tier) => { @@ -440,10 +442,11 @@ const Browser = ({ e.stopPropagation() handleFilter('tier') }} - className="header-filter-icon" + className={`header-filter-icon ${isTierFiltered ? 'active' : ''}`} title={`Filter by Tier: ${tierTooltip}`} > +
handleSort('displayType')} className={`sortable ${sortFields[0]?.field === 'displayType' ? 'active' : ''} ${sortFields[0]?.field === 'displayType' ? sortFields[0].direction : ''}`} > - Type +
+ Type + + {showTypeDropdown && ( +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > +
setSelectedTypes([])} + > + {selectedTypes.length === 0 ? : } + All +
+ {itemTypes.map(type => { + const isSelected = selectedTypes.includes(type) + return ( +
handleTypeSelect(type)} + > + {isSelected ? : } + {type} +
+ ) + })} +
+ )} +
handleSort('displayDifficulty')} className={`sortable ${sortFields[0]?.field === 'displayDifficulty' ? 'active' : ''} ${sortFields[0]?.field === 'displayDifficulty' ? sortFields[0].direction : ''}`} > - +
+ Diff +