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.
- package/README.md +2 -1
- package/mevebu/app/mevebu.js +131 -4
- package/mevebu/app/tailwind.css +136 -0
- package/mevebu/libs/js/core.js +1 -1
- package/mevebu/libs/js/icons.js +2 -2
- package/mevebu/libs/js/registry.js +10 -9
- package/mevebu/libs/js/social-icons.js +32 -0
- package/mevebu/libs/js/tools/scientific-calculator.js +366 -189
- package/mevebu/libs/js/tools/subtitle-formatter.js +1 -1
- package/mevebu/libs/js/vendor/mathjs.LICENSE.txt +34 -0
- package/mevebu/libs/js/vendor/mathjs.min.js +3 -0
- package/mevebu/libs/js/vendor/pdf-lib.LICENSE.md +21 -0
- package/mevebu/libs/js/vendor/pdf-lib.min.js +16 -0
- package/mevebu/libs/js/vendor/pdf.min.mjs +21 -0
- package/mevebu/libs/js/vendor/pdf.worker.min.mjs +21 -0
- package/mevebu/libs/js/vendor/pdfjs.LICENSE +177 -0
- package/package.json +7 -2
|
@@ -1,205 +1,256 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
44
|
-
sin
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
|
146
|
-
const
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
error.write("");
|
|
181
|
-
current = "";
|
|
165
|
+
result.textContent = "";
|
|
182
166
|
return;
|
|
183
167
|
}
|
|
184
168
|
try {
|
|
185
|
-
const
|
|
186
|
-
if (!
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
199
|
-
if (!
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
283
|
+
function clear() {
|
|
284
|
+
input.value = "";
|
|
285
|
+
result.textContent = "";
|
|
286
|
+
error.write("");
|
|
287
|
+
input.focus();
|
|
222
288
|
}
|
|
223
289
|
|
|
224
|
-
|
|
225
|
-
|
|
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("
|
|
231
|
-
panel("
|
|
232
|
-
|
|
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(
|
|
385
|
+
stack(
|
|
386
|
+
display,
|
|
387
|
+
panel("History", historyList),
|
|
388
|
+
historyEmpty,
|
|
389
|
+
panel("Variables", variablesList),
|
|
390
|
+
variablesEmpty
|
|
391
|
+
)
|
|
235
392
|
),
|
|
236
|
-
foot: actions(
|
|
393
|
+
foot: actions(clearHistory, copyResult, commitButton),
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
loading.write("Starting the math engine.");
|
|
237
397
|
slot.append(card.node);
|
|
238
|
-
|
|
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("
|
|
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
|
+
*/
|