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