<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>MVPL Extensible Calculator</title>
<!--
===============================================================================
MVPL TEACHING EXAMPLE — EXTENSIBLE CALCULATOR
===============================================================================
This file intentionally demonstrates MVPL (Minimum Viable Product Line)
architecture inside a SINGLE standalone HTML file.
MVPL Principles Demonstrated:
• Small, stable core
• Optional, removable extensions
• Safe experimentation
• Sustainable long-term growth
This is BOTH:
1. A fully usable daily calculator
2. A teaching example of scalable product design
===============================================================================
-->
<style>
body {
margin: 0;
background: #111;
font-family: system-ui, -apple-system, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
touch-action: manipulation;
}
.calculator {
width: min(420px, 100vw);
padding: 16px;
box-sizing: border-box;
}
.display {
background: #000;
color: #0f0;
font-size: 2.5rem;
padding: 20px;
border-radius: 12px;
margin-bottom: 16px;
text-align: right;
min-height: 70px;
word-wrap: break-word;
}
.buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
button {
font-size: 1.6rem;
padding: 20px;
border: none;
border-radius: 14px;
background: #333;
color: white;
}
button:active {
background: #555;
}
.operator { background: #ff9500; }
.utility { background: #777; }
.extension { background: #2a6; }
@media (orientation: landscape) {
.display { font-size: 2rem; }
}
</style>
</head>
<body>
<div class="calculator">
<div id="display" class="display">0</div>
<div id="buttons" class="buttons"></div>
</div>
<script>
/* ============================================================================
CORE MODULE (MVPL STABLE FOUNDATION)
----------------------------------------------------------------------------
In MVPL systems, the core must remain:
• Small
• Stable
• Predictable
• Highly reliable
The core provides ONLY shared infrastructure:
- State management
- Expression evaluation
- Display rendering
- Input routing
- Extension registration hooks
New features must NOT modify this code.
Extensions interact ONLY through public hooks.
This protects the product from experimental risk.
============================================================================ */
/* ==============================
CORE STATE MANAGEMENT
============================== */
const CoreState = {
displayValue: "0",
expression: "",
lastInput: null
};
/* ==============================
DISPLAY RENDERER
Shared UI rendering for entire app
============================== */
const DisplayRenderer = {
element: document.getElementById("display"),
render(value) {
this.element.textContent = value;
}
};
/* ==============================
EXPRESSION EVALUATION ENGINE
Stable mathematical logic
============================== */
const ExpressionEngine = {
evaluate(expression) {
try {
if (/\/0(?!\.)/.test(expression)) return "Error";
const result = Function('"use strict";return (' + expression + ')')();
if (!isFinite(result)) return "Error";
return parseFloat(result.toFixed(10)).toString();
} catch {
return "Error";
}
}
};
/* ==============================
INPUT CONTROLLER
Routes all input safely
============================== */
const InputController = {
inputDigit(digit) {
if (CoreState.displayValue === "0") {
CoreState.displayValue = digit;
} else {
CoreState.displayValue += digit;
}
CoreState.expression += digit;
},
inputDecimal() {
if (CoreState.displayValue.includes(".")) return;
CoreState.displayValue += ".";
CoreState.expression += ".";
},
inputOperator(op) {
if (!CoreState.expression) return;
if (/[+\-*/]$/.test(CoreState.expression)) {
CoreState.expression =
CoreState.expression.slice(0, -1) + op;
} else {
CoreState.expression += op;
}
CoreState.displayValue = op;
},
clear() {
CoreState.displayValue = "0";
CoreState.expression = "";
},
equals() {
const result = ExpressionEngine.evaluate(CoreState.expression);
CoreState.displayValue = result;
CoreState.expression = (result === "Error") ? "" : result;
}
};
/* ==============================
UI LAYOUT GENERATION
============================== */
const UILayout = {
buttonContainer: document.getElementById("buttons"),
createButton(config) {
const btn = document.createElement("button");
btn.textContent = config.label;
if (config.className) btn.classList.add(config.className);
btn.addEventListener("click", () => {
config.onPress();
DisplayRenderer.render(CoreState.displayValue);
});
return btn;
},
addButton(config) {
this.buttonContainer.appendChild(this.createButton(config));
}
};
/* ============================================================================
EXTENSION REGISTRY (MVPL EXTENSION INFRASTRUCTURE)
----------------------------------------------------------------------------
Why this exists:
- Extensions protect the core from experimental features
- Optional modules allow rapid experimentation
- Extensions can be removed without breaking core product
All extensions must register through this interface.
The core never knows extension details.
============================================================================ */
const ExtensionRegistry = {
extensions: [],
register(extension) {
this.extensions.push(extension);
if (extension.init) {
extension.init({
addButton: UILayout.addButton.bind(UILayout),
state: CoreState,
input: InputController,
display: DisplayRenderer
});
}
}
};
/* ============================================================================
CORE BUTTON SETUP
Stable shared UI used by all product variants
============================================================================ */
function buildCoreButtons() {
const coreButtons = [
{ label: "C", className: "utility", onPress: () => InputController.clear() },
{ label: "÷", className: "operator", onPress: () => InputController.inputOperator("/") },
{ label: "×", className: "operator", onPress: () => InputController.inputOperator("*") },
{ label: "−", className: "operator", onPress: () => InputController.inputOperator("-") },
...["7","8","9"].map(n => ({ label:n, onPress:()=>InputController.inputDigit(n) })),
{ label: "+", className: "operator", onPress: () => InputController.inputOperator("+") },
...["4","5","6"].map(n => ({ label:n, onPress:()=>InputController.inputDigit(n) })),
{ label: "=", className: "operator", onPress: () => InputController.equals() },
...["1","2","3"].map(n => ({ label:n, onPress:()=>InputController.inputDigit(n) })),
{ label: ".", onPress: () => InputController.inputDecimal() },
{ label: "0", onPress: () => InputController.inputDigit("0") }
];
coreButtons.forEach(btn => UILayout.addButton(btn));
}
/* ============================================================================
KEYBOARD SUPPORT
Core usability feature
============================================================================ */
document.addEventListener("keydown", e => {
if (/\d/.test(e.key)) InputController.inputDigit(e.key);
if (e.key === ".") InputController.inputDecimal();
if (["+", "-", "*", "/"].includes(e.key)) InputController.inputOperator(e.key);
if (e.key === "Enter" || e.key === "=") InputController.equals();
if (e.key === "Escape") InputController.clear();
DisplayRenderer.render(CoreState.displayValue);
});
/* ============================================================================
DEMONSTRATION EXTENSION
----------------------------------------------------------------------------
Example Feature: Percentage Calculation
MVPL Teaching Points:
• Implemented OUTSIDE the core
• Uses only extension interface
• Can be removed safely
• Demonstrates experimental feature addition
============================================================================ */
const PercentageExtension = {
name: "percentage",
init(api) {
api.addButton({
label: "%",
className: "extension",
onPress: () => {
let value = parseFloat(api.state.displayValue);
if (isNaN(value)) return;
value = value / 100;
api.state.displayValue = value.toString();
api.state.expression = value.toString();
api.display.render(api.state.displayValue);
}
});
}
};
/* ============================================================================
APPLICATION BOOTSTRAP
Core starts first → Extensions attach later
============================================================================ */
function startApp() {
buildCoreButtons();
// Register extensions here
ExtensionRegistry.register(PercentageExtension);
DisplayRenderer.render(CoreState.displayValue);
}
startApp();
</script>
</body>
</html>
No comments:
Post a Comment