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,83 @@
|
|
|
1
|
+
// Mean, median and mode from a list of numbers.
|
|
2
|
+
import { el, bench, stack, panel, textArea, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function fmt(n) {
|
|
5
|
+
return 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
|
+
function tile(label) {
|
|
15
|
+
const value = el("span", { class: "mvb-stat__v" }, "-");
|
|
16
|
+
const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
|
|
17
|
+
node.set = (text) => { value.textContent = text; };
|
|
18
|
+
return node;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function mount(slot) {
|
|
22
|
+
const source = textArea({ rows: 8, value: "4, 8, 15, 16, 23, 42", "aria-label": "Numbers", placeholder: "4, 8, 15, 16, 23, 42" });
|
|
23
|
+
const mean = tile("Mean");
|
|
24
|
+
const median = tile("Median");
|
|
25
|
+
const mode = tile("Mode");
|
|
26
|
+
const count = tile("Count");
|
|
27
|
+
const sum = tile("Sum");
|
|
28
|
+
const stats = el("div", { class: "mvb-stats" }, mean, median, mode, count, sum);
|
|
29
|
+
const error = notice("error");
|
|
30
|
+
let text = "";
|
|
31
|
+
|
|
32
|
+
function clear() {
|
|
33
|
+
for (const item of [mean, median, mode, count, sum]) item.set("-");
|
|
34
|
+
text = "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function run() {
|
|
38
|
+
error.write("");
|
|
39
|
+
const { nums, invalid } = parseNumbers(source.value);
|
|
40
|
+
if (!nums.length) {
|
|
41
|
+
clear();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (invalid) {
|
|
45
|
+
clear();
|
|
46
|
+
error.write("Use numbers only, separated by spaces, commas or new lines.");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const total = nums.reduce((acc, n) => acc + n, 0);
|
|
50
|
+
const average = total / nums.length;
|
|
51
|
+
const sorted = nums.slice().sort((x, y) => x - y);
|
|
52
|
+
const middle = Math.floor(sorted.length / 2);
|
|
53
|
+
const med = sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
54
|
+
|
|
55
|
+
const counts = new Map();
|
|
56
|
+
for (const n of nums) counts.set(n, (counts.get(n) ?? 0) + 1);
|
|
57
|
+
const top = Math.max(...counts.values());
|
|
58
|
+
const modes = top > 1 ? [...counts].filter(([, hits]) => hits === top).map(([value]) => value) : [];
|
|
59
|
+
|
|
60
|
+
mean.set(fmt(average));
|
|
61
|
+
median.set(fmt(med));
|
|
62
|
+
mode.set(modes.length ? modes.map(fmt).join(", ") : "None");
|
|
63
|
+
count.set(String(nums.length));
|
|
64
|
+
sum.set(fmt(total));
|
|
65
|
+
text = `Count ${nums.length}, sum ${fmt(total)}, mean ${fmt(average)}, median ${fmt(med)}, mode ${modes.length ? modes.map(fmt).join(", ") : "none"}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
source.addEventListener("input", run);
|
|
69
|
+
|
|
70
|
+
const card = toolcard({
|
|
71
|
+
body: bench(
|
|
72
|
+
stack(panel("Numbers", source), error),
|
|
73
|
+
stack(stats)
|
|
74
|
+
),
|
|
75
|
+
foot: actions(copyButton(() => text, "Copy summary", "fill")),
|
|
76
|
+
});
|
|
77
|
+
slot.append(card.node);
|
|
78
|
+
run();
|
|
79
|
+
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default { mount };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Sorts a reference list alphabetically by the start of each line.
|
|
2
|
+
import { bench, stack, panel, textArea, selectInput, output, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const DIRECTIONS = [
|
|
5
|
+
{ value: "az", label: "A to Z" },
|
|
6
|
+
{ value: "za", label: "Z to A" },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export function mount(slot) {
|
|
10
|
+
const source = textArea({ rows: 12, "aria-label": "References", placeholder: "One reference per line" });
|
|
11
|
+
const direction = selectInput(DIRECTIONS, { value: "az", "aria-label": "Direction" });
|
|
12
|
+
const result = output({ rows: 12 });
|
|
13
|
+
const note = notice("note");
|
|
14
|
+
note.write("References are ordered by the start of each line.");
|
|
15
|
+
let text = "";
|
|
16
|
+
|
|
17
|
+
function run() {
|
|
18
|
+
const lines = source.value.split(/\r?\n/).filter((line) => line.trim());
|
|
19
|
+
const sorted = lines
|
|
20
|
+
.slice()
|
|
21
|
+
.sort((a, b) => a.trim().localeCompare(b.trim(), undefined, { sensitivity: "base", numeric: true }));
|
|
22
|
+
if (direction.value === "za") sorted.reverse();
|
|
23
|
+
text = sorted.join("\n");
|
|
24
|
+
result.write(text);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
source.addEventListener("input", run);
|
|
28
|
+
direction.addEventListener("change", run);
|
|
29
|
+
|
|
30
|
+
const card = toolcard({
|
|
31
|
+
body: bench(
|
|
32
|
+
stack(panel("References", source), panel("Direction", direction), note),
|
|
33
|
+
stack(panel("Sorted", result))
|
|
34
|
+
),
|
|
35
|
+
foot: actions(copyButton(() => text, "Copy list", "fill")),
|
|
36
|
+
});
|
|
37
|
+
slot.append(card.node);
|
|
38
|
+
run();
|
|
39
|
+
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default { mount };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Builds a BibTeX entry with a citation key from source details.
|
|
2
|
+
import { bench, stack, panel, textInput, selectInput, output, notice, copyButton, downloadButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const TYPES = [
|
|
5
|
+
{ value: "article", label: "Article" },
|
|
6
|
+
{ value: "book", label: "Book" },
|
|
7
|
+
{ value: "inproceedings", label: "In proceedings" },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const parts = (raw) => raw.split(";").map((item) => item.trim()).filter(Boolean);
|
|
11
|
+
const surname = (author) => author.split(",")[0].trim().replace(/[^A-Za-z]/g, "");
|
|
12
|
+
|
|
13
|
+
function bibAuthors(raw) {
|
|
14
|
+
const list = parts(raw);
|
|
15
|
+
return list.join(" and ");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeKey(authors, year) {
|
|
19
|
+
const first = parts(authors)[0] ? surname(parts(authors)[0]) : "ref";
|
|
20
|
+
return `${first}${year}`.toLowerCase() || "entry";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function mount(slot) {
|
|
24
|
+
const type = selectInput(TYPES, { value: "article", "aria-label": "Entry type" });
|
|
25
|
+
const key = textInput({ placeholder: "Derived from the first author and year", "aria-label": "Citation key" });
|
|
26
|
+
const authors = textInput({ placeholder: "Smith, J. A.; Doe, B.", "aria-label": "Authors" });
|
|
27
|
+
const title = textInput({ placeholder: "Title of the work", "aria-label": "Title" });
|
|
28
|
+
const year = textInput({ placeholder: "2020", "aria-label": "Year" });
|
|
29
|
+
const container = textInput({ placeholder: "Journal or book title", "aria-label": "Journal or book title" });
|
|
30
|
+
const volume = textInput({ placeholder: "12", "aria-label": "Volume" });
|
|
31
|
+
const pages = textInput({ placeholder: "34-45", "aria-label": "Pages" });
|
|
32
|
+
const publisher = textInput({ placeholder: "Publisher", "aria-label": "Publisher" });
|
|
33
|
+
const doi = textInput({ placeholder: "10.0000/abc", "aria-label": "DOI" });
|
|
34
|
+
|
|
35
|
+
const result = output({ rows: 14 });
|
|
36
|
+
const note = notice("note");
|
|
37
|
+
note.write("Check the details against the source and the target style guide.");
|
|
38
|
+
let bibText = "";
|
|
39
|
+
|
|
40
|
+
function build() {
|
|
41
|
+
const fields = [];
|
|
42
|
+
const au = bibAuthors(authors.value);
|
|
43
|
+
if (au) fields.push(["author", au]);
|
|
44
|
+
if (title.value.trim()) fields.push(["title", title.value.trim()]);
|
|
45
|
+
if (type.value === "article") {
|
|
46
|
+
if (container.value.trim()) fields.push(["journal", container.value.trim()]);
|
|
47
|
+
} else if (type.value === "inproceedings") {
|
|
48
|
+
if (container.value.trim()) fields.push(["booktitle", container.value.trim()]);
|
|
49
|
+
} else if (publisher.value.trim()) {
|
|
50
|
+
fields.push(["publisher", publisher.value.trim()]);
|
|
51
|
+
}
|
|
52
|
+
if (year.value.trim()) fields.push(["year", year.value.trim()]);
|
|
53
|
+
if (type.value === "article" && volume.value.trim()) fields.push(["volume", volume.value.trim()]);
|
|
54
|
+
if (pages.value.trim()) fields.push(["pages", pages.value.trim()]);
|
|
55
|
+
if (type.value === "inproceedings" && publisher.value.trim()) fields.push(["publisher", publisher.value.trim()]);
|
|
56
|
+
if (doi.value.trim()) fields.push(["doi", doi.value.trim()]);
|
|
57
|
+
|
|
58
|
+
const entryKey = key.value.trim() || makeKey(authors.value, year.value.trim());
|
|
59
|
+
const lines = fields.map(([name, value]) => ` ${name} = {${value}},`);
|
|
60
|
+
if (lines.length) lines[lines.length - 1] = lines[lines.length - 1].replace(/,$/, "");
|
|
61
|
+
bibText = [`@${type.value}{${entryKey},`, ...lines, "}"].join("\n");
|
|
62
|
+
result.write(bibText);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const input of [key, authors, title, year, container, volume, pages, publisher, doi]) {
|
|
66
|
+
input.addEventListener("input", build);
|
|
67
|
+
}
|
|
68
|
+
type.addEventListener("change", build);
|
|
69
|
+
|
|
70
|
+
const card = toolcard({
|
|
71
|
+
body: bench(
|
|
72
|
+
stack(
|
|
73
|
+
panel("Entry type", type),
|
|
74
|
+
panel("Citation key", key),
|
|
75
|
+
panel("Authors", authors),
|
|
76
|
+
panel("Title", title),
|
|
77
|
+
panel("Year", year),
|
|
78
|
+
panel("Journal or book title", container),
|
|
79
|
+
panel("Volume", volume),
|
|
80
|
+
panel("Pages", pages),
|
|
81
|
+
panel("Publisher", publisher),
|
|
82
|
+
panel("DOI", doi),
|
|
83
|
+
note
|
|
84
|
+
),
|
|
85
|
+
stack(panel("BibTeX entry", result))
|
|
86
|
+
),
|
|
87
|
+
foot: actions(
|
|
88
|
+
downloadButton(() => bibText, "reference.bib", "Save .bib", "text/plain"),
|
|
89
|
+
copyButton(() => bibText, "Copy entry", "fill")
|
|
90
|
+
),
|
|
91
|
+
});
|
|
92
|
+
slot.append(card.node);
|
|
93
|
+
build();
|
|
94
|
+
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default { mount };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Adds or subtracts two binary numbers and shows the result in binary and decimal.
|
|
2
|
+
import { el, bench, stack, panel, textInput, chipRow, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function parse(text) {
|
|
5
|
+
const clean = text.trim().replace(/[\s_]/g, "").replace(/^0b/i, "");
|
|
6
|
+
if (!clean || !/^[01]+$/.test(clean)) return null;
|
|
7
|
+
return BigInt(`0b${clean}`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function mount(slot) {
|
|
11
|
+
const first = textInput({ value: "1011", class: "mvb-in mvb-in--mono", "aria-label": "First number" });
|
|
12
|
+
const second = textInput({ value: "0110", class: "mvb-in mvb-in--mono", "aria-label": "Second number" });
|
|
13
|
+
const result = el("span", { class: "mvb-stat__v" }, "-");
|
|
14
|
+
const stats = el("div", { class: "mvb-stats" }, el("div", { class: "mvb-stat" }, el("span", { class: "mvb-stat__k" }, "Binary"), result));
|
|
15
|
+
const decimal = el("p", { class: "mvb-note" });
|
|
16
|
+
const error = notice("error");
|
|
17
|
+
let mode = "add";
|
|
18
|
+
let text = "";
|
|
19
|
+
|
|
20
|
+
const chips = chipRow(
|
|
21
|
+
[{ value: "add", label: "Add" }, { value: "subtract", label: "Subtract" }],
|
|
22
|
+
{ value: mode, label: "Operation", onChange: (value) => { mode = value; run(); } }
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
function run() {
|
|
26
|
+
error.write("");
|
|
27
|
+
const a = parse(first.value);
|
|
28
|
+
const b = parse(second.value);
|
|
29
|
+
if (a === null || b === null) {
|
|
30
|
+
result.textContent = "-";
|
|
31
|
+
decimal.textContent = "";
|
|
32
|
+
text = "";
|
|
33
|
+
error.write("Use only 0 and 1.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const value = mode === "add" ? a + b : a - b;
|
|
37
|
+
result.textContent = value.toString(2);
|
|
38
|
+
decimal.textContent = `Decimal ${value.toString(10)}`;
|
|
39
|
+
text = `${value.toString(2)} (decimal ${value.toString(10)})`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
first.addEventListener("input", run);
|
|
43
|
+
second.addEventListener("input", run);
|
|
44
|
+
|
|
45
|
+
const card = toolcard({
|
|
46
|
+
body: bench(
|
|
47
|
+
stack(panel("First number", first), panel("Second number", second), panel("Operation", chips), error),
|
|
48
|
+
stack(panel("Result", stats), decimal)
|
|
49
|
+
),
|
|
50
|
+
foot: actions(copyButton(() => text, "Copy result", "fill")),
|
|
51
|
+
});
|
|
52
|
+
slot.append(card.node);
|
|
53
|
+
run();
|
|
54
|
+
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default { mount };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Round the corners of a box, each corner on its own or linked.
|
|
2
|
+
import { el, bench, stack, rangeInput, switchControl, output, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const KEYS = ["tl", "tr", "br", "bl"];
|
|
5
|
+
|
|
6
|
+
function slider(label, value) {
|
|
7
|
+
const input = rangeInput({ min: 0, max: 100, value, "aria-label": label });
|
|
8
|
+
const val = el("span", { class: "mvb-slider__val" }, `${value}px`);
|
|
9
|
+
const node = el(
|
|
10
|
+
"div",
|
|
11
|
+
{ class: "mvb-slider" },
|
|
12
|
+
el("span", { class: "mvb-slider__label" }, label),
|
|
13
|
+
input,
|
|
14
|
+
val
|
|
15
|
+
);
|
|
16
|
+
node.input = input;
|
|
17
|
+
node.value = () => parseFloat(input.value) || 0;
|
|
18
|
+
node.setVal = (text) => { val.textContent = text; };
|
|
19
|
+
return node;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function mount(slot) {
|
|
23
|
+
const corners = { tl: 16, tr: 16, br: 16, bl: 16 };
|
|
24
|
+
let linked = true;
|
|
25
|
+
|
|
26
|
+
const sliders = {
|
|
27
|
+
tl: slider("Top left", corners.tl),
|
|
28
|
+
tr: slider("Top right", corners.tr),
|
|
29
|
+
br: slider("Bottom right", corners.br),
|
|
30
|
+
bl: slider("Bottom left", corners.bl),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const linkSwitch = switchControl("Link corners", { checked: true });
|
|
34
|
+
const previewBox = el("div", { class: "mvb-preview__box" });
|
|
35
|
+
const code = output();
|
|
36
|
+
|
|
37
|
+
function css() {
|
|
38
|
+
const { tl, tr, br, bl } = corners;
|
|
39
|
+
if (tl === tr && tr === br && br === bl) return `border-radius: ${tl}px;`;
|
|
40
|
+
return `border-radius: ${tl}px ${tr}px ${br}px ${bl}px;`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function update() {
|
|
44
|
+
previewBox.style.borderRadius = `${corners.tl}px ${corners.tr}px ${corners.br}px ${corners.bl}px`;
|
|
45
|
+
code.write(css());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sync(value) {
|
|
49
|
+
for (const key of KEYS) {
|
|
50
|
+
corners[key] = value;
|
|
51
|
+
sliders[key].input.value = value;
|
|
52
|
+
sliders[key].setVal(`${value}px`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const key of KEYS) {
|
|
57
|
+
const control = sliders[key];
|
|
58
|
+
control.input.addEventListener("input", () => {
|
|
59
|
+
const value = control.value();
|
|
60
|
+
if (linked) sync(value);
|
|
61
|
+
else corners[key] = value;
|
|
62
|
+
control.setVal(`${value}px`);
|
|
63
|
+
update();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
linkSwitch.input.addEventListener("change", () => {
|
|
68
|
+
linked = linkSwitch.input.checked;
|
|
69
|
+
if (linked) {
|
|
70
|
+
sync(corners.tl);
|
|
71
|
+
update();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const card = toolcard({
|
|
76
|
+
body: bench(
|
|
77
|
+
stack(linkSwitch, sliders.tl, sliders.tr, sliders.br, sliders.bl),
|
|
78
|
+
stack(el("div", { class: "mvb-preview" }, previewBox), code)
|
|
79
|
+
),
|
|
80
|
+
foot: actions(copyButton(css, "Copy CSS", "fill")),
|
|
81
|
+
});
|
|
82
|
+
slot.append(card.node);
|
|
83
|
+
update();
|
|
84
|
+
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default { mount };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Build a CSS box-shadow with a live preview.
|
|
2
|
+
import { el, bench, stack, panel, rangeInput, colorInput, switchControl, output, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function hexToRgb(hex) {
|
|
5
|
+
const clean = String(hex).trim().replace(/^#/, "");
|
|
6
|
+
const value = parseInt(clean.length === 3 ? clean.split("").map((c) => c + c).join("") : clean, 16);
|
|
7
|
+
return { r: (value >> 16) & 255, g: (value >> 8) & 255, b: value & 255 };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function slider(label, { min, max, value, unit = "px" }) {
|
|
11
|
+
const input = rangeInput({ min, max, value, "aria-label": label });
|
|
12
|
+
const val = el("span", { class: "mvb-slider__val" }, `${value}${unit}`);
|
|
13
|
+
const node = el(
|
|
14
|
+
"div",
|
|
15
|
+
{ class: "mvb-slider" },
|
|
16
|
+
el("span", { class: "mvb-slider__label" }, label),
|
|
17
|
+
input,
|
|
18
|
+
val
|
|
19
|
+
);
|
|
20
|
+
node.input = input;
|
|
21
|
+
node.value = () => parseFloat(input.value) || 0;
|
|
22
|
+
node.setVal = (text) => { val.textContent = text; };
|
|
23
|
+
return node;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function mount(slot) {
|
|
27
|
+
const x = slider("X offset", { min: -50, max: 50, value: 0 });
|
|
28
|
+
const y = slider("Y offset", { min: -50, max: 50, value: 8 });
|
|
29
|
+
const blur = slider("Blur", { min: 0, max: 100, value: 24 });
|
|
30
|
+
const spread = slider("Spread", { min: -50, max: 50, value: 0 });
|
|
31
|
+
const opacity = slider("Opacity", { min: 0, max: 100, value: 35, unit: "%" });
|
|
32
|
+
const picker = colorInput({ value: "#1b1b1f", "aria-label": "Shadow color" });
|
|
33
|
+
const inset = switchControl("Inset", {});
|
|
34
|
+
const previewBox = el("div", { class: "mvb-preview__box" });
|
|
35
|
+
const code = output();
|
|
36
|
+
|
|
37
|
+
function css() {
|
|
38
|
+
const { r, g, b } = hexToRgb(picker.value);
|
|
39
|
+
const a = (opacity.value() / 100).toFixed(2);
|
|
40
|
+
return `${inset.input.checked ? "inset " : ""}${x.value()}px ${y.value()}px ${blur.value()}px ${spread.value()}px rgba(${r}, ${g}, ${b}, ${a})`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function update() {
|
|
44
|
+
previewBox.style.boxShadow = css();
|
|
45
|
+
code.write(`box-shadow: ${css()};`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const control of [x, y, blur, spread, opacity]) {
|
|
49
|
+
control.input.addEventListener("input", () => {
|
|
50
|
+
const unit = control === opacity ? "%" : "px";
|
|
51
|
+
control.setVal(`${control.value()}${unit}`);
|
|
52
|
+
update();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
picker.addEventListener("input", update);
|
|
56
|
+
inset.input.addEventListener("change", update);
|
|
57
|
+
|
|
58
|
+
const card = toolcard({
|
|
59
|
+
body: bench(
|
|
60
|
+
stack(x, y, blur, spread, opacity, panel("Color", picker), inset),
|
|
61
|
+
stack(el("div", { class: "mvb-preview" }, previewBox), code)
|
|
62
|
+
),
|
|
63
|
+
foot: actions(copyButton(() => `box-shadow: ${css()};`, "Copy CSS", "fill")),
|
|
64
|
+
});
|
|
65
|
+
slot.append(card.node);
|
|
66
|
+
update();
|
|
67
|
+
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default { mount };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Turns a task description into a branch name with a type prefix.
|
|
2
|
+
import { bench, stack, panel, textInput, chipRow, output, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const TYPES = [
|
|
5
|
+
{ value: "feat", label: "feat" },
|
|
6
|
+
{ value: "fix", label: "fix" },
|
|
7
|
+
{ value: "chore", label: "chore" },
|
|
8
|
+
{ value: "docs", label: "docs" },
|
|
9
|
+
{ value: "refactor", label: "refactor" },
|
|
10
|
+
{ value: "test", label: "test" },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const slugify = (text) => text.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
|
|
14
|
+
|
|
15
|
+
export function mount(slot) {
|
|
16
|
+
const description = textInput({ value: "Add dark mode", "aria-label": "Task" });
|
|
17
|
+
const out = output({ rows: 1 });
|
|
18
|
+
let type = "feat";
|
|
19
|
+
let text = "";
|
|
20
|
+
|
|
21
|
+
const chips = chipRow(TYPES, { value: type, label: "Type", onChange: (value) => { type = value; run(); } });
|
|
22
|
+
|
|
23
|
+
function run() {
|
|
24
|
+
const slug = slugify(description.value);
|
|
25
|
+
text = slug ? `${type}/${slug}` : "";
|
|
26
|
+
out.write(text);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
description.addEventListener("input", run);
|
|
30
|
+
|
|
31
|
+
const card = toolcard({
|
|
32
|
+
body: bench(
|
|
33
|
+
stack(panel("Type", chips), panel("Task", description)),
|
|
34
|
+
stack(panel("Branch name", out))
|
|
35
|
+
),
|
|
36
|
+
foot: actions(copyButton(() => text, "Copy branch name", "fill")),
|
|
37
|
+
});
|
|
38
|
+
slot.append(card.node);
|
|
39
|
+
run();
|
|
40
|
+
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default { mount };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Units and revenue needed to cover the fixed costs.
|
|
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 fixed = numberInput({ value: "1000", min: "0", "aria-label": "Fixed costs" });
|
|
17
|
+
const price = numberInput({ value: "25", min: "0", "aria-label": "Price per unit" });
|
|
18
|
+
const variable = numberInput({ value: "15", min: "0", "aria-label": "Variable cost per unit" });
|
|
19
|
+
const unitsTile = tile("Break-even units");
|
|
20
|
+
const revenueTile = tile("Revenue");
|
|
21
|
+
const marginTile = tile("Contribution");
|
|
22
|
+
const stats = el("div", { class: "mvb-stats" }, unitsTile, revenueTile, marginTile);
|
|
23
|
+
const error = notice("error");
|
|
24
|
+
let text = "";
|
|
25
|
+
|
|
26
|
+
function clear() {
|
|
27
|
+
unitsTile.set("-");
|
|
28
|
+
revenueTile.set("-");
|
|
29
|
+
marginTile.set("-");
|
|
30
|
+
text = "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function run() {
|
|
34
|
+
error.write("");
|
|
35
|
+
const inputs = [fixed, price, variable];
|
|
36
|
+
const values = inputs.map((input) => Number(input.value));
|
|
37
|
+
if (inputs.some((input) => input.value.trim() === "") || !values.every(Number.isFinite)) {
|
|
38
|
+
clear();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const [f, p, v] = values;
|
|
42
|
+
if (f < 0 || p < 0 || v < 0) { clear(); error.write("The values cannot be negative."); return; }
|
|
43
|
+
const contribution = p - v;
|
|
44
|
+
if (contribution <= 0) { clear(); error.write("The price must be above the variable cost."); return; }
|
|
45
|
+
const units = Math.ceil(f / contribution);
|
|
46
|
+
const revenue = units * p;
|
|
47
|
+
unitsTile.set(String(units));
|
|
48
|
+
revenueTile.set(fmt(revenue));
|
|
49
|
+
marginTile.set(fmt(contribution));
|
|
50
|
+
text = `Break-even ${units} units, revenue ${fmt(revenue)}, contribution ${fmt(contribution)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const input of [fixed, price, variable]) input.addEventListener("input", run);
|
|
54
|
+
|
|
55
|
+
const card = toolcard({
|
|
56
|
+
body: bench(
|
|
57
|
+
stack(panel("Fixed costs", fixed), panel("Price per unit", price), panel("Variable cost per unit", variable), error),
|
|
58
|
+
stack(stats)
|
|
59
|
+
),
|
|
60
|
+
foot: actions(copyButton(() => text, "Copy result", "fill")),
|
|
61
|
+
});
|
|
62
|
+
slot.append(card.node);
|
|
63
|
+
run();
|
|
64
|
+
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default { mount };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Counts working days between two dates, skipping weekends and listed holidays.
|
|
2
|
+
import { el, bench, stack, panel, textInput, textArea, checkbox, toggleRow, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const DAY = 86400000;
|
|
5
|
+
const DAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
6
|
+
|
|
7
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
8
|
+
|
|
9
|
+
function parseDate(value) {
|
|
10
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
11
|
+
if (!match) return null;
|
|
12
|
+
const year = Number(match[1]);
|
|
13
|
+
const month = Number(match[2]) - 1;
|
|
14
|
+
const day = Number(match[3]);
|
|
15
|
+
const date = new Date(year, month, day);
|
|
16
|
+
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return null;
|
|
17
|
+
return date;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const iso = (date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
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 start = textInput({ type: "date", value: "2026-01-01", "aria-label": "Start date" });
|
|
27
|
+
const end = textInput({ type: "date", value: "2026-01-31", "aria-label": "End date" });
|
|
28
|
+
const includeEnd = checkbox("Count the end date", { checked: true });
|
|
29
|
+
const weekdays = toggleRow(DAY_LABELS.map((label, index) => ({ value: index, label, checked: index >= 1 && index <= 5 })));
|
|
30
|
+
const holidays = textArea({ rows: 4, placeholder: "2026-01-01", "aria-label": "Holidays, one per line" });
|
|
31
|
+
|
|
32
|
+
const working = el("span", { class: "mvb-kv__v" }, "-");
|
|
33
|
+
const calendar = el("span", { class: "mvb-kv__v" }, "-");
|
|
34
|
+
const weekend = el("span", { class: "mvb-kv__v" }, "-");
|
|
35
|
+
const holidayCount = el("span", { class: "mvb-kv__v" }, "-");
|
|
36
|
+
const kv = el(
|
|
37
|
+
"div",
|
|
38
|
+
{ class: "mvb-kv" },
|
|
39
|
+
kvRow("Working days", working),
|
|
40
|
+
kvRow("Calendar days", calendar),
|
|
41
|
+
kvRow("Weekend days", weekend),
|
|
42
|
+
kvRow("Holidays", holidayCount)
|
|
43
|
+
);
|
|
44
|
+
const error = notice("error");
|
|
45
|
+
let text = "";
|
|
46
|
+
|
|
47
|
+
function clear() {
|
|
48
|
+
working.textContent = "-";
|
|
49
|
+
calendar.textContent = "-";
|
|
50
|
+
weekend.textContent = "-";
|
|
51
|
+
holidayCount.textContent = "-";
|
|
52
|
+
text = "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function run() {
|
|
56
|
+
error.write("");
|
|
57
|
+
const a = parseDate(start.value);
|
|
58
|
+
const b = parseDate(end.value);
|
|
59
|
+
if (!a || !b) {
|
|
60
|
+
clear();
|
|
61
|
+
error.write("Pick a valid start and end date.");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (b < a) {
|
|
65
|
+
clear();
|
|
66
|
+
error.write("The end date is before the start date.");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const rangeEnd = includeEnd.input.checked ? b : new Date(b.getFullYear(), b.getMonth(), b.getDate() - 1);
|
|
70
|
+
const workingSet = new Set(weekdays.values().map(Number));
|
|
71
|
+
const holidaySet = new Set();
|
|
72
|
+
for (const line of holidays.value.split("\n")) {
|
|
73
|
+
const date = parseDate(line.trim());
|
|
74
|
+
if (date) holidaySet.add(iso(date));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let work = 0;
|
|
78
|
+
let rest = 0;
|
|
79
|
+
let off = 0;
|
|
80
|
+
const cursor = new Date(a.getFullYear(), a.getMonth(), a.getDate());
|
|
81
|
+
while (cursor <= rangeEnd) {
|
|
82
|
+
if (!workingSet.has(cursor.getDay())) rest += 1;
|
|
83
|
+
else if (holidaySet.has(iso(cursor))) off += 1;
|
|
84
|
+
else work += 1;
|
|
85
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const total = rangeEnd < a ? 0 : Math.round((rangeEnd - a) / DAY) + 1;
|
|
89
|
+
working.textContent = String(work);
|
|
90
|
+
calendar.textContent = String(total);
|
|
91
|
+
weekend.textContent = String(rest);
|
|
92
|
+
holidayCount.textContent = String(off);
|
|
93
|
+
text = `${work} working days`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const input of [start, end]) input.addEventListener("input", run);
|
|
97
|
+
includeEnd.input.addEventListener("change", run);
|
|
98
|
+
holidays.addEventListener("input", run);
|
|
99
|
+
weekdays.addEventListener("change", run);
|
|
100
|
+
|
|
101
|
+
const card = toolcard({
|
|
102
|
+
body: bench(
|
|
103
|
+
stack(
|
|
104
|
+
panel("Start", start),
|
|
105
|
+
panel("End", end),
|
|
106
|
+
includeEnd,
|
|
107
|
+
weekdays,
|
|
108
|
+
error
|
|
109
|
+
),
|
|
110
|
+
stack(panel("Holidays", holidays), panel("Count", kv))
|
|
111
|
+
),
|
|
112
|
+
foot: actions(copyButton(() => text, "Copy result", "fill")),
|
|
113
|
+
});
|
|
114
|
+
slot.append(card.node);
|
|
115
|
+
run();
|
|
116
|
+
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default { mount };
|