diff --git a/iframe.html b/iframe.html index a6801ec..38573af 100644 --- a/iframe.html +++ b/iframe.html @@ -1264,6 +1264,66 @@ text-align: center; } } + + /* Lock Icon Styles */ + .dog-calculator-lock-icon { + display: inline-block; + width: 16px; + height: 16px; + margin-left: 8px; + cursor: pointer; + font-size: 14px; + line-height: 1; + vertical-align: middle; + transition: all 0.2s ease; + user-select: none; + opacity: 0.6; + } + + .dog-calculator-lock-icon:hover { + opacity: 1; + transform: scale(1.1); + } + + .dog-calculator-lock-icon.locked { + color: #f19a5f; + opacity: 1; + font-weight: bold; + } + + .dog-calculator-lock-icon.unlocked { + color: #635870; + } + + .dog-calculator-lock-icon.disabled { + opacity: 0.3; + cursor: not-allowed; + } + + .dog-calculator-lock-icon.disabled:hover { + opacity: 0.3; + transform: none; + } + + /* Dark theme support for lock icons */ + .dog-calculator-container.theme-dark .dog-calculator-lock-icon.unlocked { + color: #b8b0c2; + } + + .dog-calculator-container.theme-dark .dog-calculator-lock-icon.locked { + color: #f19a5f; + } + + /* System theme support for lock icons */ + @media (prefers-color-scheme: dark) { + .dog-calculator-container.theme-system .dog-calculator-lock-icon.unlocked { + color: #b8b0c2; + } + + .dog-calculator-container.theme-system .dog-calculator-lock-icon.locked { + color: #f19a5f; + } + } @@ -1534,7 +1594,8 @@ name: `Food Source ${this.foodSources.length + 1}`, energy: '', energyUnit: this.isImperial ? 'kcalcup' : 'kcal100g', - percentage: this.foodSources.length === 0 ? 100 : 0 + percentage: this.foodSources.length === 0 ? 100 : 0, + isLocked: false }; this.foodSources.push(foodSource); @@ -1575,12 +1636,24 @@ const count = this.foodSources.length; if (count === 0) return; - const equalPercentage = Math.floor(100 / count); - const remainder = 100 - (equalPercentage * count); + // Only redistribute among unlocked sources + const unlockedSources = this.foodSources.filter(fs => !fs.isLocked); + const lockedSources = this.foodSources.filter(fs => fs.isLocked); + + // Calculate total locked percentage + const totalLockedPercentage = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0); + + // Available percentage for unlocked sources + const availablePercentage = 100 - totalLockedPercentage; + + if (unlockedSources.length > 0) { + const equalPercentage = Math.floor(availablePercentage / unlockedSources.length); + const remainder = availablePercentage - (equalPercentage * unlockedSources.length); - this.foodSources.forEach((fs, index) => { - fs.percentage = equalPercentage + (index < remainder ? 1 : 0); - }); + unlockedSources.forEach((fs, index) => { + fs.percentage = equalPercentage + (index < remainder ? 1 : 0); + }); + } // Update the UI sliders and inputs this.updatePercentageInputs(); @@ -1605,31 +1678,41 @@ this.foodSources[changedIndex].percentage = newPercentage; - // Distribute the difference among other sources proportionally - const otherSources = this.foodSources.filter((fs, index) => index !== changedIndex); - if (otherSources.length === 0) return; - - const totalOtherPercentage = otherSources.reduce((sum, fs) => sum + fs.percentage, 0); + // Only redistribute among unlocked sources (excluding the changed one) + const otherUnlockedSources = this.foodSources.filter((fs, index) => + index !== changedIndex && !fs.isLocked + ); - if (totalOtherPercentage === 0) { - // If all others are 0, distribute equally - const remainingPercentage = 100 - newPercentage; - const equalShare = Math.floor(remainingPercentage / otherSources.length); - const remainder = remainingPercentage - (equalShare * otherSources.length); + if (otherUnlockedSources.length === 0) return; + + // Calculate total locked percentage (excluding the changed source) + const lockedSources = this.foodSources.filter((fs, index) => + index !== changedIndex && fs.isLocked + ); + const totalLockedPercentage = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0); + + // Available percentage for unlocked sources + const availablePercentage = 100 - newPercentage - totalLockedPercentage; + + const totalUnlockedPercentage = otherUnlockedSources.reduce((sum, fs) => sum + fs.percentage, 0); + + if (totalUnlockedPercentage === 0) { + // If all other unlocked sources are 0, distribute equally + const equalShare = Math.floor(availablePercentage / otherUnlockedSources.length); + const remainder = availablePercentage - (equalShare * otherUnlockedSources.length); - otherSources.forEach((fs, index) => { + otherUnlockedSources.forEach((fs, index) => { fs.percentage = equalShare + (index < remainder ? 1 : 0); }); } else { - // Distribute proportionally - const targetTotal = 100 - newPercentage; - const scale = targetTotal / totalOtherPercentage; + // Distribute proportionally among unlocked sources + const scale = availablePercentage / totalUnlockedPercentage; let distributedTotal = 0; - otherSources.forEach((fs, index) => { - if (index === otherSources.length - 1) { + otherUnlockedSources.forEach((fs, index) => { + if (index === otherUnlockedSources.length - 1) { // Last item gets the remainder to ensure exact 100% - fs.percentage = targetTotal - distributedTotal; + fs.percentage = availablePercentage - distributedTotal; } else { fs.percentage = Math.round(fs.percentage * scale); distributedTotal += fs.percentage; @@ -1700,6 +1783,7 @@
{ @@ -1759,6 +1844,63 @@ if (removeBtn) { removeBtn.addEventListener('click', () => this.removeFoodSource(id)); } + + if (lockBtn) { + lockBtn.addEventListener('click', () => this.toggleLock(id)); + } + } + + toggleLock(id) { + const foodSource = this.foodSources.find(fs => fs.id === id); + if (!foodSource) return; + + // Check if we're trying to lock the last unlocked source + const unlockedSources = this.foodSources.filter(fs => !fs.isLocked); + if (unlockedSources.length === 1 && unlockedSources[0].id === id) { + // Cannot lock the last unlocked source + alert('At least one food source must remain flexible for percentage adjustments.'); + return; + } + + // Toggle lock state + foodSource.isLocked = !foodSource.isLocked; + this.updateLockIcon(id); + this.updateLockStates(); + } + + updateLockIcon(id) { + const foodSource = this.foodSources.find(fs => fs.id === id); + const lockIcon = document.getElementById(`lock-${id}`); + + if (!lockIcon || !foodSource) return; + + if (foodSource.isLocked) { + lockIcon.classList.remove('unlocked'); + lockIcon.classList.add('locked'); + lockIcon.title = 'Unlock this percentage'; + } else { + lockIcon.classList.remove('locked'); + lockIcon.classList.add('unlocked'); + lockIcon.title = 'Lock this percentage'; + } + } + + updateLockStates() { + const unlockedSources = this.foodSources.filter(fs => !fs.isLocked); + + // Update lock icon states - disable lock for last unlocked source + this.foodSources.forEach(fs => { + const lockIcon = document.getElementById(`lock-${fs.id}`); + if (lockIcon) { + if (!fs.isLocked && unlockedSources.length === 1) { + lockIcon.classList.add('disabled'); + lockIcon.title = 'Cannot lock - at least one source must remain flexible'; + } else { + lockIcon.classList.remove('disabled'); + lockIcon.title = fs.isLocked ? 'Unlock this percentage' : 'Lock this percentage'; + } + } + }); } updateFoodSourceData(id, field, value) { @@ -2123,7 +2265,8 @@ name: fs.name, percentage: fs.percentage, dailyGrams: dailyGramsForThisFood, - calories: dailyCaloriesForThisFood + calories: dailyCaloriesForThisFood, + isLocked: fs.isLocked }); totalDailyGrams += dailyGramsForThisFood; @@ -2154,7 +2297,7 @@ if (foodBreakdownList && foodBreakdowns.length > 1) { const breakdownHTML = foodBreakdowns.map(breakdown => `
- ${breakdown.name} (${breakdown.percentage}%): + ${breakdown.name} (${breakdown.percentage}%${breakdown.isLocked ? ' - locked' : ''}): ${this.formatNumber(breakdown.dailyGrams, 1)} g/day
`).join('');