mevebu 0.3.1 → 0.4.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 +24 -8
- package/mevebu/app/mevebu.js +2 -2
- package/mevebu/libs/css/tools.css +103 -1
- package/mevebu/libs/js/core.js +1 -1
- package/mevebu/libs/js/icons.js +115 -1
- package/mevebu/libs/js/registry.js +3102 -0
- package/mevebu/libs/js/tools/abstract-checklist.js +54 -0
- package/mevebu/libs/js/tools/add-subtract-days.js +105 -0
- package/mevebu/libs/js/tools/age-calculator.js +108 -0
- package/mevebu/libs/js/tools/apa-reference.js +98 -0
- package/mevebu/libs/js/tools/api-request-builder.js +76 -0
- package/mevebu/libs/js/tools/area-converter.js +87 -0
- package/mevebu/libs/js/tools/aspect-ratio.js +81 -0
- package/mevebu/libs/js/tools/average-calculator.js +83 -0
- package/mevebu/libs/js/tools/bibliography-sorter.js +43 -0
- package/mevebu/libs/js/tools/bibtex-generator.js +98 -0
- package/mevebu/libs/js/tools/binary-calculator.js +58 -0
- package/mevebu/libs/js/tools/border-radius.js +88 -0
- package/mevebu/libs/js/tools/box-shadow.js +71 -0
- package/mevebu/libs/js/tools/branch-name.js +44 -0
- package/mevebu/libs/js/tools/break-even.js +68 -0
- package/mevebu/libs/js/tools/business-days.js +120 -0
- package/mevebu/libs/js/tools/canonical-tag.js +63 -0
- package/mevebu/libs/js/tools/caption-counter.js +66 -0
- package/mevebu/libs/js/tools/case-inspector.js +114 -0
- package/mevebu/libs/js/tools/cash-denomination.js +74 -0
- package/mevebu/libs/js/tools/cash-flow.js +124 -0
- package/mevebu/libs/js/tools/cert-expiry.js +83 -0
- package/mevebu/libs/js/tools/changelog-generator.js +48 -0
- package/mevebu/libs/js/tools/character-counter.js +80 -0
- package/mevebu/libs/js/tools/checklist.js +86 -0
- package/mevebu/libs/js/tools/citation-metadata.js +89 -0
- package/mevebu/libs/js/tools/citation-number-check.js +107 -0
- package/mevebu/libs/js/tools/color-palette.js +110 -0
- package/mevebu/libs/js/tools/color-picker.js +93 -0
- package/mevebu/libs/js/tools/commit-message.js +62 -0
- package/mevebu/libs/js/tools/content-calendar.js +111 -0
- package/mevebu/libs/js/tools/contrast-checker.js +93 -0
- package/mevebu/libs/js/tools/countdown-timer.js +174 -0
- package/mevebu/libs/js/tools/cron-builder.js +91 -0
- package/mevebu/libs/js/tools/css-clamp.js +66 -0
- package/mevebu/libs/js/tools/css-formatter.js +84 -0
- package/mevebu/libs/js/tools/css-grid.js +75 -0
- package/mevebu/libs/js/tools/css-minifier.js +98 -0
- package/mevebu/libs/js/tools/csv-to-json.js +114 -0
- package/mevebu/libs/js/tools/csv-viewer.js +132 -0
- package/mevebu/libs/js/tools/currency-format.js +69 -0
- package/mevebu/libs/js/tools/daily-planner.js +124 -0
- package/mevebu/libs/js/tools/data-masker.js +71 -0
- package/mevebu/libs/js/tools/data-storage.js +89 -0
- package/mevebu/libs/js/tools/data-validator.js +111 -0
- package/mevebu/libs/js/tools/date-difference.js +100 -0
- package/mevebu/libs/js/tools/date-format.js +107 -0
- package/mevebu/libs/js/tools/delimiter-converter.js +109 -0
- package/mevebu/libs/js/tools/discount-calculator.js +83 -0
- package/mevebu/libs/js/tools/doi-resolver.js +67 -0
- package/mevebu/libs/js/tools/env-formatter.js +72 -0
- package/mevebu/libs/js/tools/equation-formatter.js +71 -0
- package/mevebu/libs/js/tools/expense-split.js +90 -0
- package/mevebu/libs/js/tools/favicon-markup.js +53 -0
- package/mevebu/libs/js/tools/find-replace.js +88 -0
- package/mevebu/libs/js/tools/flashcard-creator.js +117 -0
- package/mevebu/libs/js/tools/font-pairing.js +88 -0
- package/mevebu/libs/js/tools/fraction-calculator.js +130 -0
- package/mevebu/libs/js/tools/git-command-builder.js +100 -0
- package/mevebu/libs/js/tools/gitignore-generator.js +60 -0
- package/mevebu/libs/js/tools/gpa-calculator.js +100 -0
- package/mevebu/libs/js/tools/grade-calculator.js +101 -0
- package/mevebu/libs/js/tools/gradient-generator.js +99 -0
- package/mevebu/libs/js/tools/habit-tracker.js +120 -0
- package/mevebu/libs/js/tools/harvard-citation.js +90 -0
- package/mevebu/libs/js/tools/hashtag-formatter.js +62 -0
- package/mevebu/libs/js/tools/heading-formatter.js +61 -0
- package/mevebu/libs/js/tools/hmac-generator.js +63 -0
- package/mevebu/libs/js/tools/hpp-calculator.js +70 -0
- package/mevebu/libs/js/tools/html-entity.js +49 -0
- package/mevebu/libs/js/tools/html-formatter.js +72 -0
- package/mevebu/libs/js/tools/html-to-text.js +47 -0
- package/mevebu/libs/js/tools/http-status.js +63 -0
- package/mevebu/libs/js/tools/ieee-reference.js +102 -0
- package/mevebu/libs/js/tools/intext-citation.js +75 -0
- package/mevebu/libs/js/tools/inventory-quantity.js +103 -0
- package/mevebu/libs/js/tools/invoice-number.js +64 -0
- package/mevebu/libs/js/tools/json-escape.js +76 -0
- package/mevebu/libs/js/tools/json-formatter.js +51 -0
- package/mevebu/libs/js/tools/json-minifier.js +58 -0
- package/mevebu/libs/js/tools/json-to-csv.js +72 -0
- package/mevebu/libs/js/tools/json-to-typescript.js +102 -0
- package/mevebu/libs/js/tools/json-validator.js +49 -0
- package/mevebu/libs/js/tools/jwt-decoder.js +74 -0
- package/mevebu/libs/js/tools/jwt-expiry.js +97 -0
- package/mevebu/libs/js/tools/length-converter.js +86 -0
- package/mevebu/libs/js/tools/license-reference.js +58 -0
- package/mevebu/libs/js/tools/line-break-formatter.js +48 -0
- package/mevebu/libs/js/tools/line-counter.js +55 -0
- package/mevebu/libs/js/tools/list-difference.js +86 -0
- package/mevebu/libs/js/tools/list-merger.js +67 -0
- package/mevebu/libs/js/tools/locale-number.js +79 -0
- package/mevebu/libs/js/tools/markdown-preview.js +143 -0
- package/mevebu/libs/js/tools/markdown-table.js +111 -0
- package/mevebu/libs/js/tools/markdown-to-text.js +48 -0
- package/mevebu/libs/js/tools/markup-calculator.js +69 -0
- package/mevebu/libs/js/tools/meeting-agenda.js +137 -0
- package/mevebu/libs/js/tools/meta-preview.js +51 -0
- package/mevebu/libs/js/tools/meta-tags.js +62 -0
- package/mevebu/libs/js/tools/mime-types.js +54 -0
- package/mevebu/libs/js/tools/mla-citation.js +91 -0
- package/mevebu/libs/js/tools/number-base-converter.js +97 -0
- package/mevebu/libs/js/tools/open-graph.js +62 -0
- package/mevebu/libs/js/tools/password-strength.js +118 -0
- package/mevebu/libs/js/tools/percent-to-fraction.js +96 -0
- package/mevebu/libs/js/tools/percentage-calculator.js +86 -0
- package/mevebu/libs/js/tools/percentage-change.js +70 -0
- package/mevebu/libs/js/tools/permutation-combination.js +106 -0
- package/mevebu/libs/js/tools/prefix-suffix.js +46 -0
- package/mevebu/libs/js/tools/priority-matrix.js +100 -0
- package/mevebu/libs/js/tools/profit-margin.js +69 -0
- package/mevebu/libs/js/tools/quadratic-solver.js +93 -0
- package/mevebu/libs/js/tools/query-builder.js +71 -0
- package/mevebu/libs/js/tools/quiz-formatter.js +70 -0
- package/mevebu/libs/js/tools/quotation-formatter.js +77 -0
- package/mevebu/libs/js/tools/ratio-calculator.js +103 -0
- package/mevebu/libs/js/tools/readability.js +112 -0
- package/mevebu/libs/js/tools/reading-time.js +76 -0
- package/mevebu/libs/js/tools/readme-generator.js +50 -0
- package/mevebu/libs/js/tools/reference-completeness.js +62 -0
- package/mevebu/libs/js/tools/regex-tester.js +86 -0
- package/mevebu/libs/js/tools/remove-duplicate-lines.js +46 -0
- package/mevebu/libs/js/tools/remove-extra-spaces.js +41 -0
- package/mevebu/libs/js/tools/robots-txt.js +60 -0
- package/mevebu/libs/js/tools/roman-numeral-converter.js +106 -0
- package/mevebu/libs/js/tools/sales-commission.js +96 -0
- package/mevebu/libs/js/tools/scientific-calculator.js +247 -0
- package/mevebu/libs/js/tools/scientific-notation.js +83 -0
- package/mevebu/libs/js/tools/script-planner.js +72 -0
- package/mevebu/libs/js/tools/secure-token.js +60 -0
- package/mevebu/libs/js/tools/selling-price.js +74 -0
- package/mevebu/libs/js/tools/semver-helper.js +54 -0
- package/mevebu/libs/js/tools/sentence-length.js +80 -0
- package/mevebu/libs/js/tools/sequence-analyzer.js +90 -0
- package/mevebu/libs/js/tools/sitemap-xml.js +82 -0
- package/mevebu/libs/js/tools/slug-converter.js +48 -0
- package/mevebu/libs/js/tools/slug-preview.js +83 -0
- package/mevebu/libs/js/tools/sort-lines.js +54 -0
- package/mevebu/libs/js/tools/speed-converter.js +83 -0
- package/mevebu/libs/js/tools/sql-formatter.js +78 -0
- package/mevebu/libs/js/tools/sql-minifier.js +102 -0
- package/mevebu/libs/js/tools/standard-deviation.js +96 -0
- package/mevebu/libs/js/tools/stopwatch.js +133 -0
- package/mevebu/libs/js/tools/study-schedule.js +88 -0
- package/mevebu/libs/js/tools/subtitle-formatter.js +74 -0
- package/mevebu/libs/js/tools/table-to-markdown.js +137 -0
- package/mevebu/libs/js/tools/tax-calculator.js +84 -0
- package/mevebu/libs/js/tools/temperature-converter.js +94 -0
- package/mevebu/libs/js/tools/text-diff.js +104 -0
- package/mevebu/libs/js/tools/text-direction.js +55 -0
- package/mevebu/libs/js/tools/text-reverser.js +51 -0
- package/mevebu/libs/js/tools/time-duration.js +83 -0
- package/mevebu/libs/js/tools/time-format-converter.js +86 -0
- package/mevebu/libs/js/tools/timestamp-converter.js +85 -0
- package/mevebu/libs/js/tools/timezone-converter.js +163 -0
- package/mevebu/libs/js/tools/title-length.js +67 -0
- package/mevebu/libs/js/tools/unicode-escape.js +80 -0
- package/mevebu/libs/js/tools/unicode-inspector.js +58 -0
- package/mevebu/libs/js/tools/url-inspector.js +98 -0
- package/mevebu/libs/js/tools/url-parser.js +83 -0
- package/mevebu/libs/js/tools/utf8-bytes.js +67 -0
- package/mevebu/libs/js/tools/version-compare.js +70 -0
- package/mevebu/libs/js/tools/video-duration.js +72 -0
- package/mevebu/libs/js/tools/volume-converter.js +88 -0
- package/mevebu/libs/js/tools/weekly-planner.js +101 -0
- package/mevebu/libs/js/tools/weight-converter.js +85 -0
- package/mevebu/libs/js/tools/weighted-average.js +86 -0
- package/mevebu/libs/js/tools/word-counter.js +90 -0
- package/mevebu/libs/js/tools/word-limit.js +60 -0
- package/mevebu/libs/js/tools/xml-formatter.js +85 -0
- package/mevebu/libs/js/tools/yaml-formatter.js +71 -0
- package/package.json +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Converts a number to a Roman numeral, or a Roman numeral to a number.
|
|
2
|
+
import { el, bench, stack, panel, textInput, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const NUMERALS = [
|
|
5
|
+
[1000, "M"], [900, "CM"], [500, "D"], [400, "CD"], [100, "C"], [90, "XC"],
|
|
6
|
+
[50, "L"], [40, "XL"], [10, "X"], [9, "IX"], [5, "V"], [4, "IV"], [1, "I"],
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
function toRoman(n) {
|
|
10
|
+
let rest = n;
|
|
11
|
+
let out = "";
|
|
12
|
+
for (const [value, symbol] of NUMERALS) {
|
|
13
|
+
while (rest >= value) {
|
|
14
|
+
out += symbol;
|
|
15
|
+
rest -= value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function fromRoman(text) {
|
|
22
|
+
const values = { i: 1, v: 5, x: 10, l: 50, c: 100, d: 500, m: 1000 };
|
|
23
|
+
let total = 0;
|
|
24
|
+
const upper = text.toUpperCase();
|
|
25
|
+
for (let i = 0; i < upper.length; i++) {
|
|
26
|
+
const current = values[upper[i].toLowerCase()];
|
|
27
|
+
const next = values[upper[i + 1]?.toLowerCase()];
|
|
28
|
+
if (next && current < next) total -= current;
|
|
29
|
+
else total += current;
|
|
30
|
+
}
|
|
31
|
+
return total;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const kvRow = (key, value) =>
|
|
35
|
+
el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
|
|
36
|
+
|
|
37
|
+
export function mount(slot) {
|
|
38
|
+
const source = textInput({ value: "2026", "aria-label": "Number or Roman numeral", placeholder: "2026 or MMXXVI" });
|
|
39
|
+
const detected = el("span", { class: "mvb-kv__v" }, "-");
|
|
40
|
+
const result = el("span", { class: "mvb-kv__v" }, "-");
|
|
41
|
+
const kv = el(
|
|
42
|
+
"div",
|
|
43
|
+
{ class: "mvb-kv" },
|
|
44
|
+
kvRow("Input", detected),
|
|
45
|
+
kvRow("Result", result)
|
|
46
|
+
);
|
|
47
|
+
const error = notice("error");
|
|
48
|
+
let text = "";
|
|
49
|
+
|
|
50
|
+
function clear() {
|
|
51
|
+
detected.textContent = "-";
|
|
52
|
+
result.textContent = "-";
|
|
53
|
+
text = "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function run() {
|
|
57
|
+
error.write("");
|
|
58
|
+
const value = source.value.trim();
|
|
59
|
+
if (!value) {
|
|
60
|
+
clear();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (/^[0-9]+$/.test(value)) {
|
|
64
|
+
const number = Number(value);
|
|
65
|
+
if (number < 1 || number > 3999) {
|
|
66
|
+
clear();
|
|
67
|
+
error.write("Roman numerals cover 1 to 3999.");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
detected.textContent = "Number";
|
|
71
|
+
result.textContent = toRoman(number);
|
|
72
|
+
text = toRoman(number);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (/^[ivxlcdm]+$/i.test(value)) {
|
|
76
|
+
const number = fromRoman(value);
|
|
77
|
+
if (number < 1 || number > 3999 || toRoman(number) !== value.toUpperCase()) {
|
|
78
|
+
clear();
|
|
79
|
+
error.write("That is not a standard Roman numeral.");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
detected.textContent = "Roman numeral";
|
|
83
|
+
result.textContent = String(number);
|
|
84
|
+
text = String(number);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
clear();
|
|
88
|
+
error.write("Enter a number from 1 to 3999, or a Roman numeral.");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
source.addEventListener("input", run);
|
|
92
|
+
|
|
93
|
+
const card = toolcard({
|
|
94
|
+
body: bench(
|
|
95
|
+
stack(panel("Number or Roman numeral", source), error),
|
|
96
|
+
stack(panel("Result", kv))
|
|
97
|
+
),
|
|
98
|
+
foot: actions(copyButton(() => text, "Copy result", "fill")),
|
|
99
|
+
});
|
|
100
|
+
slot.append(card.node);
|
|
101
|
+
run();
|
|
102
|
+
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default { mount };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Commission per sale and the total, with an optional higher tier.
|
|
2
|
+
import { el, bench, stack, row, panel, textArea, numberInput, notice, copyButton, downloadButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function fmt(n) {
|
|
5
|
+
return n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function parseNumbers(text) {
|
|
9
|
+
const tokens = text.split(/[\s,;]+/).filter(Boolean);
|
|
10
|
+
const nums = tokens.map(Number);
|
|
11
|
+
return { nums, invalid: nums.some((n) => !Number.isFinite(n)) };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function mount(slot) {
|
|
15
|
+
const sales = textArea({ rows: 8, value: "1000, 2500, 4000", "aria-label": "Sale amounts", placeholder: "One amount per line" });
|
|
16
|
+
const rate = numberInput({ value: "5", min: "0", "aria-label": "Rate" });
|
|
17
|
+
const threshold = numberInput({ placeholder: "Threshold", min: "0", "aria-label": "Tier threshold" });
|
|
18
|
+
const higher = numberInput({ placeholder: "Higher rate", min: "0", "aria-label": "Higher rate" });
|
|
19
|
+
const result = el("div", { class: "mvb-scroll" });
|
|
20
|
+
const stats = el("div", { class: "mvb-stats" });
|
|
21
|
+
const error = notice("error");
|
|
22
|
+
let text = "";
|
|
23
|
+
|
|
24
|
+
const tile = (key, value) => el("div", { class: "mvb-stat" },
|
|
25
|
+
el("span", { class: "mvb-stat__v" }, String(value)),
|
|
26
|
+
el("span", { class: "mvb-stat__k" }, key));
|
|
27
|
+
|
|
28
|
+
const tiered = () => threshold.value.trim() !== "" && higher.value.trim() !== "";
|
|
29
|
+
|
|
30
|
+
function commissionFor(amount) {
|
|
31
|
+
const base = Number(rate.value) || 0;
|
|
32
|
+
if (tiered()) {
|
|
33
|
+
const t = Number(threshold.value) || 0;
|
|
34
|
+
const h = Number(higher.value) || 0;
|
|
35
|
+
if (amount > t) return (t * base + (amount - t) * h) / 100;
|
|
36
|
+
}
|
|
37
|
+
return (amount * base) / 100;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function run() {
|
|
41
|
+
error.write("");
|
|
42
|
+
text = "";
|
|
43
|
+
const { nums, invalid } = parseNumbers(sales.value);
|
|
44
|
+
const r = Number(rate.value);
|
|
45
|
+
if (!sales.value.trim()) {
|
|
46
|
+
result.replaceChildren(el("p", { class: "mvb-note" }, "Add one sale amount per line."));
|
|
47
|
+
stats.replaceChildren();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (invalid || !Number.isFinite(r) || r < 0) {
|
|
51
|
+
error.write("Use numbers only for the sales and a rate of zero or more.");
|
|
52
|
+
result.replaceChildren();
|
|
53
|
+
stats.replaceChildren();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
result.replaceChildren(el("table", { class: "mvb-ttable" },
|
|
57
|
+
el("thead", {}, el("tr", {}, el("th", {}, "Sale"), el("th", {}, "Commission"))),
|
|
58
|
+
el("tbody", {}, ...nums.map((amount) => el("tr", {},
|
|
59
|
+
el("th", {}, fmt(amount)),
|
|
60
|
+
el("td", {}, fmt(commissionFor(amount))))))));
|
|
61
|
+
const totalSales = nums.reduce((a, b) => a + b, 0);
|
|
62
|
+
const totalCommission = nums.reduce((sum, amount) => sum + commissionFor(amount), 0);
|
|
63
|
+
stats.replaceChildren(tile("Sales", String(nums.length)), tile("Total sales", fmt(totalSales)), tile("Commission", fmt(totalCommission)));
|
|
64
|
+
text = `${nums.map((amount) => `${fmt(amount)}: ${fmt(commissionFor(amount))}`).join("\n")}\nTotal ${fmt(totalCommission)}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const input of [sales, rate, threshold, higher]) input.addEventListener("input", run);
|
|
68
|
+
|
|
69
|
+
function csv() {
|
|
70
|
+
const { nums } = parseNumbers(sales.value);
|
|
71
|
+
const rows = [["Sale", "Commission"], ...nums.map((amount) => [String(amount), commissionFor(amount).toFixed(2)])];
|
|
72
|
+
return rows.map((cells) => cells.map((cell) => `"${cell}"`).join(",")).join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const card = toolcard({
|
|
76
|
+
body: bench(
|
|
77
|
+
stack(
|
|
78
|
+
panel("Sale amounts", sales),
|
|
79
|
+
panel("Rate %", rate),
|
|
80
|
+
row(panel("Tier threshold", threshold), panel("Higher rate %", higher)),
|
|
81
|
+
error
|
|
82
|
+
),
|
|
83
|
+
stack(stats, panel("Per sale", result))
|
|
84
|
+
),
|
|
85
|
+
foot: actions(
|
|
86
|
+
downloadButton(() => csv(), "commission.csv", "Save CSV", "text/csv"),
|
|
87
|
+
copyButton(() => text, "Copy commission", "fill")
|
|
88
|
+
),
|
|
89
|
+
});
|
|
90
|
+
slot.append(card.node);
|
|
91
|
+
run();
|
|
92
|
+
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default { mount };
|
|
@@ -0,0 +1,247 @@
|
|
|
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;
|
|
41
|
+
}
|
|
42
|
+
|
|
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
|
+
|
|
57
|
+
function evaluate(src, degrees) {
|
|
58
|
+
const tokens = tokenize(src);
|
|
59
|
+
let pos = 0;
|
|
60
|
+
const peek = () => tokens[pos];
|
|
61
|
+
const eat = () => tokens[pos++];
|
|
62
|
+
const toRad = (x) => (degrees ? (x * Math.PI) / 180 : x);
|
|
63
|
+
const fromRad = (x) => (degrees ? (x * 180) / Math.PI : x);
|
|
64
|
+
|
|
65
|
+
function expr() {
|
|
66
|
+
let value = term();
|
|
67
|
+
while (peek() && peek().type === "op" && (peek().value === "+" || peek().value === "-")) {
|
|
68
|
+
const op = eat().value;
|
|
69
|
+
const rhs = term();
|
|
70
|
+
value = op === "+" ? value + rhs : value - rhs;
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function term() {
|
|
76
|
+
let value = unary();
|
|
77
|
+
for (;;) {
|
|
78
|
+
const token = peek();
|
|
79
|
+
if (token && token.type === "op" && (token.value === "*" || token.value === "/")) {
|
|
80
|
+
eat();
|
|
81
|
+
const rhs = unary();
|
|
82
|
+
value = token.value === "*" ? value * rhs : value / rhs;
|
|
83
|
+
} else if (token && (token.type === "num" || token.type === "name" || (token.type === "op" && token.value === "("))) {
|
|
84
|
+
value *= unary();
|
|
85
|
+
} else break;
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function unary() {
|
|
91
|
+
const token = peek();
|
|
92
|
+
if (token && token.type === "op" && (token.value === "-" || token.value === "+")) {
|
|
93
|
+
eat();
|
|
94
|
+
const value = unary();
|
|
95
|
+
return token.value === "-" ? -value : value;
|
|
96
|
+
}
|
|
97
|
+
return power();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function power() {
|
|
101
|
+
const base = primary();
|
|
102
|
+
if (peek() && peek().type === "op" && peek().value === "^") {
|
|
103
|
+
eat();
|
|
104
|
+
return base ** unary();
|
|
105
|
+
}
|
|
106
|
+
return base;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function primary() {
|
|
110
|
+
const token = eat();
|
|
111
|
+
if (!token) throw new Error("Unexpected end of expression");
|
|
112
|
+
if (token.type === "num") return token.value;
|
|
113
|
+
if (token.type === "name") {
|
|
114
|
+
if (token.value === "pi") return Math.PI;
|
|
115
|
+
if (token.value === "e") return Math.E;
|
|
116
|
+
if (!FUNCTIONS[token.value]) throw new Error("Unknown name");
|
|
117
|
+
const open = eat();
|
|
118
|
+
if (!open || open.value !== "(") throw new Error("Expected an opening bracket");
|
|
119
|
+
const arg = expr();
|
|
120
|
+
const close = eat();
|
|
121
|
+
if (!close || close.value !== ")") throw new Error("Expected a closing bracket");
|
|
122
|
+
const raw = FUNCTIONS[token.value](token.value === "sin" || token.value === "cos" || token.value === "tan" ? toRad(arg) : arg);
|
|
123
|
+
return token.value === "asin" || token.value === "acos" || token.value === "atan" ? fromRad(raw) : raw;
|
|
124
|
+
}
|
|
125
|
+
if (token.type === "op" && token.value === "(") {
|
|
126
|
+
const value = expr();
|
|
127
|
+
const close = eat();
|
|
128
|
+
if (!close || close.value !== ")") throw new Error("Expected a closing bracket");
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
throw new Error("Unexpected token");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const result = expr();
|
|
135
|
+
if (pos < tokens.length) throw new Error("Unexpected token");
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function fmt(n) {
|
|
140
|
+
return n.toLocaleString(undefined, { maximumFractionDigits: 10 });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const KEYS = ["sin(", "cos(", "tan(", "log(", "ln(", "sqrt(", "pi", "e", "^", "(", ")", "/", "7", "8", "9", "*", "4", "5", "6", "-", "1", "2", "3", "+", "0", ".", "="];
|
|
144
|
+
|
|
145
|
+
export function mount(slot) {
|
|
146
|
+
const input = textInput({ value: "sin(30) + 2^3", "aria-label": "Expression", placeholder: "sin(30) + 2^3" });
|
|
147
|
+
const display = output({ rows: 2 });
|
|
148
|
+
const error = notice("error");
|
|
149
|
+
const history = el("div", { class: "mvb-kv" });
|
|
150
|
+
const chips = chipRow(
|
|
151
|
+
[
|
|
152
|
+
{ value: "deg", label: "Degrees" },
|
|
153
|
+
{ value: "rad", label: "Radians" },
|
|
154
|
+
],
|
|
155
|
+
{
|
|
156
|
+
value: "deg",
|
|
157
|
+
label: "Angle unit",
|
|
158
|
+
onChange: (value) => {
|
|
159
|
+
degrees = value === "deg";
|
|
160
|
+
run();
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
const recent = [];
|
|
165
|
+
let degrees = true;
|
|
166
|
+
let current = "";
|
|
167
|
+
|
|
168
|
+
function renderHistory() {
|
|
169
|
+
history.replaceChildren();
|
|
170
|
+
for (const entry of recent) {
|
|
171
|
+
history.append(
|
|
172
|
+
el("div", { class: "mvb-kv__row" },
|
|
173
|
+
el("span", { class: "mvb-kv__k" }, entry.expression),
|
|
174
|
+
el("span", { class: "mvb-kv__v" }, entry.result))
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function run() {
|
|
180
|
+
const text = input.value.trim();
|
|
181
|
+
if (!text) {
|
|
182
|
+
display.write("");
|
|
183
|
+
error.write("");
|
|
184
|
+
current = "";
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const value = evaluate(text, degrees);
|
|
189
|
+
if (!Number.isFinite(value)) throw new Error("Not a finite number");
|
|
190
|
+
current = fmt(value);
|
|
191
|
+
display.write(current);
|
|
192
|
+
error.write("");
|
|
193
|
+
} catch {
|
|
194
|
+
current = "";
|
|
195
|
+
display.write("");
|
|
196
|
+
error.write("That expression could not be read.");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function commit() {
|
|
201
|
+
run();
|
|
202
|
+
if (!current) return;
|
|
203
|
+
recent.unshift({ expression: input.value.trim(), result: current });
|
|
204
|
+
recent.splice(5);
|
|
205
|
+
renderHistory();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function insert(token) {
|
|
209
|
+
if (token === "=") {
|
|
210
|
+
commit();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const start = input.selectionStart ?? input.value.length;
|
|
214
|
+
const end = input.selectionEnd ?? start;
|
|
215
|
+
input.value = input.value.slice(0, start) + token + input.value.slice(end);
|
|
216
|
+
const caret = start + token.length;
|
|
217
|
+
input.focus();
|
|
218
|
+
input.setSelectionRange(caret, caret);
|
|
219
|
+
run();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const keypad = el("div", { class: "mvb-chips" });
|
|
223
|
+
for (const key of KEYS) {
|
|
224
|
+
keypad.append(el("button", { class: "mvb-chip", type: "button", onclick: () => insert(key) }, key));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
input.addEventListener("input", run);
|
|
228
|
+
const evaluateButton = button("Evaluate", { icon: "calculator", kind: "fill", onclick: commit });
|
|
229
|
+
|
|
230
|
+
const card = toolcard({
|
|
231
|
+
body: bench(
|
|
232
|
+
stack(
|
|
233
|
+
panel("Expression", input),
|
|
234
|
+
panel("Angle unit", chips),
|
|
235
|
+
panel("Keys", keypad)
|
|
236
|
+
),
|
|
237
|
+
stack(display, error, panel("History", history))
|
|
238
|
+
),
|
|
239
|
+
foot: actions(copyButton(() => current, "Copy result"), evaluateButton),
|
|
240
|
+
});
|
|
241
|
+
slot.append(card.node);
|
|
242
|
+
run();
|
|
243
|
+
|
|
244
|
+
return {};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export default { mount };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Converts a number to and from scientific notation.
|
|
2
|
+
import { el, bench, stack, panel, textInput, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function parseNumber(text) {
|
|
5
|
+
const clean = text.trim().toLowerCase().replace(/×/g, "x").replace(/\s+/g, "");
|
|
6
|
+
if (!clean) return null;
|
|
7
|
+
const match = clean.match(/^([+-]?\d*\.?\d+)x10\^?([+-]?\d+)$/);
|
|
8
|
+
if (match) return Number(match[1]) * 10 ** Number(match[2]);
|
|
9
|
+
const value = Number(clean);
|
|
10
|
+
return Number.isFinite(value) ? value : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function decimalForm(n) {
|
|
14
|
+
if (n === 0) return "0";
|
|
15
|
+
if (Math.abs(n) >= 1e21 || Math.abs(n) < 1e-6) return n.toExponential();
|
|
16
|
+
return n.toLocaleString(undefined, { maximumFractionDigits: 20 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const kvRow = (key, value) =>
|
|
20
|
+
el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
|
|
21
|
+
|
|
22
|
+
export function mount(slot) {
|
|
23
|
+
const source = textInput({ value: "3.2e8", "aria-label": "Number", placeholder: "3.2e8 or 320000000" });
|
|
24
|
+
const scientific = el("span", { class: "mvb-kv__v" }, "-");
|
|
25
|
+
const eForm = el("span", { class: "mvb-kv__v" }, "-");
|
|
26
|
+
const decimal = el("span", { class: "mvb-kv__v" }, "-");
|
|
27
|
+
const coefficient = el("span", { class: "mvb-kv__v" }, "-");
|
|
28
|
+
const exponent = el("span", { class: "mvb-kv__v" }, "-");
|
|
29
|
+
const kv = el(
|
|
30
|
+
"div",
|
|
31
|
+
{ class: "mvb-kv" },
|
|
32
|
+
kvRow("Scientific", scientific),
|
|
33
|
+
kvRow("E notation", eForm),
|
|
34
|
+
kvRow("Decimal", decimal),
|
|
35
|
+
kvRow("Coefficient", coefficient),
|
|
36
|
+
kvRow("Exponent", exponent)
|
|
37
|
+
);
|
|
38
|
+
const error = notice("error");
|
|
39
|
+
let text = "";
|
|
40
|
+
|
|
41
|
+
function clear() {
|
|
42
|
+
for (const node of [scientific, eForm, decimal, coefficient, exponent]) node.textContent = "-";
|
|
43
|
+
text = "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function run() {
|
|
47
|
+
error.write("");
|
|
48
|
+
if (!source.value.trim()) {
|
|
49
|
+
clear();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const value = parseNumber(source.value);
|
|
53
|
+
if (value === null) {
|
|
54
|
+
clear();
|
|
55
|
+
error.write("That is not a number this browser can read.");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const [mantissa, rawExponent] = value.toExponential().split("e");
|
|
59
|
+
const power = Number(rawExponent);
|
|
60
|
+
scientific.textContent = `${mantissa} x 10^${power}`;
|
|
61
|
+
eForm.textContent = value.toExponential();
|
|
62
|
+
decimal.textContent = decimalForm(value);
|
|
63
|
+
coefficient.textContent = mantissa;
|
|
64
|
+
exponent.textContent = String(power);
|
|
65
|
+
text = `${mantissa} x 10^${power}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
source.addEventListener("input", run);
|
|
69
|
+
|
|
70
|
+
const card = toolcard({
|
|
71
|
+
body: bench(
|
|
72
|
+
stack(panel("Number", source), error),
|
|
73
|
+
stack(panel("Forms", kv))
|
|
74
|
+
),
|
|
75
|
+
foot: actions(copyButton(() => text, "Copy scientific", "fill")),
|
|
76
|
+
});
|
|
77
|
+
slot.append(card.node);
|
|
78
|
+
run();
|
|
79
|
+
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default { mount };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// A video script laid out in sections, with word and time estimates.
|
|
2
|
+
import { el, bench, stack, panel, textArea, numberInput, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const SECTIONS = [
|
|
5
|
+
["hook", "Hook"],
|
|
6
|
+
["intro", "Intro"],
|
|
7
|
+
["body", "Body"],
|
|
8
|
+
["outro", "Outro"],
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const clock = (seconds) => {
|
|
12
|
+
const total = Math.max(0, Math.round(seconds));
|
|
13
|
+
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const countWords = (text) => (text.trim() ? text.trim().split(/\s+/).length : 0);
|
|
17
|
+
|
|
18
|
+
const kvRow = (key, value) => el("div", { class: "mvb-kv__row" },
|
|
19
|
+
el("span", { class: "mvb-kv__k" }, key),
|
|
20
|
+
el("span", { class: "mvb-kv__v" }, value));
|
|
21
|
+
|
|
22
|
+
export function mount(slot) {
|
|
23
|
+
const inputs = {};
|
|
24
|
+
const fields = SECTIONS.map(([id, label]) => {
|
|
25
|
+
const area = textArea({ rows: 4, "aria-label": label, placeholder: `${label} text` });
|
|
26
|
+
inputs[id] = area;
|
|
27
|
+
return panel(label, area);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const rate = numberInput({ min: "60", max: "400", value: "150", "aria-label": "Speaking rate in words per minute" });
|
|
31
|
+
|
|
32
|
+
const rows = {};
|
|
33
|
+
const list = el("div", { class: "mvb-kv" });
|
|
34
|
+
for (const [id, label] of SECTIONS) {
|
|
35
|
+
const value = el("span", { class: "mvb-kv__v" }, "0 words");
|
|
36
|
+
rows[id] = value;
|
|
37
|
+
list.append(kvRow(label, value));
|
|
38
|
+
}
|
|
39
|
+
const total = el("span", { class: "mvb-kv__v" }, "0 words");
|
|
40
|
+
list.append(kvRow("Total", total));
|
|
41
|
+
|
|
42
|
+
let fullScript = "";
|
|
43
|
+
|
|
44
|
+
function run() {
|
|
45
|
+
const wpm = Math.max(1, parseInt(rate.value, 10) || 150);
|
|
46
|
+
let totalWords = 0;
|
|
47
|
+
for (const [id] of SECTIONS) {
|
|
48
|
+
const words = countWords(inputs[id].value);
|
|
49
|
+
totalWords += words;
|
|
50
|
+
rows[id].textContent = `${words} words - ${clock((words / wpm) * 60)}`;
|
|
51
|
+
}
|
|
52
|
+
total.textContent = `${totalWords} words - ${clock((totalWords / wpm) * 60)}`;
|
|
53
|
+
fullScript = SECTIONS.map(([id, label]) => `${label}\n${inputs[id].value.trim()}`).join("\n\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const area of Object.values(inputs)) area.addEventListener("input", run);
|
|
57
|
+
rate.addEventListener("input", run);
|
|
58
|
+
|
|
59
|
+
const card = toolcard({
|
|
60
|
+
body: bench(
|
|
61
|
+
stack(...fields),
|
|
62
|
+
stack(panel("Speaking rate", rate, "Words per minute"), list)
|
|
63
|
+
),
|
|
64
|
+
foot: actions(copyButton(() => fullScript, "Copy script", "fill")),
|
|
65
|
+
});
|
|
66
|
+
slot.append(card.node);
|
|
67
|
+
run();
|
|
68
|
+
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default { mount };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Random tokens from the browser random source, in hex or URL-safe Base64.
|
|
2
|
+
import { el, numberInput, chipRow, output, notice, button, copyButton, bench, stack, panel, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function toHex(bytes) {
|
|
5
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function toBase64Url(bytes) {
|
|
9
|
+
let text = "";
|
|
10
|
+
for (const byte of bytes) text += String.fromCharCode(byte);
|
|
11
|
+
return btoa(text).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function mount(slot) {
|
|
15
|
+
const bytesField = numberInput({ min: "1", max: "256", value: "32", "aria-label": "Byte length" });
|
|
16
|
+
const result = output({ rows: 3 });
|
|
17
|
+
const error = notice("error");
|
|
18
|
+
let format = "hex";
|
|
19
|
+
|
|
20
|
+
const formats = chipRow(
|
|
21
|
+
[{ value: "hex", label: "Hex" }, { value: "base64", label: "Base64 URL-safe" }],
|
|
22
|
+
{ value: format, label: "Format", onChange: (value) => { format = value; draw(); } }
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
function draw() {
|
|
26
|
+
error.write("");
|
|
27
|
+
const size = parseInt(bytesField.value, 10);
|
|
28
|
+
if (!Number.isFinite(size) || size < 1 || size > 256) {
|
|
29
|
+
result.write("");
|
|
30
|
+
error.write("Enter a byte length between 1 and 256.");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const bytes = new Uint8Array(size);
|
|
34
|
+
crypto.getRandomValues(bytes);
|
|
35
|
+
result.write(format === "hex" ? toHex(bytes) : toBase64Url(bytes));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
bytesField.addEventListener("input", draw);
|
|
39
|
+
const generateButton = button("Generate", { icon: "refresh-cw", kind: "fill" });
|
|
40
|
+
generateButton.addEventListener("click", draw);
|
|
41
|
+
|
|
42
|
+
const card = toolcard({
|
|
43
|
+
body: bench(
|
|
44
|
+
stack(
|
|
45
|
+
panel("Bytes", bytesField),
|
|
46
|
+
panel("Format", formats),
|
|
47
|
+
el("p", { class: "mvb-note" }, "The token is drawn from the browser random source in this tab. Store real tokens safely."),
|
|
48
|
+
error
|
|
49
|
+
),
|
|
50
|
+
stack(panel("Token", result))
|
|
51
|
+
),
|
|
52
|
+
foot: actions(copyButton(() => result.textContent, "Copy token"), generateButton),
|
|
53
|
+
});
|
|
54
|
+
slot.append(card.node);
|
|
55
|
+
draw();
|
|
56
|
+
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default { mount };
|