Calc App
This commit is contained in:
parent
5cc1188c5e
commit
9053de0e51
|
|
@ -0,0 +1,16 @@
|
|||
FROM node:16-alpine as build
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install a simple HTTP server for serving static content
|
||||
RUN npm install -g http-server
|
||||
|
||||
# Copy application files
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Command to run the application
|
||||
CMD ["http-server", ".", "-p", "8080", "--cors"]
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
# Puffin Carbon Offset Calculator (iFrame version)
|
||||
|
||||
A lightweight, embeddable version of the Puffin Carbon Offset Calculator for yachts and marine vessels.
|
||||
|
||||
## Features
|
||||
|
||||
- Carbon emissions calculator based on fuel consumption or trip distance
|
||||
- Support for multiple currencies
|
||||
- Responsive design that works in any iFrame
|
||||
- Automatic height adjustment
|
||||
- Dark/light theme support
|
||||
- Docker-ready for easy deployment
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Running Locally
|
||||
|
||||
1. Clone this repository
|
||||
2. Open the project directory
|
||||
3. Open `index.html` in your browser
|
||||
|
||||
### Using Docker
|
||||
|
||||
Build and run using Docker Compose:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
This will start a server on port 8080. You can then access the calculator at http://localhost:8080.
|
||||
|
||||
## Embedding in Your Website
|
||||
|
||||
Add the following code to your website:
|
||||
|
||||
```html
|
||||
<iframe
|
||||
src="http://your-server:8080"
|
||||
id="puffin-calculator-iframe"
|
||||
width="100%"
|
||||
height="600"
|
||||
frameborder="0"
|
||||
allow="clipboard-write"
|
||||
title="Puffin Carbon Offset Calculator"
|
||||
></iframe>
|
||||
|
||||
<script>
|
||||
// Optional: Handle iframe resizing for responsive behavior
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data.type === 'puffin-calculator-resize') {
|
||||
const iframe = document.getElementById('puffin-calculator-iframe');
|
||||
if (iframe) {
|
||||
// Add a bit of extra space to avoid scrollbars
|
||||
iframe.style.height = (event.data.height + 50) + 'px';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
See `iframe-example.html` for a complete integration example.
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
The calculator exposes a JavaScript API that you can use to control it from the parent page:
|
||||
|
||||
```javascript
|
||||
// Access the iframe's window object
|
||||
const calculatorFrame = document.getElementById('puffin-calculator-iframe');
|
||||
|
||||
// Set the theme (once the iframe has loaded)
|
||||
calculatorFrame.addEventListener('load', function() {
|
||||
// Access the API methods
|
||||
calculatorFrame.contentWindow.PuffinCalculator.setTheme('dark');
|
||||
|
||||
// Reset the calculator to initial state
|
||||
calculatorFrame.contentWindow.PuffinCalculator.resetCalculator();
|
||||
});
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
puffin-calculator-iframe/
|
||||
├── index.html # Main HTML file
|
||||
├── iframe-example.html # Example of embedding the calculator
|
||||
├── styles.css # Main stylesheet
|
||||
├── Dockerfile # Docker configuration
|
||||
├── docker-compose.yml # Docker Compose configuration
|
||||
├── README.md # This file
|
||||
└── js/
|
||||
├── app.js # Main application code
|
||||
├── components.js # React components
|
||||
└── utils.js # Utility functions
|
||||
```
|
||||
|
||||
## Carbon Calculation Methods
|
||||
|
||||
This calculator offers three methods for calculating carbon emissions:
|
||||
|
||||
1. **Fuel-Based**: Calculate emissions based on the amount of fuel consumed (in liters or gallons)
|
||||
2. **Distance-Based**: Calculate emissions based on distance traveled, vessel speed, and fuel consumption rate
|
||||
3. **Custom Amount**: Enter a monetary amount directly for offsetting without a specific carbon calculation
|
||||
|
||||
## Integration with WordPress
|
||||
|
||||
To integrate with WordPress, see the companion PHP file `puffin-calculator-integrated.php` in the parent directory.
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) Puffin Offset. All rights reserved.
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
puffin-calculator:
|
||||
build: .
|
||||
container_name: puffin-calculator-iframe
|
||||
ports:
|
||||
- "8080:8080"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./:/app
|
||||
- /app/node_modules
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Puffin Calculator iFrame Integration Example</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
color: #333;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 1rem;
|
||||
color: #0f4c81;
|
||||
}
|
||||
.container {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.example-code {
|
||||
background-color: #f5f5f5;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
code {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
iframe {
|
||||
width: 100%;
|
||||
border: none;
|
||||
min-height: 600px;
|
||||
margin-top: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Puffin Calculator iFrame Integration</h1>
|
||||
|
||||
<p>This page demonstrates how to embed the Puffin Carbon Offset Calculator into your website using an iFrame.</p>
|
||||
|
||||
<div class="container">
|
||||
<h2>Integration Code</h2>
|
||||
<div class="example-code">
|
||||
<pre><code><iframe
|
||||
src="http://localhost:8080"
|
||||
id="puffin-calculator-iframe"
|
||||
width="100%"
|
||||
height="600"
|
||||
frameborder="0"
|
||||
allow="clipboard-write"
|
||||
title="Puffin Carbon Offset Calculator"
|
||||
></iframe>
|
||||
|
||||
<script>
|
||||
// Optional: Handle iframe resizing for responsive behavior
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data.type === 'puffin-calculator-resize') {
|
||||
const iframe = document.getElementById('puffin-calculator-iframe');
|
||||
if (iframe) {
|
||||
// Add a bit of extra space to avoid scrollbars
|
||||
iframe.style.height = (event.data.height + 50) + 'px';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h2>Live Example</h2>
|
||||
<p>Here's the calculator embedded in this page:</p>
|
||||
|
||||
<iframe
|
||||
src="index.html"
|
||||
id="puffin-calculator-iframe"
|
||||
width="100%"
|
||||
height="600"
|
||||
frameborder="0"
|
||||
allow="clipboard-write"
|
||||
title="Puffin Carbon Offset Calculator"
|
||||
></iframe>
|
||||
|
||||
<script>
|
||||
// Handle iframe resizing
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data.type === 'puffin-calculator-resize') {
|
||||
const iframe = document.getElementById('puffin-calculator-iframe');
|
||||
if (iframe) {
|
||||
// Add a bit of extra space to avoid scrollbars
|
||||
iframe.style.height = (event.data.height + 50) + 'px';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h2>Advanced Usage</h2>
|
||||
<p>You can also communicate with the calculator through the iframe to set options or perform actions:</p>
|
||||
|
||||
<div class="example-code">
|
||||
<pre><code>// Access the iframe's window object
|
||||
const calculatorFrame = document.getElementById('puffin-calculator-iframe');
|
||||
|
||||
// Set the theme (once the iframe has loaded)
|
||||
calculatorFrame.addEventListener('load', function() {
|
||||
// Access the API methods
|
||||
calculatorFrame.contentWindow.PuffinCalculator.setTheme('dark');
|
||||
|
||||
// Reset the calculator to initial state
|
||||
calculatorFrame.contentWindow.PuffinCalculator.resetCalculator();
|
||||
});</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Puffin Carbon Offset Calculator</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<div id="root" class="container mx-auto p-4"></div>
|
||||
|
||||
<!-- React and ReactDOM -->
|
||||
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<!-- Babel for JSX (for development, would be compiled in production) -->
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<!-- Axios for API requests -->
|
||||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||||
|
||||
<!-- Application Scripts -->
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/components.js"></script>
|
||||
<script type="text/babel" src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
// Main App Component
|
||||
function App() {
|
||||
const [showOffsetOrder, setShowOffsetOrder] = React.useState(false);
|
||||
const [offsetTons, setOffsetTons] = React.useState(0);
|
||||
const [monetaryAmount, setMonetaryAmount] = React.useState(undefined);
|
||||
const [calculatorType, setCalculatorType] = React.useState('trip');
|
||||
|
||||
React.useEffect(() => {
|
||||
// Log page view when component mounts
|
||||
analytics.pageView('calculator');
|
||||
|
||||
// Make the app available globally for iframe communication
|
||||
window.PuffinCalculator = {
|
||||
setTheme: (theme) => {
|
||||
document.body.className = theme === 'dark' ? 'dark bg-gray-900' : 'bg-gray-50';
|
||||
},
|
||||
resetCalculator: () => {
|
||||
setShowOffsetOrder(false);
|
||||
setOffsetTons(0);
|
||||
setMonetaryAmount(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle iframe resize
|
||||
const resizeObserver = new ResizeObserver(entries => {
|
||||
for (let entry of entries) {
|
||||
// Notify parent if using postMessage API
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'puffin-calculator-resize',
|
||||
height: entry.contentRect.height
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (rootElement) {
|
||||
resizeObserver.observe(rootElement);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (rootElement) {
|
||||
resizeObserver.unobserve(rootElement);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleOffsetClick = (tons, monetaryAmount) => {
|
||||
setOffsetTons(tons);
|
||||
setMonetaryAmount(monetaryAmount);
|
||||
setShowOffsetOrder(true);
|
||||
|
||||
// Scroll to top
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
|
||||
// Track event
|
||||
analytics.event('calculator', 'offset_click', `${tons} tons`);
|
||||
};
|
||||
|
||||
// Render the appropriate component based on state
|
||||
const renderContent = () => {
|
||||
if (showOffsetOrder) {
|
||||
return React.createElement(OffsetOrder, {
|
||||
tons: offsetTons,
|
||||
monetaryAmount: monetaryAmount,
|
||||
onBack: () => {
|
||||
setShowOffsetOrder(false);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
},
|
||||
calculatorType: calculatorType
|
||||
});
|
||||
}
|
||||
|
||||
return React.createElement(TripCalculator, {
|
||||
vesselData: sampleVessel,
|
||||
onOffsetClick: handleOffsetClick
|
||||
});
|
||||
};
|
||||
|
||||
return React.createElement('div', {
|
||||
className: 'puffin-calculator min-h-[600px]'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'content',
|
||||
className: 'px-4 sm:px-6 flex justify-center'
|
||||
}, renderContent())
|
||||
]);
|
||||
}
|
||||
|
||||
// Initialize the application when the DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const rootElement = document.getElementById('root');
|
||||
|
||||
if (!rootElement) {
|
||||
console.error('Root element not found. Cannot mount Puffin Calculator.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for React and ReactDOM
|
||||
if (!window.React || !window.ReactDOM) {
|
||||
console.error('React or ReactDOM not loaded. Cannot mount Puffin Calculator.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize React app
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(React.createElement(App, {}));
|
||||
|
||||
// Notify when ready
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'puffin-calculator-ready'
|
||||
}, '*');
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,186 @@
|
|||
// Constants for carbon calculations
|
||||
const EMISSION_FACTOR = 3.206; // tons of CO₂ per ton of fuel
|
||||
const FUEL_DENSITY = 0.85; // tons per m³ (or metric tons per kiloliter)
|
||||
const GALLONS_TO_LITERS = 3.78541; // 1 US gallon = 3.78541 liters
|
||||
const LITERS_TO_CUBIC_METERS = 0.001; // 1 liter = 0.001 m³
|
||||
|
||||
// Available currencies
|
||||
const currencies = {
|
||||
USD: { code: 'USD', symbol: '$', rate: 1 },
|
||||
EUR: { code: 'EUR', symbol: '€', rate: 0.92 },
|
||||
GBP: { code: 'GBP', symbol: '£', rate: 0.79 },
|
||||
CHF: { code: 'CHF', symbol: 'CHF', rate: 0.88 },
|
||||
};
|
||||
|
||||
// Format currency
|
||||
function formatCurrency(amountUSD, currency) {
|
||||
if (!currency) {
|
||||
currency = currencies.USD;
|
||||
}
|
||||
|
||||
// Convert USD amount to target currency
|
||||
const convertedAmount = amountUSD * currency.rate;
|
||||
|
||||
return `${currency.symbol}${convertedAmount.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
})}`;
|
||||
}
|
||||
|
||||
// Get currency by code
|
||||
function getCurrencyByCode(code) {
|
||||
return currencies[code] || currencies.USD;
|
||||
}
|
||||
|
||||
// Calculate trip carbon
|
||||
function calculateTripCarbon(vesselData, distance, speed, fuelRateLitersPerHour) {
|
||||
const tripHours = distance / speed;
|
||||
|
||||
// Calculate total fuel consumption in liters
|
||||
const fuelConsumptionLiters = fuelRateLitersPerHour * tripHours;
|
||||
|
||||
// Convert liters to tons for CO₂ calculation
|
||||
const fuelConsumptionTons = (fuelConsumptionLiters * LITERS_TO_CUBIC_METERS) * FUEL_DENSITY;
|
||||
|
||||
// Calculate CO₂ emissions
|
||||
const fuelRateTonsPerHour = (fuelRateLitersPerHour * LITERS_TO_CUBIC_METERS) * FUEL_DENSITY;
|
||||
const emissionsPerNM = (fuelRateTonsPerHour * EMISSION_FACTOR) / speed;
|
||||
const totalEmissions = emissionsPerNM * distance;
|
||||
|
||||
return {
|
||||
distance,
|
||||
duration: tripHours,
|
||||
fuelConsumption: Math.round(fuelConsumptionLiters),
|
||||
co2Emissions: Number(totalEmissions.toFixed(2))
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate carbon from fuel
|
||||
function calculateCarbonFromFuel(fuelAmount, isGallons = false) {
|
||||
// Convert to liters if input is in gallons
|
||||
const liters = isGallons ? fuelAmount * GALLONS_TO_LITERS : fuelAmount;
|
||||
|
||||
// Convert liters to cubic meters (m³)
|
||||
const cubicMeters = liters * LITERS_TO_CUBIC_METERS;
|
||||
|
||||
// Convert volume to mass (tons)
|
||||
const fuelTons = cubicMeters * FUEL_DENSITY;
|
||||
|
||||
// Calculate CO₂ emissions
|
||||
const co2Emissions = fuelTons * EMISSION_FACTOR;
|
||||
|
||||
return Number(co2Emissions.toFixed(2));
|
||||
}
|
||||
|
||||
// Sample vessel data
|
||||
const sampleVessel = {
|
||||
imo: '1234567',
|
||||
vesselName: 'Sample Yacht',
|
||||
type: 'Yacht',
|
||||
length: 50,
|
||||
width: 9,
|
||||
estimatedEnginePower: 2250
|
||||
};
|
||||
|
||||
// Default portfolio for fallback
|
||||
const DEFAULT_PORTFOLIO = {
|
||||
id: 1,
|
||||
name: "Puffin Maritime Portfolio",
|
||||
description: "A curated selection of high-impact carbon removal projects focused on maritime sustainability.",
|
||||
projects: [
|
||||
{
|
||||
id: "dac-1",
|
||||
name: "Direct Air Capture",
|
||||
description: "Permanent carbon removal through direct air capture technology",
|
||||
shortDescription: "Direct air capture in Iceland",
|
||||
imageUrl: "https://images.unsplash.com/photo-1553547274-0df401ae03c9",
|
||||
pricePerTon: 200,
|
||||
location: "Iceland",
|
||||
type: "Direct Air Capture",
|
||||
verificationStandard: "Verified Carbon Standard",
|
||||
impactMetrics: {
|
||||
co2Reduced: 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "ocean-1",
|
||||
name: "Ocean Carbon Removal",
|
||||
description: "Enhanced ocean carbon capture through marine permaculture",
|
||||
shortDescription: "Marine carbon capture",
|
||||
imageUrl: "https://images.unsplash.com/photo-1498623116890-37e912163d5d",
|
||||
pricePerTon: 200,
|
||||
location: "Global Oceans",
|
||||
type: "Ocean-Based",
|
||||
verificationStandard: "Gold Standard",
|
||||
impactMetrics: {
|
||||
co2Reduced: 5000
|
||||
}
|
||||
}
|
||||
],
|
||||
pricePerTon: 200,
|
||||
currency: 'USD'
|
||||
};
|
||||
|
||||
// Simplified analytics
|
||||
const analytics = {
|
||||
pageView: function(path) {
|
||||
console.log(`Analytics pageView: ${path}`);
|
||||
},
|
||||
event: function(category, action, label) {
|
||||
console.log(`Analytics event: ${category} - ${action} - ${label}`);
|
||||
},
|
||||
error: function(error, message) {
|
||||
console.error(`Analytics error: ${message}`, error);
|
||||
}
|
||||
};
|
||||
|
||||
// Create portfolio API call (with fallback to default)
|
||||
async function getPortfolios() {
|
||||
try {
|
||||
// In a real implementation, this would be an API call
|
||||
// For demo purposes, we'll just return the default portfolio after a small delay
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve([DEFAULT_PORTFOLIO]), 1000);
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch portfolios from API, using fallback');
|
||||
return [DEFAULT_PORTFOLIO];
|
||||
}
|
||||
}
|
||||
|
||||
// Create offset order API call (simulated)
|
||||
async function createOffsetOrder(portfolioId, tons, dryRun = false) {
|
||||
try {
|
||||
// Simulate API call
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
id: `order-${Math.floor(Math.random() * 1000000)}`,
|
||||
amountCharged: tons * 200 * 100, // in cents
|
||||
currency: 'USD',
|
||||
tons: tons,
|
||||
portfolio: DEFAULT_PORTFOLIO,
|
||||
status: 'completed',
|
||||
createdAt: new Date().toISOString(),
|
||||
dryRun: dryRun
|
||||
});
|
||||
}, 1500);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create offset order', error);
|
||||
throw new Error('Failed to create offset order. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
// Email validation
|
||||
function validateEmail(email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
// Send form submission (simulated)
|
||||
async function sendFormSubmission(data) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve({ success: true }), 1000);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/* Puffin Calculator Styles */
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.puffin-calculator {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Custom focus styles for better accessibility */
|
||||
input:focus, select:focus, button:focus {
|
||||
outline: 3px solid rgba(59, 130, 246, 0.5);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #94a3b8;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #64748b;
|
||||
}
|
||||
|
||||
/* Safari input number button styling */
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
opacity: 1;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 640px) {
|
||||
.puffin-calculator {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for loading state */
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
Binary file not shown.
Loading…
Reference in New Issue