mevebu 0.3.0 → 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 +3 -3
- 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,74 @@
|
|
|
1
|
+
// Sets a selling price from a cost and a target margin or markup.
|
|
2
|
+
import { el, bench, stack, panel, numberInput, chipRow, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function fmt(n) {
|
|
5
|
+
return n.toLocaleString(undefined, { maximumFractionDigits: 2 });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function tile(label) {
|
|
9
|
+
const value = el("span", { class: "mvb-stat__v" }, "-");
|
|
10
|
+
const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
|
|
11
|
+
node.set = (text) => { value.textContent = text; };
|
|
12
|
+
return node;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function mount(slot) {
|
|
16
|
+
const cost = numberInput({ value: "40", min: "0", "aria-label": "Cost per unit" });
|
|
17
|
+
const percent = numberInput({ value: "40", "aria-label": "Target percent" });
|
|
18
|
+
let mode = "margin";
|
|
19
|
+
const chips = chipRow(
|
|
20
|
+
[{ value: "margin", label: "Margin" }, { value: "markup", label: "Markup" }],
|
|
21
|
+
{ value: "margin", label: "Target", onChange: (next) => { mode = next; run(); } }
|
|
22
|
+
);
|
|
23
|
+
const price = tile("Selling price");
|
|
24
|
+
const profit = tile("Profit");
|
|
25
|
+
const stats = el("div", { class: "mvb-stats" }, price, profit);
|
|
26
|
+
const error = notice("error");
|
|
27
|
+
let text = "";
|
|
28
|
+
|
|
29
|
+
function clear() {
|
|
30
|
+
price.set("-");
|
|
31
|
+
profit.set("-");
|
|
32
|
+
text = "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function run() {
|
|
36
|
+
error.write("");
|
|
37
|
+
const c = Number(cost.value);
|
|
38
|
+
const p = Number(percent.value);
|
|
39
|
+
if (cost.value.trim() === "" || percent.value.trim() === "" || !Number.isFinite(c) || !Number.isFinite(p)) {
|
|
40
|
+
clear();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (c < 0) { clear(); error.write("The cost cannot be negative."); return; }
|
|
44
|
+
let selling;
|
|
45
|
+
if (mode === "markup") {
|
|
46
|
+
if (p < -100) { clear(); error.write("The markup cannot drop below -100%."); return; }
|
|
47
|
+
selling = c * (1 + p / 100);
|
|
48
|
+
} else {
|
|
49
|
+
if (p >= 100) { clear(); error.write("A margin must be below 100%."); return; }
|
|
50
|
+
selling = c / (1 - p / 100);
|
|
51
|
+
}
|
|
52
|
+
const gain = selling - c;
|
|
53
|
+
price.set(fmt(selling));
|
|
54
|
+
profit.set(fmt(gain));
|
|
55
|
+
text = `Selling price ${fmt(selling)}, profit ${fmt(gain)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
cost.addEventListener("input", run);
|
|
59
|
+
percent.addEventListener("input", run);
|
|
60
|
+
|
|
61
|
+
const card = toolcard({
|
|
62
|
+
body: bench(
|
|
63
|
+
stack(panel("Cost per unit", cost), panel("Target", chips), panel("Percent", percent), error),
|
|
64
|
+
stack(stats)
|
|
65
|
+
),
|
|
66
|
+
foot: actions(copyButton(() => text, "Copy result", "fill")),
|
|
67
|
+
});
|
|
68
|
+
slot.append(card.node);
|
|
69
|
+
run();
|
|
70
|
+
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default { mount };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Bumps a semantic version by major, minor or patch, with an optional prerelease tag.
|
|
2
|
+
import { bench, stack, panel, textInput, chipRow, output, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const BUMPS = [
|
|
5
|
+
{ value: "major", label: "Major" },
|
|
6
|
+
{ value: "minor", label: "Minor" },
|
|
7
|
+
{ value: "patch", label: "Patch" },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export function mount(slot) {
|
|
11
|
+
const version = textInput({ value: "1.2.3", class: "mvb-in mvb-in--mono", "aria-label": "Current version" });
|
|
12
|
+
const prerelease = textInput({ value: "", placeholder: "rc.1", "aria-label": "Prerelease tag" });
|
|
13
|
+
const error = notice("error");
|
|
14
|
+
const out = output({ rows: 1 });
|
|
15
|
+
let bump = "patch";
|
|
16
|
+
let text = "";
|
|
17
|
+
|
|
18
|
+
const chips = chipRow(BUMPS, { value: bump, label: "Bump", onChange: (value) => { bump = value; run(); } });
|
|
19
|
+
|
|
20
|
+
function run() {
|
|
21
|
+
error.write("");
|
|
22
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(version.value.trim());
|
|
23
|
+
if (!match) {
|
|
24
|
+
text = "";
|
|
25
|
+
out.write("");
|
|
26
|
+
error.write("Enter a version like 1.2.3.");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
let [major, minor, patch] = match.slice(1).map(Number);
|
|
30
|
+
if (bump === "major") { major += 1; minor = 0; patch = 0; }
|
|
31
|
+
else if (bump === "minor") { minor += 1; patch = 0; }
|
|
32
|
+
else patch += 1;
|
|
33
|
+
const tag = prerelease.value.trim().replace(/^-/, "");
|
|
34
|
+
text = `${major}.${minor}.${patch}${tag ? `-${tag}` : ""}`;
|
|
35
|
+
out.write(text);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
version.addEventListener("input", run);
|
|
39
|
+
prerelease.addEventListener("input", run);
|
|
40
|
+
|
|
41
|
+
const card = toolcard({
|
|
42
|
+
body: bench(
|
|
43
|
+
stack(panel("Current version", version), panel("Bump", chips), panel("Prerelease", prerelease), error),
|
|
44
|
+
stack(panel("New version", out))
|
|
45
|
+
),
|
|
46
|
+
foot: actions(copyButton(() => text, "Copy version", "fill")),
|
|
47
|
+
});
|
|
48
|
+
slot.append(card.node);
|
|
49
|
+
run();
|
|
50
|
+
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default { mount };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Word counts per sentence, with a threshold that flags the long ones.
|
|
2
|
+
import { el, blank, bench, stack, panel, textArea, numberInput, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const splitSentences = (text) => text.trim()
|
|
5
|
+
? text.trim().split(/[.!?\u2026]+/).map((part) => part.trim()).filter(Boolean)
|
|
6
|
+
: [];
|
|
7
|
+
|
|
8
|
+
const countWords = (text) => text.trim() ? text.trim().split(/\s+/).length : 0;
|
|
9
|
+
|
|
10
|
+
function tile(label) {
|
|
11
|
+
const value = el("span", { class: "mvb-stat__v" }, "0");
|
|
12
|
+
const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
|
|
13
|
+
node.set = (text) => { value.textContent = text; };
|
|
14
|
+
return node;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const kvRow = (key, value) => el("div", { class: "mvb-kv__row" },
|
|
18
|
+
el("span", { class: "mvb-kv__k" }, key),
|
|
19
|
+
el("span", { class: "mvb-kv__v" }, value));
|
|
20
|
+
|
|
21
|
+
export function mount(slot) {
|
|
22
|
+
const source = textArea({ rows: 12, "aria-label": "Text", placeholder: "Type or paste text" });
|
|
23
|
+
const threshold = numberInput({ min: "1", max: "120", value: "25", "aria-label": "Long sentence threshold in words" });
|
|
24
|
+
|
|
25
|
+
const sentencesTile = tile("Sentences");
|
|
26
|
+
const shortestTile = tile("Shortest");
|
|
27
|
+
const longestTile = tile("Longest");
|
|
28
|
+
const averageTile = tile("Average");
|
|
29
|
+
const stats = el("div", { class: "mvb-stats" }, sentencesTile, shortestTile, longestTile, averageTile);
|
|
30
|
+
|
|
31
|
+
const listWrap = el("div", { class: "mvb-scroll" });
|
|
32
|
+
const note = notice("note");
|
|
33
|
+
|
|
34
|
+
let longText = "";
|
|
35
|
+
|
|
36
|
+
function run() {
|
|
37
|
+
const sentences = splitSentences(source.value);
|
|
38
|
+
const limit = Math.max(1, parseInt(threshold.value, 10) || 25);
|
|
39
|
+
const lengths = sentences.map((sentence) => countWords(sentence));
|
|
40
|
+
|
|
41
|
+
sentencesTile.set(String(sentences.length));
|
|
42
|
+
if (lengths.length) {
|
|
43
|
+
shortestTile.set(String(Math.min(...lengths)));
|
|
44
|
+
longestTile.set(String(Math.max(...lengths)));
|
|
45
|
+
averageTile.set((lengths.reduce((sum, value) => sum + value, 0) / lengths.length).toFixed(1));
|
|
46
|
+
} else {
|
|
47
|
+
shortestTile.set("0");
|
|
48
|
+
longestTile.set("0");
|
|
49
|
+
averageTile.set("0");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const long = sentences.filter((sentence) => countWords(sentence) > limit);
|
|
53
|
+
longText = long.map((sentence) => `${countWords(sentence)} words ${sentence}`).join("\n");
|
|
54
|
+
note.write(long.length ? `${long.length} sentences run past ${limit} words.` : "");
|
|
55
|
+
|
|
56
|
+
if (!long.length) {
|
|
57
|
+
listWrap.replaceChildren(blank("text-align-start", "No sentence runs past the threshold."));
|
|
58
|
+
} else {
|
|
59
|
+
listWrap.replaceChildren(el("div", { class: "mvb-kv" },
|
|
60
|
+
...long.map((sentence) => kvRow(`${countWords(sentence)} words`, sentence))));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
source.addEventListener("input", run);
|
|
65
|
+
threshold.addEventListener("input", run);
|
|
66
|
+
|
|
67
|
+
const card = toolcard({
|
|
68
|
+
body: bench(
|
|
69
|
+
stack(panel("Text", source), panel("Flag past", threshold, "Words")),
|
|
70
|
+
stack(stats, note, panel("Long sentences", listWrap))
|
|
71
|
+
),
|
|
72
|
+
foot: actions(copyButton(() => longText, "Copy long sentences", "fill")),
|
|
73
|
+
});
|
|
74
|
+
slot.append(card.node);
|
|
75
|
+
run();
|
|
76
|
+
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default { mount };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Looks for arithmetic, geometric and quadratic patterns in a number series.
|
|
2
|
+
import { el, bench, stack, panel, textArea, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function fmt(n) {
|
|
5
|
+
return Number.isFinite(n) ? n.toLocaleString(undefined, { maximumFractionDigits: 6 }) : "-";
|
|
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
|
+
const kvRow = (key, value) =>
|
|
15
|
+
el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
|
|
16
|
+
|
|
17
|
+
export function mount(slot) {
|
|
18
|
+
const source = textArea({ rows: 6, value: "2, 5, 8, 11", "aria-label": "Numbers", placeholder: "2, 5, 8, 11" });
|
|
19
|
+
const error = notice("error");
|
|
20
|
+
|
|
21
|
+
const terms = el("span", { class: "mvb-kv__v" }, "-");
|
|
22
|
+
const differences = el("span", { class: "mvb-kv__v" }, "-");
|
|
23
|
+
const pattern = el("span", { class: "mvb-kv__v" }, "-");
|
|
24
|
+
const next = el("span", { class: "mvb-kv__v" }, "-");
|
|
25
|
+
const kv = el("div", { class: "mvb-kv" },
|
|
26
|
+
kvRow("Terms", terms),
|
|
27
|
+
kvRow("Differences", differences),
|
|
28
|
+
kvRow("Pattern", pattern),
|
|
29
|
+
kvRow("Next term", next));
|
|
30
|
+
let text = "";
|
|
31
|
+
|
|
32
|
+
function clear() {
|
|
33
|
+
terms.textContent = "-";
|
|
34
|
+
differences.textContent = "-";
|
|
35
|
+
pattern.textContent = "-";
|
|
36
|
+
next.textContent = "-";
|
|
37
|
+
text = "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function run() {
|
|
41
|
+
error.write("");
|
|
42
|
+
const { nums, invalid } = parseNumbers(source.value);
|
|
43
|
+
if (!source.value.trim()) { clear(); return; }
|
|
44
|
+
if (invalid) { clear(); error.write("Use numbers only, separated by spaces, commas or new lines."); return; }
|
|
45
|
+
if (nums.length < 2) { clear(); error.write("Add at least two numbers."); return; }
|
|
46
|
+
|
|
47
|
+
const diffs = nums.slice(1).map((n, i) => n - nums[i]);
|
|
48
|
+
const ratios = nums.slice(1).map((n, i) => (nums[i] === 0 ? NaN : n / nums[i]));
|
|
49
|
+
const second = diffs.slice(1).map((n, i) => n - diffs[i]);
|
|
50
|
+
|
|
51
|
+
const same = (list) => list.length > 0 && list.every((value) => Math.abs(value - list[0]) < 1e-9);
|
|
52
|
+
const sameRatio = ratios.length > 0 && ratios.every((value) => Number.isFinite(value) && Math.abs(value - ratios[0]) < 1e-9);
|
|
53
|
+
|
|
54
|
+
let name = "No simple pattern";
|
|
55
|
+
let nextTerm = NaN;
|
|
56
|
+
|
|
57
|
+
if (same(diffs)) {
|
|
58
|
+
name = `Arithmetic, step ${fmt(diffs[0])}`;
|
|
59
|
+
nextTerm = nums[nums.length - 1] + diffs[0];
|
|
60
|
+
} else if (sameRatio && ratios[0] !== 1) {
|
|
61
|
+
name = `Geometric, ratio ${fmt(ratios[0])}`;
|
|
62
|
+
nextTerm = nums[nums.length - 1] * ratios[0];
|
|
63
|
+
} else if (same(second)) {
|
|
64
|
+
name = "Quadratic, constant second difference";
|
|
65
|
+
nextTerm = nums[nums.length - 1] + diffs[diffs.length - 1] + second[0];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
terms.textContent = String(nums.length);
|
|
69
|
+
differences.textContent = diffs.map(fmt).join(", ");
|
|
70
|
+
pattern.textContent = name;
|
|
71
|
+
next.textContent = fmt(nextTerm);
|
|
72
|
+
text = `Pattern ${name}, next term ${Number.isFinite(nextTerm) ? fmt(nextTerm) : "unknown"}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
source.addEventListener("input", run);
|
|
76
|
+
|
|
77
|
+
const card = toolcard({
|
|
78
|
+
body: bench(
|
|
79
|
+
stack(panel("Numbers", source), error),
|
|
80
|
+
stack(panel("Analysis", kv))
|
|
81
|
+
),
|
|
82
|
+
foot: actions(copyButton(() => text, "Copy result", "fill")),
|
|
83
|
+
});
|
|
84
|
+
slot.append(card.node);
|
|
85
|
+
run();
|
|
86
|
+
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default { mount };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Builds a sitemap.xml from a site address and one page path or URL per line.
|
|
2
|
+
import { bench, stack, panel, textInput, textArea, selectInput, numberInput, output, notice, copyButton, downloadButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const esc = (value) =>
|
|
5
|
+
String(value).replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char]));
|
|
6
|
+
|
|
7
|
+
const isUrl = (value) => {
|
|
8
|
+
try {
|
|
9
|
+
new URL(value);
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function mount(slot) {
|
|
17
|
+
const base = textInput({ value: "https://example.com", "aria-label": "Base URL" });
|
|
18
|
+
const pages = textArea({ rows: 8, class: "mvb-in mvb-in--area mvb-in--mono", value: "/\n/about", placeholder: "/\n/about\n/blog", "aria-label": "Pages" });
|
|
19
|
+
const freq = selectInput(["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"], { value: "weekly", "aria-label": "Change frequency" });
|
|
20
|
+
const priority = numberInput({ value: "0.8", min: "0", max: "1", step: "0.1", "aria-label": "Priority" });
|
|
21
|
+
const result = output({ rows: 14 });
|
|
22
|
+
const error = notice("error");
|
|
23
|
+
|
|
24
|
+
function build() {
|
|
25
|
+
error.write("");
|
|
26
|
+
const site = base.value.trim().replace(/\/+$/, "");
|
|
27
|
+
if (!site || !isUrl(site)) {
|
|
28
|
+
error.write("Enter a valid site address, for example https://example.com.");
|
|
29
|
+
result.write("");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const paths = pages.value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
33
|
+
if (!paths.length) {
|
|
34
|
+
result.write("");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
38
|
+
const entries = paths.map((path) => {
|
|
39
|
+
const loc = /^https?:\/\//i.test(path) ? path : `${site}${path.startsWith("/") ? "" : "/"}${path}`;
|
|
40
|
+
return [
|
|
41
|
+
" <url>",
|
|
42
|
+
` <loc>${esc(loc)}</loc>`,
|
|
43
|
+
` <lastmod>${today}</lastmod>`,
|
|
44
|
+
` <changefreq>${esc(freq.value)}</changefreq>`,
|
|
45
|
+
` <priority>${esc(priority.value || "0.5")}</priority>`,
|
|
46
|
+
" </url>",
|
|
47
|
+
].join("\n");
|
|
48
|
+
});
|
|
49
|
+
result.write([
|
|
50
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
51
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
52
|
+
...entries,
|
|
53
|
+
"</urlset>",
|
|
54
|
+
].join("\n"));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const input of [base, pages, priority]) input.addEventListener("input", build);
|
|
58
|
+
freq.addEventListener("change", build);
|
|
59
|
+
|
|
60
|
+
const card = toolcard({
|
|
61
|
+
body: bench(
|
|
62
|
+
stack(
|
|
63
|
+
panel("Base URL", base),
|
|
64
|
+
panel("Pages", pages),
|
|
65
|
+
panel("Change frequency", freq),
|
|
66
|
+
panel("Priority", priority),
|
|
67
|
+
error
|
|
68
|
+
),
|
|
69
|
+
stack(panel("sitemap.xml", result))
|
|
70
|
+
),
|
|
71
|
+
foot: actions(
|
|
72
|
+
downloadButton(() => result.textContent, "sitemap.xml", "Save XML", "application/xml"),
|
|
73
|
+
copyButton(() => result.textContent, "Copy XML", "fill")
|
|
74
|
+
),
|
|
75
|
+
});
|
|
76
|
+
slot.append(card.node);
|
|
77
|
+
build();
|
|
78
|
+
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default { mount };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Turns a title into a URL slug: lowercase, punctuation removed, words joined
|
|
2
|
+
// by the chosen separator.
|
|
3
|
+
import { bench, stack, panel, textInput, chipRow, copyButton, actions, toolcard } from "../core.js";
|
|
4
|
+
|
|
5
|
+
function slugify(text, sep) {
|
|
6
|
+
const base = text
|
|
7
|
+
.normalize("NFKD")
|
|
8
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
11
|
+
.replace(/^-+|-+$/g, "");
|
|
12
|
+
return sep === "-" ? base : base.replace(/-/g, sep);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function mount(slot) {
|
|
16
|
+
const source = textInput({ value: "Hello mevebu world", placeholder: "Title", "aria-label": "Title" });
|
|
17
|
+
const result = textInput({ readonly: true, class: "mvb-in mvb-in--mono", "aria-label": "Slug" });
|
|
18
|
+
|
|
19
|
+
let sep = "-";
|
|
20
|
+
const chips = chipRow(
|
|
21
|
+
[
|
|
22
|
+
{ value: "-", label: "Hyphen" },
|
|
23
|
+
{ value: "_", label: "Underscore" },
|
|
24
|
+
{ value: ".", label: "Dot" },
|
|
25
|
+
],
|
|
26
|
+
{ value: "-", label: "Separator", onChange: (value) => { sep = value; run(); } }
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
function run() {
|
|
30
|
+
result.value = slugify(source.value, sep);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
source.addEventListener("input", run);
|
|
34
|
+
|
|
35
|
+
const card = toolcard({
|
|
36
|
+
body: bench(
|
|
37
|
+
stack(panel("Title", source), panel("Separator", chips)),
|
|
38
|
+
stack(panel("Slug", result))
|
|
39
|
+
),
|
|
40
|
+
foot: actions(copyButton(() => result.value, "Copy slug", "fill")),
|
|
41
|
+
});
|
|
42
|
+
slot.append(card.node);
|
|
43
|
+
run();
|
|
44
|
+
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default { mount };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Turns a title into a slug and shows the full address and the slug length.
|
|
2
|
+
import { el, bench, stack, panel, textInput, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const isUrl = (value) => {
|
|
5
|
+
try {
|
|
6
|
+
new URL(value);
|
|
7
|
+
return true;
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function slugify(text) {
|
|
14
|
+
return text
|
|
15
|
+
.normalize("NFKD")
|
|
16
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
19
|
+
.replace(/^-+|-+$/g, "");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const kvRow = (key, value) =>
|
|
23
|
+
el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
|
|
24
|
+
|
|
25
|
+
export function mount(slot) {
|
|
26
|
+
const title = textInput({ value: "Best running shoes", "aria-label": "Title" });
|
|
27
|
+
const base = textInput({ value: "https://example.com", "aria-label": "Base URL" });
|
|
28
|
+
const error = notice("error");
|
|
29
|
+
|
|
30
|
+
const slugValue = el("span", { class: "mvb-kv__v" }, "-");
|
|
31
|
+
const urlValue = el("span", { class: "mvb-kv__v" }, "-");
|
|
32
|
+
const lengthValue = el("span", { class: "mvb-kv__v" }, "-");
|
|
33
|
+
const kv = el(
|
|
34
|
+
"div",
|
|
35
|
+
{ class: "mvb-kv" },
|
|
36
|
+
kvRow("Slug", slugValue),
|
|
37
|
+
kvRow("URL", urlValue),
|
|
38
|
+
kvRow("Length", lengthValue)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
function run() {
|
|
42
|
+
error.write("");
|
|
43
|
+
const slug = slugify(title.value);
|
|
44
|
+
slugValue.textContent = slug || "-";
|
|
45
|
+
lengthValue.textContent = String(slug.length);
|
|
46
|
+
|
|
47
|
+
const site = base.value.trim().replace(/\/+$/, "");
|
|
48
|
+
if (!slug) {
|
|
49
|
+
urlValue.textContent = "-";
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!site) {
|
|
53
|
+
urlValue.textContent = slug;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!isUrl(site)) {
|
|
57
|
+
error.write("The base URL is not valid.");
|
|
58
|
+
urlValue.textContent = "-";
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
urlValue.textContent = `${site}/${slug}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
title.addEventListener("input", run);
|
|
65
|
+
base.addEventListener("input", run);
|
|
66
|
+
|
|
67
|
+
const card = toolcard({
|
|
68
|
+
body: bench(
|
|
69
|
+
stack(panel("Title", title), panel("Base URL", base), error),
|
|
70
|
+
stack(panel("Preview", kv))
|
|
71
|
+
),
|
|
72
|
+
foot: actions(
|
|
73
|
+
copyButton(() => urlValue.textContent === "-" ? "" : urlValue.textContent, "Copy URL"),
|
|
74
|
+
copyButton(() => slugValue.textContent === "-" ? "" : slugValue.textContent, "Copy slug", "fill")
|
|
75
|
+
),
|
|
76
|
+
});
|
|
77
|
+
slot.append(card.node);
|
|
78
|
+
run();
|
|
79
|
+
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default { mount };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Sorts a list of lines alphabetically or numerically, either direction.
|
|
2
|
+
import { bench, stack, panel, textArea, chipRow, toggleRow, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
export function mount(slot) {
|
|
5
|
+
const source = textArea({ rows: 12, "aria-label": "Text", placeholder: "Paste your list, one entry per line" });
|
|
6
|
+
const result = textArea({ rows: 12, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Result" });
|
|
7
|
+
|
|
8
|
+
let direction = "asc";
|
|
9
|
+
const dirChips = chipRow(
|
|
10
|
+
[{ value: "asc", label: "A to Z" }, { value: "desc", label: "Z to A" }],
|
|
11
|
+
{ value: "asc", label: "Direction", onChange: (value) => { direction = value; run(); } }
|
|
12
|
+
);
|
|
13
|
+
const options = toggleRow([
|
|
14
|
+
{ value: "ignore", label: "Ignore case", checked: false },
|
|
15
|
+
{ value: "numeric", label: "Numeric", checked: false },
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function run() {
|
|
19
|
+
const text = source.value;
|
|
20
|
+
if (!text) { result.value = ""; return; }
|
|
21
|
+
const opts = options.values();
|
|
22
|
+
const ignore = opts.includes("ignore");
|
|
23
|
+
const numeric = opts.includes("numeric");
|
|
24
|
+
const collator = new Intl.Collator(undefined, { numeric, sensitivity: ignore ? "base" : "variant" });
|
|
25
|
+
const lines = text.split("\n");
|
|
26
|
+
lines.sort((a, b) => {
|
|
27
|
+
if (numeric) {
|
|
28
|
+
const na = parseFloat(a);
|
|
29
|
+
const nb = parseFloat(b);
|
|
30
|
+
if (!Number.isNaN(na) && !Number.isNaN(nb) && na !== nb) return na - nb;
|
|
31
|
+
}
|
|
32
|
+
return collator.compare(a, b);
|
|
33
|
+
});
|
|
34
|
+
if (direction === "desc") lines.reverse();
|
|
35
|
+
result.value = lines.join("\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
source.addEventListener("input", run);
|
|
39
|
+
options.addEventListener("change", run);
|
|
40
|
+
|
|
41
|
+
const card = toolcard({
|
|
42
|
+
body: bench(
|
|
43
|
+
stack(panel("Direction", dirChips), options),
|
|
44
|
+
stack(panel("Result", result))
|
|
45
|
+
),
|
|
46
|
+
foot: actions(copyButton(() => result.value, "Copy result", "fill")),
|
|
47
|
+
});
|
|
48
|
+
slot.append(card.node);
|
|
49
|
+
run();
|
|
50
|
+
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default { mount };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Converts a speed between metric, imperial and nautical units.
|
|
2
|
+
import { el, bench, stack, panel, numberInput, selectInput, notice, copyButton, button, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const UNITS = [
|
|
5
|
+
{ value: "mps", label: "Meter per second", factor: 1 },
|
|
6
|
+
{ value: "kmh", label: "Kilometer per hour", factor: 0.2777777777777778 },
|
|
7
|
+
{ value: "mph", label: "Mile per hour", factor: 0.44704 },
|
|
8
|
+
{ value: "fps", label: "Foot per second", factor: 0.3048 },
|
|
9
|
+
{ value: "kn", label: "Knot", factor: 0.5144444444444445 },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const byId = Object.fromEntries(UNITS.map((unit) => [unit.value, unit]));
|
|
13
|
+
const options = UNITS.map(({ value, label }) => ({ value, label }));
|
|
14
|
+
|
|
15
|
+
function fmt(value) {
|
|
16
|
+
if (!Number.isFinite(value)) return "-";
|
|
17
|
+
if (value === 0) return "0";
|
|
18
|
+
const abs = Math.abs(value);
|
|
19
|
+
if (abs >= 1e-6 && abs < 1e15) return String(Number(value.toPrecision(8)));
|
|
20
|
+
return value.toExponential(4);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function mount(slot) {
|
|
24
|
+
const value = numberInput({ value: "1", step: "any", "aria-label": "Value" });
|
|
25
|
+
const from = selectInput(options, { value: "kmh", "aria-label": "From unit" });
|
|
26
|
+
const to = selectInput(options, { value: "mph", "aria-label": "To unit" });
|
|
27
|
+
const result = el("span", { class: "mvb-stat__v" }, "-");
|
|
28
|
+
const stats = el("div", { class: "mvb-stats" }, el("div", { class: "mvb-stat" }, el("span", { class: "mvb-stat__k" }, "Result"), result));
|
|
29
|
+
const factor = el("p", { class: "mvb-note" });
|
|
30
|
+
const error = notice("error");
|
|
31
|
+
const scroll = el("div", { class: "mvb-scroll" });
|
|
32
|
+
let text = "";
|
|
33
|
+
|
|
34
|
+
function run() {
|
|
35
|
+
error.write("");
|
|
36
|
+
const raw = value.value.trim();
|
|
37
|
+
const n = Number(raw);
|
|
38
|
+
if (!raw || !Number.isFinite(n)) {
|
|
39
|
+
result.textContent = "-";
|
|
40
|
+
factor.textContent = "";
|
|
41
|
+
scroll.replaceChildren();
|
|
42
|
+
text = "";
|
|
43
|
+
error.write("Enter a number.");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const a = byId[from.value];
|
|
47
|
+
const b = byId[to.value];
|
|
48
|
+
const out = (n * a.factor) / b.factor;
|
|
49
|
+
result.textContent = `${fmt(out)} ${b.value}`;
|
|
50
|
+
factor.textContent = `1 ${a.value} = ${fmt(a.factor / b.factor)} ${b.value}`;
|
|
51
|
+
scroll.replaceChildren(el("table", { class: "mvb-ttable" },
|
|
52
|
+
el("thead", {}, el("tr", {}, el("th", {}, "Unit"), el("th", {}, "Value"))),
|
|
53
|
+
el("tbody", {}, ...UNITS.map((unit) => el("tr", {},
|
|
54
|
+
el("td", {}, unit.label),
|
|
55
|
+
el("td", {}, fmt((n * a.factor) / unit.factor)))))));
|
|
56
|
+
text = `${fmt(out)} ${b.value}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
value.addEventListener("input", run);
|
|
60
|
+
from.addEventListener("change", run);
|
|
61
|
+
to.addEventListener("change", run);
|
|
62
|
+
|
|
63
|
+
const swap = button("Swap", { icon: "arrow-left-right", onclick: () => {
|
|
64
|
+
const held = from.value;
|
|
65
|
+
from.value = to.value;
|
|
66
|
+
to.value = held;
|
|
67
|
+
run();
|
|
68
|
+
}});
|
|
69
|
+
|
|
70
|
+
const card = toolcard({
|
|
71
|
+
body: bench(
|
|
72
|
+
stack(panel("Value", value), panel("From", from), panel("To", to), error),
|
|
73
|
+
stack(panel("Result", stats), factor, panel("All units", scroll))
|
|
74
|
+
),
|
|
75
|
+
foot: actions(swap, copyButton(() => text, "Copy result", "fill")),
|
|
76
|
+
});
|
|
77
|
+
slot.append(card.node);
|
|
78
|
+
run();
|
|
79
|
+
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default { mount };
|