mevebu 1.9.0 → 1.10.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.
@@ -1,205 +1,256 @@
1
- // Scientific math from a typed expression or the keypad. Degrees or radians
2
- // drive the trig functions, and recent results stay in a short history.
3
- import { el, bench, stack, panel, textInput, output, notice, button, copyButton, chipRow, actions, toolcard } from "../core.js";
4
-
5
- function tokenize(src) {
6
- const tokens = [];
7
- let i = 0;
8
- while (i < src.length) {
9
- const ch = src[i];
10
- if (/\s/.test(ch)) { i++; continue; }
11
- if (/[0-9.]/.test(ch)) {
12
- let j = i;
13
- while (j < src.length && /[0-9.]/.test(src[j])) j++;
14
- if (src[j] === "e" || src[j] === "E") {
15
- let k = j + 1;
16
- if (src[k] === "+" || src[k] === "-") k++;
17
- if (/[0-9]/.test(src[k] ?? "")) {
18
- while (k < src.length && /[0-9]/.test(src[k])) k++;
19
- j = k;
20
- }
21
- }
22
- tokens.push({ type: "num", value: Number(src.slice(i, j)) });
23
- i = j;
24
- continue;
25
- }
26
- if (/[a-zA-Z]/.test(ch)) {
27
- let j = i;
28
- while (j < src.length && /[a-zA-Z]/.test(src[j])) j++;
29
- tokens.push({ type: "name", value: src.slice(i, j).toLowerCase() });
30
- i = j;
31
- continue;
32
- }
33
- if ("+-*/^()".includes(ch)) {
34
- tokens.push({ type: "op", value: ch });
35
- i++;
36
- continue;
37
- }
38
- throw new Error("Unexpected character");
39
- }
40
- return tokens;
1
+ // A scientific calculator with a Desmos style keypad: one expression, a live
2
+ // result, variables that persist, and a history you can click back into.
3
+ // mathjs is vendored and loaded on demand, so the page still works offline and
4
+ // the other 187 tools never pay for a parser they do not use.
5
+ import {
6
+ el, icon, toast, bench, stack, panel, textInput, notice, chipRow, button,
7
+ copyButton, actions, toolcard,
8
+ } from "../core.js";
9
+
10
+ const VENDOR = new URL("../vendor/mathjs.min.js", import.meta.url).href;
11
+
12
+ let engine = null;
13
+ function loadEngine() {
14
+ if (globalThis.math?.parse) return Promise.resolve(globalThis.math);
15
+ if (engine) return engine;
16
+ engine = new Promise((resolve, reject) => {
17
+ const script = document.createElement("script");
18
+ script.src = VENDOR;
19
+ script.onload = () => (globalThis.math?.parse ? resolve(globalThis.math) : reject(new Error("The math engine did not start.")));
20
+ script.onerror = () => reject(new Error("The math engine could not be loaded."));
21
+ document.head.append(script);
22
+ });
23
+ return engine;
41
24
  }
42
25
 
43
- const FUNCTIONS = {
44
- sin: (x) => Math.sin(x),
45
- cos: (x) => Math.cos(x),
46
- tan: (x) => Math.tan(x),
47
- asin: (x) => Math.asin(x),
48
- acos: (x) => Math.acos(x),
49
- atan: (x) => Math.atan(x),
50
- log: (x) => Math.log10(x),
51
- ln: (x) => Math.log(x),
52
- sqrt: (x) => Math.sqrt(x),
53
- abs: (x) => Math.abs(x),
54
- exp: (x) => Math.exp(x)};
55
-
56
- function evaluate(src, degrees) {
57
- const tokens = tokenize(src);
58
- let pos = 0;
59
- const peek = () => tokens[pos];
60
- const eat = () => tokens[pos++];
61
- const toRad = (x) => (degrees ? (x * Math.PI) / 180 : x);
62
- const fromRad = (x) => (degrees ? (x * 180) / Math.PI : x);
63
-
64
- function expr() {
65
- let value = term();
66
- while (peek() && peek().type === "op" && (peek().value === "+" || peek().value === "-")) {
67
- const op = eat().value;
68
- const rhs = term();
69
- value = op === "+" ? value + rhs : value - rhs;
70
- }
71
- return value;
72
- }
26
+ const INJECTED = new Set([
27
+ "sin", "cos", "tan", "asin", "acos", "atan",
28
+ "sinh", "cosh", "tanh", "asinh", "acosh", "atanh",
29
+ "log", "ln", "nCr", "nPr", "ans",
30
+ ]);
73
31
 
74
- function term() {
75
- let value = unary();
76
- for (;;) {
77
- const token = peek();
78
- if (token && token.type === "op" && (token.value === "*" || token.value === "/")) {
79
- eat();
80
- const rhs = unary();
81
- value = token.value === "*" ? value * rhs : value / rhs;
82
- } else if (token && (token.type === "num" || token.type === "name" || (token.type === "op" && token.value === "("))) {
83
- value *= unary();
84
- } else break;
85
- }
86
- return value;
87
- }
32
+ const EXTRA = [
33
+ "asin", "acos", "atan", "sinh", "cosh", "tanh",
34
+ "abs", "round", "floor", "ceil", "exp", "nthRoot",
35
+ "min", "max", "mod", "gcd", "lcm", "nCr", "nPr",
36
+ ];
88
37
 
89
- function unary() {
90
- const token = peek();
91
- if (token && token.type === "op" && (token.value === "-" || token.value === "+")) {
92
- eat();
93
- const value = unary();
94
- return token.value === "-" ? -value : value;
95
- }
96
- return power();
97
- }
38
+ const KEYS = [
39
+ ["sin(", "sin"], ["cos(", "cos"], ["tan(", "tan"], ["ln(", "ln"],
40
+ ["log(", "log"], ["sqrt(", "√"], ["^2", "x²"], ["!", "x!"],
41
+ ["(", "("], [")", ")"], ["pi", "π"], ["ans", "ans"],
42
+ ["7", "7"], ["8", "8"], ["9", "9"], ["/", "÷"],
43
+ ["4", "4"], ["5", "5"], ["6", "6"], ["*", "×"],
44
+ ["1", "1"], ["2", "2"], ["3", "3"], ["-", "−"],
45
+ ["0", "0"], [".", "."], ["+", "+"], ["%", "%"],
46
+ ];
98
47
 
99
- function power() {
100
- const base = primary();
101
- if (peek() && peek().type === "op" && peek().value === "^") {
102
- eat();
103
- return base ** unary();
104
- }
105
- return base;
106
- }
48
+ const FUNC_BTN =
49
+ "min-h-[var(--ctl)] cursor-pointer rounded-r1 border border-orbit bg-deck-2 px-1 text-sm font-semibold text-ink transition-colors duration-150 hover:border-orbit-hard hover:bg-deck active:scale-[0.97]";
107
50
 
108
- function primary() {
109
- const token = eat();
110
- if (!token) throw new Error("Unexpected end of expression");
111
- if (token.type === "num") return token.value;
112
- if (token.type === "name") {
113
- if (token.value === "pi") return Math.PI;
114
- if (token.value === "e") return Math.E;
115
- if (!FUNCTIONS[token.value]) throw new Error("Unknown name");
116
- const open = eat();
117
- if (!open || open.value !== "(") throw new Error("Expected an opening bracket");
118
- const arg = expr();
119
- const close = eat();
120
- if (!close || close.value !== ")") throw new Error("Expected a closing bracket");
121
- const raw = FUNCTIONS[token.value](token.value === "sin" || token.value === "cos" || token.value === "tan" ? toRad(arg) : arg);
122
- return token.value === "asin" || token.value === "acos" || token.value === "atan" ? fromRad(raw) : raw;
123
- }
124
- if (token.type === "op" && token.value === "(") {
125
- const value = expr();
126
- const close = eat();
127
- if (!close || close.value !== ")") throw new Error("Expected a closing bracket");
128
- return value;
51
+ function smallFraction(value) {
52
+ if (!Number.isFinite(value) || Number.isInteger(value)) return null;
53
+ const sign = value < 0 ? -1 : 1;
54
+ let x = Math.abs(value);
55
+ let h0 = 0, h1 = 1, k0 = 1, k1 = 0;
56
+ for (let i = 0; i < 24; i++) {
57
+ const a = Math.floor(x);
58
+ const h2 = a * h1 + h0;
59
+ const k2 = a * k1 + k0;
60
+ if (k2 > 10000) return null;
61
+ h0 = h1; h1 = h2; k0 = k1; k1 = k2;
62
+ if (Math.abs(Math.abs(value) - h1 / k1) < 1e-10) {
63
+ return k1 === 1 ? null : { n: sign * h1, d: k1 };
129
64
  }
130
- throw new Error("Unexpected token");
65
+ const rest = x - a;
66
+ if (rest < 1e-12) break;
67
+ x = 1 / rest;
131
68
  }
132
-
133
- const result = expr();
134
- if (pos < tokens.length) throw new Error("Unexpected token");
135
- return result;
69
+ return null;
136
70
  }
137
71
 
138
- function fmt(n) {
139
- return n.toLocaleString(undefined, { maximumFractionDigits: 10 });
140
- }
141
-
142
- const KEYS = ["sin(", "cos(", "tan(", "log(", "ln(", "sqrt(", "pi", "e", "^", "(", ")", "/", "7", "8", "9", "*", "4", "5", "6", "-", "1", "2", "3", "+", "0", ".", "="];
143
-
144
72
  export function mount(slot) {
145
- const input = textInput({ value: "sin(30) + 2^3", "aria-label": "Expression", placeholder: "sin(30) + 2^3" });
146
- const display = output({ rows: 2 });
73
+ const math = { current: null };
74
+ const scope = { ans: 0 };
75
+ const history = [];
76
+ const state = { degrees: true, fractions: false };
77
+
78
+ const input = textInput({
79
+ value: "",
80
+ placeholder: "sin(30) + 2^3",
81
+ "aria-label": "Expression",
82
+ class: "min-h-[56px] border-0 bg-transparent px-0 py-1 font-mono text-2xl font-semibold tracking-[-0.01em] focus:ring-0",
83
+ inputmode: "text",
84
+ autocapitalize: "off",
85
+ autocorrect: "off",
86
+ });
87
+ const result = el("p", {
88
+ class: "m-0 min-h-[1.2em] overflow-x-auto font-mono text-3xl font-bold tracking-[-0.02em] text-ink tabular-nums",
89
+ "aria-live": "polite",
90
+ "aria-atomic": "true",
91
+ });
147
92
  const error = notice("error");
148
- const history = el("div", { class: "grid gap-px overflow-hidden rounded-r1 border border-orbit bg-orbit" });
149
- const chips = chipRow(
150
- [
151
- { value: "deg", label: "Degrees" },
152
- { value: "rad", label: "Radians" }],
153
- {
154
- value: "deg",
155
- label: "Angle unit",
156
- onChange: (value) => {
157
- degrees = value === "deg";
158
- run();
159
- }}
93
+ const loading = notice("note");
94
+
95
+ const historyList = el("div", { class: "grid gap-px overflow-hidden rounded-r1 border border-orbit bg-orbit" });
96
+ const historyEmpty = el("p", { class: "m-0 text-sm text-ink-3" }, "Every result you commit lands here.");
97
+ const variablesList = el("div", { class: "flex flex-wrap gap-1.5" });
98
+ const variablesEmpty = el("p", { class: "m-0 text-sm text-ink-3" }, "Type a = 5 to keep a value.");
99
+
100
+ const display = el(
101
+ "div",
102
+ { class: "grid gap-1 rounded-r2 border border-orbit bg-deck-2 px-4 py-3 focus-within:border-jade focus-within:ring-3 focus-within:ring-jade-soft" },
103
+ input,
104
+ el("div", { class: "h-px bg-orbit" }),
105
+ result
160
106
  );
161
- const recent = [];
162
- let degrees = true;
163
- let current = "";
164
107
 
165
- function renderHistory() {
166
- history.replaceChildren();
167
- for (const entry of recent) {
168
- history.append(
169
- el("div", { class: "grid gap-3 bg-deck px-3 py-[9px] [grid-template-columns:minmax(90px,34%)_minmax(0,1fr)]" },
170
- el("span", { class: "wrap-anywhere text-2xs font-semibold text-ink-2" }, entry.expression),
171
- el("span", { class: "wrap-anywhere font-mono text-2xs" }, entry.result))
172
- );
108
+ function format(value) {
109
+ const m = math.current;
110
+ if (!m) return "";
111
+ if (typeof value === "number") {
112
+ if (!Number.isFinite(value)) return String(value);
113
+ if (state.fractions) {
114
+ const fraction = smallFraction(value);
115
+ if (fraction) return `${fraction.n}/${fraction.d}`;
116
+ }
117
+ const size = Math.abs(value);
118
+ const notation = size !== 0 && (size >= 1e12 || size < 1e-6) ? "exponential" : "auto";
119
+ return m.format(value, { precision: 12, notation });
173
120
  }
121
+ try {
122
+ return m.format(value, { precision: 12 });
123
+ } catch {
124
+ return String(value);
125
+ }
126
+ }
127
+
128
+ function inject(m) {
129
+ const toRad = (x) => (state.degrees ? (x * Math.PI) / 180 : x);
130
+ const fromRad = (x) => (state.degrees ? (x * 180) / Math.PI : x);
131
+ scope.sin = (x) => Math.sin(toRad(x));
132
+ scope.cos = (x) => Math.cos(toRad(x));
133
+ scope.tan = (x) => Math.tan(toRad(x));
134
+ scope.asin = (x) => fromRad(Math.asin(x));
135
+ scope.acos = (x) => fromRad(Math.acos(x));
136
+ scope.atan = (x) => fromRad(Math.atan(x));
137
+ scope.sinh = (x) => Math.sinh(x);
138
+ scope.cosh = (x) => Math.cosh(x);
139
+ scope.tanh = (x) => Math.tanh(x);
140
+ scope.asinh = (x) => Math.asinh(x);
141
+ scope.acosh = (x) => Math.acosh(x);
142
+ scope.atanh = (x) => Math.atanh(x);
143
+ // Desmos reads log as base 10 and ln as natural, which is the opposite of
144
+ // mathjs's default for log, so both names are pinned here.
145
+ scope.log = (x, base) => (base === undefined ? Math.log10(x) : Math.log(x) / Math.log(base));
146
+ scope.ln = (x) => Math.log(x);
147
+ scope.nCr = (n, r) => m.combinations(n, r);
148
+ scope.nPr = (n, r) => m.permutations(n, r);
149
+ math.current = m;
150
+ }
151
+
152
+ function read(text, target) {
153
+ const m = math.current;
154
+ if (!m) throw new Error("The math engine is still loading.");
155
+ const node = m.parse(text);
156
+ return node.evaluate(target);
174
157
  }
175
158
 
176
- function run() {
159
+ let previewToken = 0;
160
+ async function preview() {
161
+ const token = ++previewToken;
177
162
  const text = input.value.trim();
163
+ error.write("");
178
164
  if (!text) {
179
- display.write("");
180
- error.write("");
181
- current = "";
165
+ result.textContent = "";
182
166
  return;
183
167
  }
184
168
  try {
185
- const value = evaluate(text, degrees);
186
- if (!Number.isFinite(value)) throw new Error("Not a finite number");
187
- current = fmt(value);
188
- display.write(current);
189
- error.write("");
190
- } catch {
191
- current = "";
192
- display.write("");
193
- error.write("That expression could not be read.");
169
+ const m = await loadEngine();
170
+ if (!math.current) inject(m);
171
+ if (token !== previewToken) return;
172
+ const value = read(text, { ...scope });
173
+ if (token !== previewToken) return;
174
+ result.textContent = format(value);
175
+ result.dataset.preview = "";
176
+ } catch (caught) {
177
+ if (token !== previewToken) return;
178
+ result.textContent = "";
179
+ error.write(shortError(caught));
194
180
  }
195
181
  }
196
182
 
183
+ function shortError(caught) {
184
+ const text = String(caught?.message ?? "Check the expression.");
185
+ return text.length > 120 ? `${text.slice(0, 117)}...` : text;
186
+ }
187
+
197
188
  function commit() {
198
- run();
199
- if (!current) return;
200
- recent.unshift({ expression: input.value.trim(), result: current });
201
- recent.splice(5);
202
- renderHistory();
189
+ const text = input.value.trim();
190
+ if (!text) return;
191
+ try {
192
+ const value = read(text, scope);
193
+ if (typeof value === "number") scope.ans = value;
194
+ const shown = format(value);
195
+ result.textContent = shown;
196
+ delete result.dataset.preview;
197
+ error.write("");
198
+ if (history[0]?.text !== text) {
199
+ history.unshift({ text, value });
200
+ history.splice(20);
201
+ }
202
+ renderHistory();
203
+ renderVariables();
204
+ input.select();
205
+ } catch (caught) {
206
+ error.write(shortError(caught));
207
+ }
208
+ }
209
+
210
+ function renderHistory() {
211
+ historyList.replaceChildren(
212
+ ...history.map((entry) =>
213
+ el(
214
+ "button",
215
+ {
216
+ class: "grid w-full cursor-pointer gap-2 border-0 bg-deck px-3 py-2.5 text-start transition-colors duration-150 hover:bg-deck-2 [grid-template-columns:minmax(0,1fr)_auto]",
217
+ type: "button",
218
+ title: "Use this expression again",
219
+ onclick: () => {
220
+ input.value = entry.text;
221
+ input.focus();
222
+ input.setSelectionRange(entry.text.length, entry.text.length);
223
+ preview();
224
+ },
225
+ },
226
+ el("span", { class: "wrap-anywhere font-mono text-sm text-ink-2" }, entry.text),
227
+ el("span", { class: "font-mono text-sm font-semibold text-ink tabular-nums" }, format(entry.value))
228
+ )
229
+ )
230
+ );
231
+ historyEmpty.hidden = history.length > 0;
232
+ historyList.hidden = history.length === 0;
233
+ }
234
+
235
+ function renderVariables() {
236
+ const names = Object.keys(scope).filter((name) => !INJECTED.has(name) && name !== "ans");
237
+ variablesList.replaceChildren(
238
+ ...names.map((name) =>
239
+ el(
240
+ "button",
241
+ {
242
+ class: "inline-flex cursor-pointer items-baseline gap-1.5 rounded-full border border-orbit bg-deck-2 px-2.5 py-1 font-mono text-sm transition-colors duration-150 hover:border-jade hover:bg-jade-soft",
243
+ type: "button",
244
+ title: `Insert ${name}`,
245
+ onclick: () => insert(name),
246
+ },
247
+ el("span", { class: "font-semibold text-jade" }, name),
248
+ el("span", { class: "text-ink-2" }, format(scope[name]))
249
+ )
250
+ )
251
+ );
252
+ variablesEmpty.hidden = names.length > 0;
253
+ variablesList.hidden = names.length === 0;
203
254
  }
204
255
 
205
256
  function insert(token) {
@@ -213,31 +264,157 @@ export function mount(slot) {
213
264
  const caret = start + token.length;
214
265
  input.focus();
215
266
  input.setSelectionRange(caret, caret);
216
- run();
267
+ preview();
268
+ }
269
+
270
+ function backspace() {
271
+ const start = input.selectionStart ?? input.value.length;
272
+ const end = input.selectionEnd ?? start;
273
+ if (start !== end) {
274
+ input.value = input.value.slice(0, start) + input.value.slice(end);
275
+ } else if (start > 0) {
276
+ input.value = input.value.slice(0, start - 1) + input.value.slice(start);
277
+ input.setSelectionRange(start - 1, start - 1);
278
+ }
279
+ input.focus();
280
+ preview();
217
281
  }
218
282
 
219
- const keypad = el("div", { class: "flex flex-wrap gap-1.5" });
220
- for (const key of KEYS) {
221
- keypad.append(el("button", { class: "min-h-[var(--ctl)] cursor-pointer rounded-r1 border border-orbit bg-deck px-[13px] text-sm font-medium text-ink-2 transition-colors duration-150 hover:border-orbit-hard hover:text-ink data-[on]:border-jade data-[on]:bg-jade-soft data-[on]:text-ink", type: "button", onclick: () => insert(key) }, key));
283
+ function clear() {
284
+ input.value = "";
285
+ result.textContent = "";
286
+ error.write("");
287
+ input.focus();
222
288
  }
223
289
 
224
- input.addEventListener("input", run);
225
- const evaluateButton = button("Evaluate", { icon: "calculator", kind: "fill", onclick: commit });
290
+ const keypad = el("div", { class: "grid grid-cols-4 gap-1.5" });
291
+ for (const [token, label] of KEYS) {
292
+ keypad.append(
293
+ el("button", { class: FUNC_BTN, type: "button", "aria-label": label, onclick: () => insert(token) }, label)
294
+ );
295
+ }
296
+ keypad.append(
297
+ el("button", { class: FUNC_BTN, type: "button", "aria-label": "Backspace", onclick: backspace }, "DEL"),
298
+ el("button", { class: FUNC_BTN, type: "button", onclick: clear }, "C"),
299
+ el("button", {
300
+ class: "col-span-2 min-h-[var(--ctl)] cursor-pointer rounded-r1 border border-jade bg-jade text-sm font-bold text-on-jade transition-colors duration-150 hover:border-jade-deep hover:bg-jade-deep active:scale-[0.97]",
301
+ type: "button",
302
+ onclick: commit,
303
+ }, "=")
304
+ );
305
+
306
+ const extras = el("div", { class: "flex flex-wrap gap-1.5" });
307
+ for (const name of EXTRA) {
308
+ extras.append(
309
+ el("button", {
310
+ class: "inline-flex min-h-8 cursor-pointer items-center rounded-full border border-orbit bg-deck px-2.5 font-mono text-2xs font-semibold text-ink-2 transition-colors duration-150 hover:border-orbit-hard hover:text-ink",
311
+ type: "button",
312
+ onclick: () => insert(`${name}(`),
313
+ }, name)
314
+ );
315
+ }
316
+
317
+ const angle = chipRow(
318
+ [
319
+ { value: "deg", label: "Degrees" },
320
+ { value: "rad", label: "Radians" },
321
+ ],
322
+ {
323
+ value: "deg",
324
+ label: "Angle unit",
325
+ onChange: (value) => {
326
+ state.degrees = value === "deg";
327
+ preview();
328
+ },
329
+ }
330
+ );
331
+
332
+ const fraction = chipRow(
333
+ [
334
+ { value: "decimal", label: "Decimal" },
335
+ { value: "fraction", label: "Fraction" },
336
+ ],
337
+ {
338
+ value: "decimal",
339
+ label: "Result format",
340
+ onChange: (value) => {
341
+ state.fractions = value === "fraction";
342
+ renderHistory();
343
+ preview();
344
+ },
345
+ }
346
+ );
347
+
348
+ const copyResult = copyButton(() => result.textContent, "Copy result");
349
+ const clearHistory = button("Clear history", {
350
+ icon: "trash",
351
+ kind: "quiet",
352
+ onclick: () => {
353
+ history.length = 0;
354
+ renderHistory();
355
+ },
356
+ });
357
+ const commitButton = button("Evaluate", { icon: "calculator", kind: "fill", onclick: commit });
358
+
359
+ input.addEventListener("input", preview);
360
+ input.addEventListener("keydown", (event) => {
361
+ if (event.key === "Enter") {
362
+ event.preventDefault();
363
+ commit();
364
+ } else if (event.key === "Escape") {
365
+ event.preventDefault();
366
+ clear();
367
+ } else if (event.key === "ArrowUp" && history.length) {
368
+ event.preventDefault();
369
+ input.value = history[0].text;
370
+ input.setSelectionRange(input.value.length, input.value.length);
371
+ preview();
372
+ }
373
+ });
226
374
 
227
375
  const card = toolcard({
228
376
  body: bench(
229
377
  stack(
230
- panel("Expression", input),
231
- panel("Angle unit", chips),
232
- panel("Keys", keypad)
378
+ panel("Angle", angle),
379
+ panel("Result", fraction),
380
+ el("div", { class: "grid gap-1.5" }, el("span", { class: "text-sm font-semibold text-ink-2" }, "Keypad"), keypad),
381
+ el("div", { class: "grid gap-1.5" }, el("span", { class: "text-sm font-semibold text-ink-2" }, "More functions"), extras),
382
+ loading,
383
+ error
233
384
  ),
234
- stack(display, error, panel("History", history))
385
+ stack(
386
+ display,
387
+ panel("History", historyList),
388
+ historyEmpty,
389
+ panel("Variables", variablesList),
390
+ variablesEmpty
391
+ )
235
392
  ),
236
- foot: actions(copyButton(() => current, "Copy result"), evaluateButton)});
393
+ foot: actions(clearHistory, copyResult, commitButton),
394
+ });
395
+
396
+ loading.write("Starting the math engine.");
237
397
  slot.append(card.node);
238
- run();
398
+ renderHistory();
399
+ renderVariables();
400
+ input.focus();
401
+
402
+ loadEngine()
403
+ .then((m) => {
404
+ inject(m);
405
+ loading.write("");
406
+ preview();
407
+ })
408
+ .catch((caught) => {
409
+ loading.write("");
410
+ error.write(shortError(caught));
411
+ });
239
412
 
240
- return {};
413
+ return {
414
+ unmount() {
415
+ previewToken++;
416
+ },
417
+ };
241
418
  }
242
419
 
243
420
  export default { mount };
@@ -39,7 +39,7 @@ export function mount(slot) {
39
39
  note.write(long.length ? `${long.length} lines run past ${cap} characters.` : "");
40
40
 
41
41
  if (!long.length) {
42
- longWrap.replaceChildren(blank("film", "No line runs past the limit."));
42
+ longWrap.replaceChildren(blank("video", "No line runs past the limit."));
43
43
  } else {
44
44
  longWrap.replaceChildren(el("div", { class: "grid gap-px overflow-hidden rounded-r1 border border-orbit bg-orbit" },
45
45
  ...long.map((line) => el("div", { class: "grid gap-3 bg-deck px-3 py-[9px] [grid-template-columns:minmax(90px,34%)_minmax(0,1fr)]" },
@@ -0,0 +1,34 @@
1
+ /*!
2
+ * decimal.js v10.5.0
3
+ * An arbitrary-precision Decimal type for JavaScript.
4
+ * https://github.com/MikeMcl/decimal.js
5
+ * Copyright (c) 2025 Michael Mclaughlin <M8ch88l@gmail.com>
6
+ * MIT Licence
7
+ */
8
+
9
+ /**
10
+ * math.js
11
+ * https://github.com/josdejong/mathjs
12
+ *
13
+ * Math.js is an extensive math library for JavaScript and Node.js,
14
+ * It features real and complex numbers, units, matrices, a large set of
15
+ * mathematical functions, and a flexible expression parser.
16
+ *
17
+ * @version 15.2.0
18
+ * @date 2026-04-07
19
+ *
20
+ * @license
21
+ * Copyright (C) 2013-2026 Jos de Jong <wjosdejong@gmail.com>
22
+ *
23
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
24
+ * use this file except in compliance with the License. You may obtain a copy
25
+ * of the License at
26
+ *
27
+ * https://www.apache.org/licenses/LICENSE-2.0
28
+ *
29
+ * Unless required by applicable law or agreed to in writing, software
30
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
31
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
32
+ * License for the specific language governing permissions and limitations under
33
+ * the License.
34
+ */