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,106 @@
|
|
|
1
|
+
// Counts arrangements and selections with nPr and nCr.
|
|
2
|
+
import { el, bench, stack, panel, numberInput, notice, copyButton, chipRow, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function fmt(n) {
|
|
5
|
+
return n.toLocaleString(undefined, { maximumFractionDigits: 6 });
|
|
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 nInput = numberInput({ value: "5", min: "0", step: "1", "aria-label": "n" });
|
|
17
|
+
const rInput = numberInput({ value: "3", min: "0", step: "1", "aria-label": "r" });
|
|
18
|
+
const count = tile("Result");
|
|
19
|
+
const stats = el("div", { class: "mvb-stats" }, count);
|
|
20
|
+
const formulaRow = el("span", { class: "mvb-kv__v" }, "-");
|
|
21
|
+
const kv = el(
|
|
22
|
+
"div",
|
|
23
|
+
{ class: "mvb-kv" },
|
|
24
|
+
el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, "Formula"), formulaRow)
|
|
25
|
+
);
|
|
26
|
+
const error = notice("error");
|
|
27
|
+
let mode = "npr";
|
|
28
|
+
let text = "";
|
|
29
|
+
|
|
30
|
+
function permutations(n, r) {
|
|
31
|
+
let value = 1;
|
|
32
|
+
for (let i = 0; i < r; i++) value *= n - i;
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function run() {
|
|
37
|
+
error.write("");
|
|
38
|
+
const n = Number(nInput.value);
|
|
39
|
+
const r = Number(rInput.value);
|
|
40
|
+
if (nInput.value.trim() === "" || rInput.value.trim() === "" || !Number.isInteger(n) || !Number.isInteger(r)) {
|
|
41
|
+
count.set("-");
|
|
42
|
+
formulaRow.textContent = "-";
|
|
43
|
+
text = "";
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (n < 0 || r < 0) {
|
|
47
|
+
count.set("-");
|
|
48
|
+
formulaRow.textContent = "-";
|
|
49
|
+
text = "";
|
|
50
|
+
error.write("n and r must be zero or more.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (r > n) {
|
|
54
|
+
count.set("-");
|
|
55
|
+
formulaRow.textContent = "-";
|
|
56
|
+
text = "";
|
|
57
|
+
error.write("r cannot be larger than n.");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const base = permutations(n, r);
|
|
61
|
+
const value = mode === "npr" ? base : base / permutations(r, r);
|
|
62
|
+
if (!Number.isFinite(value) || value > Number.MAX_SAFE_INTEGER) {
|
|
63
|
+
count.set("-");
|
|
64
|
+
formulaRow.textContent = "-";
|
|
65
|
+
text = "";
|
|
66
|
+
error.write("That result is too large to show exactly.");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const rounded = Math.round(value);
|
|
70
|
+
count.set(fmt(rounded));
|
|
71
|
+
formulaRow.textContent = mode === "npr" ? "n! / (n - r)!" : "n! / (r! (n - r)!)";
|
|
72
|
+
text = `${mode === "npr" ? "P" : "C"}(${n}, ${r}) = ${fmt(rounded)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const chips = chipRow(
|
|
76
|
+
[
|
|
77
|
+
{ value: "npr", label: "Permutation nPr" },
|
|
78
|
+
{ value: "ncr", label: "Combination nCr" },
|
|
79
|
+
],
|
|
80
|
+
{
|
|
81
|
+
value: mode,
|
|
82
|
+
label: "Mode",
|
|
83
|
+
onChange: (value) => {
|
|
84
|
+
mode = value;
|
|
85
|
+
run();
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
nInput.addEventListener("input", run);
|
|
91
|
+
rInput.addEventListener("input", run);
|
|
92
|
+
|
|
93
|
+
const card = toolcard({
|
|
94
|
+
body: bench(
|
|
95
|
+
stack(panel("n", nInput), panel("r", rInput), panel("Mode", chips), error),
|
|
96
|
+
stack(stats, panel("Working", 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,46 @@
|
|
|
1
|
+
// Adds a prefix, a suffix, or automatic numbering to every line.
|
|
2
|
+
import { bench, stack, panel, textArea, textInput, checkbox, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
export function mount(slot) {
|
|
5
|
+
const source = textArea({ rows: 11, "aria-label": "Text", placeholder: "Paste your list, one entry per line" });
|
|
6
|
+
const prefixField = textInput({ placeholder: "Prefix", "aria-label": "Prefix" });
|
|
7
|
+
const suffixField = textInput({ placeholder: "Suffix", "aria-label": "Suffix" });
|
|
8
|
+
const numbering = checkbox("Numbering", { checked: false });
|
|
9
|
+
const result = textArea({ rows: 11, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Result" });
|
|
10
|
+
|
|
11
|
+
function run() {
|
|
12
|
+
const text = source.value;
|
|
13
|
+
if (!text) { result.value = ""; return; }
|
|
14
|
+
const prefix = prefixField.value;
|
|
15
|
+
const suffix = suffixField.value;
|
|
16
|
+
const numbered = numbering.input.checked;
|
|
17
|
+
result.value = text
|
|
18
|
+
.split("\n")
|
|
19
|
+
.map((line, index) => `${numbered ? `${index + 1}. ` : ""}${prefix}${line}${suffix}`)
|
|
20
|
+
.join("\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
source.addEventListener("input", run);
|
|
24
|
+
prefixField.addEventListener("input", run);
|
|
25
|
+
suffixField.addEventListener("input", run);
|
|
26
|
+
numbering.input.addEventListener("change", run);
|
|
27
|
+
|
|
28
|
+
const card = toolcard({
|
|
29
|
+
body: bench(
|
|
30
|
+
stack(
|
|
31
|
+
panel("Text", source),
|
|
32
|
+
panel("Prefix", prefixField),
|
|
33
|
+
panel("Suffix", suffixField),
|
|
34
|
+
numbering
|
|
35
|
+
),
|
|
36
|
+
stack(panel("Result", result))
|
|
37
|
+
),
|
|
38
|
+
foot: actions(copyButton(() => result.value, "Copy result", "fill")),
|
|
39
|
+
});
|
|
40
|
+
slot.append(card.node);
|
|
41
|
+
run();
|
|
42
|
+
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default { mount };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Sorts tasks into four boxes by urgency and importance.
|
|
2
|
+
import { el, toast, bench, stack, row, panel, textInput, selectInput, button, copyButton, actions, toolcard, readPin, writePin } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
|
5
|
+
const QUADRANTS = [
|
|
6
|
+
{ id: "do", name: "Do now" },
|
|
7
|
+
{ id: "schedule", name: "Schedule" },
|
|
8
|
+
{ id: "delegate", name: "Delegate" },
|
|
9
|
+
{ id: "drop", name: "Drop" },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export function mount(slot) {
|
|
13
|
+
const PIN = "priority-matrix";
|
|
14
|
+
const saved = readPin(PIN, []);
|
|
15
|
+
let list = Array.isArray(saved) ? saved : [];
|
|
16
|
+
|
|
17
|
+
const task = textInput({ placeholder: "Task", "aria-label": "Task" });
|
|
18
|
+
const quadrant = selectInput(QUADRANTS.map((q) => ({ value: q.id, label: q.name })), {
|
|
19
|
+
value: "do",
|
|
20
|
+
"aria-label": "Quadrant",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const matrix = stack();
|
|
24
|
+
|
|
25
|
+
const addButton = button("Add", { icon: "plus", kind: "fill" });
|
|
26
|
+
const copyMatrix = copyButton(() => matrixText(), "Copy matrix");
|
|
27
|
+
|
|
28
|
+
const save = () => writePin(PIN, list);
|
|
29
|
+
|
|
30
|
+
function matrixText() {
|
|
31
|
+
return QUADRANTS.map((q) => {
|
|
32
|
+
const items = list.filter((item) => item.q === q.id);
|
|
33
|
+
const lines = items.map((item) => `- ${item.text}`);
|
|
34
|
+
return lines.length ? `${q.name}\n${lines.join("\n")}` : q.name;
|
|
35
|
+
}).join("\n\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function taskRow(item) {
|
|
39
|
+
const text = el("span", {}, item.text);
|
|
40
|
+
const move = selectInput(QUADRANTS.map((q) => ({ value: q.id, label: q.name })), {
|
|
41
|
+
value: item.q,
|
|
42
|
+
"aria-label": `Move ${item.text}`,
|
|
43
|
+
});
|
|
44
|
+
move.addEventListener("change", () => {
|
|
45
|
+
item.q = move.value;
|
|
46
|
+
save();
|
|
47
|
+
render();
|
|
48
|
+
});
|
|
49
|
+
const remove = button("", { icon: "trash", "aria-label": `Remove ${item.text}` });
|
|
50
|
+
remove.addEventListener("click", () => {
|
|
51
|
+
list = list.filter((entry) => entry.id !== item.id);
|
|
52
|
+
save();
|
|
53
|
+
render();
|
|
54
|
+
});
|
|
55
|
+
return el("div", { class: "mvb-row" }, text, move, remove);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function quadrantBox(q) {
|
|
59
|
+
const items = list.filter((item) => item.q === q.id);
|
|
60
|
+
const body = items.length ? stack(...items.map(taskRow)) : el("span", { class: "mvb-note" }, "Empty");
|
|
61
|
+
return panel(`${q.name} (${items.length})`, body);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function render() {
|
|
65
|
+
matrix.replaceChildren(
|
|
66
|
+
row(quadrantBox(QUADRANTS[0]), quadrantBox(QUADRANTS[1])),
|
|
67
|
+
row(quadrantBox(QUADRANTS[2]), quadrantBox(QUADRANTS[3]))
|
|
68
|
+
);
|
|
69
|
+
copyMatrix.disabled = list.length === 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function add() {
|
|
73
|
+
const text = task.value.trim();
|
|
74
|
+
if (!text) return toast("Type a task first");
|
|
75
|
+
list.push({ id: uid(), text, q: quadrant.value });
|
|
76
|
+
task.value = "";
|
|
77
|
+
task.focus();
|
|
78
|
+
save();
|
|
79
|
+
render();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
task.addEventListener("keydown", (event) => {
|
|
83
|
+
if (event.key === "Enter") add();
|
|
84
|
+
});
|
|
85
|
+
addButton.addEventListener("click", add);
|
|
86
|
+
|
|
87
|
+
const card = toolcard({
|
|
88
|
+
body: bench(
|
|
89
|
+
stack(panel("Task", task), panel("Quadrant", quadrant)),
|
|
90
|
+
matrix
|
|
91
|
+
),
|
|
92
|
+
foot: actions(copyMatrix, addButton),
|
|
93
|
+
});
|
|
94
|
+
slot.append(card.node);
|
|
95
|
+
render();
|
|
96
|
+
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default { mount };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Profit, margin and markup from a cost and a selling price.
|
|
2
|
+
import { el, bench, stack, panel, numberInput, 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", "aria-label": "Cost" });
|
|
17
|
+
const price = numberInput({ value: "100", "aria-label": "Selling price" });
|
|
18
|
+
const profit = tile("Profit");
|
|
19
|
+
const margin = tile("Margin");
|
|
20
|
+
const markup = tile("Markup");
|
|
21
|
+
const stats = el("div", { class: "mvb-stats" }, profit, margin, markup);
|
|
22
|
+
const error = notice("error");
|
|
23
|
+
let text = "";
|
|
24
|
+
|
|
25
|
+
function clear() {
|
|
26
|
+
profit.set("-");
|
|
27
|
+
margin.set("-");
|
|
28
|
+
markup.set("-");
|
|
29
|
+
text = "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function run() {
|
|
33
|
+
error.write("");
|
|
34
|
+
const c = Number(cost.value);
|
|
35
|
+
const p = Number(price.value);
|
|
36
|
+
if (cost.value.trim() === "" || price.value.trim() === "" || !Number.isFinite(c) || !Number.isFinite(p)) {
|
|
37
|
+
clear();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (c < 0 || p < 0) {
|
|
41
|
+
clear();
|
|
42
|
+
error.write("The cost and price cannot be negative.");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const gain = p - c;
|
|
46
|
+
profit.set(fmt(gain));
|
|
47
|
+
margin.set(p === 0 ? "-" : `${fmt((gain / p) * 100)}%`);
|
|
48
|
+
markup.set(c === 0 ? "-" : `${fmt((gain / c) * 100)}%`);
|
|
49
|
+
text = `Profit ${fmt(gain)}, margin ${p === 0 ? "n/a" : `${fmt((gain / p) * 100)}%`}, markup ${c === 0 ? "n/a" : `${fmt((gain / c) * 100)}%`}`;
|
|
50
|
+
if (p === 0 || c === 0) error.write("Margin needs a price and markup needs a cost above zero.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
cost.addEventListener("input", run);
|
|
54
|
+
price.addEventListener("input", run);
|
|
55
|
+
|
|
56
|
+
const card = toolcard({
|
|
57
|
+
body: bench(
|
|
58
|
+
stack(panel("Cost", cost), panel("Selling price", price), error),
|
|
59
|
+
stack(stats)
|
|
60
|
+
),
|
|
61
|
+
foot: actions(copyButton(() => text, "Copy result", "fill")),
|
|
62
|
+
});
|
|
63
|
+
slot.append(card.node);
|
|
64
|
+
run();
|
|
65
|
+
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default { mount };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Solves a quadratic equation and labels the discriminant and the roots.
|
|
2
|
+
import { el, bench, stack, panel, numberInput, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function fmt(n) {
|
|
5
|
+
return n.toLocaleString(undefined, { maximumFractionDigits: 6 });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const kvRow = (key, value) =>
|
|
9
|
+
el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
|
|
10
|
+
|
|
11
|
+
export function mount(slot) {
|
|
12
|
+
const a = numberInput({ value: "1", "aria-label": "Coefficient a" });
|
|
13
|
+
const b = numberInput({ value: "-3", "aria-label": "Coefficient b" });
|
|
14
|
+
const c = numberInput({ value: "2", "aria-label": "Coefficient c" });
|
|
15
|
+
const discriminant = el("span", { class: "mvb-kv__v" }, "-");
|
|
16
|
+
const root1 = el("span", { class: "mvb-kv__v" }, "-");
|
|
17
|
+
const root2 = el("span", { class: "mvb-kv__v" }, "-");
|
|
18
|
+
const nature = el("span", { class: "mvb-kv__v" }, "-");
|
|
19
|
+
const kv = el(
|
|
20
|
+
"div",
|
|
21
|
+
{ class: "mvb-kv" },
|
|
22
|
+
kvRow("Discriminant", discriminant),
|
|
23
|
+
kvRow("Root 1", root1),
|
|
24
|
+
kvRow("Root 2", root2),
|
|
25
|
+
kvRow("Nature", nature)
|
|
26
|
+
);
|
|
27
|
+
const error = notice("error");
|
|
28
|
+
let text = "";
|
|
29
|
+
|
|
30
|
+
function clear() {
|
|
31
|
+
discriminant.textContent = "-";
|
|
32
|
+
root1.textContent = "-";
|
|
33
|
+
root2.textContent = "-";
|
|
34
|
+
nature.textContent = "-";
|
|
35
|
+
text = "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function run() {
|
|
39
|
+
error.write("");
|
|
40
|
+
const A = Number(a.value);
|
|
41
|
+
const B = Number(b.value);
|
|
42
|
+
const C = Number(c.value);
|
|
43
|
+
if ([a, b, c].some((input) => input.value.trim() === "") || ![A, B, C].every(Number.isFinite)) {
|
|
44
|
+
clear();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (A === 0) {
|
|
48
|
+
clear();
|
|
49
|
+
error.write("The coefficient a cannot be zero, that is not a quadratic.");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const disc = B * B - 4 * A * C;
|
|
53
|
+
discriminant.textContent = fmt(disc);
|
|
54
|
+
if (disc > 0) {
|
|
55
|
+
const root = Math.sqrt(disc);
|
|
56
|
+
const r1 = (-B + root) / (2 * A);
|
|
57
|
+
const r2 = (-B - root) / (2 * A);
|
|
58
|
+
root1.textContent = fmt(r1);
|
|
59
|
+
root2.textContent = fmt(r2);
|
|
60
|
+
nature.textContent = "Two real roots";
|
|
61
|
+
text = `Roots ${fmt(r1)} and ${fmt(r2)}, discriminant ${fmt(disc)}`;
|
|
62
|
+
} else if (disc === 0) {
|
|
63
|
+
const r = -B / (2 * A);
|
|
64
|
+
root1.textContent = fmt(r);
|
|
65
|
+
root2.textContent = fmt(r);
|
|
66
|
+
nature.textContent = "One real root";
|
|
67
|
+
text = `Root ${fmt(r)}, discriminant 0`;
|
|
68
|
+
} else {
|
|
69
|
+
const real = -B / (2 * A);
|
|
70
|
+
const imag = Math.sqrt(-disc) / (2 * A);
|
|
71
|
+
root1.textContent = `${fmt(real)} + ${fmt(Math.abs(imag))}i`;
|
|
72
|
+
root2.textContent = `${fmt(real)} - ${fmt(Math.abs(imag))}i`;
|
|
73
|
+
nature.textContent = "Two complex roots";
|
|
74
|
+
text = `Roots ${fmt(real)} + ${fmt(Math.abs(imag))}i and ${fmt(real)} - ${fmt(Math.abs(imag))}i, discriminant ${fmt(disc)}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const input of [a, b, c]) input.addEventListener("input", run);
|
|
79
|
+
|
|
80
|
+
const card = toolcard({
|
|
81
|
+
body: bench(
|
|
82
|
+
stack(panel("a", a), panel("b", b), panel("c", c), error),
|
|
83
|
+
stack(panel("Solution", kv))
|
|
84
|
+
),
|
|
85
|
+
foot: actions(copyButton(() => text, "Copy roots", "fill")),
|
|
86
|
+
});
|
|
87
|
+
slot.append(card.node);
|
|
88
|
+
run();
|
|
89
|
+
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default { mount };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Builds a query string from key=value lines and joins it to an optional base
|
|
2
|
+
// URL.
|
|
3
|
+
import { bench, stack, panel, textInput, textArea, checkbox, notice, output, copyButton, actions, toolcard } from "../core.js";
|
|
4
|
+
|
|
5
|
+
const isUrl = (value) => {
|
|
6
|
+
try {
|
|
7
|
+
new URL(value);
|
|
8
|
+
return true;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function mount(slot) {
|
|
15
|
+
const base = textInput({ value: "https://example.com/search", "aria-label": "Base URL" });
|
|
16
|
+
const params = textArea({ rows: 8, class: "mvb-in mvb-in--area mvb-in--mono", value: "q=hello world\npage=2", placeholder: "q=hello world\npage=2", "aria-label": "Parameters" });
|
|
17
|
+
const encode = checkbox("Encode values", { checked: true });
|
|
18
|
+
const result = output({ rows: 4 });
|
|
19
|
+
const error = notice("error");
|
|
20
|
+
|
|
21
|
+
function build() {
|
|
22
|
+
error.write("");
|
|
23
|
+
const pairs = params.value
|
|
24
|
+
.split(/\r?\n/)
|
|
25
|
+
.map((line) => line.trim())
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.map((line) => {
|
|
28
|
+
const at = line.indexOf("=");
|
|
29
|
+
return at === -1 ? [line, ""] : [line.slice(0, at), line.slice(at + 1)];
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
let query;
|
|
33
|
+
if (encode.input.checked) {
|
|
34
|
+
const search = new URLSearchParams();
|
|
35
|
+
for (const [key, value] of pairs) search.append(key, value);
|
|
36
|
+
query = search.toString();
|
|
37
|
+
} else {
|
|
38
|
+
query = pairs.map(([key, value]) => (value === "" ? key : `${key}=${value}`)).join("&");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const site = base.value.trim();
|
|
42
|
+
let out = query;
|
|
43
|
+
if (site) {
|
|
44
|
+
if (!isUrl(site)) {
|
|
45
|
+
error.write("The base URL is not valid.");
|
|
46
|
+
} else {
|
|
47
|
+
const separator = site.includes("?") ? (site.endsWith("?") || site.endsWith("&") ? "" : "&") : "?";
|
|
48
|
+
out = query ? `${site}${separator}${query}` : site;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
result.write(out);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
base.addEventListener("input", build);
|
|
55
|
+
params.addEventListener("input", build);
|
|
56
|
+
encode.input.addEventListener("change", build);
|
|
57
|
+
|
|
58
|
+
const card = toolcard({
|
|
59
|
+
body: bench(
|
|
60
|
+
stack(panel("Base URL", base), panel("Parameters", params), encode, error),
|
|
61
|
+
stack(panel("URL", result))
|
|
62
|
+
),
|
|
63
|
+
foot: actions(copyButton(() => result.textContent, "Copy URL", "fill")),
|
|
64
|
+
});
|
|
65
|
+
slot.append(card.node);
|
|
66
|
+
build();
|
|
67
|
+
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default { mount };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Lays out pasted questions and options, and separates the answer key.
|
|
2
|
+
import { bench, stack, panel, textArea, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const OPTION = /^[-*]\s+/;
|
|
5
|
+
const LETTERS = "ABCDEFGHIJ";
|
|
6
|
+
|
|
7
|
+
function parse(text) {
|
|
8
|
+
const out = [];
|
|
9
|
+
for (const block of text.split(/\n\s*\n/)) {
|
|
10
|
+
const lines = block.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
11
|
+
if (!lines.length) continue;
|
|
12
|
+
const question = lines.find((line) => !OPTION.test(line));
|
|
13
|
+
if (!question) continue;
|
|
14
|
+
const options = lines
|
|
15
|
+
.filter((line) => OPTION.test(line))
|
|
16
|
+
.map((line) => ({ text: line.replace(OPTION, ""), correct: line.startsWith("*") }));
|
|
17
|
+
out.push({ question: question.replace(/^\d+[.)]\s*/, ""), options });
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function mount(slot) {
|
|
23
|
+
const source = textArea({
|
|
24
|
+
rows: 14,
|
|
25
|
+
"aria-label": "Questions",
|
|
26
|
+
placeholder: "Question one\n- First option\n- Second option\n* The correct option\n\nQuestion two\n- ...",
|
|
27
|
+
});
|
|
28
|
+
const quiz = textArea({ rows: 12, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Quiz" });
|
|
29
|
+
const key = textArea({ rows: 12, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Answer key" });
|
|
30
|
+
const error = notice("error");
|
|
31
|
+
|
|
32
|
+
function render() {
|
|
33
|
+
const items = source.value.trim() ? parse(source.value) : [];
|
|
34
|
+
if (!items.length) {
|
|
35
|
+
quiz.value = "";
|
|
36
|
+
key.value = "";
|
|
37
|
+
error.write(source.value.trim() ? "No questions found. Put the question on the first line and start each option with - or *." : "");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
error.write("");
|
|
41
|
+
quiz.value = items.map((item, i) => {
|
|
42
|
+
const head = `${i + 1}. ${item.question}`;
|
|
43
|
+
const body = item.options.map((option, j) => ` ${LETTERS[j] || "?"}. ${option.text}`);
|
|
44
|
+
return [head, ...body].join("\n");
|
|
45
|
+
}).join("\n\n");
|
|
46
|
+
key.value = items.map((item, i) => {
|
|
47
|
+
const at = item.options.findIndex((option) => option.correct);
|
|
48
|
+
return `${i + 1}. ${at >= 0 ? LETTERS[at] : "-"}`;
|
|
49
|
+
}).join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
source.addEventListener("input", render);
|
|
53
|
+
|
|
54
|
+
const card = toolcard({
|
|
55
|
+
body: bench(
|
|
56
|
+
stack(panel("Questions", source), error),
|
|
57
|
+
stack(panel("Quiz", quiz), panel("Answer key", key))
|
|
58
|
+
),
|
|
59
|
+
foot: actions(
|
|
60
|
+
copyButton(() => key.value, "Copy key"),
|
|
61
|
+
copyButton(() => quiz.value, "Copy quiz", "fill")
|
|
62
|
+
),
|
|
63
|
+
});
|
|
64
|
+
slot.append(card.node);
|
|
65
|
+
render();
|
|
66
|
+
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default { mount };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Formats a direct or indirect quotation with its citation.
|
|
2
|
+
import { bench, stack, panel, textArea, textInput, selectInput, output, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const MODES = [
|
|
5
|
+
{ value: "direct", label: "Direct quote" },
|
|
6
|
+
{ value: "paraphrase", label: "Paraphrase" },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
const parts = (raw) => raw.split(";").map((item) => item.trim()).filter(Boolean);
|
|
10
|
+
const surname = (author) => author.split(",")[0].trim();
|
|
11
|
+
const countWords = (text) => (text.trim() ? text.trim().split(/\s+/).length : 0);
|
|
12
|
+
|
|
13
|
+
function inText(raw, year, page) {
|
|
14
|
+
const list = parts(raw);
|
|
15
|
+
const y = year.trim();
|
|
16
|
+
const p = page.trim();
|
|
17
|
+
let who = "";
|
|
18
|
+
if (list.length === 1) who = surname(list[0]);
|
|
19
|
+
else if (list.length === 2) who = `${surname(list[0])} & ${surname(list[1])}`;
|
|
20
|
+
else if (list.length > 2) who = `${surname(list[0])} et al.`;
|
|
21
|
+
return `(${[who, y, p ? `p. ${p}` : ""].filter(Boolean).join(", ")})`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function mount(slot) {
|
|
25
|
+
const quote = textArea({ rows: 8, "aria-label": "Quotation", placeholder: "Paste the quoted text" });
|
|
26
|
+
const authors = textInput({ placeholder: "Smith, J. A.; Doe, B.", "aria-label": "Authors" });
|
|
27
|
+
const year = textInput({ placeholder: "2020", "aria-label": "Year" });
|
|
28
|
+
const page = textInput({ placeholder: "5", "aria-label": "Page" });
|
|
29
|
+
const mode = selectInput(MODES, { value: "direct", "aria-label": "Mode" });
|
|
30
|
+
|
|
31
|
+
const result = output({ rows: 10 });
|
|
32
|
+
const note = notice("note");
|
|
33
|
+
note.write("Check your institution's rules for block quotes and page numbers.");
|
|
34
|
+
let text = "";
|
|
35
|
+
|
|
36
|
+
function build() {
|
|
37
|
+
const body = quote.value.trim();
|
|
38
|
+
if (!body) {
|
|
39
|
+
text = "";
|
|
40
|
+
result.write("");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const cite = inText(authors.value, year.value, page.value);
|
|
44
|
+
if (mode.value === "paraphrase") {
|
|
45
|
+
text = `${body} ${cite}`.trim();
|
|
46
|
+
} else if (countWords(body) >= 40) {
|
|
47
|
+
text = `${body}\n\n${cite}`.trim();
|
|
48
|
+
} else {
|
|
49
|
+
text = `"${body}" ${cite}`.trim();
|
|
50
|
+
}
|
|
51
|
+
result.write(text);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const input of [quote, authors, year, page]) input.addEventListener("input", build);
|
|
55
|
+
mode.addEventListener("change", build);
|
|
56
|
+
|
|
57
|
+
const card = toolcard({
|
|
58
|
+
body: bench(
|
|
59
|
+
stack(
|
|
60
|
+
panel("Quotation", quote),
|
|
61
|
+
panel("Authors", authors),
|
|
62
|
+
panel("Year", year),
|
|
63
|
+
panel("Page", page),
|
|
64
|
+
panel("Mode", mode),
|
|
65
|
+
note
|
|
66
|
+
),
|
|
67
|
+
stack(panel("Formatted", result))
|
|
68
|
+
),
|
|
69
|
+
foot: actions(copyButton(() => text, "Copy quote", "fill")),
|
|
70
|
+
});
|
|
71
|
+
slot.append(card.node);
|
|
72
|
+
build();
|
|
73
|
+
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default { mount };
|