Thursday, 12 February 2026

Source Code for Calculator (MVPL Platform) Core HTML

 <!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8" />

<meta name="viewport" content="width=device-width, initial-scale=1.0"/>

<title>MVPL Modular Calculator</title>


<style>

/*

============================================================

MVPL MODULAR CALCULATOR PLATFORM

============================================================


This file implements the full MVPL architecture:


MODEL:

- Profiles

- Extension state namespace isolation


VIEW:

- UI zones

- Slot keypad system


PRESENTER:

- Extension registry

- Activation controller

- Layout manager

- Extension API


============================================================

TESTING INSTRUCTIONS

============================================================

1. Open index.html in Safari / iPad Safari / Textastic.

2. Load inverse.js using script tag OR place in same directory.

3. Use topbar dropdown to switch profiles.

4. Confirm:

   - 1/x only appears in scientific profile

   - Extension removed when leaving scientific profile

   - Arithmetic remains stable

   - Developer panel shows extension state

5. Confirm divide-by-zero and invalid input show "Error".

============================================================

*/


/* ---------- GLOBAL STYLES ---------- */


body {

  margin: 0;

  font-family: system-ui, sans-serif;

  background: #111;

  color: #fff;

  display: flex;

  flex-direction: column;

  height: 100vh;

}


.zone {

  padding: 8px;

}


#topbar {

  background: #1f1f1f;

  display: flex;

  justify-content: space-between;

  align-items: center;

}


#display {

  background: #000;

  font-size: 2.5rem;

  padding: 16px;

  text-align: right;

  word-wrap: break-word;

}


#mainpad {

  flex: 1;

  display: grid;

  grid-template-columns: repeat(4, 1fr);

  gap: 8px;

  padding: 8px;

}


#sidebar {

  background: #181818;

  font-size: 12px;

}


#footer {

  background: #1a1a1a;

}


#statusbar {

  background: #000;

  font-size: 12px;

  padding: 4px;

}


/* Slot container */

.slot {

  display: flex;

}


button {

  width: 100%;

  padding: 16px;

  font-size: 1.2rem;

  border: none;

  border-radius: 8px;

  background: #2c2c2c;

  color: white;

  touch-action: manipulation;

}


button:active {

  background: #444;

}


.devpanel {

  white-space: pre-wrap;

  font-size: 11px;

}

</style>

</head>


<body>


<!-- ================= UI ZONES ================= -->


<div id="topbar" class="zone">

  <div>MVPL Calculator</div>

  <select id="profileSelect"></select>

</div>


<div id="display" class="zone">0</div>


<div id="mainpad" class="zone">

  <!-- SLOT KEYPAD SYSTEM

  Core buttons NEVER move. Extensions can only use extension slots.

  -->

  <div class="slot" id="slot-clear"></div>

  <div class="slot" id="slot-cancel"></div>

  <div class="slot" id="slot-divide"></div>

  <div class="slot" id="slot-multiply"></div>


  <div class="slot" id="slot-7"></div>

  <div class="slot" id="slot-8"></div>

  <div class="slot" id="slot-9"></div>

  <div class="slot" id="slot-minus"></div>


  <div class="slot" id="slot-4"></div>

  <div class="slot" id="slot-5"></div>

  <div class="slot" id="slot-6"></div>

  <div class="slot" id="slot-plus"></div>


  <div class="slot" id="slot-1"></div>

  <div class="slot" id="slot-2"></div>

  <div class="slot" id="slot-3"></div>

  <div class="slot" id="slot-equals"></div>


  <div class="slot" id="slot-0"></div>

  <div class="slot" id="slot-decimal"></div>

  <div class="slot" id="slot-extension-1"></div>

  <div class="slot" id="slot-extension-2"></div>

</div>


<div id="sidebar" class="zone">

  <div><b>Developer Panel</b></div>

  <div id="devpanel" class="devpanel"></div>

</div>


<div id="footer" class="zone">MVPL Platform</div>

<div id="statusbar" class="zone">Ready</div>


<script>

/*

============================================================

MODEL LAYER

============================================================

Profiles isolate extension state.


Extension state path:

profiles[currentProfile].state[extensionName]

*/


const profiles = {

  default: {

    name: "Default",

    layout: "minimal",

    enabledExtensions: [],

    state: {}

  },

  scientific: {

    name: "Scientific",

    layout: "scientific",

    enabledExtensions: ["InverseExtension"],

    state: {}

  },

  education: {

    name: "Education",

    layout: "education",

    enabledExtensions: [],

    state: {}

  }

};


let currentProfile = "default";


/* ---------- DISPLAY & CALCULATOR CORE ---------- */


let displayValue = "0";

let operand = null;

let operator = null;

let resetNext = false;


const displayEl = document.getElementById("display");


function updateDisplay() {

  displayEl.textContent = displayValue;

}


function inputDigit(d) {

  if (resetNext) {

    displayValue = d;

    resetNext = false;

  } else {

    displayValue = displayValue === "0" ? d : displayValue + d;

  }

  updateDisplay();

}


function inputDecimal() {

  if (!displayValue.includes(".")) {

    displayValue += ".";

  }

  updateDisplay();

}


function clearAll() {

  displayValue = "0";

  operand = null;

  operator = null;

  updateDisplay();

}


function backspace() {

  displayValue = displayValue.length > 1

    ? displayValue.slice(0, -1)

    : "0";

  updateDisplay();

}


function performOperation(op) {

  const val = parseFloat(displayValue);

  if (operand === null) {

    operand = val;

  } else if (operator) {

    const result = calculate(operand, val, operator);

    if (result === null) return;

    operand = result;

    displayValue = String(result);

    updateDisplay();

  }

  operator = op;

  resetNext = true;

}


function calculate(a, b, op) {

  let r = null;

  if (op === "+") r = a + b;

  if (op === "-") r = a - b;

  if (op === "*") r = a * b;

  if (op === "/") {

    if (b === 0) return errorDisplay();

    r = a / b;

  }

  return r;

}


function equals() {

  if (!operator) return;

  const val = parseFloat(displayValue);

  const result = calculate(operand, val, operator);

  if (result === null) return;

  displayValue = String(result);

  operand = null;

  operator = null;

  updateDisplay();

}


function errorDisplay() {

  displayValue = "Error";

  operand = null;

  operator = null;

  updateDisplay();

  return null;

}


/*

============================================================

VIEW: SLOT KEYPAD SYSTEM

============================================================

Core buttons placed into fixed slots.

*/


function addCoreButton(slot, label, handler) {

  const btn = document.createElement("button");

  btn.textContent = label;

  btn.onclick = handler;

  document.getElementById(slot).appendChild(btn);

}


function buildCorePad() {

  addCoreButton("slot-clear", "C", clearAll);

  addCoreButton("slot-cancel", "⌫", backspace);

  addCoreButton("slot-divide", "÷", () => performOperation("/"));

  addCoreButton("slot-multiply", "×", () => performOperation("*"));


  ["7","8","9"].forEach(n => addCoreButton("slot-"+n,n,()=>inputDigit(n)));

  addCoreButton("slot-minus","−",()=>performOperation("-"));


  ["4","5","6"].forEach(n => addCoreButton("slot-"+n,n,()=>inputDigit(n)));

  addCoreButton("slot-plus","+",()=>performOperation("+"));


  ["1","2","3"].forEach(n => addCoreButton("slot-"+n,n,()=>inputDigit(n)));

  addCoreButton("slot-equals","=",equals);


  addCoreButton("slot-0","0",()=>inputDigit("0"));

  addCoreButton("slot-decimal",".",inputDecimal);

}


/*

============================================================

EXTENSION REGISTRY

============================================================

Stores extension manifests without activating them.

Supports dependency and priority ordering.

*/


const extensionRegistry = new Map();

const activeExtensions = new Map();

const extensionErrors = [];


function registerExtension(manifest) {

  extensionRegistry.set(manifest.name, manifest);

  renderDevPanel();

}


/*

============================================================

ACTIVATION CONTROLLER

Authoritative extension lifecycle manager

============================================================

Why this exists:

- Guarantees lifecycle reliability

- Prevents duplicate UI

- Handles dependency ordering

- Maintains backwards compatibility

*/


function resolveDependencies(names) {

  const resolved = [];

  const visited = new Set();


  function visit(name) {

    if (visited.has(name)) return;

    visited.add(name);


    const ext = extensionRegistry.get(name);

    if (!ext) return;


    if (ext.dependencies) {

      ext.dependencies.forEach(visit);

    }


    resolved.push(name);

  }


  names.forEach(visit);


  return resolved.sort((a,b)=>{

    return (extensionRegistry.get(a).priority||0)

      - (extensionRegistry.get(b).priority||0);

  });

}


function evaluateActiveExtensions(triggerType) {

  const profile = profiles[currentProfile];

  const enabled = resolveDependencies(profile.enabledExtensions);


  /* ---- ACTIVATE ---- */

  enabled.forEach(name=>{

    if (!extensionRegistry.has(name)) return;


    if (!activeExtensions.has(name)) {

      const manifest = extensionRegistry.get(name);


      try {

        const state = getExtensionState(name);


        if (manifest.onInit) manifest.onInit(extensionAPI(name), state);

        else if (manifest.init) manifest.init(extensionAPI(name));


        activeExtensions.set(name, manifest);

      } catch(e) {

        extensionErrors.push(name + ": " + e.message);

      }

    }

  });


  /* ---- DEACTIVATE ---- */

  [...activeExtensions.keys()].forEach(name=>{

    if (!enabled.includes(name)) {

      const manifest = activeExtensions.get(name);

      try {

        if (manifest.onDestroy) {

          manifest.onDestroy(extensionAPI(name), getExtensionState(name));

        }

      } catch(e){}

      activeExtensions.delete(name);

    }

  });


  /* ---- PROFILE SWITCH HOOK ---- */

  if (triggerType === "profile") {

    activeExtensions.forEach((manifest,name)=>{

      if (manifest.onProfileSwitch) {

        manifest.onProfileSwitch(extensionAPI(name), getExtensionState(name));

      }

    });

  }


  /* ---- LAYOUT CHANGE HOOK ---- */

  if (triggerType === "layout") {

    activeExtensions.forEach((manifest,name)=>{

      if (manifest.onLayoutChange) {

        manifest.onLayoutChange(extensionAPI(name), getExtensionState(name));

      }

    });

  }


  renderDevPanel();

}


/*

============================================================

EXTENSION STATE NAMESPACE

============================================================

Each extension stores state per profile.

*/


function getExtensionState(name) {

  const profile = profiles[currentProfile];

  if (!profile.state[name]) profile.state[name] = {};

  return profile.state[name];

}


/*

============================================================

EXTENSION API CONTRACT

============================================================

Extensions must only use this interface.

*/


function extensionAPI(extensionName) {


  const ownedUI = new Set();


  return {

    addUIComponent({zone, element}) {

      document.getElementById(zone).appendChild(element);

      ownedUI.add(element);

    },


    addButton(slot, element) {

      document.getElementById(slot).appendChild(element);

      ownedUI.add(element);

    },


    removeUIComponent(element) {

      if (element && element.remove) element.remove();

      ownedUI.delete(element);

    },


    getDisplayValue() {

      return displayValue;

    },


    setDisplayValue(v) {

      displayValue = v;

      updateDisplay();

    },


    getProfile() {

      return currentProfile;

    },


    state() {

      return getExtensionState(extensionName);

    },


    subscribeLayoutChanges() {

      /* Placeholder */

    },


    __ownedUI: ownedUI

  };

}


/*

============================================================

LAYOUT MANAGER

============================================================

Profiles automatically apply layout presets.

*/


function applyLayoutPreset(layout) {

  document.body.dataset.layout = layout;

  evaluateActiveExtensions("layout");

}


/*

============================================================

PROFILE SWITCHING

============================================================

*/


function switchProfile(key) {

  currentProfile = key;

  applyLayoutPreset(profiles[key].layout);

  evaluateActiveExtensions("profile");

}


/*

============================================================

DEVELOPER PANEL

============================================================

*/


function renderDevPanel() {

  const dev = document.getElementById("devpanel");

  dev.textContent =

    "Profile: " + currentProfile +

    "\nActive Extensions: " + [...activeExtensions.keys()].join(", ") +

    "\nRegistered: " + [...extensionRegistry.keys()].join(", ") +

    "\nErrors: " + extensionErrors.join(" | ");

}


/*

============================================================

INITIALIZATION

============================================================

*/


function initProfilesUI() {

  const sel = document.getElementById("profileSelect");


  Object.keys(profiles).forEach(k=>{

    const opt = document.createElement("option");

    opt.value = k;

    opt.textContent = profiles[k].name;

    sel.appendChild(opt);

  });


  sel.onchange = e => switchProfile(e.target.value);

}


buildCorePad();

initProfilesUI();

updateDisplay();

applyLayoutPreset(profiles[currentProfile].layout);


/* Load extension file */

</script>


<script src="inverse.js"></script>


</body>

</html>


No comments:

Post a Comment