<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Unit Conversion</title>
<style>
:root {
/* Colors */
--color-primary: hsl(220, 70%, 50%);
--color-primary-light: hsl(220, 70%, 60%);
--color-primary-dark: hsl(220, 70%, 40%);
--color-primary-bg: hsla(220, 70%, 50%, 0.1);
--color-success: hsl(120, 50%, 45%);
--color-warning: hsl(45, 70%, 50%);
--color-error: hsl(0, 60%, 50%);
--color-text-primary: hsl(220, 10%, 20%);
--color-text-secondary: hsl(220, 10%, 40%);
--color-text-tertiary: hsl(220, 10%, 60%);
--color-bg-primary: hsl(0, 0%, 100%);
--color-bg-secondary: hsl(220, 15%, 96%);
--color-bg-tertiary: hsl(220, 15%, 92%);
--color-border: hsl(220, 10%, 85%);
--color-border-light: hsl(220, 10%, 90%);
/* Typography */
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--font-size-2xl: 1.5rem;
--font-size-3xl: 1.875rem;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--line-height-tight: 1.25;
--line-height-normal: 1.5;
--line-height-relaxed: 1.75;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
/* Borders */
--border-radius-sm: 0.25rem;
--border-radius-md: 0.375rem;
--border-radius-lg: 0.5rem;
--border-radius-xl: 0.75rem;
--border-radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
/* Transitions */
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-normal: 300ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 500ms cubic-bezier(0.4, 0, 0.2, 1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-family);
font-size: var(--font-size-base);
line-height: var(--line-height-normal);
color: var(--color-text-primary);
background-color: var(--color-bg-primary);
margin: 0;
padding: 0;
display: flex;
}
.container {
width: 100%;
max-width: 100%;
padding: var(--spacing-lg);
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.units-container {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.unit-row {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.unit-label {
font-weight: var(--font-weight-medium);
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
}
.unit-input {
flex: 1;
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--font-size-lg);
font-family: var(--font-family);
border: 2px solid var(--color-border);
border-radius: var(--border-radius-md);
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
transition: border-color var(--transition-fast), background-color var(--transition-fast);
}
.unit-input:focus {
outline: none;
border-color: var(--color-primary);
background-color: var(--color-bg-primary);
}
.unit-input::-webkit-inner-spin-button,
.unit-input::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.unit-input[type=number] {
-moz-appearance: textfield;
}
@media (max-width: 480px) {
.container {
padding: var(--spacing-md);
}
}
</style>
</head>
<body>
<script>
const ro = new ResizeObserver( ( entries ) => {
for ( const entry of entries ) {
window.parent.postMessage(
{ type: "ui-size-change", payload: { height: entry.contentRect.height } },
"*"
);
}
} );
ro.observe( document.documentElement );
</script>
<div class="container">
<div class="units-container" id="unitsContainer">
<!-- Units will be dynamically inserted here -->
</div>
</div>
<script>
const TEMPLATE_CONFIG = {
units: [
{
id: 'ft',
name: 'Feet',
formulas: {
in: '{ft} * 12',
m: '{ft} * 0.3048',
cm: '{ft} * 30.48'
}
},
{
id: 'in',
name: 'Inches',
formulas: {
ft: '{in} / 12',
m: '{in} * 0.0254',
cm: '{in} * 2.54'
}
},
{
id: 'm',
name: 'Meters',
formulas: {
ft: '{m} / 0.3048',
in: '{m} / 0.0254',
cm: '{m} * 100'
}
},
{
id: 'cm',
name: 'Centimeters',
formulas: {
ft: '{cm} / 30.48',
in: '{cm} / 2.54',
m: '{cm} / 100'
}
}
],
initialValue: {
id: 'ft',
value: 1
}
};
class UnitConverter {
constructor( config ) {
this.units = config.units;
this.values = {};
this.container = document.getElementById( 'unitsContainer' );
this.updating = false;
this.init( config.initialValue );
}
init( initialValue ) {
this.units.forEach( unit => {
const row = document.createElement( 'div' );
row.className = 'unit-row';
const label = document.createElement( 'label' );
label.className = 'unit-label';
label.textContent = unit.name;
label.htmlFor = `unit-${unit.id}`;
const input = document.createElement( 'input' );
input.type = 'number';
input.className = 'unit-input';
input.id = `unit-${unit.id}`;
input.step = 'any';
input.addEventListener( 'input', () => {
if ( !this.updating ) {
this.updateFromUnit( unit.id, parseFloat( input.value ) || 0 );
}
} );
row.appendChild( label );
row.appendChild( input );
this.container.appendChild( row );
} );
// Set initial value
if ( initialValue ) {
this.updateFromUnit( initialValue.id, initialValue.value );
}
}
updateFromUnit( sourceId, value ) {
this.updating = true;
// Update the source unit
this.values[sourceId] = value;
document.getElementById( `unit-${sourceId}` ).value = value;
// Find the source unit
const sourceUnit = this.units.find( u => u.id === sourceId );
if ( !sourceUnit ) return;
// Update all other units
this.units.forEach( targetUnit => {
if ( targetUnit.id === sourceId ) return;
// Get the formula for converting from source to target
const formula = sourceUnit.formulas[targetUnit.id];
if ( !formula ) return;
// Replace the placeholder with the actual value
const expression = formula.replace( `{${sourceId}}`, value );
try {
// Safely evaluate the expression
const result = this.evaluateExpression( expression );
this.values[targetUnit.id] = result;
const input = document.getElementById( `unit-${targetUnit.id}` );
if ( input ) {
// Format the result to avoid long decimals
input.value = this.formatNumber( result );
}
} catch ( e ) {
console.error( `Error evaluating formula: ${formula}`, e );
}
} );
this.updating = false;
}
evaluateExpression( expr ) {
// Simple safe math expression evaluator
// Only allows numbers, operators, and parentheses
const cleaned = expr.replace( /[^0-9+\-*/().\s]/g, '' );
// Parse and evaluate the mathematical expression safely
return this.parseExpression( cleaned );
}
parseExpression( expr ) {
// Remove whitespace
expr = expr.replace( /\s+/g, '' );
// Parse the expression into tokens
const tokens = this.tokenize( expr );
// Evaluate the tokens
return this.evaluateTokens( tokens );
}
tokenize( expr ) {
const tokens = [];
let i = 0;
while ( i < expr.length ) {
const char = expr[i];
if ( /[0-9.]/.test( char ) ) {
// Parse number
let num = '';
while ( i < expr.length && /[0-9.]/.test( expr[i] ) ) {
num += expr[i];
i++;
}
tokens.push( { type: 'number', value: parseFloat( num ) } );
} else if ( /[+\-*/()]/.test( char ) ) {
// Parse operator or parenthesis
tokens.push( { type: 'operator', value: char } );
i++;
} else {
// Skip invalid characters
i++;
}
}
return tokens;
}
evaluateTokens( tokens ) {
// Convert infix to postfix (Shunting Yard algorithm)
const output = [];
const operators = [];
const precedence = { '+': 1, '-': 1, '*': 2, '/': 2 };
for ( const token of tokens ) {
if ( token.type === 'number' ) {
output.push( token.value );
} else if ( token.type === 'operator' ) {
if ( token.value === '(' ) {
operators.push( token.value );
} else if ( token.value === ')' ) {
while ( operators.length && operators[operators.length - 1] !== '(' ) {
output.push( operators.pop() );
}
operators.pop(); // Remove the '('
} else {
while ( operators.length &&
operators[operators.length - 1] !== '(' &&
precedence[operators[operators.length - 1]] >= precedence[token.value] ) {
output.push( operators.pop() );
}
operators.push( token.value );
}
}
}
while ( operators.length ) {
output.push( operators.pop() );
}
// Evaluate postfix expression
const stack = [];
for ( const item of output ) {
if ( typeof item === 'number' ) {
stack.push( item );
} else {
const b = stack.pop();
const a = stack.pop();
switch ( item ) {
case '+': stack.push( a + b ); break;
case '-': stack.push( a - b ); break;
case '*': stack.push( a * b ); break;
case '/': stack.push( a / b ); break;
}
}
}
return stack[0] || 0;
}
formatNumber( num ) {
// Format number to reasonable decimal places
if ( Math.abs( num ) < 0.01 ) {
return num.toExponential( 2 );
} else if ( Math.abs( num ) < 1 ) {
return num.toFixed( 4 );
} else if ( Math.abs( num ) < 100 ) {
return num.toFixed( 2 );
} else {
return num.toFixed( 0 );
}
}
}
// Initialize the converter
const converter = new UnitConverter( TEMPLATE_CONFIG );
</script>
</body>
</html>