stem-lab-toolkit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/js/main.js ADDED
@@ -0,0 +1,301 @@
1
+ // ── Calculator state ────────────────────────────────────────────────────────
2
+ let displayVal = '0';
3
+ let operand = null;
4
+ let operator = null;
5
+ let currentInput = '';
6
+ let justEvaled = false;
7
+ let parenDepth = 0;
8
+ let memory = 0;
9
+ let isInverse = false;
10
+ let isDeg = true;
11
+
12
+ const mainEl = document.getElementById('main');
13
+ const exprEl = document.getElementById('expr');
14
+
15
+ function toRad(n) { return isDeg ? n * Math.PI / 180 : n; }
16
+ function fromRad(n) { return isDeg ? n * 180 / Math.PI : n; }
17
+
18
+ function setDisplay(val) {
19
+ displayVal = String(val);
20
+ const len = displayVal.length;
21
+ mainEl.style.fontSize = len > 12 ? '1.6rem' : len > 9 ? '2.2rem' : len > 6 ? '2.6rem' : '3rem';
22
+ mainEl.textContent = displayVal;
23
+ }
24
+
25
+ function fmt(n) {
26
+ if (!isFinite(n)) return 'Error';
27
+ const s = parseFloat(n.toPrecision(12)).toString();
28
+ return s;
29
+ }
30
+
31
+ function applyOp(a, op, b) {
32
+ switch (op) {
33
+ case '+': return a + b;
34
+ case '−': return a - b;
35
+ case '×': return a * b;
36
+ case '÷': return b === 0 ? NaN : a / b;
37
+ }
38
+ }
39
+
40
+ function pressDigit(d) {
41
+ if (justEvaled) { currentInput = d; setDisplay(d); justEvaled = false; return; }
42
+ if (operator && operand !== null && currentInput === '') {
43
+ currentInput = d; setDisplay(d); return;
44
+ }
45
+ if (displayVal === '0' && d !== '.') {
46
+ currentInput = d; setDisplay(d);
47
+ } else {
48
+ if (currentInput.replace(/[-.]/g, '').length >= 12) return;
49
+ currentInput += d;
50
+ setDisplay(displayVal + d);
51
+ }
52
+ }
53
+
54
+ function pressDot() {
55
+ if (justEvaled) { currentInput = '0.'; setDisplay('0.'); justEvaled = false; return; }
56
+ if (displayVal.includes('.')) return;
57
+ currentInput += '.';
58
+ setDisplay(displayVal + '.');
59
+ }
60
+
61
+ function pressOp(op) {
62
+ document.querySelectorAll('.btn-op').forEach(b => b.classList.remove('lit'));
63
+ document.querySelector(`[data-op="${op}"]`)?.classList.add('lit');
64
+ const val = parseFloat(displayVal) || 0;
65
+ if (operand !== null && !justEvaled && currentInput !== '') {
66
+ const result = applyOp(operand, operator, val);
67
+ const rs = fmt(result);
68
+ exprEl.textContent = `${operand} ${operator} ${val} =`;
69
+ setDisplay(rs);
70
+ operand = parseFloat(rs);
71
+ } else {
72
+ operand = val;
73
+ }
74
+ operator = op;
75
+ currentInput = '';
76
+ justEvaled = false;
77
+ exprEl.textContent = `${operand} ${op}`;
78
+ }
79
+
80
+ function pressEquals() {
81
+ const entry = currentInput || displayVal;
82
+
83
+ // Check PIN shortcuts
84
+ if (checkPin(entry)) return;
85
+
86
+ if (operator === null || operand === null) { justEvaled = true; return; }
87
+ document.querySelectorAll('.btn-op').forEach(b => b.classList.remove('lit'));
88
+ const val = parseFloat(displayVal) || 0;
89
+ const result = applyOp(operand, operator, val);
90
+ const rs = fmt(result);
91
+ exprEl.textContent = `${operand} ${operator} ${val} =`;
92
+ setDisplay(rs);
93
+ operand = null;
94
+ operator = null;
95
+ currentInput = '';
96
+ justEvaled = true;
97
+ }
98
+
99
+ function pressClear() {
100
+ document.querySelectorAll('.btn-op').forEach(b => b.classList.remove('lit'));
101
+ setDisplay('0');
102
+ exprEl.textContent = '';
103
+ operand = null; operator = null; currentInput = ''; justEvaled = false; parenDepth = 0;
104
+ }
105
+
106
+ function pressSign() {
107
+ if (displayVal === '0') return;
108
+ const n = parseFloat(displayVal) * -1;
109
+ currentInput = fmt(n);
110
+ setDisplay(currentInput);
111
+ }
112
+
113
+ function pressPercent() {
114
+ const n = parseFloat(displayVal) / 100;
115
+ currentInput = fmt(n);
116
+ setDisplay(currentInput);
117
+ }
118
+
119
+ // Scientific
120
+ function pressSci(fn) {
121
+ const val = parseFloat(displayVal) || 0;
122
+ let result;
123
+ switch (fn) {
124
+ case 'sin': result = isInverse ? fromRad(Math.asin(val)) : Math.sin(toRad(val)); break;
125
+ case 'cos': result = isInverse ? fromRad(Math.acos(val)) : Math.cos(toRad(val)); break;
126
+ case 'tan': result = isInverse ? fromRad(Math.atan(val)) : Math.tan(toRad(val)); break;
127
+ case 'log': result = isInverse ? Math.pow(10, val) : Math.log10(val); break;
128
+ case 'ln': result = isInverse ? Math.exp(val) : Math.log(val); break;
129
+ case 'sqrt': result = isInverse ? val * val : Math.sqrt(val); break;
130
+ case 'sq': result = val * val; break;
131
+ case 'pi': result = Math.PI; break;
132
+ case 'e': result = Math.E; break;
133
+ case 'open':
134
+ exprEl.textContent += '(';
135
+ parenDepth++;
136
+ return;
137
+ case 'close':
138
+ if (parenDepth > 0) { exprEl.textContent += ')'; parenDepth--; }
139
+ return;
140
+ case 'inv':
141
+ isInverse = !isInverse;
142
+ document.getElementById('inv-btn').classList.toggle('inv-active', isInverse);
143
+ return;
144
+ default: return;
145
+ }
146
+ const rs = fmt(result);
147
+ exprEl.textContent = `${fn}(${val}) =`;
148
+ setDisplay(rs);
149
+ currentInput = rs;
150
+ justEvaled = true;
151
+ if (fn !== 'inv') { isInverse = false; document.getElementById('inv-btn').classList.remove('inv-active'); }
152
+ }
153
+
154
+ // Events
155
+ document.querySelector('.calc-buttons').addEventListener('click', e => {
156
+ const btn = e.target.closest('button');
157
+ if (!btn) return;
158
+ const action = btn.dataset.action;
159
+ if (action === 'digit') pressDigit(btn.dataset.digit);
160
+ if (action === 'dot') pressDot();
161
+ if (action === 'op') pressOp(btn.dataset.op);
162
+ if (action === 'equals') pressEquals();
163
+ if (action === 'clear') pressClear();
164
+ if (action === 'sign') pressSign();
165
+ if (action === 'percent') pressPercent();
166
+ });
167
+
168
+ document.querySelector('.sci-rows').addEventListener('click', e => {
169
+ const btn = e.target.closest('button');
170
+ if (!btn) return;
171
+ pressSci(btn.dataset.sci);
172
+ });
173
+
174
+ document.addEventListener('keydown', e => {
175
+ if (pinOverlay.classList.contains('hidden')) {
176
+ if (e.key >= '0' && e.key <= '9') pressDigit(e.key);
177
+ else if (e.key === '.') pressDot();
178
+ else if (e.key === '+') pressOp('+');
179
+ else if (e.key === '-') pressOp('−');
180
+ else if (e.key === '*') pressOp('×');
181
+ else if (e.key === '/') { e.preventDefault(); pressOp('÷'); }
182
+ else if (e.key === 'Enter' || e.key === '=') pressEquals();
183
+ else if (e.key === 'Escape') pressClear();
184
+ else if (e.key === 'Backspace') {
185
+ if (currentInput.length > 1) { currentInput = currentInput.slice(0, -1); setDisplay(currentInput); }
186
+ else { currentInput = ''; setDisplay('0'); }
187
+ }
188
+ } else {
189
+ if (e.key >= '0' && e.key <= '9') addPinDigit(e.key);
190
+ else if (e.key === 'Backspace') delPinDigit();
191
+ else if (e.key === 'Escape') closePinOverlay();
192
+ }
193
+ });
194
+
195
+ // ── Hidden PIN trigger (3 header clicks) ────────────────────────────────────
196
+ let clickCount = 0;
197
+ let clickTimer = null;
198
+
199
+ document.getElementById('calc-header').addEventListener('click', () => {
200
+ clickCount++;
201
+ clearTimeout(clickTimer);
202
+ if (clickCount >= 3) {
203
+ clickCount = 0;
204
+ openPinOverlay();
205
+ } else {
206
+ clickTimer = setTimeout(() => { clickCount = 0; }, 1500);
207
+ }
208
+ });
209
+
210
+ // ── PIN overlay ──────────────────────────────────────────────────────────────
211
+ const pinOverlay = document.getElementById('pin-overlay');
212
+ const pinError = document.getElementById('pin-error');
213
+ let pinEntry = '';
214
+
215
+ function openPinOverlay() {
216
+ pinEntry = '';
217
+ pinError.textContent = '';
218
+ updatePinDots();
219
+ pinOverlay.classList.remove('hidden');
220
+ }
221
+
222
+ function closePinOverlay() {
223
+ pinOverlay.classList.add('hidden');
224
+ pinEntry = '';
225
+ }
226
+
227
+ function updatePinDots(state) {
228
+ for (let i = 0; i < 4; i++) {
229
+ const dot = document.getElementById('d' + i);
230
+ dot.className = 'pin-dot';
231
+ if (state === 'error') { dot.classList.add('error'); }
232
+ else if (i < pinEntry.length) { dot.classList.add('filled'); }
233
+ }
234
+ }
235
+
236
+ function addPinDigit(d) {
237
+ if (pinEntry.length >= 4) return;
238
+ pinEntry += d;
239
+ updatePinDots();
240
+ if (pinEntry.length === 4) submitPin();
241
+ }
242
+
243
+ function delPinDigit() {
244
+ pinEntry = pinEntry.slice(0, -1);
245
+ updatePinDots();
246
+ pinError.textContent = '';
247
+ }
248
+
249
+ async function submitPin() {
250
+ const pin = pinEntry;
251
+ try {
252
+ const res = await fetch('./api/pin', {
253
+ method: 'POST',
254
+ headers: { 'Content-Type': 'application/json' },
255
+ body: JSON.stringify({ pin }),
256
+ });
257
+ const data = await res.json();
258
+ if (data.role === 'admin') {
259
+ localStorage.setItem('mm_admin_session', '1');
260
+ window.location.href = './admin.html';
261
+ } else if (data.role === 'user') {
262
+ window.location.href = './browse.html';
263
+ } else {
264
+ showPinError();
265
+ }
266
+ } catch {
267
+ // Fallback for unpkg / static hosting
268
+ if (pin === '1234') {
269
+ localStorage.setItem('mm_admin_session', '1');
270
+ window.location.href = './admin.html';
271
+ } else if (pin === '5555') {
272
+ window.location.href = './browse.html';
273
+ } else {
274
+ showPinError();
275
+ }
276
+ }
277
+ }
278
+
279
+ function showPinError() {
280
+ updatePinDots('error');
281
+ pinError.textContent = 'Incorrect PIN';
282
+ setTimeout(() => {
283
+ pinEntry = '';
284
+ updatePinDots();
285
+ pinError.textContent = '';
286
+ }, 1200);
287
+ }
288
+
289
+ function checkPin(entry) {
290
+ // Called from pressEquals — not used, PIN only via header click
291
+ return false;
292
+ }
293
+
294
+ document.querySelector('.pin-keypad').addEventListener('click', e => {
295
+ const btn = e.target.closest('button');
296
+ if (!btn) return;
297
+ const k = btn.dataset.k;
298
+ if (k === 'del') delPinDigit();
299
+ else if (k === 'cancel') closePinOverlay();
300
+ else addPinDigit(k);
301
+ });
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "stem-lab-toolkit",
3
+ "version": "1.0.0",
4
+ "description": "STEM educational toolkit",
5
+ "main": "index.html",
6
+ "scripts": {
7
+ "start": "node api.js"
8
+ },
9
+ "keywords": [
10
+ "education",
11
+ "stem",
12
+ "toolkit",
13
+ "math"
14
+ ],
15
+ "license": "MIT",
16
+ "author": ""
17
+ }