diff --git a/README.md b/README.md index f7b5cac..b957374 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # πŸ• Sundog Dog Food Calorie Calculator -A professional veterinary nutrition tool for calculating dogs' daily calorie requirements and food amounts. Features advanced multi-food source management, percentage locking, and detailed food amount breakdowns. Built for embedding on websites with complete brand protection options. +A professional veterinary nutrition tool for calculating dogs' daily calorie requirements and food amounts. Features advanced multi-food source management, percentage locking, and detailed food amount breakdowns. Distributed as a standalone page; third‑party embedding is no longer supported. **By [Canine Nutrition and Wellness](https://caninenutritionandwellness.com)** @@ -28,10 +28,9 @@ A professional veterinary nutrition tool for calculating dogs' daily calorie req - **Lock Indicators**: Visual indicators showing which percentages are locked ### User Experience -- **Scalable Widget**: Easily resize from 50% to 200% with data attributes +- **Scalable UI**: Resize from 50% to 200% via query params - **Theme Support**: Light, dark, and system themes - **Responsive Design**: Mobile-first, optimized layouts for all devices -- **Two Embedding Options**: JavaScript widget and iframe - **Accessibility**: Full keyboard navigation and screen reader support ### Brand & Integration @@ -39,36 +38,16 @@ A professional veterinary nutrition tool for calculating dogs' daily calorie req - **Professional Design**: Clean, veterinary-grade interface - **Brand Protection**: Complete iframe isolation option -## πŸš€ Quick Start +## πŸš€ Usage + +- Open `iframe.html` locally, or host it as a standalone page on your site. +- Embedding is allowed only on these domains: `caninenutritionandwellness.com`, `www.caninenutritionandwellness.com`. +- Use an iframe to embed on your site, for example: -### Option 1: JavaScript Widget (Recommended) ```html - - -
- - -
-``` - -### Option 2: iframe Embed -```html - - - - ``` @@ -134,7 +113,7 @@ Choose from three themes: - `system` - Follows user's OS preference (default) ### Scale Options -Resize the widget from 50% to 200%: +Resize the interface from 50% to 200%: - Range: `0.5` to `2.0` - Default: `1.0` (100% size) - Examples: `0.8` (80%), `1.2` (120%), `1.5` (150%) @@ -146,27 +125,21 @@ Support for regional differences: - `kcal/cup` - US/Canada dry food - `kcal/can` - US/Canada wet food -### Advanced JavaScript Usage -```javascript -new DogCalorieCalculatorWidget(container, { - theme: 'dark', // 'light', 'dark', 'system' - scale: 1.2 // 0.5 to 2.0 -}); -``` + ## πŸ› οΈ Development ### Build System -This project uses a single source of truth approach: +This project uses an organized source layout compiled into a single page: -- **Master Source**: `iframe.html` - Contains all functionality, styles, and calculations -- **Build Script**: `build.js` - Generates the widget from iframe.html -- **Generated Output**: `sundog-dog-food-calculator.js` - Embeddable widget +- **Sources**: `src/` (HTML, CSS, JS modules) +- **Build Script**: `build.js` - Generates `iframe.html` from `src/` +- **Output**: `iframe.html` - Standalone calculator page ### Development Workflow -1. **Make changes to `iframe.html`** - Edit calculations, design, layout, or functionality +1. **Make changes in `src/`** - Edit calculations, design, layout, or functionality 2. **Run the build script**: `node build.js` -3. **Done!** - Both iframe and widget now have identical functionality +3. **Done!** - `iframe.html` is regenerated ### Why This Approach? - βœ… **Single Source of Truth** - No need to maintain two separate files @@ -175,18 +148,16 @@ This project uses a single source of truth approach: - βœ… **No Sync Issues** - Build script ensures consistency ### Build Script Features -- Extracts CSS, HTML, and JavaScript from iframe.html -- Transforms CSS classes for widget namespacing (`dog-calculator-` β†’ `dog-calc-`) +- Compiles CSS, HTML, and JavaScript from `src/` - Preserves all functionality including unit switching and calculations -- Maintains theme and scale support via data attributes +- Maintains theme and scale support via URL query parameters ## πŸ“ Project Structure ``` -β”œβ”€β”€ iframe.html # 🎯 MASTER SOURCE - Edit this file +β”œβ”€β”€ src/ # ✏️ Source (HTML/CSS/JS) β”œβ”€β”€ build.js # πŸ”§ Build script - Run after changes -β”œβ”€β”€ sundog-dog-food-calculator.js # πŸ“¦ Generated widget (don't edit) -β”œβ”€β”€ test-widget.html # πŸ§ͺ Test file for widget +β”œβ”€β”€ iframe.html # πŸ“¦ Generated standalone page └── README.md # πŸ“– This file ``` @@ -217,71 +188,32 @@ Colors automatically adapt to light/dark themes via CSS custom properties. | Working dog - heavy work | 5.0 | Intensive work | | Senior dog | 1.1 | Reduced activity | -## πŸ”§ Technical Implementation +## πŸ”§ Technical Notes +- Standalone page with theme and scale controls via URL params. +- Embedding is allowlisted at runtime to your domains and should be enforced with server headers. -### JavaScript Widget Features -- **Auto-initialization**: Detects `#dog-calorie-calculator` containers -- **CSS Namespacing**: All classes prefixed with `dog-calc-` -- **Shadow DOM Ready**: Prepared for better style isolation -- **Real-time Validation**: Input validation with error messages -- **Mobile Optimized**: Responsive breakpoints and touch-friendly +### Server Headers (required for robust enforcement) +Configure your server or CDN to send this header on `iframe.html`: -### iframe Features -- **Auto-resize**: Communicates height changes to parent -- **Style Isolation**: Complete protection from host site CSS -- **Loading Animation**: Smooth fade-in when ready -- **Cross-origin Ready**: PostMessage communication for integration +``` +Content-Security-Policy: frame-ancestors https://caninenutritionandwellness.com https://www.caninenutritionandwellness.com; +``` + +Optional legacy header (deprecated but harmless as a supplement): + +``` +X-Frame-Options: SAMEORIGIN +``` + +If you serve the calculator from a subdomain (e.g., `embed.caninenutritionandwellness.com`) and embed it on the root domain, prefer the CSP `frame-ancestors` directive above. ## πŸš€ Deployment Guide -### 1. Host the Files -Upload these files to your web server: -- `sundog-dog-food-calculator.js` (for widget embedding) -- `iframe.html` (for iframe embedding) - -### 2. Update URLs -Replace `https://yourdomain.com` in: -- `test-widget.html` examples -- `sundog-dog-food-calculator.js` comments -- This README - -### 3. CDN Distribution (Optional) -For better performance, serve the widget script via CDN: -- Use CloudFlare, AWS CloudFront, or similar -- Enable CORS headers for cross-origin requests -- Set appropriate cache headers (1 day for updates) - -### 4. Analytics Integration -Add tracking to understand usage: - -```javascript -// Track widget interactions -document.addEventListener('DOMContentLoaded', function() { - // Track when calculator is used - document.addEventListener('change', function(e) { - if (e.target.closest('.dog-calc-widget')) { - gtag('event', 'calculator_interaction', { - 'event_category': 'dog_calculator', - 'event_label': e.target.id - }); - } - }); -}); -``` +### Deployment +Host `iframe.html` (e.g., on `embed.caninenutritionandwellness.com`) and embed via iframe on your approved domains. ## πŸ”’ Brand Protection - -### JavaScript Widget Risks -Users can override your styling with: -```css -.dog-calc-footer { display: none !important; } -``` - -### iframe Protection -Your branding is completely protected in iframe mode. Users cannot: -- Remove your footer link -- Modify your styling -- Access your content with JavaScript +Embedding is disabled to protect branding and ensure consistent presentation. ## πŸ“± Mobile Optimization @@ -353,9 +285,8 @@ This calculator is provided for educational and professional use. The formulas a ## πŸ”— Links - **Website**: [caninenutritionandwellness.com](https://caninenutritionandwellness.com) -- **Widget Demo**: Open `test-widget.html` in your browser - **Standalone**: Open `iframe.html` in your browser --- -**Built with ❀️ for canine nutrition professionals** \ No newline at end of file +**Built with ❀️ for canine nutrition professionals** diff --git a/build.js b/build.js index 7e7e64c..16630a7 100644 --- a/build.js +++ b/build.js @@ -3,8 +3,8 @@ /** * Dog Calculator Build System - ORGANIZED VERSION * - * This build script generates iframe.html and sundog-dog-food-calculator.js - * from organized source files in the src/ directory. + * This build script generates iframe.html from organized source files + * in the src/ directory. * * Source structure: * - src/index.html - HTML template @@ -15,7 +15,6 @@ * * Output files: * - iframe.html - Standalone calculator page - * - sundog-dog-food-calculator.js - Embeddable widget * * Usage: node build.js */ @@ -85,7 +84,7 @@ function backupFiles() { } const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - const filesToBackup = ['iframe.html', 'sundog-dog-food-calculator.js']; + const filesToBackup = ['iframe.html']; filesToBackup.forEach(file => { if (fs.existsSync(file)) { @@ -299,10 +298,7 @@ function build() { fs.writeFileSync('iframe.html', iframeContent); console.log(' βœ… Generated iframe.html'); - // Generate widget - const widgetCode = createWidgetJS(css, html, js); - fs.writeFileSync('sundog-dog-food-calculator.js', widgetCode); - console.log(' βœ… Generated sundog-dog-food-calculator.js'); + // Embeddable widget generation removed (embedding no longer supported) console.log(''); console.log('πŸŽ‰ Build completed successfully!'); @@ -317,12 +313,11 @@ function build() { console.log(''); console.log(' Generated files:'); console.log(' β€’ iframe.html - Standalone calculator'); - console.log(' β€’ sundog-dog-food-calculator.js - Embeddable widget'); console.log(''); console.log('πŸ”„ Your workflow:'); console.log(' 1. Edit organized files in src/'); console.log(' 2. Run: node build.js'); - console.log(' 3. Both output files are regenerated!'); + console.log(' 3. Output file is regenerated!'); console.log(''); console.log('πŸ’‘ Clean, organized structure - easy to maintain!'); @@ -338,4 +333,4 @@ if (require.main === module) { build(); } -module.exports = { build }; \ No newline at end of file +module.exports = { build }; diff --git a/iframe.html b/iframe.html index 2db4ceb..c38d605 100644 --- a/iframe.html +++ b/iframe.html @@ -25,7 +25,7 @@ margin: 0; padding: 0; background: transparent; - overflow-x: hidden; + overflow: hidden; /* hide internal scrollbars; parent resizes iframe */ font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; line-height: 1.5; color: var(--text-primary); @@ -331,10 +331,7 @@ color: #9f5999; } - .dog-calculator-btn-embed:hover { - border-color: var(--success-color); - color: var(--success-color); - } + /* Embed button removed */ .dog-calculator-footer { text-align: center; @@ -729,10 +726,7 @@ color: #f19a5f; } - .dog-calculator-container.theme-dark .dog-calculator-btn-embed:hover { - border-color: var(--success-color); - color: var(--success-color); - } + /* Embed button removed */ /* Dark theme feeding configuration styles */ .dog-calculator-container.theme-dark .dog-calculator-feeding-config { @@ -935,10 +929,7 @@ color: #f19a5f; } - .dog-calculator-container.theme-system .dog-calculator-btn-embed:hover { - border-color: var(--success-color); - color: var(--success-color); - } + /* Embed button removed */ /* System theme feeding configuration styles in dark mode */ .dog-calculator-container.theme-system .dog-calculator-feeding-config { @@ -980,13 +971,18 @@ /* Modal Styles */ .dog-calculator-modal { - display: none; + display: none; /* set to flex via JS when opened */ position: fixed; z-index: 10000; left: 0; top: 0; width: 100%; height: 100%; + padding: 20px; + box-sizing: border-box; + overflow: auto; /* allow modal content scroll if needed */ + align-items: center; + justify-content: center; animation: fadeIn 0.3s ease; } @@ -998,19 +994,19 @@ .dog-calculator-modal-content { position: relative; background-color: var(--bg-secondary); - margin: 5% auto; + margin: 0; padding: 30px; border: 1px solid var(--border-color); border-radius: 12px; width: 90%; max-width: 500px; + max-height: 90vh; /* ensure it fits viewport */ + overflow: auto; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); animation: slideIn 0.3s ease; } - .dog-calculator-modal-embed { - max-width: 700px; - } + /* Embed modal removed */ @keyframes slideIn { from { @@ -1096,74 +1092,7 @@ color: var(--text-primary); } - /* Embed Modal */ - .dog-calculator-embed-options { - display: flex; - flex-direction: column; - gap: 24px; - } - - .dog-calculator-embed-option { - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 20px; - background: #fcfafd; - } - - .dog-calculator-embed-option h4 { - margin: 0 0 8px 0; - color: var(--text-primary); - font-size: 1.1rem; - } - - .dog-calculator-embed-option p { - margin: 0 0 16px 0; - color: var(--text-label); - font-size: 0.9rem; - } - - /* Default (light theme) code containers */ - .dog-calculator-code-container { - position: relative; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 6px; - overflow: hidden; - } - - .dog-calculator-code-container pre { - margin: 0; - padding: 16px 60px 16px 16px; - overflow-x: auto; - } - - .dog-calculator-code-container code { - color: var(--text-primary); - font-family: 'Consolas', 'Monaco', 'Courier New', monospace; - font-size: 0.85rem; - line-height: 1.4; - } - - .dog-calculator-copy-btn { - position: absolute; - top: 8px; - right: 8px; - padding: 6px 10px; - background: #f19a5f; - color: white; - border: none; - border-radius: 4px; - font-size: 0.8rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - font-family: inherit; - z-index: 1; - } - - .dog-calculator-copy-btn:hover { background: #e87741; } - .dog-calculator-copy-btn.copied { background: var(--success-color); } - .dog-calculator-copy-btn.copied:hover { background: var(--success-color); } + /* Embed UI removed */ /* Dark theme modal styles */ .dog-calculator-container.theme-dark .dog-calculator-modal-content { @@ -1189,28 +1118,7 @@ color: var(--text-primary); } - .dog-calculator-container.theme-dark .dog-calculator-embed-option { - background: var(--bg-secondary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-dark .dog-calculator-embed-option h4 { - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-embed-option p { - color: var(--text-secondary) - } - - /* Dark theme code containers - different from embed option background */ - .dog-calculator-container.theme-dark .dog-calculator-code-container { - background: #1a1621; - border-color: #2a2330; - } - - .dog-calculator-container.theme-dark .dog-calculator-code-container code { - color: var(--text-primary); - } + /* Embed UI removed for dark theme */ /* System theme modal styles */ @media (prefers-color-scheme: dark) { @@ -1237,28 +1145,7 @@ color: var(--text-primary); } - .dog-calculator-container.theme-system .dog-calculator-embed-option { - background: var(--bg-secondary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-system .dog-calculator-embed-option h4 { - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-embed-option p { - color: var(--text-secondary) - } - - /* System theme code containers - different from embed option background */ - .dog-calculator-container.theme-system .dog-calculator-code-container { - background: #1a1621; - border-color: #2a2330; - } - - .dog-calculator-container.theme-system .dog-calculator-code-container code { - color: var(--text-primary); - } + /* Embed UI removed for system theme */ } /* Multi-Food Source Styles */ @@ -2219,9 +2106,6 @@ - - - + diff --git a/src/css/main.css b/src/css/main.css index d08276a..5758566 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -15,7 +15,7 @@ margin: 0; padding: 0; background: transparent; - overflow-x: hidden; + overflow: hidden; /* hide internal scrollbars; parent resizes iframe */ font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; line-height: 1.5; color: var(--text-primary); @@ -321,10 +321,7 @@ color: #9f5999; } - .dog-calculator-btn-embed:hover { - border-color: var(--success-color); - color: var(--success-color); - } + /* Embed button removed */ .dog-calculator-footer { text-align: center; diff --git a/src/css/themes.css b/src/css/themes.css index 5c81819..9d91e63 100644 --- a/src/css/themes.css +++ b/src/css/themes.css @@ -160,10 +160,7 @@ color: #f19a5f; } - .dog-calculator-container.theme-dark .dog-calculator-btn-embed:hover { - border-color: var(--success-color); - color: var(--success-color); - } + /* Embed button removed */ /* Dark theme feeding configuration styles */ .dog-calculator-container.theme-dark .dog-calculator-feeding-config { @@ -366,10 +363,7 @@ color: #f19a5f; } - .dog-calculator-container.theme-system .dog-calculator-btn-embed:hover { - border-color: var(--success-color); - color: var(--success-color); - } + /* Embed button removed */ /* System theme feeding configuration styles in dark mode */ .dog-calculator-container.theme-system .dog-calculator-feeding-config { @@ -411,13 +405,18 @@ /* Modal Styles */ .dog-calculator-modal { - display: none; + display: none; /* set to flex via JS when opened */ position: fixed; z-index: 10000; left: 0; top: 0; width: 100%; height: 100%; + padding: 20px; + box-sizing: border-box; + overflow: auto; /* allow modal content scroll if needed */ + align-items: center; + justify-content: center; animation: fadeIn 0.3s ease; } @@ -429,19 +428,19 @@ .dog-calculator-modal-content { position: relative; background-color: var(--bg-secondary); - margin: 5% auto; + margin: 0; padding: 30px; border: 1px solid var(--border-color); border-radius: 12px; width: 90%; max-width: 500px; + max-height: 90vh; /* ensure it fits viewport */ + overflow: auto; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); animation: slideIn 0.3s ease; } - .dog-calculator-modal-embed { - max-width: 700px; - } + /* Embed modal removed */ @keyframes slideIn { from { @@ -527,74 +526,7 @@ color: var(--text-primary); } - /* Embed Modal */ - .dog-calculator-embed-options { - display: flex; - flex-direction: column; - gap: 24px; - } - - .dog-calculator-embed-option { - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 20px; - background: #fcfafd; - } - - .dog-calculator-embed-option h4 { - margin: 0 0 8px 0; - color: var(--text-primary); - font-size: 1.1rem; - } - - .dog-calculator-embed-option p { - margin: 0 0 16px 0; - color: var(--text-label); - font-size: 0.9rem; - } - - /* Default (light theme) code containers */ - .dog-calculator-code-container { - position: relative; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 6px; - overflow: hidden; - } - - .dog-calculator-code-container pre { - margin: 0; - padding: 16px 60px 16px 16px; - overflow-x: auto; - } - - .dog-calculator-code-container code { - color: var(--text-primary); - font-family: 'Consolas', 'Monaco', 'Courier New', monospace; - font-size: 0.85rem; - line-height: 1.4; - } - - .dog-calculator-copy-btn { - position: absolute; - top: 8px; - right: 8px; - padding: 6px 10px; - background: #f19a5f; - color: white; - border: none; - border-radius: 4px; - font-size: 0.8rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - font-family: inherit; - z-index: 1; - } - - .dog-calculator-copy-btn:hover { background: #e87741; } - .dog-calculator-copy-btn.copied { background: var(--success-color); } - .dog-calculator-copy-btn.copied:hover { background: var(--success-color); } + /* Embed UI removed */ /* Dark theme modal styles */ .dog-calculator-container.theme-dark .dog-calculator-modal-content { @@ -620,28 +552,7 @@ color: var(--text-primary); } - .dog-calculator-container.theme-dark .dog-calculator-embed-option { - background: var(--bg-secondary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-dark .dog-calculator-embed-option h4 { - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-embed-option p { - color: var(--text-secondary) - } - - /* Dark theme code containers - different from embed option background */ - .dog-calculator-container.theme-dark .dog-calculator-code-container { - background: #1a1621; - border-color: #2a2330; - } - - .dog-calculator-container.theme-dark .dog-calculator-code-container code { - color: var(--text-primary); - } + /* Embed UI removed for dark theme */ /* System theme modal styles */ @media (prefers-color-scheme: dark) { @@ -668,28 +579,7 @@ color: var(--text-primary); } - .dog-calculator-container.theme-system .dog-calculator-embed-option { - background: var(--bg-secondary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-system .dog-calculator-embed-option h4 { - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-embed-option p { - color: var(--text-secondary) - } - - /* System theme code containers - different from embed option background */ - .dog-calculator-container.theme-system .dog-calculator-code-container { - background: #1a1621; - border-color: #2a2330; - } - - .dog-calculator-container.theme-system .dog-calculator-code-container code { - color: var(--text-primary); - } + /* Embed UI removed for system theme */ } /* Multi-Food Source Styles */ diff --git a/src/index.html b/src/index.html index c515b13..53503c0 100644 --- a/src/index.html +++ b/src/index.html @@ -143,9 +143,6 @@ - - - + diff --git a/src/js/calculator.js b/src/js/calculator.js index 66edde8..1dee12c 100644 --- a/src/js/calculator.js +++ b/src/js/calculator.js @@ -60,10 +60,8 @@ container.style.transform = `scale(${clampedScale})`; container.style.transformOrigin = 'top center'; - // Adjust container to account for scaling + // Recalculate height for parent without adding artificial margins setTimeout(() => { - const actualHeight = container.offsetHeight * clampedScale; - container.style.marginBottom = `${(clampedScale - 1) * container.offsetHeight}px`; this.sendHeightToParent(); }, 100); } @@ -899,14 +897,10 @@ // Modal event listeners const shareBtn = document.getElementById('shareBtn'); - const embedBtn = document.getElementById('embedBtn'); const shareModalClose = document.getElementById('shareModalClose'); - const embedModalClose = document.getElementById('embedModalClose'); if (shareBtn) shareBtn.addEventListener('click', () => this.showShareModal()); - if (embedBtn) embedBtn.addEventListener('click', () => this.showEmbedModal()); if (shareModalClose) shareModalClose.addEventListener('click', () => this.hideShareModal()); - if (embedModalClose) embedModalClose.addEventListener('click', () => this.hideEmbedModal()); // Share buttons const shareFacebook = document.getElementById('shareFacebook'); @@ -921,16 +915,10 @@ if (shareEmail) shareEmail.addEventListener('click', () => this.shareViaEmail()); if (shareCopy) shareCopy.addEventListener('click', () => this.copyShareLink()); - // Copy buttons - const copyWidget = document.getElementById('copyWidget'); - const copyIframe = document.getElementById('copyIframe'); - - if (copyWidget) copyWidget.addEventListener('click', () => this.copyEmbedCode('widget')); - if (copyIframe) copyIframe.addEventListener('click', () => this.copyEmbedCode('iframe')); + // Embed copy buttons removed (embedding disabled) // Close modals on outside click const shareModal = document.getElementById('shareModal'); - const embedModal = document.getElementById('embedModal'); if (shareModal) { shareModal.addEventListener('click', (e) => { @@ -938,11 +926,7 @@ }); } - if (embedModal) { - embedModal.addEventListener('click', (e) => { - if (e.target === embedModal) this.hideEmbedModal(); - }); - } + // Embed modal removed } toggleUnits() { @@ -1223,7 +1207,6 @@ calorieResults.style.display = 'block'; this.updateFoodCalculations(); - this.sendHeightToParent(); } updateCupsButtonState() { @@ -1687,9 +1670,8 @@ } } - foodAmountsSection.style.display = 'block'; - - this.sendHeightToParent(); + foodAmountsSection.style.display = 'block'; + this.sendHeightToParent(); } getFoodSourceEnergyPer100g(foodSource) { @@ -1715,13 +1697,18 @@ } } + // Iframe auto-resize for allowed embeddings setupIframeResize() { - // Send height to parent window for iframe auto-resize - this.sendHeightToParent(); + // Only when embedded in an iframe + if (window.top === window.self) return; - // Monitor for content changes that might affect height + // Initial send once UI is ready + setTimeout(() => this.sendHeightToParent(), 50); + + // Monitor for content/attribute changes const observer = new MutationObserver(() => { - setTimeout(() => this.sendHeightToParent(), 100); + clearTimeout(this._resizeTimer); + this._resizeTimer = setTimeout(() => this.sendHeightToParent(), 100); }); observer.observe(document.body, { @@ -1730,18 +1717,25 @@ attributes: true }); - // Send height on window resize + // On viewport resize window.addEventListener('resize', () => this.sendHeightToParent()); } sendHeightToParent() { - const height = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); - if (window.parent && window.parent !== window) { - window.parent.postMessage({ - type: 'dogCalculatorResize', - height: height - }, '*'); + if (!(window.parent && window.parent !== window)) return; + const container = document.getElementById('dogCalculator'); + // Prefer visual height including transform scaling + let height = 0; + if (container) { + const rect = container.getBoundingClientRect(); + height = Math.ceil(rect.height); + } else { + height = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); } + window.parent.postMessage({ + type: 'dogCalculatorResize', + height: height + }, '*'); } // Modal functionality @@ -1750,50 +1744,25 @@ const shareUrl = document.getElementById('shareUrl'); if (modal && shareUrl) { shareUrl.value = window.location.href; - modal.style.display = 'block'; + // Use flex so content is centered within modal viewport + modal.style.display = 'flex'; + // Sync modal scroll position with current page scroll so content is visible + try { modal.scrollTop = window.scrollY || document.documentElement.scrollTop || 0; } catch (e) {} + // Ensure the modal is visible even when the page is scrolled + // by recalculating parent iframe height (defensive) + this.sendHeightToParent(); } } hideShareModal() { const modal = document.getElementById('shareModal'); if (modal) modal.style.display = 'none'; + this.sendHeightToParent(); } - showEmbedModal() { - const modal = document.getElementById('embedModal'); - const widgetCode = document.getElementById('widgetCode'); - const iframeCode = document.getElementById('iframeCode'); - - if (modal && widgetCode && iframeCode) { - // Build embed URL - const baseUrl = window.location.protocol + '//embed.' + window.location.hostname; - - // Create widget code using createElement to avoid quote issues - const scriptTag = document.createElement('script'); - scriptTag.src = baseUrl + '/dog-calorie-calculator/dog-food-calculator-widget.js'; - const divTag = document.createElement('div'); - divTag.id = 'dog-calorie-calculator'; - - const widgetHtml = scriptTag.outerHTML + '\n' + divTag.outerHTML; - widgetCode.textContent = widgetHtml; - - // Create iframe code using createElement - const iframe = document.createElement('iframe'); - iframe.src = baseUrl + '/dog-calorie-calculator/iframe.html'; - iframe.width = '100%'; - iframe.height = '600'; - iframe.frameBorder = '0'; - iframe.title = 'Dog Calorie Calculator'; - - iframeCode.textContent = iframe.outerHTML; - modal.style.display = 'block'; - } - } + // Embed modal removed (embedding disabled) - hideEmbedModal() { - const modal = document.getElementById('embedModal'); - if (modal) modal.style.display = 'none'; - } + // Embed modal removed (embedding disabled) shareToFacebook() { const url = encodeURIComponent(window.location.href); @@ -1840,30 +1809,40 @@ } } - async copyEmbedCode(type) { - const codeElement = document.getElementById(type === 'widget' ? 'widgetCode' : 'iframeCode'); - const copyBtn = document.getElementById(type === 'widget' ? 'copyWidget' : 'copyIframe'); - - if (codeElement && copyBtn) { - try { - await navigator.clipboard.writeText(codeElement.textContent); - const originalText = copyBtn.textContent; - copyBtn.textContent = 'Copied!'; - copyBtn.classList.add('copied'); - - setTimeout(() => { - copyBtn.textContent = originalText; - copyBtn.classList.remove('copied'); - }, 2000); - } catch (err) { - // Fallback for older browsers - console.log('Copy fallback needed'); - } - } - } + // Embed code copy removed (embedding disabled) } // Initialize calculator when DOM is ready document.addEventListener('DOMContentLoaded', function() { + // Allow embedding only from approved parent hosts + if (window.top !== window.self) { + const allowedHosts = ['caninenutritionandwellness.com', 'www.caninenutritionandwellness.com']; + let parentAllowed = false; + + // Prefer document.referrer when available + try { + if (document.referrer) { + const r = new URL(document.referrer); + parentAllowed = allowedHosts.includes(r.hostname); + } + } catch (e) {} + + // Fallback: Chrome's ancestorOrigins (may be empty or absent) + if (!parentAllowed && window.location.ancestorOrigins && window.location.ancestorOrigins.length) { + parentAllowed = Array.from(window.location.ancestorOrigins).some((originStr) => { + try { + const o = new URL(originStr); + return allowedHosts.includes(o.hostname); + } catch (e) { + return false; + } + }); + } + + if (!parentAllowed) { + document.body.innerHTML = '
Embedding of this calculator is only allowed on caninenutritionandwellness.com.
'; + return; + } + } new DogCalorieCalculator(); }); diff --git a/sundog-dog-food-calculator.js b/sundog-dog-food-calculator.js deleted file mode 100644 index 3effe59..0000000 --- a/sundog-dog-food-calculator.js +++ /dev/null @@ -1,4236 +0,0 @@ -/** - * Dog Calorie Calculator Widget - * Embeddable JavaScript widget for websites - * - * THIS CODE IS AUTO-GENERATED FROM src/ FILES - DO NOT EDIT MANUALLY - * Edit files in src/ directory and run 'node build.js' to update - * - * Usage: - * - *
- * - * Or with options: - *
- * - * By Canine Nutrition and Wellness - * https://caninenutritionandwellness.com - */ - -(function() { - 'use strict'; - - // Inject widget styles - const CSS_STYLES = `/* Sundog Dog Food Calorie Calculator Styles */ - - /* CSS Variables for theming */ - :root { - --bg-primary: #fdfcfe; - --bg-secondary: #ffffff; - --border-color: #e8e3ed; - --text-primary: #6f3f6d; - --text-secondary: #8f7a8e; - --accent-color: #f19a5f; - --text-label: #635870; /* For form labels, secondary UI text */ - --success-color: #7fa464; /* Green for success states */ - --bg-tertiary: #f8f5fa; /* Light background variant */ - --error-color: #e87159; /* Error states and messages */ - } - - body { - margin: 0; - padding: 0; - background: transparent; - overflow-x: hidden; - font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; - line-height: 1.5; - color: var(--text-primary); - } - - .dog-calculator-container { - max-width: 640px; - margin: 0 auto; - padding: 24px; - box-sizing: border-box; - opacity: 0; - transition: opacity 0.3s ease; - } - - .dog-calculator-container.loaded { - opacity: 1; - } - - .dog-calculator-container *, - .dog-calculator-container *::before, - .dog-calculator-container *::after { - box-sizing: border-box; - } - - .dog-calculator-section { - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: 8px 8px 0 0; - padding: 24px; - margin-bottom: 0; - box-shadow: 0 3px 6px rgba(0, 0, 0, 0.08); - } - - .dog-calculator-section-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 24px; - flex-wrap: wrap; - gap: 16px; - } - - .dog-calculator-section h2 { - margin: 0; - color: var(--text-primary); - font-size: 1.5rem; - font-weight: 600; - } - - /* Unit Switch */ - .dog-calculator-unit-switch { - display: flex; - align-items: center; - gap: 12px; - } - - .dog-calculator-unit-label { - font-size: 0.9rem; - font-weight: 500; - color: var(--text-label); - transition: color 0.2s ease; - } - - .dog-calculator-unit-label.active { - color: var(--text-primary); - font-weight: 600; - } - - .dog-calculator-switch { - position: relative; - display: inline-block; - width: 48px; - height: 24px; - } - - .dog-calculator-switch input { - opacity: 0; - width: 0; - height: 0; - } - - .dog-calculator-slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: var(--border-color); - transition: 0.3s; - border-radius: 24px; - } - - .dog-calculator-slider:before { - position: absolute; - content: ""; - height: 18px; - width: 18px; - left: 3px; - bottom: 3px; - background-color: white; - transition: 0.3s; - border-radius: 50%; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); - } - - .dog-calculator-switch input:checked + .dog-calculator-slider { - background-color: #f19a5f; - } - - .dog-calculator-switch input:checked + .dog-calculator-slider:before { - transform: translateX(24px); - } - - .dog-calculator-form-group { - margin-bottom: 20px; - } - - .dog-calculator-form-group label { - display: block; - margin-bottom: 8px; - font-weight: 500; - color: var(--text-primary); - font-size: 1rem; - } - - .dog-calculator-form-group select, - .dog-calculator-form-group input[type="number"], - .dog-calculator-form-group input[type="text"] { - width: 100%; - padding: 12px 16px; - border: 1px solid var(--border-color); - border-radius: 6px; - font-size: 1rem; - font-family: inherit; - background-color: var(--bg-secondary); - color: var(--text-primary); - transition: all 0.2s ease; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236f3f6d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); - background-repeat: no-repeat; - background-position: right 12px center; - background-size: 20px; - padding-right: 40px; - } - - .dog-calculator-form-group select option { - background-color: var(--bg-secondary); - color: var(--text-primary); - } - - .dog-calculator-form-group input[type="number"], - .dog-calculator-form-group input[type="text"] { - background-image: none; - padding-right: 16px; - } - - .dog-calculator-form-group select:focus, - .dog-calculator-form-group input[type="number"]:focus, - .dog-calculator-form-group input[type="text"]:focus { - outline: none; - border-color: #f19a5f; - background-color: var(--bg-secondary); - box-shadow: 0 0 0 3px rgba(241, 154, 95, 0.1); - } - - .dog-calculator-form-group input[readonly] { - background-color: var(--bg-tertiary); - cursor: not-allowed; - color: var(--text-label); - } - - .dog-calculator-results { - background: linear-gradient(135deg, rgba(241, 154, 95, 0.08) 0%, rgba(241, 154, 95, 0.04) 100%); - border: 1px solid rgba(241, 154, 95, 0.2); - border-radius: 6px; - padding: 20px; - margin-top: 24px; - } - - .dog-calculator-result-item { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; - gap: 10px; /* Add gap between label and value */ - } - - .dog-calculator-result-item:last-child { - margin-bottom: 0; - } - - .dog-calculator-result-label { - font-weight: 500; - color: var(--text-primary); - font-size: 0.95rem; - } - - .dog-calculator-result-value { - font-weight: 600; - color: var(--text-primary); - font-size: 1.1rem; - padding: 4px 12px; - background: rgba(241, 154, 95, 0.15); - border-radius: 4px; - white-space: nowrap; /* Prevent text from wrapping to multiple lines */ - } - - .dog-calculator-collapsible { - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-top: none; - margin-bottom: 0; - overflow: hidden; - box-shadow: 0 3px 6px rgba(0, 0, 0, 0.08); - } - - .dog-calculator-collapsible-header { - background: var(--bg-tertiary); - padding: 20px 24px; - border-bottom: 1px solid var(--border-color); - } - - .dog-calculator-collapsible-header h3 { - margin: 0; - font-size: 1.25rem; - color: var(--text-primary); - font-weight: 600; - } - - .dog-calculator-collapsible-content { - display: block; - } - - .dog-calculator-collapsible-inner { - padding: 24px; - } - - .dog-calculator-input-group { - display: flex; - gap: 16px; - align-items: flex-end; - } - - .dog-calculator-input-group .dog-calculator-form-group { - flex: 1; - margin-bottom: 0; - } - - .dog-calculator-unit-select { - min-width: 120px; - } - - .dog-calculator-error { - color: var(--error-color); - font-size: 0.875rem; - margin-top: 6px; - font-weight: 500; - } - - .dog-calculator-hidden { - display: none; - } - - /* Action Buttons */ - .dog-calculator-action-buttons { - display: flex; - justify-content: center; - gap: 16px; - padding: 20px; - background: var(--bg-tertiary); - border-left: 1px solid var(--border-color); - border-right: 1px solid var(--border-color); - margin-top: -1px; - box-shadow: 0 3px 6px rgba(0, 0, 0, 0.08); - } - - .dog-calculator-btn { - padding: 8px 16px; - border: 1px solid var(--border-color); - border-radius: 6px; - font-size: 0.9rem; - font-weight: 500; - font-family: inherit; - cursor: pointer; - transition: all 0.2s ease; - display: inline-flex; - align-items: center; - gap: 6px; - background: white; - color: var(--text-primary); - } - - .dog-calculator-btn:hover { - transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - } - - .dog-calculator-btn-share:hover { - border-color: #9f5999; - color: #9f5999; - } - - .dog-calculator-btn-embed:hover { - border-color: var(--success-color); - color: var(--success-color); - } - - .dog-calculator-footer { - text-align: center; - padding: 20px; - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: 0 0 8px 8px; - border-top: none; - margin-top: -1px; - box-shadow: 0 3px 6px rgba(0, 0, 0, 0.08); - } - - .dog-calculator-footer a { - color: #9f5999; - text-decoration: none; - font-size: 0.9rem; - font-weight: 500; - transition: color 0.2s ease; - } - - .dog-calculator-footer a:hover { - color: #f19a5f; - text-decoration: underline; - } - - /* Mobile Responsive Design */ - @media (max-width: 576px) { - .dog-calculator-container { - padding: 16px; - } - - .dog-calculator-section, - .dog-calculator-collapsible-inner { - padding: 20px; - } - - .dog-calculator-section h2, - .dog-calculator-collapsible-header h3 { - font-size: 1.3rem; - } - - .dog-calculator-section-header { - flex-direction: column; - align-items: stretch; - gap: 12px; - } - - .dog-calculator-section h2 { - text-align: center; - } - - .dog-calculator-unit-switch { - justify-content: center; - } - - .dog-calculator-action-buttons { - flex-direction: column; - padding: 16px; - } - - .dog-calculator-btn { - width: 100%; - justify-content: center; - } - - .dog-calculator-input-group { - flex-direction: row; - gap: 12px; - align-items: flex-end; - } - - .dog-calculator-input-group .dog-calculator-form-group { - margin-bottom: 0; - } - - /* First form group takes 55%, second takes 40% with some flex */ - .dog-calculator-input-group .dog-calculator-form-group:first-child { - flex: 0 0 55%; - } - - .dog-calculator-input-group .dog-calculator-form-group:last-child { - flex: 1 1 40%; - min-width: 100px; - } - - /* Make sure number inputs don't get too wide */ - .dog-calculator-input-group input[type="number"] { - max-width: 100%; - } - - /* Ensure dropdowns don't overflow their containers */ - .dog-calculator-input-group select { - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - } - - - .dog-calculator-result-item { - flex-direction: column; - align-items: flex-start; - gap: 8px; - } - - .dog-calculator-result-value { - align-self: stretch; - text-align: center; - } - - .dog-calculator-collapsible-header { - padding: 16px 20px; - } - } - - /* Feeding Configuration Styles */ - .dog-calculator-container .dog-calculator-feeding-config { - margin-top: 20px; - padding: 16px; - background: var(--bg-tertiary); - border: 1px solid var(--border-color); - border-radius: 8px; - } - - .dog-calculator-container .dog-calculator-frequency-row { - display: flex; - align-items: center; - gap: 16px; - flex-wrap: wrap; - } - - .dog-calculator-container .dog-calculator-frequency-row > label { - font-weight: 500; - color: var(--text-label); - margin: 0; - } - - .dog-calculator-container .dog-calculator-radio-group { - display: flex; - gap: 20px; - align-items: center; - } - - .dog-calculator-container .dog-calculator-radio-group label { - display: flex; - align-items: center; - gap: 6px; - cursor: pointer; - color: var(--text-primary); - font-size: 0.95rem; - } - - .dog-calculator-container .dog-calculator-radio-group input[type="radio"] { - cursor: pointer; - margin: 0; - } - - .dog-calculator-container .dog-calculator-radio-group input[type="radio"]:checked + span { - font-weight: 600; - color: var(--accent-color); - } - - .dog-calculator-container .dog-calculator-meal-input { - display: inline-flex; - align-items: center; - gap: 6px; - margin: 0 auto; - } - - .dog-calculator-container .dog-calculator-meal-input span { - color: var(--text-secondary); - font-size: 0.95rem; - } - - .dog-calculator-container .dog-calculator-meal-input input[type="number"] { - width: 50px; - padding: 4px 8px; - border: 1px solid var(--border-color); - border-radius: 4px; - font-size: 0.95rem; - color: var(--text-primary); - background: var(--bg-secondary); - text-align: center; - } - - .dog-calculator-container .dog-calculator-meal-input input[type="number"]:focus { - outline: none; - border-color: var(--accent-color); - box-shadow: 0 0 0 2px rgba(241, 154, 95, 0.1); - } - - /* Update meal note styling */ - .dog-calculator-container #mealNote { - color: var(--text-secondary); - font-size: 0.9rem; - font-weight: normal; - margin-left: 4px; - } - - /* Mobile responsive adjustments for feeding config */ - @media (max-width: 480px) { - .dog-calculator-meal-input { - margin-left: 0; - width: 100%; - margin-top: 8px; - } - - .dog-calculator-frequency-row { - flex-direction: column; - align-items: flex-start; - } - - /* Stack result items vertically on small screens */ - .dog-calculator-result-item { - flex-direction: column; - align-items: flex-start; - gap: 8px; - } - - .dog-calculator-result-label { - margin-right: 0; - font-size: 0.9rem; - } - - .dog-calculator-result-value { - font-size: 1rem; - align-self: stretch; - text-align: center; - } - } - - /* Dark theme - manual override */ - -.dog-calculator-container.theme-dark { - --bg-primary: #24202d; - --bg-secondary: #312b3b; - --bg-tertiary: #1f1b26; - --border-color: #433c4f; - --text-primary: #f5f3f7; - --text-secondary: #b8b0c2; - --text-label: #9f94ae; - --success-color: #7fa464; - --error-color: #e87159; - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-section, - .dog-calculator-container.theme-dark .dog-calculator-collapsible { - background: var(--bg-primary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-dark .dog-calculator-collapsible-header { - background: var(--bg-secondary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-dark .dog-calculator-collapsible-header:hover { - background: #3a3446; - } - - .dog-calculator-container.theme-dark .dog-calculator-section h2, - .dog-calculator-container.theme-dark .dog-calculator-collapsible-header h3, - .dog-calculator-container.theme-dark .dog-calculator-form-group label, - .dog-calculator-container.theme-dark .dog-calculator-result-label { - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-unit-label { - color: var(--text-secondary) - } - - .dog-calculator-container.theme-dark .dog-calculator-unit-label.active { - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-slider { - background-color: var(--border-color); - } - - .dog-calculator-container.theme-dark .dog-calculator-form-group select, - .dog-calculator-container.theme-dark .dog-calculator-form-group input[type="number"], - .dog-calculator-container.theme-dark .dog-calculator-form-group input[type="text"] { - background-color: var(--bg-secondary); - border-color: var(--border-color); - color: var(--text-primary); - background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f5f3f7' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); - } - - .dog-calculator-container.theme-dark .dog-calculator-form-group select option { - background-color: var(--bg-secondary); - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-form-group select:focus, - .dog-calculator-container.theme-dark .dog-calculator-form-group input[type="number"]:focus, - .dog-calculator-container.theme-dark .dog-calculator-form-group input[type="text"]:focus { - background-color: var(--bg-secondary); - border-color: #f19a5f; - } - - .dog-calculator-container.theme-dark .dog-calculator-form-group input[readonly] { - background-color: var(--border-color); - color: var(--text-secondary) - } - - .dog-calculator-container.theme-dark .dog-calculator-inline-unit { - background-color: var(--bg-secondary); - border-color: rgba(241, 154, 95, 0.5); - color: var(--text-primary); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); - background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f5f3f7' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); - } - - .dog-calculator-container.theme-dark .dog-calculator-inline-unit:hover { - border-color: #f19a5f; - box-shadow: 0 2px 6px rgba(241, 154, 95, 0.3); - } - - .dog-calculator-container.theme-dark .dog-calculator-inline-unit:focus { - border-color: #f19a5f; - box-shadow: 0 0 0 3px rgba(241, 154, 95, 0.15); - } - - .dog-calculator-container.theme-dark .dog-calculator-inline-unit option { - background-color: var(--bg-secondary); - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-unit-btn { - background-color: var(--bg-secondary); - border-color: var(--border-color); - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-unit-btn:hover { - border-color: #f19a5f; - background: rgba(241, 154, 95, 0.2); - } - - .dog-calculator-container.theme-dark .dog-calculator-unit-btn.active { - border-color: #f19a5f; - background: #f19a5f; - color: white; - } - - .dog-calculator-container.theme-dark .dog-calculator-unit-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - border-color: var(--border-color); - background: var(--bg-tertiary); - color: var(--text-secondary); - } - - .dog-calculator-container.theme-dark .dog-calculator-unit-btn:disabled:hover { - border-color: var(--border-color); - background: var(--bg-tertiary); - } - - .dog-calculator-container.theme-dark .dog-calculator-results { - background: linear-gradient(135deg, rgba(241, 154, 95, 0.15) 0%, rgba(241, 154, 95, 0.08) 100%); - border-color: rgba(241, 154, 95, 0.3); - } - - .dog-calculator-container.theme-dark .dog-calculator-result-value { - color: var(--text-primary); - background: rgba(241, 154, 95, 0.2); - } - - .dog-calculator-container.theme-dark .dog-calculator-footer { - background: var(--bg-primary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-dark .dog-calculator-action-buttons { - background: var(--bg-secondary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-dark .dog-calculator-btn { - background: var(--border-color); - border-color: var(--border-color); - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-btn:hover { - background: #524a5f; - border-color: #524a5f; - } - - .dog-calculator-container.theme-dark .dog-calculator-btn-share:hover { - border-color: #9f5999; - color: #f19a5f; - } - - .dog-calculator-container.theme-dark .dog-calculator-btn-embed:hover { - border-color: var(--success-color); - color: var(--success-color); - } - - /* Dark theme feeding configuration styles */ - .dog-calculator-container.theme-dark .dog-calculator-feeding-config { - background: var(--bg-tertiary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-dark .dog-calculator-frequency-row > label { - color: var(--text-label); - } - - .dog-calculator-container.theme-dark .dog-calculator-radio-group label { - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-radio-group input[type="radio"]:checked + span { - color: #f19a5f; - } - - .dog-calculator-container.theme-dark .dog-calculator-meal-input span { - color: var(--text-secondary); - } - - .dog-calculator-container.theme-dark .dog-calculator-meal-input input[type="number"] { - background: var(--bg-secondary); - border-color: var(--border-color); - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-meal-input input[type="number"]:focus { - border-color: #f19a5f; - box-shadow: 0 0 0 2px rgba(241, 154, 95, 0.15); - } - - .dog-calculator-container.theme-dark #mealNote { - color: var(--text-secondary); - } - - /* System theme - follows user's OS preference */ - @media (prefers-color-scheme: dark) { - .dog-calculator-container.theme-system { - --bg-primary: #24202d; - --bg-secondary: #312b3b; - --bg-tertiary: #1f1b26; - --border-color: #433c4f; - --text-primary: #f5f3f7; - --text-secondary: #b8b0c2; - --text-label: #9f94ae; - --success-color: #7fa464; - --error-color: #e87159; - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-section, - .dog-calculator-container.theme-system .dog-calculator-collapsible { - background: var(--bg-primary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-system .dog-calculator-collapsible-header { - background: var(--bg-secondary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-system .dog-calculator-collapsible-header:hover { - background: #3a3446; - } - - .dog-calculator-container.theme-system .dog-calculator-section h2, - .dog-calculator-container.theme-system .dog-calculator-collapsible-header h3, - .dog-calculator-container.theme-system .dog-calculator-form-group label, - .dog-calculator-container.theme-system .dog-calculator-result-label { - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-unit-label { - color: var(--text-secondary) - } - - .dog-calculator-container.theme-system .dog-calculator-unit-label.active { - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-slider { - background-color: var(--border-color); - } - - .dog-calculator-container.theme-system .dog-calculator-form-group select, - .dog-calculator-container.theme-system .dog-calculator-form-group input[type="number"], - .dog-calculator-container.theme-system .dog-calculator-form-group input[type="text"] { - background-color: var(--bg-secondary); - border-color: var(--border-color); - color: var(--text-primary); - background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f5f3f7' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); - } - - .dog-calculator-container.theme-system .dog-calculator-form-group select option { - background-color: var(--bg-secondary); - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-form-group select:focus, - .dog-calculator-container.theme-system .dog-calculator-form-group input[type="number"]:focus, - .dog-calculator-container.theme-system .dog-calculator-form-group input[type="text"]:focus { - background-color: var(--bg-secondary); - border-color: #f19a5f; - } - - .dog-calculator-container.theme-system .dog-calculator-form-group input[readonly] { - background-color: var(--border-color); - color: var(--text-secondary) - } - - .dog-calculator-container.theme-system .dog-calculator-inline-unit { - background-color: var(--bg-secondary); - border-color: rgba(241, 154, 95, 0.5); - color: var(--text-primary); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); - background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f5f3f7' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); - } - - .dog-calculator-container.theme-system .dog-calculator-inline-unit:hover { - border-color: #f19a5f; - box-shadow: 0 2px 6px rgba(241, 154, 95, 0.3); - } - - .dog-calculator-container.theme-system .dog-calculator-inline-unit:focus { - border-color: #f19a5f; - box-shadow: 0 0 0 3px rgba(241, 154, 95, 0.15); - } - - .dog-calculator-container.theme-system .dog-calculator-inline-unit option { - background-color: var(--bg-secondary); - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-unit-btn { - background-color: var(--bg-secondary); - border-color: var(--border-color); - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-unit-btn:hover { - border-color: #f19a5f; - background: rgba(241, 154, 95, 0.2); - } - - .dog-calculator-container.theme-system .dog-calculator-unit-btn.active { - border-color: #f19a5f; - background: #f19a5f; - color: white; - } - - .dog-calculator-container.theme-system .dog-calculator-unit-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - border-color: var(--border-color); - background: var(--bg-tertiary); - color: var(--text-secondary); - } - - .dog-calculator-container.theme-system .dog-calculator-unit-btn:disabled:hover { - border-color: var(--border-color); - background: var(--bg-tertiary); - } - - .dog-calculator-container.theme-system .dog-calculator-results { - background: linear-gradient(135deg, rgba(241, 154, 95, 0.15) 0%, rgba(241, 154, 95, 0.08) 100%); - border-color: rgba(241, 154, 95, 0.3); - } - - .dog-calculator-container.theme-system .dog-calculator-result-value { - color: var(--text-primary); - background: rgba(241, 154, 95, 0.2); - } - - .dog-calculator-container.theme-system .dog-calculator-footer { - background: var(--bg-primary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-system .dog-calculator-action-buttons { - background: var(--bg-secondary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-system .dog-calculator-btn { - background: var(--border-color); - border-color: var(--border-color); - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-btn:hover { - background: #524a5f; - border-color: #524a5f; - } - - .dog-calculator-container.theme-system .dog-calculator-btn-share:hover { - border-color: #9f5999; - color: #f19a5f; - } - - .dog-calculator-container.theme-system .dog-calculator-btn-embed:hover { - border-color: var(--success-color); - color: var(--success-color); - } - - /* System theme feeding configuration styles in dark mode */ - .dog-calculator-container.theme-system .dog-calculator-feeding-config { - background: var(--bg-tertiary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-system .dog-calculator-frequency-row > label { - color: var(--text-label); - } - - .dog-calculator-container.theme-system .dog-calculator-radio-group label { - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-radio-group input[type="radio"]:checked + span { - color: #f19a5f; - } - - .dog-calculator-container.theme-system .dog-calculator-meal-input span { - color: var(--text-secondary); - } - - .dog-calculator-container.theme-system .dog-calculator-meal-input input[type="number"] { - background: var(--bg-secondary); - border-color: var(--border-color); - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-meal-input input[type="number"]:focus { - border-color: #f19a5f; - box-shadow: 0 0 0 2px rgba(241, 154, 95, 0.15); - } - - .dog-calculator-container.theme-system #mealNote { - color: var(--text-secondary); - } - } - - /* Modal Styles */ - .dog-calculator-modal { - display: none; - position: fixed; - z-index: 10000; - left: 0; - top: 0; - width: 100%; - height: 100%; - animation: fadeIn 0.3s ease; - } - - @keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } - } - - .dog-calculator-modal-content { - position: relative; - background-color: var(--bg-secondary); - margin: 5% auto; - padding: 30px; - border: 1px solid var(--border-color); - border-radius: 12px; - width: 90%; - max-width: 500px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); - animation: slideIn 0.3s ease; - } - - .dog-calculator-modal-embed { - max-width: 700px; - } - - @keyframes slideIn { - from { - opacity: 0; - transform: translateY(-20px); - } - to { - opacity: 1; - transform: translateY(0); - } - } - - .dog-calculator-modal-close { - position: absolute; - right: 20px; - top: 20px; - font-size: 28px; - font-weight: 300; - color: var(--text-primary); - cursor: pointer; - transition: color 0.2s ease; - } - - .dog-calculator-modal-close:hover { - color: #f19a5f; - } - - .dog-calculator-modal h3 { - margin: 0 0 24px 0; - color: var(--text-primary); - font-size: 1.5rem; - } - - /* Share Modal */ - .dog-calculator-share-buttons { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); - gap: 12px; - margin-bottom: 20px; - } - - .dog-calculator-share-btn { - padding: 12px 16px; - border: none; - border-radius: 6px; - font-size: 0.9rem; - font-weight: 500; - color: white; - cursor: pointer; - transition: all 0.2s ease; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - font-family: inherit; - } - - .dog-calculator-share-facebook { background: #1877f2; } - .dog-calculator-share-facebook:hover { background: #1664d1; transform: translateY(-1px); } - .dog-calculator-share-twitter { background: #1da1f2; } - .dog-calculator-share-twitter:hover { background: #1991da; transform: translateY(-1px); } - .dog-calculator-share-linkedin { background: #0a66c2; } - .dog-calculator-share-linkedin:hover { background: #084d95; transform: translateY(-1px); } - .dog-calculator-share-email { background: #6f3f6d; } - .dog-calculator-share-email:hover { background: #5a3357; transform: translateY(-1px); } - .dog-calculator-share-copy { background: #f19a5f; } - .dog-calculator-share-copy:hover { background: #e87741; transform: translateY(-1px); } - - .dog-calculator-share-url { - display: flex; - width: 100%; - } - - .dog-calculator-share-url input { - flex: 1; - width: 100%; - padding: 10px 16px; - border: 1px solid var(--border-color); - border-radius: 6px; - font-size: 0.9rem; - font-family: monospace; - background: var(--bg-tertiary); - color: var(--text-primary); - } - - /* Embed Modal */ - .dog-calculator-embed-options { - display: flex; - flex-direction: column; - gap: 24px; - } - - .dog-calculator-embed-option { - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 20px; - background: #fcfafd; - } - - .dog-calculator-embed-option h4 { - margin: 0 0 8px 0; - color: var(--text-primary); - font-size: 1.1rem; - } - - .dog-calculator-embed-option p { - margin: 0 0 16px 0; - color: var(--text-label); - font-size: 0.9rem; - } - - /* Default (light theme) code containers */ - .dog-calculator-code-container { - position: relative; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 6px; - overflow: hidden; - } - - .dog-calculator-code-container pre { - margin: 0; - padding: 16px 60px 16px 16px; - overflow-x: auto; - } - - .dog-calculator-code-container code { - color: var(--text-primary); - font-family: 'Consolas', 'Monaco', 'Courier New', monospace; - font-size: 0.85rem; - line-height: 1.4; - } - - .dog-calculator-copy-btn { - position: absolute; - top: 8px; - right: 8px; - padding: 6px 10px; - background: #f19a5f; - color: white; - border: none; - border-radius: 4px; - font-size: 0.8rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - font-family: inherit; - z-index: 1; - } - - .dog-calculator-copy-btn:hover { background: #e87741; } - .dog-calculator-copy-btn.copied { background: var(--success-color); } - .dog-calculator-copy-btn.copied:hover { background: var(--success-color); } - - /* Dark theme modal styles */ - .dog-calculator-container.theme-dark .dog-calculator-modal-content { - background-color: var(--bg-primary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-dark .dog-calculator-modal h3 { - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-modal-close { - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-modal-close:hover { - color: #f19a5f; - } - - .dog-calculator-container.theme-dark .dog-calculator-share-url input { - background: var(--bg-secondary); - border-color: var(--border-color); - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-embed-option { - background: var(--bg-secondary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-dark .dog-calculator-embed-option h4 { - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-embed-option p { - color: var(--text-secondary) - } - - /* Dark theme code containers - different from embed option background */ - .dog-calculator-container.theme-dark .dog-calculator-code-container { - background: #1a1621; - border-color: #2a2330; - } - - .dog-calculator-container.theme-dark .dog-calculator-code-container code { - color: var(--text-primary); - } - - /* System theme modal styles */ - @media (prefers-color-scheme: dark) { - .dog-calculator-container.theme-system .dog-calculator-modal-content { - background-color: var(--bg-primary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-system .dog-calculator-modal h3 { - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-modal-close { - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-modal-close:hover { - color: #f19a5f; - } - - .dog-calculator-container.theme-system .dog-calculator-share-url input { - background: var(--bg-secondary); - border-color: var(--border-color); - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-embed-option { - background: var(--bg-secondary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-system .dog-calculator-embed-option h4 { - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-embed-option p { - color: var(--text-secondary) - } - - /* System theme code containers - different from embed option background */ - .dog-calculator-container.theme-system .dog-calculator-code-container { - background: #1a1621; - border-color: #2a2330; - } - - .dog-calculator-container.theme-system .dog-calculator-code-container code { - color: var(--text-primary); - } - } - - /* Multi-Food Source Styles */ - .dog-calculator-food-sources { - display: flex; - flex-direction: column; - gap: 16px; - } - - .dog-calculator-food-source-card { - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 20px; - position: relative; - transition: all 0.2s ease; - } - - .dog-calculator-food-source-card:hover { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - } - - .dog-calculator-food-source-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; - } - - .dog-calculator-food-source-title { - font-weight: 600; - color: var(--text-primary); - font-size: 1.1rem; - margin: 0; - } - - .dog-calculator-remove-food-btn { - background: var(--error-color); - color: white; - border: none; - border-radius: 50%; - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - font-size: 16px; - font-weight: 600; - transition: all 0.2s ease; - line-height: 1; - } - - .dog-calculator-remove-food-btn:hover { - background: #d65a47; - transform: scale(1.1); - } - - .dog-calculator-percentage-group { - margin-top: 16px; - padding-top: 16px; - border-top: 1px solid var(--border-color); - } - - .dog-calculator-percentage-label { - display: block; - margin-bottom: 8px; - font-weight: 500; - color: var(--text-primary); - font-size: 1rem; - } - - .dog-calculator-percentage-input-group { - display: flex; - align-items: center; - gap: 12px; - } - - .dog-calculator-percentage-slider { - flex: 1; - height: 6px; - border-radius: 3px; - background: var(--border-color); - outline: none; - transition: all 0.2s ease; - -webkit-appearance: none; - appearance: none; - } - - .dog-calculator-percentage-slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 20px; - height: 20px; - border-radius: 50%; - background: #f19a5f; - cursor: pointer; - border: 2px solid white; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); - transition: all 0.2s ease; - } - - .dog-calculator-percentage-slider::-webkit-slider-thumb:hover { - background: #e87741; - transform: scale(1.1); - } - - .dog-calculator-percentage-slider::-moz-range-thumb { - width: 20px; - height: 20px; - border-radius: 50%; - background: #f19a5f; - cursor: pointer; - border: 2px solid white; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); - transition: all 0.2s ease; - } - - .dog-calculator-percentage-input { - width: 70px; - padding: 8px 12px; - border: 1px solid var(--border-color); - border-radius: 6px; - font-size: 0.9rem; - text-align: center; - background-color: var(--bg-secondary); - color: var(--text-primary); - } - - .dog-calculator-percentage-input:focus { - outline: none; - border-color: #f19a5f; - box-shadow: 0 0 0 3px rgba(241, 154, 95, 0.1); - } - - .dog-calculator-add-food-btn { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - width: 100%; - padding: 16px; - border: 2px dashed var(--border-color); - border-radius: 8px; - background: transparent; - color: var(--text-label); - font-size: 1rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - font-family: inherit; - margin-top: 16px; - } - - .dog-calculator-add-food-btn:hover { - border-color: #f19a5f; - color: #f19a5f; - background: rgba(241, 154, 95, 0.05); - } - - .dog-calculator-add-food-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - border-color: var(--border-color); - color: var(--text-label); - background: transparent; - } - - .dog-calculator-add-food-btn:disabled:hover { - border-color: var(--border-color); - color: var(--text-label); - background: transparent; - } - - .dog-calculator-food-results { - background: linear-gradient(135deg, rgba(241, 154, 95, 0.08) 0%, rgba(241, 154, 95, 0.04) 100%); - border: 1px solid rgba(241, 154, 95, 0.2); - border-radius: 6px; - padding: 16px; - margin-top: 20px; - } - - .dog-calculator-food-result-item { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; - font-size: 0.9rem; - } - - .dog-calculator-food-result-item:last-child { - margin-bottom: 0; - } - - .dog-calculator-food-result-label { - font-weight: 500; - color: var(--text-primary); - } - - .dog-calculator-food-result-value { - font-weight: 600; - color: var(--text-primary); - padding: 2px 8px; - background: rgba(241, 154, 95, 0.15); - border-radius: 3px; - font-size: 0.85rem; - } - - /* Dark theme support for food sources */ - .dog-calculator-container.theme-dark .dog-calculator-food-source-card { - background: var(--bg-secondary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-dark .dog-calculator-food-source-title { - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-percentage-label { - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-percentage-slider { - background: var(--border-color); - } - - .dog-calculator-container.theme-dark .dog-calculator-percentage-input { - background: var(--border-color); - border-color: #524a5f; - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-percentage-group { - border-color: var(--border-color); - } - - .dog-calculator-container.theme-dark .dog-calculator-add-food-btn { - border-color: var(--border-color); - color: var(--text-secondary) - } - - .dog-calculator-container.theme-dark .dog-calculator-add-food-btn:hover { - border-color: #f19a5f; - color: #f19a5f; - background: rgba(241, 154, 95, 0.1); - } - - .dog-calculator-container.theme-dark .dog-calculator-food-results { - background: linear-gradient(135deg, rgba(241, 154, 95, 0.15) 0%, rgba(241, 154, 95, 0.08) 100%); - border-color: rgba(241, 154, 95, 0.3); - } - - .dog-calculator-container.theme-dark .dog-calculator-food-result-label { - color: var(--text-primary); - } - - .dog-calculator-container.theme-dark .dog-calculator-food-result-value { - color: var(--text-primary); - background: rgba(241, 154, 95, 0.2); - } - - /* System theme support for food sources */ - @media (prefers-color-scheme: dark) { - .dog-calculator-container.theme-system .dog-calculator-food-source-card { - background: var(--bg-secondary); - border-color: var(--border-color); - } - - .dog-calculator-container.theme-system .dog-calculator-food-source-title { - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-percentage-label { - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-percentage-slider { - background: var(--border-color); - } - - .dog-calculator-container.theme-system .dog-calculator-percentage-input { - background: var(--border-color); - border-color: #524a5f; - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-percentage-group { - border-color: var(--border-color); - } - - .dog-calculator-container.theme-system .dog-calculator-add-food-btn { - border-color: var(--border-color); - color: var(--text-secondary) - } - - .dog-calculator-container.theme-system .dog-calculator-add-food-btn:hover { - border-color: #f19a5f; - color: #f19a5f; - background: rgba(241, 154, 95, 0.1); - } - - .dog-calculator-container.theme-system .dog-calculator-food-results { - background: linear-gradient(135deg, rgba(241, 154, 95, 0.15) 0%, rgba(241, 154, 95, 0.08) 100%); - border-color: rgba(241, 154, 95, 0.3); - } - - .dog-calculator-container.theme-system .dog-calculator-food-result-label { - color: var(--text-primary); - } - - .dog-calculator-container.theme-system .dog-calculator-food-result-value { - color: var(--text-primary); - background: rgba(241, 154, 95, 0.2); - } - } - - /* Mobile responsive design for food sources */ - @media (max-width: 576px) { - .dog-calculator-food-source-card { - padding: 16px; - } - - .dog-calculator-food-source-header { - flex-direction: column; - align-items: flex-start; - gap: 12px; - } - - .dog-calculator-remove-food-btn { - align-self: flex-end; - margin-top: -8px; - } - - .dog-calculator-percentage-input-group { - flex-direction: column; - gap: 8px; - align-items: stretch; - } - - .dog-calculator-percentage-input { - width: 100%; - } - - .dog-calculator-add-food-btn { - padding: 12px; - font-size: 0.9rem; - } - - .dog-calculator-food-result-item { - flex-direction: column; - align-items: flex-start; - gap: 4px; - } - - .dog-calculator-food-result-value { - align-self: stretch; - 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: var(--text-label); - } - - .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: var(--text-secondary) - } - - .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: var(--text-secondary) - } - - .dog-calculator-container.theme-system .dog-calculator-lock-icon.locked { - color: #f19a5f; - } - } - - /* Disabled slider and input styles */ - .dog-calculator-percentage-slider:disabled { - opacity: 0.5; - cursor: not-allowed; - background: #f0f0f0; - pointer-events: none; - } - - .dog-calculator-percentage-slider:disabled::-webkit-slider-thumb { - background: #ccc; - cursor: not-allowed; - } - - .dog-calculator-percentage-slider:disabled::-webkit-slider-thumb:hover { - background: #ccc; - transform: none; - } - - .dog-calculator-percentage-slider:disabled::-moz-range-thumb { - background: #ccc; - cursor: not-allowed; - } - - .dog-calculator-percentage-input:disabled { - opacity: 0.5; - cursor: not-allowed; - background-color: #f8f8f8; - border-color: #ddd; - pointer-events: none; - } - - /* Dark theme disabled styles */ - .dog-calculator-container.theme-dark .dog-calculator-percentage-slider:disabled { - background: #2a2530; - } - - .dog-calculator-container.theme-dark .dog-calculator-percentage-input:disabled { - background-color: #2a2530; - border-color: #3a3442; - color: #8a8a8a; - } - - /* System theme disabled styles */ - @media (prefers-color-scheme: dark) { - .dog-calculator-container.theme-system .dog-calculator-percentage-slider:disabled { - background: #2a2530; - } - - .dog-calculator-container.theme-system .dog-calculator-percentage-input:disabled { - background-color: #2a2530; - border-color: #3a3442; - color: #8a8a8a; - } - } - - /* Food Amount Breakdown Styling */ - .dog-calculator-food-amounts-section { - margin-top: 1.5rem; - padding: 1rem; - background: var(--bg-secondary); - border-radius: 8px; - border: 1px solid var(--border-color); - } - - .dog-calculator-section-title { - margin: 0 0 1rem 0; - font-size: 1rem; - font-weight: 600; - color: var(--text-primary); - } - - .dog-calculator-food-amounts-list { - display: flex; - flex-direction: column; - gap: 0.75rem; - margin-bottom: 1rem; - } - - .dog-calculator-food-amount-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem; - background: var(--bg-primary); - border-radius: 6px; - border: 1px solid var(--border-color); - } - - .dog-calculator-food-amount-label { - font-weight: 500; - color: var(--text-primary); - display: flex; - align-items: center; - gap: 0.5rem; - } - - .dog-calculator-food-percentage { - background: var(--primary-color); - color: white; - padding: 0.2rem 0.5rem; - border-radius: 12px; - font-size: 0.8rem; - font-weight: 500; - } - - .dog-calculator-lock-indicator { - font-size: 0.8rem; - opacity: 0.7; - } - - .dog-calculator-food-amount-value { - font-weight: 600; - color: var(--text-primary); - font-size: 1rem; - } - - /* Warning styles for missing energy content */ - .dog-calculator-warning { - color: #e11d48; - font-weight: 500; - font-size: 1.2rem; - text-align: left; - cursor: help; - } - - /* Inline unit selector in results */ - .dog-calculator-inline-unit { - margin-left: 12px; - min-width: 110px; - padding: 4px 8px; - background: var(--bg-primary); - border: 1px solid rgba(241, 154, 95, 0.4); - border-radius: 6px; - color: var(--text-primary); - font-size: 0.9rem; - font-weight: 500; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - transition: all 0.2s ease; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236f3f6d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); - background-repeat: no-repeat; - background-position: right 8px center; - background-size: 16px; - padding-right: 32px; - } - - .dog-calculator-inline-unit:hover { - border-color: #f19a5f; - box-shadow: 0 2px 6px rgba(241, 154, 95, 0.2); - } - - .dog-calculator-inline-unit:focus { - outline: none; - border-color: #f19a5f; - box-shadow: 0 0 0 3px rgba(241, 154, 95, 0.1); - } - - /* Inline days input in breakdown header */ - .dog-calculator-inline-days { - width: 60px; - padding: 2px 6px; - border: 1px solid var(--border-color); - border-radius: 4px; - text-align: center; - font-size: inherit; - font-family: inherit; - margin: 0 4px; - } - - /* Unit selection buttons */ - .dog-calculator-unit-buttons { - display: flex; - justify-content: center; - gap: 16px; - margin: 24px auto; - flex-wrap: wrap; - width: fit-content; - } - - .dog-calculator-unit-btn { - padding: 8px 14px; - border: 2px solid var(--border-color); - border-radius: 6px; - background: var(--bg-primary); - color: var(--text-primary); - font-size: 0.9rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - min-width: 50px; - text-align: center; - } - - .dog-calculator-unit-btn:hover { - border-color: #f19a5f; - background: rgba(241, 154, 95, 0.1); - } - - .dog-calculator-unit-btn.active { - border-color: #f19a5f; - background: #f19a5f; - color: white; - } - - .dog-calculator-unit-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - border-color: var(--border-color); - background: var(--bg-tertiary); - color: var(--text-secondary); - } - - .dog-calculator-unit-btn:disabled:hover { - border-color: var(--border-color); - background: var(--bg-tertiary); - transform: none; - } - - /* Hidden unit select for compatibility */ - .dog-calculator-unit-select-hidden { - display: none; - } - - /* Mobile responsive adjustments for inline unit selector */ - @media (max-width: 576px) { - .dog-calculator-result-item { - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - } - - .dog-calculator-result-label { - width: 100%; - text-align: center; - margin-bottom: 4px; - } - - .dog-calculator-result-value { - display: inline-block; - } - - .dog-calculator-inline-unit { - display: inline-block; - margin-left: 8px; - min-width: 90px; - vertical-align: middle; - } - - /* Center the breakdown header on mobile */ - .dog-calculator-section-title { - text-align: center; - } - - /* Ensure food breakdown items stay on one line */ - .dog-calculator-food-amount-item { - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: nowrap; - text-align: left; - } - - .dog-calculator-food-amount-label { - flex: 1; - min-width: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - text-align: left; - } - - .dog-calculator-food-amount-value { - flex-shrink: 0; - margin-left: 8px; - text-align: right; - } - } - - .dog-calculator-total-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem; - background: var(--primary-color); - color: white; - border-radius: 6px; - font-weight: 600; - margin-top: 0.5rem; - } - - .dog-calculator-total-label { - font-size: 1rem; - } - - .dog-calculator-total-value { - font-size: 1.1rem; - font-weight: 700; - } - - .dog-calculator-full-width { - flex: 1; - } - - /* Editable Food Source Name Styling */ - .dog-calculator-food-source-name-input { - background: transparent; - border: 2px solid transparent; - color: var(--text-primary); - font-size: 1.1rem; - font-weight: 600; - font-family: inherit; - padding: 0.5rem 0; - border-radius: 4px; - width: 100%; - outline: none; - transition: all 0.2s ease; - cursor: text; - } - - .dog-calculator-food-source-name-input:hover { - border-color: var(--border-color); - background: var(--bg-secondary); - padding: 0.5rem; - } - - .dog-calculator-food-source-name-input:focus { - border-color: var(--primary-color); - background: var(--bg-primary); - box-shadow: 0 0 0 3px rgba(241, 154, 95, 0.1); - padding: 0.5rem; - } - - .dog-calculator-food-source-name-input::placeholder { - color: var(--text-secondary); - opacity: 0.7; - } - - /* Dark theme adjustments */ - .dog-calculator-container.theme-dark .dog-calculator-food-source-name-input:hover { - background: #2a2530; - } - - .dog-calculator-container.theme-dark .dog-calculator-food-source-name-input:focus { - background: #1e1a24; - } - - /* System theme adjustments */ - @media (prefers-color-scheme: dark) { - .dog-calculator-container.theme-system .dog-calculator-food-source-name-input:hover { - background: #2a2530; - } - - .dog-calculator-container.theme-system .dog-calculator-food-source-name-input:focus { - background: #1e1a24; - } - } - - /* Responsive adjustments */ - @media (max-width: 576px) { - .dog-calculator-food-amount-item { - flex-direction: row !important; - gap: 0.5rem; - text-align: left !important; - justify-content: space-between !important; - align-items: center !important; - flex-wrap: nowrap !important; - } - - .dog-calculator-food-amount-label { - justify-content: flex-start !important; - text-align: left !important; - flex: 1; - min-width: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .dog-calculator-food-amount-value { - flex-shrink: 0; - margin-left: 8px; - text-align: right !important; - } - - .dog-calculator-food-source-name-input { - font-size: 1rem; - } - - }`; - - function injectStyles() { - if (document.getElementById('dog-calculator-styles')) return; - - const style = document.createElement('style'); - style.id = 'dog-calculator-styles'; - style.textContent = CSS_STYLES; - document.head.appendChild(style); - } - - // JavaScript from src/calculator.js (transformed for widget use) - /** - * Configuration constants for Dog Calorie Calculator - */ - -const CALCULATOR_CONFIG = { - defaultTheme: 'system', - defaultScale: 1.0, - maxFoodSources: 5, - minScale: 0.5, - maxScale: 2.0 -}; - -/** - * Dog Calorie Calculator - iframe version - * by Canine Nutrition and Wellness - */ - - class DogCalorieCalculatorWidget { - constructor(container, options = {}) { - this.container = container; - this.options = { - theme: options.theme || this.getThemeFromURL() || 'system', - scale: options.scale || this.getScaleFromURL() || 1.0, - ...options - }; - this.theme = this.options.theme; - this.scale = this.options.scale; - this.currentMER = 0; - this.currentMERMin = 0; // For range calculations - this.currentMERMax = 0; // For range calculations - this.isImperial = false; - - - this.foodSources = []; - this.maxFoodSources = CALCULATOR_CONFIG.maxFoodSources; - this.mealsPerDay = 2; - this.showPerMeal = false; - this.init(); - } - - init() { - // Inject the calculator HTML into the container - this.container.innerHTML = `
-
-
-

Dog's Characteristics

-
- Metric - - Imperial -
-
- -
- - -
- -
- - -
Please enter a valid weight (minimum 0.1 kg)
-
- - -
- -
-
-

How much should I feed?

-
-
-
- -
- -
- - - - - - - - - - - - - - - - - - - - -
-
-
- -
- - -
- - - - - - - -
`; - - // Apply widget-specific settings - this.applyTheme(); - this.applyScale(); - - // Continue with original init logic - this.applyTheme(); - this.applyScale(); - this.initializeFoodSources(); - this.bindEvents(); - this.updateUnitLabels(); - this.setupIframeResize(); - - // Show the calculator with fade-in - const container = this.container.querySelector('#dogCalculator'); - container.classList.add('loaded'); - } - - getThemeFromURL() { - const urlParams = new URLSearchParams(window.location.search); - const theme = urlParams.get('theme'); - return ['light', 'dark', 'system'].includes(theme) ? theme : null; - } - - getScaleFromURL() { - const urlParams = new URLSearchParams(window.location.search); - const scale = parseFloat(urlParams.get('scale')); - return (!isNaN(scale) && scale >= CALCULATOR_CONFIG.minScale && scale <= CALCULATOR_CONFIG.maxScale) ? scale : null; - } - - applyTheme() { - const container = this.container.querySelector('#dogCalculator'); - container.classList.remove('theme-light', 'theme-dark', 'theme-system'); - container.classList.add('theme-' + this.theme); - } - - applyScale() { - const container = this.container.querySelector('#dogCalculator'); - if (!container) return; - - // Clamp scale between min and max for usability - const clampedScale = Math.max(CALCULATOR_CONFIG.minScale, Math.min(CALCULATOR_CONFIG.maxScale, this.scale)); - - if (clampedScale !== 1.0) { - container.style.transform = `scale(${clampedScale})`; - container.style.transformOrigin = 'top center'; - - // Adjust container to account for scaling - setTimeout(() => { - const actualHeight = container.offsetHeight * clampedScale; - container.style.marginBottom = `${(clampedScale - 1) * container.offsetHeight}px`; - this.sendHeightToParent(); - }, 100); - } - } - - // Food Source Management Methods - initializeFoodSources() { - this.addFoodSource(); - this.updateAddButton(); - } - - addFoodSource() { - if (this.foodSources.length >= this.maxFoodSources) { - return; - } - - const id = this.generateFoodSourceId(); - const foodSource = { - id: id, - name: `Food Source ${this.foodSources.length + 1}`, - energy: '', - energyUnit: this.isImperial ? 'kcalcup' : 'kcal100g', - percentage: this.foodSources.length === 0 ? 100 : 0, - isLocked: false - }; - - this.foodSources.push(foodSource); - this.redistributePercentages(); - this.renderFoodSource(foodSource); - this.updateAddButton(); - this.updateRemoveButtons(); - this.refreshAllPercentageUI(); - } - - removeFoodSource(id) { - if (this.foodSources.length <= 1) { - return; // Cannot remove the last food source - } - - const index = this.foodSources.findIndex(fs => fs.id === id); - if (index === -1) return; - - this.foodSources.splice(index, 1); - - // Remove the DOM element - const element = document.getElementById(`foodSource-${id}`); - if (element) { - element.remove(); - } - - // Redistribute percentages among remaining sources - this.redistributePercentages(); - this.updateFoodSourceNames(); - this.updateAddButton(); - this.updateRemoveButtons(); - this.refreshAllPercentageUI(); - } - - generateFoodSourceId() { - return 'fs_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); - } - - redistributePercentages() { - const count = this.foodSources.length; - if (count === 0) return; - - // 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); - - unlockedSources.forEach((fs, index) => { - fs.percentage = equalPercentage + (index < remainder ? 1 : 0); - }); - } - - // Update the UI sliders and inputs - this.refreshAllPercentageUI(); - } - - // OBSOLETE METHODS - Replaced by new validation system - // Keeping for reference but these are no longer used - - /* - updatePercentageInputs() { - this.foodSources.forEach(fs => { - const slider = document.getElementById(`percentage-slider-${fs.id}`); - const input = document.getElementById(`percentage-input-${fs.id}`); - const display = document.getElementById(`percentage-display-${fs.id}`); - - if (slider) slider.value = fs.percentage; - if (input) input.value = fs.percentage; - if (display) display.textContent = `${fs.percentage}%`; - }); - - // Update constraints after values are set - this.updatePercentageConstraints(); - } - - updatePercentageConstraints() { - this.foodSources.forEach(fs => { - const slider = document.getElementById(`percentage-slider-${fs.id}`); - const input = document.getElementById(`percentage-input-${fs.id}`); - - if (!slider || !input) return; - - // Always keep full 0-100 scale for all sliders - slider.max = 100; - input.max = 100; - - if (fs.isLocked) { - // Locked sources can't be changed - slider.disabled = true; - input.disabled = true; - } else { - // Calculate the maximum this source can have - const lockedSources = this.foodSources.filter(other => other.id !== fs.id && other.isLocked); - const totalLockedPercentage = lockedSources.reduce((sum, other) => sum + other.percentage, 0); - const maxAllowed = 100 - totalLockedPercentage; - - // Re-enable - slider.disabled = false; - input.disabled = false; - - // Store max allowed for validation (we'll check this in event handlers) - slider.dataset.maxAllowed = maxAllowed; - input.dataset.maxAllowed = maxAllowed; - - // If current value exceeds max, adjust it - if (fs.percentage > maxAllowed) { - fs.percentage = maxAllowed; - slider.value = maxAllowed; - input.value = maxAllowed; - document.getElementById(`percentage-display-${fs.id}`).textContent = `${maxAllowed}%`; - } - } - }); - } - - adjustPercentages(changedId, newPercentage) { - const changedIndex = this.foodSources.findIndex(fs => fs.id === changedId); - if (changedIndex === -1) return; - - const oldPercentage = this.foodSources[changedIndex].percentage; - const difference = newPercentage - oldPercentage; - - this.foodSources[changedIndex].percentage = newPercentage; - - // Only redistribute among unlocked sources (excluding the changed one) - const otherUnlockedSources = this.foodSources.filter((fs, index) => - index !== changedIndex && !fs.isLocked - ); - - // If this is the only unlocked source, force it to fill remaining percentage - if (otherUnlockedSources.length === 0) { - const lockedSources = this.foodSources.filter((fs, index) => - index !== changedIndex && fs.isLocked - ); - const totalLockedPercentage = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0); - const requiredPercentage = 100 - totalLockedPercentage; - - // Force the changed source to the required percentage - this.foodSources[changedIndex].percentage = requiredPercentage; - this.updatePercentageInputs(); - this.updateFoodCalculations(); - 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); - - otherUnlockedSources.forEach((fs, index) => { - fs.percentage = equalShare + (index < remainder ? 1 : 0); - }); - } else { - // Distribute proportionally among unlocked sources - const scale = availablePercentage / totalUnlockedPercentage; - - let distributedTotal = 0; - otherUnlockedSources.forEach((fs, index) => { - if (index === otherUnlockedSources.length - 1) { - // Last item gets the remainder to ensure exact 100% - fs.percentage = availablePercentage - distributedTotal; - } else { - fs.percentage = Math.round(fs.percentage * scale); - distributedTotal += fs.percentage; - } - }); - } - - this.updatePercentageInputs(); - this.updateFoodCalculations(); - } - */ - - // New validation system methods - validatePercentageChange(sourceId, requestedValue) { - // Find the source being changed - const changedSource = this.foodSources.find(fs => fs.id === sourceId); - if (!changedSource) { - return { isValid: false, reason: 'Source not found' }; - } - - // If the source is locked, no change allowed - if (changedSource.isLocked) { - return { isValid: false, reason: 'Source is locked' }; - } - - // Ensure requested value is within bounds - const clampedValue = Math.max(0, Math.min(100, requestedValue)); - - // Calculate locked and other unlocked totals - const lockedSources = this.foodSources.filter(fs => fs.id !== sourceId && fs.isLocked); - const otherUnlockedSources = this.foodSources.filter(fs => fs.id !== sourceId && !fs.isLocked); - - const totalLocked = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0); - - // Check if the only unlocked source - if (otherUnlockedSources.length === 0) { - // This is the only unlocked source, must fill remaining percentage - const requiredPercentage = 100 - totalLocked; - return { - isValid: true, - actualValue: requiredPercentage, - affectedSources: [{ id: sourceId, newPercentage: requiredPercentage }], - reason: 'Only unlocked source, forced to fill remainder' - }; - } - - // Calculate available percentage for redistribution - const availableForOthers = 100 - clampedValue - totalLocked; - - // Check if redistribution is possible - if (availableForOthers < 0) { - // Cannot accommodate this value - const maxAllowed = 100 - totalLocked; - return { - isValid: true, - actualValue: maxAllowed, - affectedSources: this.calculateRedistribution(sourceId, maxAllowed, otherUnlockedSources), - reason: 'Value clamped to maximum allowed' - }; - } - - // Calculate redistribution - const affectedSources = this.calculateRedistribution(sourceId, clampedValue, otherUnlockedSources); - - return { - isValid: true, - actualValue: clampedValue, - affectedSources: affectedSources, - reason: 'Valid change' - }; - } - - calculateRedistribution(sourceId, newValue, otherUnlockedSources) { - const result = [{ id: sourceId, newPercentage: newValue }]; - - if (otherUnlockedSources.length === 0) { - return result; - } - - // Calculate total locked percentage - const lockedSources = this.foodSources.filter(fs => fs.id !== sourceId && fs.isLocked); - const totalLocked = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0); - - // Available percentage for other unlocked sources - const availableForOthers = 100 - newValue - totalLocked; - - // Current total of other unlocked sources - const currentOtherTotal = otherUnlockedSources.reduce((sum, fs) => sum + fs.percentage, 0); - - if (currentOtherTotal === 0 || availableForOthers === 0) { - // Distribute equally among other unlocked sources - const equalShare = Math.floor(availableForOthers / otherUnlockedSources.length); - const remainder = availableForOthers - (equalShare * otherUnlockedSources.length); - - otherUnlockedSources.forEach((fs, index) => { - const newPercentage = equalShare + (index < remainder ? 1 : 0); - result.push({ id: fs.id, newPercentage }); - }); - } else { - // Distribute proportionally - const scale = availableForOthers / currentOtherTotal; - let distributedTotal = 0; - - otherUnlockedSources.forEach((fs, index) => { - let newPercentage; - if (index === otherUnlockedSources.length - 1) { - // Last item gets remainder to ensure exact total - newPercentage = availableForOthers - distributedTotal; - } else { - newPercentage = Math.round(fs.percentage * scale); - distributedTotal += newPercentage; - } - result.push({ id: fs.id, newPercentage }); - }); - } - - return result; - } - - applyValidatedChanges(validationResult) { - if (!validationResult.isValid) { - return false; - } - - // Apply all percentage changes - validationResult.affectedSources.forEach(change => { - const source = this.foodSources.find(fs => fs.id === change.id); - if (source) { - source.percentage = change.newPercentage; - } - }); - - return true; - } - - refreshAllPercentageUI() { - this.foodSources.forEach(fs => { - // Update all UI elements from single source of truth - const slider = document.getElementById(`percentage-slider-${fs.id}`); - const input = document.getElementById(`percentage-input-${fs.id}`); - const display = document.getElementById(`percentage-display-${fs.id}`); - - if (slider) slider.value = fs.percentage; - if (input) input.value = fs.percentage; - if (display) display.textContent = `${fs.percentage}%`; - - // Update constraints and disabled states - this.updateSliderConstraints(fs); - }); - - // Update food calculations - this.updateFoodCalculations(); - } - - updateSliderConstraints(foodSource) { - const slider = document.getElementById(`percentage-slider-${foodSource.id}`); - const input = document.getElementById(`percentage-input-${foodSource.id}`); - - if (!slider || !input) return; - - // Always keep 0-100 scale - slider.max = 100; - input.max = 100; - - if (foodSource.isLocked) { - slider.disabled = true; - input.disabled = true; - } else { - // Calculate maximum allowed and store for validation - const maxAllowed = this.calculateMaxAllowed(foodSource.id); - slider.disabled = (maxAllowed <= 0); - input.disabled = (maxAllowed <= 0); - slider.dataset.maxAllowed = maxAllowed; - input.dataset.maxAllowed = maxAllowed; - } - } - - calculateMaxAllowed(sourceId) { - const lockedSources = this.foodSources.filter(fs => fs.id !== sourceId && fs.isLocked); - const otherUnlockedSources = this.foodSources.filter(fs => fs.id !== sourceId && !fs.isLocked); - - const totalLocked = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0); - - // If this is the only unlocked source, it must take up the remainder - if (otherUnlockedSources.length === 0) { - return 100 - totalLocked; - } - - // Otherwise, maximum is 100 minus locked percentages - return Math.max(0, 100 - totalLocked); - } - - updateFoodSourceNames() { - this.foodSources.forEach((fs, index) => { - // Only update if the name is still the default pattern - if (fs.name.match(/^Food Source \d+$/)) { - fs.name = `Food Source ${index + 1}`; - const titleElement = document.getElementById(`food-title-${fs.id}`); - if (titleElement) { - titleElement.value = fs.name; - } - } - }); - } - - updateAddButton() { - const addBtn = this.container.querySelector('#addFoodBtn'); - - if (addBtn) { - const remaining = this.maxFoodSources - this.foodSources.length; - const buttonText = addBtn.querySelector('span:last-child'); - - if (remaining <= 0) { - // Disable button and show max reached message - addBtn.disabled = true; - if (buttonText) { - buttonText.textContent = `Maximum ${this.maxFoodSources} sources reached`; - } - } else { - // Enable button with normal text - addBtn.disabled = false; - if (buttonText) { - buttonText.textContent = 'Add another food source'; - } - } - } - } - - updateRemoveButtons() { - // Show/hide remove buttons based on whether we have more than one source - const hasMultipleSources = this.foodSources.length > 1; - - this.foodSources.forEach(fs => { - const removeBtn = document.getElementById(`remove-${fs.id}`); - if (removeBtn) { - removeBtn.style.display = hasMultipleSources ? 'block' : 'none'; - } - }); - } - - renderFoodSource(foodSource) { - const container = this.container.querySelector('#foodSources'); - if (!container) return; - - const cardHTML = ` -
-
- - -
- -
-
- - -
-
- - -
-
-
Please enter a valid energy content
- -
- -
- - -
-
-
- `; - - container.insertAdjacentHTML('beforeend', cardHTML); - - // Bind events for the new food source - this.bindFoodSourceEvents(foodSource.id); - } - - bindFoodSourceEvents(id) { - // Name input events - const nameInput = document.getElementById(`food-title-${id}`); - - // Energy input events - const energyInput = document.getElementById(`energy-${id}`); - const energyUnitSelect = document.getElementById(`energy-unit-${id}`); - const percentageSlider = document.getElementById(`percentage-slider-${id}`); - const percentageInput = document.getElementById(`percentage-input-${id}`); - const removeBtn = document.getElementById(`remove-${id}`); - const lockBtn = document.getElementById(`lock-${id}`); - - if (nameInput) { - nameInput.addEventListener('input', () => { - const newName = nameInput.value.trim() || `Food Source ${this.foodSources.findIndex(fs => fs.id === id) + 1}`; - this.updateFoodSourceData(id, 'name', newName); - this.updateFoodCalculations(); // This will refresh the food amount breakdown with new names - }); - - nameInput.addEventListener('blur', () => { - // If field is empty, restore default name - if (!nameInput.value.trim()) { - const defaultName = `Food Source ${this.foodSources.findIndex(fs => fs.id === id) + 1}`; - nameInput.value = defaultName; - this.updateFoodSourceData(id, 'name', defaultName); - this.updateFoodCalculations(); - } - }); - } - - if (energyInput) { - energyInput.addEventListener('input', () => { - this.updateFoodSourceData(id, 'energy', energyInput.value); - - // Auto-select cups when entering energy for kcal/cup - const foodSource = this.foodSources.find(fs => fs.id === id); - if (foodSource && foodSource.energyUnit === 'kcalcup' && parseFloat(energyInput.value) > 0) { - const unitSelect = this.container.querySelector('#unit'); - const cupsButton = this.container.querySelector('#cupsButton'); - - // First check if cups button will be enabled after update - const willEnableCups = this.foodSources.some(fs => - fs.energyUnit === 'kcalcup' && fs.energy && parseFloat(fs.energy) > 0 - ); - - if (willEnableCups && unitSelect) { - // Set cups BEFORE updating calculations - unitSelect.value = 'cups'; - unitSelect.setAttribute('value', 'cups'); - this.setActiveUnitButton('cups'); - - // Enable the cups button manually since we know it will be valid - if (cupsButton) { - cupsButton.disabled = false; - cupsButton.title = 'Show amounts in cups'; - } - } - - // Now update calculations with cups already selected - this.updateFoodCalculations(); - } else { - this.updateFoodCalculations(); - } - }); - energyInput.addEventListener('blur', () => this.validateFoodSourceEnergy(id)); - } - - if (energyUnitSelect) { - energyUnitSelect.addEventListener('change', () => { - this.updateFoodSourceData(id, 'energyUnit', energyUnitSelect.value); - - // Auto-select the most appropriate unit based on energy unit - const unitSelect = this.container.querySelector('#unit'); - const energyInput = document.getElementById(`energy-${id}`); - - if (unitSelect) { - switch(energyUnitSelect.value) { - case 'kcalcup': - // Check if we have energy value to enable cups - const foodSource = this.foodSources.find(fs => fs.id === id); - if (foodSource && foodSource.energy && parseFloat(foodSource.energy) > 0) { - // Set cups BEFORE updating calculations - unitSelect.value = 'cups'; - unitSelect.setAttribute('value', 'cups'); - this.setActiveUnitButton('cups'); - - // Enable the cups button manually - const cupsButton = this.container.querySelector('#cupsButton'); - if (cupsButton) { - cupsButton.disabled = false; - cupsButton.title = 'Show amounts in cups'; - } - } - this.updateFoodCalculations(); - break; - case 'kcal100g': - // For kcal/100g, select grams - unitSelect.value = 'g'; - this.setActiveUnitButton('g'); - this.updateFoodCalculations(); - break; - case 'kcalkg': - // For kcal/kg, also select grams (or could be kg) - unitSelect.value = 'g'; - this.setActiveUnitButton('g'); - this.updateFoodCalculations(); - break; - case 'kcalcan': - // For kcal/can, use grams as default (or ounces in imperial) - unitSelect.value = this.isImperial ? 'oz' : 'g'; - this.setActiveUnitButton(unitSelect.value); - this.updateFoodCalculations(); - break; - } - } else { - // No unit select, just update calculations - this.updateFoodCalculations(); - } - }); - } - - if (percentageSlider) { - percentageSlider.addEventListener('input', () => { - const requestedValue = parseInt(percentageSlider.value); - const result = this.validatePercentageChange(id, requestedValue); - - if (result.isValid) { - this.applyValidatedChanges(result); - } - // Always refresh to ensure valid state - this.refreshAllPercentageUI(); - }); - } - - if (percentageInput) { - percentageInput.addEventListener('change', () => { - const requestedValue = parseInt(percentageInput.value) || 0; - const result = this.validatePercentageChange(id, requestedValue); - - if (result.isValid) { - this.applyValidatedChanges(result); - } - this.refreshAllPercentageUI(); - }); - } - - 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(); - this.refreshAllPercentageUI(); - } - - 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'; - } - } - }); - - // Update percentage constraints based on lock states - this.refreshAllPercentageUI(); - } - - updateFoodSourceData(id, field, value) { - const foodSource = this.foodSources.find(fs => fs.id === id); - if (foodSource) { - foodSource[field] = value; - } - } - - validateFoodSourceEnergy(id) { - const energyInput = document.getElementById(`energy-${id}`); - const energyUnitSelect = document.getElementById(`energy-unit-${id}`); - const errorElement = document.getElementById(`energy-error-${id}`); - - if (!energyInput || !energyUnitSelect || !errorElement) return; - - const energy = parseFloat(energyInput.value); - const unit = energyUnitSelect.value; - - let minValue = 1; - switch (unit) { - case 'kcal100g': minValue = 1; break; - case 'kcalkg': minValue = 10; break; - case 'kcalcup': minValue = 50; break; - case 'kcalcan': minValue = 100; break; - } - - if (!this.validateInput(energy, minValue)) { - errorElement.classList.remove('dog-calculator-hidden'); - } else { - errorElement.classList.add('dog-calculator-hidden'); - } - } - - bindEvents() { - const weightInput = this.container.querySelector('#weight'); - const dogTypeSelect = this.container.querySelector('#dogType'); - const daysInput = this.container.querySelector('#days'); - const unitSelect = this.container.querySelector('#unit'); - const unitToggle = this.container.querySelector('#unitToggle'); - const addFoodBtn = this.container.querySelector('#addFoodBtn'); - - if (weightInput) { - weightInput.addEventListener('input', () => this.updateCalorieCalculations()); - weightInput.addEventListener('blur', () => this.validateWeight()); - } - - if (dogTypeSelect) dogTypeSelect.addEventListener('change', () => this.updateCalorieCalculations()); - - if (daysInput) { - daysInput.addEventListener('input', () => { - this.updateDayLabel(); - this.updateFoodCalculations(); - }); - daysInput.addEventListener('blur', () => this.validateDays()); - } - - if (unitSelect) unitSelect.addEventListener('change', () => this.updateFoodCalculations()); - - // Unit button event listeners - const unitButtons = this.container.querySelectorAll('.dog-calculator-unit-btn'); - unitButtons.forEach(button => { - button.addEventListener('click', (e) => { - const selectedUnit = e.target.dataset.unit; - this.setActiveUnitButton(selectedUnit); - // Update hidden select to trigger existing logic - if (unitSelect) { - unitSelect.value = selectedUnit; - this.updateFoodCalculations(); - } - }); - }); - - if (unitToggle) unitToggle.addEventListener('change', () => this.toggleUnits()); - - if (addFoodBtn) addFoodBtn.addEventListener('click', () => this.addFoodSource()); - - // Feeding configuration event listeners - const showDaily = this.container.querySelector('#showDaily'); - const showPerMeal = this.container.querySelector('#showPerMeal'); - const mealsPerDayInput = this.container.querySelector('#mealsPerDay'); - const mealInputGroup = this.container.querySelector('#mealInputGroup'); - - if (showDaily) { - showDaily.addEventListener('change', () => { - if (showDaily.checked) { - this.showPerMeal = false; - if (mealInputGroup) mealInputGroup.style.display = 'none'; - this.updateDayLabel(); - this.updateFoodCalculations(); - } - }); - } - - if (showPerMeal) { - showPerMeal.addEventListener('change', () => { - if (showPerMeal.checked) { - this.showPerMeal = true; - if (mealInputGroup) mealInputGroup.style.display = 'inline-flex'; - this.updateDayLabel(); - this.updateFoodCalculations(); - } - }); - } - - if (mealsPerDayInput) { - mealsPerDayInput.addEventListener('input', () => { - const meals = parseInt(mealsPerDayInput.value); - if (meals && meals >= 1 && meals <= 10) { - this.mealsPerDay = meals; - if (this.showPerMeal) { - this.updateDayLabel(); - this.updateFoodCalculations(); - } - } - }); - - mealsPerDayInput.addEventListener('blur', () => { - if (!mealsPerDayInput.value || parseInt(mealsPerDayInput.value) < 1) { - mealsPerDayInput.value = 2; - this.mealsPerDay = 2; - if (this.showPerMeal) { - this.updateDayLabel(); - this.updateFoodCalculations(); - } - } - }); - } - - // Modal event listeners - const shareBtn = this.container.querySelector('#shareBtn'); - const embedBtn = this.container.querySelector('#embedBtn'); - const shareModalClose = this.container.querySelector('#shareModalClose'); - const embedModalClose = this.container.querySelector('#embedModalClose'); - - if (shareBtn) shareBtn.addEventListener('click', () => this.showShareModal()); - if (embedBtn) embedBtn.addEventListener('click', () => this.showEmbedModal()); - if (shareModalClose) shareModalClose.addEventListener('click', () => this.hideShareModal()); - if (embedModalClose) embedModalClose.addEventListener('click', () => this.hideEmbedModal()); - - // Share buttons - const shareFacebook = this.container.querySelector('#shareFacebook'); - const shareTwitter = this.container.querySelector('#shareTwitter'); - const shareLinkedIn = this.container.querySelector('#shareLinkedIn'); - const shareEmail = this.container.querySelector('#shareEmail'); - const shareCopy = this.container.querySelector('#shareCopy'); - - if (shareFacebook) shareFacebook.addEventListener('click', () => this.shareToFacebook()); - if (shareTwitter) shareTwitter.addEventListener('click', () => this.shareToTwitter()); - if (shareLinkedIn) shareLinkedIn.addEventListener('click', () => this.shareToLinkedIn()); - if (shareEmail) shareEmail.addEventListener('click', () => this.shareViaEmail()); - if (shareCopy) shareCopy.addEventListener('click', () => this.copyShareLink()); - - // Copy buttons - const copyWidget = this.container.querySelector('#copyWidget'); - const copyIframe = this.container.querySelector('#copyIframe'); - - if (copyWidget) copyWidget.addEventListener('click', () => this.copyEmbedCode('widget')); - if (copyIframe) copyIframe.addEventListener('click', () => this.copyEmbedCode('iframe')); - - // Close modals on outside click - const shareModal = this.container.querySelector('#shareModal'); - const embedModal = this.container.querySelector('#embedModal'); - - if (shareModal) { - shareModal.addEventListener('click', (e) => { - if (e.target === shareModal) this.hideShareModal(); - }); - } - - if (embedModal) { - embedModal.addEventListener('click', (e) => { - if (e.target === embedModal) this.hideEmbedModal(); - }); - } - } - - toggleUnits() { - const toggle = this.container.querySelector('#unitToggle'); - this.isImperial = toggle.checked; - - this.updateUnitLabels(); - this.convertExistingValues(); - this.updateCalorieCalculations(); - } - - updateUnitLabels() { - const metricLabel = this.container.querySelector('#metricLabel'); - const imperialLabel = this.container.querySelector('#imperialLabel'); - const weightLabel = this.container.querySelector('#weightLabel'); - const weightInput = this.container.querySelector('#weight'); - const unitSelect = this.container.querySelector('#unit'); - - if (metricLabel && imperialLabel) { - metricLabel.classList.toggle('active', !this.isImperial); - imperialLabel.classList.toggle('active', this.isImperial); - } - - if (this.isImperial) { - if (weightLabel) weightLabel.textContent = "Dog's Weight (lbs):"; - if (weightInput) { - weightInput.placeholder = "Enter weight in lbs"; - weightInput.min = "0.2"; - weightInput.step = "0.1"; - } - if (unitSelect) { - unitSelect.innerHTML = '' + - '' + - '' + - ''; - unitSelect.value = 'oz'; // Auto-select ounces for imperial - this.setActiveUnitButton('oz'); // Sync unit buttons - } - - // Update energy units for all food sources to kcal/cup for imperial - this.foodSources.forEach(fs => { - if (fs.energyUnit === 'kcal100g') { - fs.energyUnit = 'kcalcup'; - const energyUnitSelect = document.getElementById(`energy-unit-${fs.id}`); - if (energyUnitSelect) { - energyUnitSelect.value = 'kcalcup'; - } - } - }); - } else { - if (weightLabel) weightLabel.textContent = "Dog's Weight (kg):"; - if (weightInput) { - weightInput.placeholder = "Enter weight in kg"; - weightInput.min = "0.1"; - weightInput.step = "0.1"; - } - if (unitSelect) { - unitSelect.innerHTML = '' + - '' + - '' + - ''; - unitSelect.value = 'g'; // Auto-select grams for metric - this.setActiveUnitButton('g'); // Sync unit buttons - } - - // Update energy units for all food sources to kcal/100g for metric - this.foodSources.forEach(fs => { - if (fs.energyUnit === 'kcalcup') { - fs.energyUnit = 'kcal100g'; - const energyUnitSelect = document.getElementById(`energy-unit-${fs.id}`); - if (energyUnitSelect) { - energyUnitSelect.value = 'kcal100g'; - } - } - }); - } - } - - convertExistingValues() { - const weightInput = this.container.querySelector('#weight'); - - if (weightInput && weightInput.value) { - const currentWeight = parseFloat(weightInput.value); - if (!isNaN(currentWeight)) { - if (this.isImperial) { - weightInput.value = this.formatNumber(currentWeight * 2.20462, 1); - } else { - weightInput.value = this.formatNumber(currentWeight / 2.20462, 1); - } - } - } - } - - getWeightInKg() { - const weightInput = this.container.querySelector('#weight'); - if (!weightInput || !weightInput.value) return null; - - const weight = parseFloat(weightInput.value); - if (isNaN(weight)) return null; - - return this.isImperial ? weight / 2.20462 : weight; - } - - - calculateRER(weightKg) { - return 70 * Math.pow(weightKg, 0.75); - } - - calculateMER(rer, factor) { - return rer * factor; - } - - // Get the range multipliers for each life stage - getLifeStageRange(factor) { - // Define ranges based on the reference image - const ranges = { - '3.0': { min: 3.0, max: 3.0 }, // Puppy 0-4 months (no range) - '2.0': { min: 2.0, max: 2.0 }, // Puppy 4m-adult OR Working light (no range for puppies) - '1.2': { min: 1.2, max: 1.4 }, // Adult inactive/obese - '1.6': { min: 1.4, max: 1.6 }, // Adult neutered/spayed - '1.8': { min: 1.6, max: 1.8 }, // Adult intact - '1.0': { min: 1.0, max: 1.0 }, // Weight loss (fixed) - '1.7': { min: 1.2, max: 1.8 }, // Weight gain (wide range) - '5.0': { min: 5.0, max: 5.0 }, // Working heavy (upper bound) - '1.1': { min: 1.1, max: 1.1 } // Senior (no range) - }; - - const key = factor.toFixed(1); - return ranges[key] || { min: factor, max: factor }; - } - - validateInput(value, min = 0, isInteger = false) { - const num = parseFloat(value); - if (isNaN(num) || num < min) return false; - if (isInteger && !Number.isInteger(num)) return false; - return true; - } - - showError(elementId, show = true) { - const errorElement = document.getElementById(elementId); - if (errorElement) { - if (show) { - errorElement.classList.remove('dog-calculator-hidden'); - } else { - errorElement.classList.add('dog-calculator-hidden'); - } - } - } - - convertUnits(grams, unit, foodSource = null) { - switch (unit) { - case 'kg': - return grams / 1000; - case 'oz': - return grams / 28.3495; - case 'lb': - return grams / 453.592; - case 'cups': - // For cups, we need to convert from grams worth of calories to cups - if (foodSource && foodSource.energyUnit === 'kcalcup' && foodSource.energy) { - // Get calories per 100g for this food - const caloriesPerGram = this.getFoodSourceEnergyPer100g(foodSource) / 100; - // Calculate total calories represented by these grams - const totalCalories = grams * caloriesPerGram; - // Divide by calories per cup to get number of cups - const caloriesPerCup = parseFloat(foodSource.energy); - return totalCalories / caloriesPerCup; - } - return null; // Cannot convert to cups without kcal/cup - default: - return grams; - } - } - - formatNumber(num, decimals = 0) { - if (decimals === 0) { - return Math.round(num).toString(); - } - return num.toFixed(decimals).replace(/\.?0+$/, ''); - } - - validateWeight() { - const weightKg = this.getWeightInKg(); - if (weightKg !== null && weightKg < 0.1) { - this.showError('weightError', true); - } else { - this.showError('weightError', false); - } - } - - - validateDays() { - const days = this.container.querySelector('#days')?.value; - if (days && !this.validateInput(days, 1, true)) { - this.showError('daysError', true); - } else { - this.showError('daysError', false); - } - } - - updateDayLabel() { - const days = this.container.querySelector('#days')?.value; - const dayLabel = this.container.querySelector('#dayLabel'); - const mealNote = this.container.querySelector('#mealNote'); - if (dayLabel && days) { - const numDays = parseInt(days); - dayLabel.textContent = numDays === 1 ? 'day' : 'days'; - } - if (mealNote) { - if (this.showPerMeal && days) { - const numDays = parseInt(days); - const totalMeals = numDays * this.mealsPerDay; - mealNote.textContent = ` (${totalMeals} meal${totalMeals === 1 ? '' : 's'} total)`; - mealNote.style.display = 'inline'; - } else { - mealNote.style.display = 'none'; - } - } - } - - setActiveUnitButton(unit) { - const unitButtons = this.container.querySelectorAll('.dog-calculator-unit-btn'); - unitButtons.forEach(button => { - button.classList.remove('active'); - if (button.dataset.unit === unit) { - button.classList.add('active'); - } - }); - } - - updateCalorieCalculations() { - const dogTypeSelect = this.container.querySelector('#dogType'); - const calorieResults = this.container.querySelector('#calorieResults'); - const rerValue = this.container.querySelector('#rerValue'); - const merValue = this.container.querySelector('#merValue'); - - if (!dogTypeSelect || !calorieResults || !rerValue || !merValue) { - return; - } - - const weightKg = this.getWeightInKg(); - const dogTypeFactor = dogTypeSelect.value; - - this.showError('weightError', false); - - if (!weightKg || weightKg < 0.1) { - const weightInput = this.container.querySelector('#weight'); - if (weightInput && weightInput.value) this.showError('weightError', true); - calorieResults.style.display = 'none'; - return; - } - - if (!dogTypeFactor) { - calorieResults.style.display = 'none'; - return; - } - - const factor = parseFloat(dogTypeFactor); - - const rer = this.calculateRER(weightKg); - const mer = this.calculateMER(rer, factor); - - // Calculate range for MER - const range = this.getLifeStageRange(factor); - this.currentMERMin = this.calculateMER(rer, range.min); - this.currentMERMax = this.calculateMER(rer, range.max); - this.currentMER = mer; // Keep middle/selected value for compatibility - - rerValue.textContent = this.formatNumber(rer, 0) + ' cal/day'; - - // Show MER as range if applicable - if (range.min !== range.max) { - merValue.textContent = this.formatNumber(this.currentMERMin, 0) + '-' + - this.formatNumber(this.currentMERMax, 0) + ' cal/day'; - } else { - merValue.textContent = this.formatNumber(mer, 0) + ' cal/day'; - } - calorieResults.style.display = 'block'; - - this.updateFoodCalculations(); - this.sendHeightToParent(); - } - - updateCupsButtonState() { - const cupsButton = this.container.querySelector('#cupsButton'); - if (!cupsButton) return; - - // Check if any food source has kcal/cup selected - const hasKcalCup = this.foodSources.some(fs => - fs.energyUnit === 'kcalcup' && fs.energy && parseFloat(fs.energy) > 0 - ); - - if (hasKcalCup) { - cupsButton.disabled = false; - cupsButton.title = 'Show amounts in cups'; - } else { - cupsButton.disabled = true; - cupsButton.title = 'Available when using kcal/cup measurement'; - - // If cups was selected, switch back to grams - const unitSelect = this.container.querySelector('#unit'); - if (unitSelect && unitSelect.value === 'cups') { - unitSelect.value = 'g'; - this.setActiveUnitButton('g'); - } - } - } - - updateFoodCalculations() { - if (this.currentMER === 0) return; - - // Check if we have a range - const hasRange = this.currentMERMin !== this.currentMERMax; - - const daysInput = this.container.querySelector('#days'); - const unitSelect = this.container.querySelector('#unit'); - const dailyFoodResults = this.container.querySelector('#dailyFoodResults'); - const dailyFoodValue = this.container.querySelector('#dailyFoodValue'); - const foodAmountsSection = this.container.querySelector('#foodAmountsSection'); - const foodAmountsList = this.container.querySelector('#foodAmountsList'); - const totalAmountDisplay = this.container.querySelector('#totalAmountDisplay'); - const foodBreakdownResults = this.container.querySelector('#foodBreakdownResults'); - const foodBreakdownList = this.container.querySelector('#foodBreakdownList'); - const feedingConfig = this.container.querySelector('#feedingConfig'); - - // Update cups button state - this.updateCupsButtonState(); - - if (!daysInput || !unitSelect || !dailyFoodResults || !dailyFoodValue || !foodAmountsSection) { - return; - } - - const days = daysInput.value; - let unit = unitSelect.value; - - // Failsafe: if unit is empty string but cups button is active, use 'cups' - if (!unit || unit === '') { - const activeButton = this.container.querySelector('.dog-calculator-unit-btn.active'); - if (activeButton) { - unit = activeButton.dataset.unit || 'g'; - } else { - unit = 'g'; // Default fallback - } - } - - const unitLabel = unit === 'g' ? 'g' : unit === 'kg' ? 'kg' : unit === 'oz' ? 'oz' : unit === 'lb' ? 'lb' : 'cups'; - const decimals = unit === 'g' ? 0 : unit === 'kg' ? 2 : unit === 'cups' ? 1 : 1; - - // Debug: log what unit is being used - console.log('UpdateFoodCalculations - unit:', unit, 'unitLabel:', unitLabel); - - // Determine frequency suffix for display - const frequencySuffix = this.showPerMeal ? '/meal' : '/day'; - - // Clear all food source errors first - this.foodSources.forEach(fs => { - this.showError(`energy-error-${fs.id}`, false); - }); - this.showError('daysError', false); - - // Validate days input - if (!days || !this.validateInput(days, 1, true)) { - if (days) this.showError('daysError', true); - foodAmountsSection.style.display = 'none'; - dailyFoodResults.style.display = 'none'; - if (foodBreakdownResults) foodBreakdownResults.style.display = 'none'; - if (feedingConfig) feedingConfig.style.display = 'none'; - - // Hide unit buttons when validation fails - const unitButtons = this.container.querySelector('#unitButtons'); - if (unitButtons) unitButtons.style.display = 'none'; - return; - } - - const numDays = parseInt(days); - - // Calculate per-food breakdown - const foodBreakdowns = []; - let totalDailyGrams = 0; - let hasValidFoods = false; - - this.foodSources.forEach(fs => { - const energyPer100g = this.getFoodSourceEnergyPer100g(fs); - - if (energyPer100g && energyPer100g > 0.1 && fs.percentage > 0) { - const dailyCaloriesForThisFood = (this.currentMER * fs.percentage) / 100; - // Calculate range values if applicable - const dailyCaloriesMin = hasRange ? (this.currentMERMin * fs.percentage) / 100 : dailyCaloriesForThisFood; - const dailyCaloriesMax = hasRange ? (this.currentMERMax * fs.percentage) / 100 : dailyCaloriesForThisFood; - - let dailyGramsForThisFood; - let dailyGramsMin, dailyGramsMax; - let dailyCupsForThisFood = null; - let dailyCupsMin, dailyCupsMax; - - // For kcal/cup, calculate cups directly from calories - if (fs.energyUnit === 'kcalcup' && fs.energy) { - const caloriesPerCup = parseFloat(fs.energy); - dailyCupsForThisFood = dailyCaloriesForThisFood / caloriesPerCup; - dailyCupsMin = dailyCaloriesMin / caloriesPerCup; - dailyCupsMax = dailyCaloriesMax / caloriesPerCup; - // We still need grams for total calculation, use approximation - dailyGramsForThisFood = (dailyCaloriesForThisFood / energyPer100g) * 100; - dailyGramsMin = (dailyCaloriesMin / energyPer100g) * 100; - dailyGramsMax = (dailyCaloriesMax / energyPer100g) * 100; - } else { - // For other units, calculate grams normally - dailyGramsForThisFood = (dailyCaloriesForThisFood / energyPer100g) * 100; - dailyGramsMin = (dailyCaloriesMin / energyPer100g) * 100; - dailyGramsMax = (dailyCaloriesMax / energyPer100g) * 100; - } - - // Calculate per-meal amounts if needed - const displayGrams = this.showPerMeal ? dailyGramsForThisFood / this.mealsPerDay : dailyGramsForThisFood; - const displayGramsMin = this.showPerMeal ? dailyGramsMin / this.mealsPerDay : dailyGramsMin; - const displayGramsMax = this.showPerMeal ? dailyGramsMax / this.mealsPerDay : dailyGramsMax; - - const displayCups = dailyCupsForThisFood !== null ? - (this.showPerMeal ? dailyCupsForThisFood / this.mealsPerDay : dailyCupsForThisFood) : null; - const displayCupsMin = dailyCupsMin !== undefined ? - (this.showPerMeal ? dailyCupsMin / this.mealsPerDay : dailyCupsMin) : null; - const displayCupsMax = dailyCupsMax !== undefined ? - (this.showPerMeal ? dailyCupsMax / this.mealsPerDay : dailyCupsMax) : null; - - const displayCalories = this.showPerMeal ? dailyCaloriesForThisFood / this.mealsPerDay : dailyCaloriesForThisFood; - const displayCaloriesMin = this.showPerMeal ? dailyCaloriesMin / this.mealsPerDay : dailyCaloriesMin; - const displayCaloriesMax = this.showPerMeal ? dailyCaloriesMax / this.mealsPerDay : dailyCaloriesMax; - - foodBreakdowns.push({ - name: fs.name, - percentage: fs.percentage, - dailyGrams: dailyGramsForThisFood, - dailyGramsMin: dailyGramsMin, - dailyGramsMax: dailyGramsMax, - displayGrams: displayGrams, - displayGramsMin: displayGramsMin, - displayGramsMax: displayGramsMax, - dailyCups: dailyCupsForThisFood, - dailyCupsMin: dailyCupsMin, - dailyCupsMax: dailyCupsMax, - displayCups: displayCups, - displayCupsMin: displayCupsMin, - displayCupsMax: displayCupsMax, - calories: dailyCaloriesForThisFood, - displayCalories: displayCalories, - displayCaloriesMin: displayCaloriesMin, - displayCaloriesMax: displayCaloriesMax, - isLocked: fs.isLocked, - hasEnergyContent: true, - hasRange: hasRange, - foodSource: fs // Store reference for cups conversion - }); - - totalDailyGrams += dailyGramsForThisFood; - hasValidFoods = true; - } else if (fs.percentage > 0) { - // Include food sources without energy content but show them as needing energy content - foodBreakdowns.push({ - name: fs.name, - percentage: fs.percentage, - dailyGrams: 0, - displayGrams: 0, - dailyCups: null, - displayCups: null, - calories: 0, - displayCalories: 0, - isLocked: fs.isLocked, - hasEnergyContent: false, - foodSource: fs // Store reference for cups conversion - }); - } - }); - - if (!hasValidFoods) { - // Show errors for invalid food sources - this.foodSources.forEach(fs => { - const energyInput = document.getElementById(`energy-${fs.id}`); - if (energyInput && energyInput.value && (!this.getFoodSourceEnergyPer100g(fs) || this.getFoodSourceEnergyPer100g(fs) <= 0.1)) { - this.showError(`energy-error-${fs.id}`, true); - } - }); - - dailyFoodResults.style.display = 'none'; - if (foodBreakdownResults) foodBreakdownResults.style.display = 'none'; - if (feedingConfig) feedingConfig.style.display = 'none'; - - // Hide unit buttons when no valid foods - const unitButtons = this.container.querySelector('#unitButtons'); - if (unitButtons) unitButtons.style.display = 'none'; - - // If we have any food sources without energy content, still show the breakdown section - if (foodBreakdowns.length > 0) { - // Show food amounts section with warnings for missing energy content - const unitLabel = unit === 'g' ? 'g' : unit === 'kg' ? 'kg' : unit === 'oz' ? 'oz' : 'lb'; - - const foodAmountsHTML = foodBreakdowns.map(breakdown => { - const lockIndicator = breakdown.isLocked ? 'πŸ”’' : ''; - - return ` -
-
- ${breakdown.name} - ${breakdown.percentage}% - ${lockIndicator} -
-
- ⚠️ -
-
- `; - }).join(''); - - if (foodAmountsList) { - foodAmountsList.innerHTML = foodAmountsHTML; - } - - if (totalAmountDisplay) { - totalAmountDisplay.textContent = "Enter energy content for all foods"; - } - - foodAmountsSection.style.display = 'block'; - this.sendHeightToParent(); - } else { - foodAmountsSection.style.display = 'none'; - } - return; - } - - // Update daily food results (total) - will be updated with proper units later - dailyFoodResults.style.display = 'block'; - - // Show feeding configuration when we have valid foods - if (feedingConfig) { - feedingConfig.style.display = 'block'; - - // Ensure "Per day" is checked when feeding config becomes visible - const showDaily = this.container.querySelector('#showDaily'); - if (showDaily && !showDaily.checked && !this.container.querySelector('#showPerMeal').checked) { - showDaily.checked = true; - } - } - - // Show unit buttons when daily results are shown - const unitButtons = this.container.querySelector('#unitButtons'); - if (unitButtons) unitButtons.style.display = 'flex'; - - // Update per-food breakdown - if (foodBreakdownList && foodBreakdowns.length > 1) { - const breakdownHTML = foodBreakdowns.map(breakdown => { - let valueContent; - if (breakdown.hasEnergyContent) { - if (unit === 'cups') { - // For cups, use the pre-calculated cups value if available - if (breakdown.displayCups !== null) { - if (breakdown.hasRange && breakdown.displayCupsMin !== breakdown.displayCupsMax) { - valueContent = `${this.formatNumber(breakdown.displayCupsMin, decimals)}-${this.formatNumber(breakdown.displayCupsMax, decimals)} ${unitLabel}${frequencySuffix}`; - } else { - valueContent = `${this.formatNumber(breakdown.displayCups, decimals)} ${unitLabel}${frequencySuffix}`; - } - } else { - valueContent = `N/A`; - } - } else { - // For other units (g, kg, oz, lb) - if (breakdown.hasRange && breakdown.displayGramsMin !== breakdown.displayGramsMax) { - const minConverted = this.convertUnits(breakdown.displayGramsMin, unit); - const maxConverted = this.convertUnits(breakdown.displayGramsMax, unit); - valueContent = `${this.formatNumber(minConverted, decimals)}-${this.formatNumber(maxConverted, decimals)} ${unitLabel}${frequencySuffix}`; - } else { - valueContent = `${this.formatNumber(this.convertUnits(breakdown.displayGrams, unit), decimals)} ${unitLabel}${frequencySuffix}`; - } - } - } else { - valueContent = `⚠️`; - } - - return ` -
- ${breakdown.name} (${breakdown.percentage}%${breakdown.isLocked ? ' - locked' : ''}): - ${valueContent} -
- `; - }).join(''); - - foodBreakdownList.innerHTML = breakdownHTML; - if (foodBreakdownResults) foodBreakdownResults.style.display = 'block'; - } else { - if (foodBreakdownResults) foodBreakdownResults.style.display = 'none'; - } - - // Generate individual food amount breakdown - - // Update daily food value with correct units - const displayTotal = this.showPerMeal ? totalDailyGrams / this.mealsPerDay : totalDailyGrams; - let convertedTotal; - let totalDisplayText; - - if (unit === 'cups') { - console.log('Unit is cups, checking validity...'); - // For cups, we can only show total if all foods with percentage > 0 have kcal/cup - const validForCups = foodBreakdowns.filter(b => b.percentage > 0) - .every(b => b.displayCups !== null && b.displayCups !== undefined); - - console.log('Valid for cups?', validForCups, 'Breakdowns:', foodBreakdowns); - - if (validForCups) { - // Calculate total cups using pre-calculated values - let totalCups = 0; - let totalCupsMin = 0; - let totalCupsMax = 0; - foodBreakdowns.forEach(breakdown => { - if (breakdown.percentage > 0 && breakdown.displayCups !== null) { - totalCups += breakdown.displayCups; - if (breakdown.hasRange) { - totalCupsMin += breakdown.displayCupsMin || breakdown.displayCups; - totalCupsMax += breakdown.displayCupsMax || breakdown.displayCups; - } else { - totalCupsMin += breakdown.displayCups; - totalCupsMax += breakdown.displayCups; - } - } - }); - - if (hasRange && totalCupsMin !== totalCupsMax) { - totalDisplayText = `${this.formatNumber(totalCupsMin, decimals)}-${this.formatNumber(totalCupsMax, decimals)} ${unitLabel}${frequencySuffix}`; - } else { - totalDisplayText = this.formatNumber(totalCups, decimals) + ` ${unitLabel}${frequencySuffix}`; - } - } else { - totalDisplayText = 'Mixed units - see breakdown'; - } - } else { - // Calculate totals for ranges - if (hasRange) { - let totalGramsMin = 0; - let totalGramsMax = 0; - foodBreakdowns.forEach(breakdown => { - if (breakdown.percentage > 0 && breakdown.hasEnergyContent) { - totalGramsMin += breakdown.displayGramsMin || breakdown.displayGrams; - totalGramsMax += breakdown.displayGramsMax || breakdown.displayGrams; - } - }); - - const convertedMin = this.convertUnits(totalGramsMin, unit); - const convertedMax = this.convertUnits(totalGramsMax, unit); - - if (totalGramsMin !== totalGramsMax) { - totalDisplayText = `${this.formatNumber(convertedMin, decimals)}-${this.formatNumber(convertedMax, decimals)} ${unitLabel}${frequencySuffix}`; - } else { - totalDisplayText = this.formatNumber(convertedMin, decimals) + ` ${unitLabel}${frequencySuffix}`; - } - } else { - convertedTotal = this.convertUnits(displayTotal, unit); - totalDisplayText = this.formatNumber(convertedTotal, decimals) + ` ${unitLabel}${frequencySuffix}`; - } - } - - dailyFoodValue.textContent = totalDisplayText; - - // Build HTML for individual food amounts - const foodAmountsHTML = foodBreakdowns.map(breakdown => { - const lockIndicator = breakdown.isLocked ? 'πŸ”’' : ''; - - if (!breakdown.hasEnergyContent) { - // Show warning for food sources without energy content - return ` -
-
- ${breakdown.name} - ${breakdown.percentage}% - ${lockIndicator} -
-
- ⚠️ -
-
- `; - } else { - // For multi-day calculations: show total amount for all days - let amountDisplay; - if (unit === 'cups') { - // For cups, use pre-calculated cups value - if (breakdown.dailyCups !== null) { - const totalCupsForDays = breakdown.dailyCups * numDays; - amountDisplay = `${this.formatNumber(totalCupsForDays, decimals)} ${unitLabel}`; - } else { - amountDisplay = `N/A`; - } - } else { - // For other units, calculate from grams - const totalGramsForDays = this.showPerMeal - ? (breakdown.dailyGrams / this.mealsPerDay) * numDays * this.mealsPerDay - : breakdown.dailyGrams * numDays; - const convertedAmount = this.convertUnits(totalGramsForDays, unit); - amountDisplay = `${this.formatNumber(convertedAmount, decimals)} ${unitLabel}`; - } - - return ` -
-
- ${breakdown.name} - ${breakdown.percentage}% - ${lockIndicator} -
-
- ${amountDisplay} -
-
- `; - } - }).join(''); - - // Calculate and display total - const totalFoodGrams = totalDailyGrams * numDays; - - // Update the display - if (foodAmountsList) { - foodAmountsList.innerHTML = foodAmountsHTML; - } - - if (totalAmountDisplay) { - if (unit === 'cups') { - // For cups total, check if all foods can be converted - const validForCups = foodBreakdowns.filter(b => b.percentage > 0) - .every(b => b.dailyCups !== null && b.dailyCups !== undefined); - - if (validForCups) { - // Calculate total cups using pre-calculated values - let totalCups = 0; - foodBreakdowns.forEach(breakdown => { - if (breakdown.percentage > 0 && breakdown.dailyCups !== null) { - totalCups += breakdown.dailyCups * numDays; - } - }); - totalAmountDisplay.textContent = `${this.formatNumber(totalCups, decimals)} ${unitLabel}`; - } else { - totalAmountDisplay.textContent = 'Mixed units - see individual amounts'; - } - } else { - const totalConverted = this.convertUnits(totalFoodGrams, unit); - totalAmountDisplay.textContent = `${this.formatNumber(totalConverted, decimals)} ${unitLabel}`; - } - } - - foodAmountsSection.style.display = 'block'; - - this.sendHeightToParent(); - } - - getFoodSourceEnergyPer100g(foodSource) { - if (!foodSource.energy || !foodSource.energyUnit) return null; - - const energy = parseFloat(foodSource.energy); - if (isNaN(energy)) return null; - - const unit = foodSource.energyUnit; - - // Convert all units to kcal/100g for internal calculations - switch (unit) { - case 'kcal100g': - return energy; - case 'kcalkg': - return energy / 10; // 1 kg = 10 Γ— 100g - case 'kcalcup': - return energy / 1.2; // Assume 1 cup β‰ˆ 120g for dry dog food - case 'kcalcan': - return energy / 4.5; // Assume 1 can β‰ˆ 450g for wet dog food - default: - return energy; - } - } - - setupIframeResize() { - // Send height to parent window for iframe auto-resize - this.sendHeightToParent(); - - // Monitor for content changes that might affect height - const observer = new MutationObserver(() => { - setTimeout(() => this.sendHeightToParent(), 100); - }); - - observer.observe(document.body, { - childList: true, - subtree: true, - attributes: true - }); - - // Send height on window resize - window.addEventListener('resize', () => this.sendHeightToParent()); - } - - sendHeightToParent() { - const height = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); - if (window.parent && window.parent !== window) { - window.parent.postMessage({ - type: 'dogCalculatorResize', - height: height - }, '*'); - } - } - - // Modal functionality - showShareModal() { - const modal = this.container.querySelector('#shareModal'); - const shareUrl = this.container.querySelector('#shareUrl'); - if (modal && shareUrl) { - shareUrl.value = window.location.href; - modal.style.display = 'block'; - } - } - - hideShareModal() { - const modal = this.container.querySelector('#shareModal'); - if (modal) modal.style.display = 'none'; - } - - showEmbedModal() { - const modal = this.container.querySelector('#embedModal'); - const widgetCode = this.container.querySelector('#widgetCode'); - const iframeCode = this.container.querySelector('#iframeCode'); - - if (modal && widgetCode && iframeCode) { - // Build embed URL - const baseUrl = window.location.protocol + '//embed.' + window.location.hostname; - - // Create widget code using createElement to avoid quote issues - const scriptTag = document.createElement('script'); - scriptTag.src = baseUrl + '/dog-calorie-calculator/dog-food-calculator-widget.js'; - const divTag = document.createElement('div'); - divTag.id = 'dog-calorie-calculator'; - - const widgetHtml = scriptTag.outerHTML + '\n' + divTag.outerHTML; - widgetCode.textContent = widgetHtml; - - // Create iframe code using createElement - const iframe = document.createElement('iframe'); - iframe.src = baseUrl + '/dog-calorie-calculator/iframe.html'; - iframe.width = '100%'; - iframe.height = '600'; - iframe.frameBorder = '0'; - iframe.title = 'Dog Calorie Calculator'; - - iframeCode.textContent = iframe.outerHTML; - modal.style.display = 'block'; - } - } - - hideEmbedModal() { - const modal = this.container.querySelector('#embedModal'); - if (modal) modal.style.display = 'none'; - } - - shareToFacebook() { - const url = encodeURIComponent(window.location.href); - window.open('https://www.facebook.com/sharer/sharer.php?u=' + url, '_blank', 'width=600,height=400'); - } - - shareToTwitter() { - const url = encodeURIComponent(window.location.href); - const text = encodeURIComponent('Check out this useful dog calorie calculator!'); - window.open('https://twitter.com/intent/tweet?url=' + url + '&text=' + text, '_blank', 'width=600,height=400'); - } - - shareToLinkedIn() { - const url = encodeURIComponent(window.location.href); - window.open('https://www.linkedin.com/sharing/share-offsite/?url=' + url, '_blank', 'width=600,height=400'); - } - - shareViaEmail() { - const subject = encodeURIComponent('Dog Calorie Calculator'); - const body = encodeURIComponent('Check out this useful dog calorie calculator: ' + window.location.href); - window.location.href = 'mailto:?subject=' + subject + '&body=' + body; - } - - async copyShareLink() { - const shareUrl = this.container.querySelector('#shareUrl'); - const copyBtn = this.container.querySelector('#shareCopy'); - - if (shareUrl && copyBtn) { - try { - await navigator.clipboard.writeText(shareUrl.value); - const originalText = copyBtn.textContent; - copyBtn.textContent = 'Copied!'; - copyBtn.classList.add('copied'); - - setTimeout(() => { - copyBtn.textContent = originalText; - copyBtn.classList.remove('copied'); - }, 2000); - } catch (err) { - // Fallback for older browsers - shareUrl.select(); - document.execCommand('copy'); - } - } - } - - async copyEmbedCode(type) { - const codeElement = document.getElementById(type === 'widget' ? 'widgetCode' : 'iframeCode'); - const copyBtn = document.getElementById(type === 'widget' ? 'copyWidget' : 'copyIframe'); - - if (codeElement && copyBtn) { - try { - await navigator.clipboard.writeText(codeElement.textContent); - const originalText = copyBtn.textContent; - copyBtn.textContent = 'Copied!'; - copyBtn.classList.add('copied'); - - setTimeout(() => { - copyBtn.textContent = originalText; - copyBtn.classList.remove('copied'); - }, 2000); - } catch (err) { - // Fallback for older browsers - console.log('Copy fallback needed'); - } - } - } - } - - // Initialize calculator when DOM is ready - - - // Auto-initialize widgets on page load - function initializeWidget() { - injectStyles(); - - const containers = document.querySelectorAll('#dog-calorie-calculator, .dog-calorie-calculator'); - containers.forEach(container => { - if (container.dataset.initialized) return; - - const options = { - theme: container.dataset.theme || 'system', - scale: parseFloat(container.dataset.scale) || 1.0 - }; - - new DogCalorieCalculatorWidget(container, options); - container.dataset.initialized = 'true'; - }); - } - - // Initialize when DOM is ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializeWidget); - } else { - initializeWidget(); - } - - // Export for manual initialization - window.DogCalorieCalculatorWidget = DogCalorieCalculatorWidget; - -})(); \ No newline at end of file diff --git a/test-widget.html b/test-widget.html deleted file mode 100644 index 404397e..0000000 --- a/test-widget.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - Widget Test - - - -

Dog Calculator Widget Test

- -
-

Test 1: Basic Widget

-
-
- -
-

Test 2: Dark Theme Widget

-
-
- - - - \ No newline at end of file