Wednesday, 4 February 2026

MVPL Simple Browser-based Calculator Working Example

 <!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