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,63 @@
|
|
|
1
|
+
// Builds a canonical link tag and shows the normalized address the tag points
|
|
2
|
+
// to.
|
|
3
|
+
import { el, bench, stack, panel, textInput, output, notice, copyButton, actions, toolcard } from "../core.js";
|
|
4
|
+
|
|
5
|
+
const esc = (value) =>
|
|
6
|
+
String(value).replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char]));
|
|
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 source = textInput({ value: "https://example.com/blog/post/?utm_source=news#comments", "aria-label": "Page URL" });
|
|
13
|
+
const result = output({ rows: 2 });
|
|
14
|
+
const error = notice("error");
|
|
15
|
+
const normalized = el("span", { class: "mvb-kv__v" }, "-");
|
|
16
|
+
const length = el("span", { class: "mvb-kv__v" }, "-");
|
|
17
|
+
const kv = el("div", { class: "mvb-kv" }, kvRow("Canonical URL", normalized), kvRow("Length", length));
|
|
18
|
+
let canonical = "";
|
|
19
|
+
|
|
20
|
+
function clear() {
|
|
21
|
+
canonical = "";
|
|
22
|
+
result.write("");
|
|
23
|
+
normalized.textContent = "-";
|
|
24
|
+
length.textContent = "-";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function run() {
|
|
28
|
+
error.write("");
|
|
29
|
+
const raw = source.value.trim();
|
|
30
|
+
if (!raw) return clear();
|
|
31
|
+
let url;
|
|
32
|
+
try {
|
|
33
|
+
url = new URL(raw);
|
|
34
|
+
} catch {
|
|
35
|
+
error.write("That is not a valid URL.");
|
|
36
|
+
return clear();
|
|
37
|
+
}
|
|
38
|
+
url.hash = "";
|
|
39
|
+
canonical = url.href;
|
|
40
|
+
result.write(`<link rel="canonical" href="${esc(canonical)}" />`);
|
|
41
|
+
normalized.textContent = canonical;
|
|
42
|
+
length.textContent = String(canonical.length);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
source.addEventListener("input", run);
|
|
46
|
+
|
|
47
|
+
const card = toolcard({
|
|
48
|
+
body: bench(
|
|
49
|
+
stack(panel("Page URL", source), error),
|
|
50
|
+
stack(panel("Canonical tag", result), kv)
|
|
51
|
+
),
|
|
52
|
+
foot: actions(
|
|
53
|
+
copyButton(() => canonical, "Copy URL"),
|
|
54
|
+
copyButton(() => result.textContent, "Copy tag", "fill")
|
|
55
|
+
),
|
|
56
|
+
});
|
|
57
|
+
slot.append(card.node);
|
|
58
|
+
run();
|
|
59
|
+
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default { mount };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// A caption checked against a character limit, with the room left.
|
|
2
|
+
import { el, bench, stack, panel, textArea, numberInput, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function tile(label) {
|
|
5
|
+
const value = el("span", { class: "mvb-stat__v" }, "0");
|
|
6
|
+
const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
|
|
7
|
+
node.set = (text) => { value.textContent = text; };
|
|
8
|
+
return node;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function mount(slot) {
|
|
12
|
+
const source = textArea({ rows: 8, "aria-label": "Caption", placeholder: "Type or paste the caption" });
|
|
13
|
+
const limit = numberInput({ min: "0", placeholder: "No limit", "aria-label": "Character limit" });
|
|
14
|
+
|
|
15
|
+
const charsTile = tile("Characters");
|
|
16
|
+
const wordsTile = tile("Words");
|
|
17
|
+
const linesTile = tile("Lines");
|
|
18
|
+
const leftTile = tile("Left");
|
|
19
|
+
const stats = el("div", { class: "mvb-stats" }, charsTile, wordsTile, linesTile, leftTile);
|
|
20
|
+
const note = notice("note");
|
|
21
|
+
|
|
22
|
+
let summary = "";
|
|
23
|
+
|
|
24
|
+
function run() {
|
|
25
|
+
const text = source.value;
|
|
26
|
+
const chars = text.length;
|
|
27
|
+
const words = text.trim() ? text.trim().split(/\s+/).length : 0;
|
|
28
|
+
const lines = text ? text.split("\n").length : 0;
|
|
29
|
+
charsTile.set(String(chars));
|
|
30
|
+
wordsTile.set(String(words));
|
|
31
|
+
linesTile.set(String(lines));
|
|
32
|
+
|
|
33
|
+
const cap = parseInt(limit.value, 10) || 0;
|
|
34
|
+
if (cap > 0) {
|
|
35
|
+
leftTile.set(String(cap - chars));
|
|
36
|
+
note.write(chars > cap ? `${chars - cap} characters over the limit.` : "");
|
|
37
|
+
} else {
|
|
38
|
+
leftTile.set("-");
|
|
39
|
+
note.write("");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
summary = [
|
|
43
|
+
`${chars} characters`,
|
|
44
|
+
`${words} words`,
|
|
45
|
+
`${lines} lines`,
|
|
46
|
+
cap > 0 ? `${cap - chars} left of ${cap}` : "No limit set",
|
|
47
|
+
].join("\n");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
source.addEventListener("input", run);
|
|
51
|
+
limit.addEventListener("input", run);
|
|
52
|
+
|
|
53
|
+
const card = toolcard({
|
|
54
|
+
body: bench(
|
|
55
|
+
stack(panel("Caption", source), panel("Character limit", limit)),
|
|
56
|
+
stack(stats, note)
|
|
57
|
+
),
|
|
58
|
+
foot: actions(copyButton(() => summary, "Copy summary", "fill")),
|
|
59
|
+
});
|
|
60
|
+
slot.append(card.node);
|
|
61
|
+
run();
|
|
62
|
+
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default { mount };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Names the letter case a text uses and lists every case it could become.
|
|
2
|
+
import { el, toast, copyText, bench, stack, panel, textArea, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const wordsOf = (text) => text
|
|
5
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
6
|
+
.replace(/[_\-.]+/g, " ")
|
|
7
|
+
.replace(/\s+/g, " ")
|
|
8
|
+
.trim()
|
|
9
|
+
.split(" ")
|
|
10
|
+
.filter(Boolean);
|
|
11
|
+
|
|
12
|
+
const cap = (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
13
|
+
|
|
14
|
+
const CASES = [
|
|
15
|
+
["UPPERCASE", (words) => words.join(" ").toUpperCase()],
|
|
16
|
+
["lowercase", (words) => words.join(" ").toLowerCase()],
|
|
17
|
+
["Title Case", (words) => words.map(cap).join(" ")],
|
|
18
|
+
["Sentence case", (words) => {
|
|
19
|
+
const line = words.join(" ").toLowerCase();
|
|
20
|
+
return line.charAt(0).toUpperCase() + line.slice(1);
|
|
21
|
+
}],
|
|
22
|
+
["camelCase", (words) => (words[0]?.toLowerCase() ?? "") + words.slice(1).map(cap).join("")],
|
|
23
|
+
["PascalCase", (words) => words.map(cap).join("")],
|
|
24
|
+
["snake_case", (words) => words.map((word) => word.toLowerCase()).join("_")],
|
|
25
|
+
["kebab-case", (words) => words.map((word) => word.toLowerCase()).join("-")],
|
|
26
|
+
["CONSTANT_CASE", (words) => words.map((word) => word.toUpperCase()).join("_")],
|
|
27
|
+
["Train-Case", (words) => words.map(cap).join("-")],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function detectCase(text) {
|
|
31
|
+
const t = text.trim();
|
|
32
|
+
if (!t || !/[a-zA-Z]/.test(t)) return "No letters";
|
|
33
|
+
if (/\s/.test(t)) {
|
|
34
|
+
if (t === t.toUpperCase()) return "UPPERCASE";
|
|
35
|
+
if (t === t.toLowerCase()) return "lowercase";
|
|
36
|
+
const words = t.split(/\s+/);
|
|
37
|
+
if (words.every((word) => /^[A-Z]/.test(word))) return "Title Case";
|
|
38
|
+
if (/^[A-Z]/.test(t) && t.slice(1) === t.slice(1).toLowerCase()) return "Sentence case";
|
|
39
|
+
return "Mixed";
|
|
40
|
+
}
|
|
41
|
+
if (/^[A-Z0-9]+(_[A-Z0-9]+)+$/.test(t)) return "CONSTANT_CASE";
|
|
42
|
+
if (/^[a-z0-9]+(_[a-z0-9]+)+$/.test(t)) return "snake_case";
|
|
43
|
+
if (/^[A-Za-z0-9]+(-[A-Za-z0-9]+)+$/.test(t)) {
|
|
44
|
+
if (t === t.toLowerCase()) return "kebab-case";
|
|
45
|
+
return t.split("-").every((word) => /^[A-Z]/.test(word)) ? "Train-Case" : "Mixed";
|
|
46
|
+
}
|
|
47
|
+
if (/^[a-z]+([A-Z][a-z0-9]*)+$/.test(t)) return "camelCase";
|
|
48
|
+
if (/^([A-Z][a-z0-9]*)+$/.test(t)) return "PascalCase";
|
|
49
|
+
if (/^[A-Z]+$/.test(t)) return "UPPERCASE";
|
|
50
|
+
if (/^[a-z]+$/.test(t)) return "lowercase";
|
|
51
|
+
return "Mixed";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function mount(slot) {
|
|
55
|
+
const source = textArea({ rows: 5, value: "helloWorld", "aria-label": "Text", placeholder: "Type or paste text" });
|
|
56
|
+
const detected = el("span", { class: "mvb-kv__v" }, "No letters");
|
|
57
|
+
const kv = el(
|
|
58
|
+
"div",
|
|
59
|
+
{ class: "mvb-kv" },
|
|
60
|
+
el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, "Detected case"), detected)
|
|
61
|
+
);
|
|
62
|
+
const list = el("div", { class: "mvb-cases" });
|
|
63
|
+
let active = "No letters";
|
|
64
|
+
|
|
65
|
+
function detectedValue() {
|
|
66
|
+
const entry = CASES.find(([name]) => name === active);
|
|
67
|
+
return entry ? entry[1](wordsOf(source.value)) : source.value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function run() {
|
|
71
|
+
const words = wordsOf(source.value);
|
|
72
|
+
active = detectCase(source.value);
|
|
73
|
+
detected.textContent = active;
|
|
74
|
+
for (const row of list.children) {
|
|
75
|
+
const convert = CASES.find(([name]) => name === row.dataset.case)[1];
|
|
76
|
+
row.querySelector(".mvb-case__value").textContent = words.length ? convert(words) : "";
|
|
77
|
+
const on = row.dataset.case === active;
|
|
78
|
+
row.classList.toggle("mvb-on", on);
|
|
79
|
+
row.setAttribute("aria-pressed", String(on));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const [name] of CASES) {
|
|
84
|
+
const button = el(
|
|
85
|
+
"button",
|
|
86
|
+
{
|
|
87
|
+
class: "mvb-case", type: "button", dataset: { case: name }, "aria-pressed": "false",
|
|
88
|
+
onclick: async () => {
|
|
89
|
+
const value = button.querySelector(".mvb-case__value").textContent.trim();
|
|
90
|
+
if (value) toast((await copyText(value)) ? "Copied" : "Copy failed");
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
el("span", { class: "mvb-case__name" }, name),
|
|
94
|
+
el("span", { class: "mvb-case__value" })
|
|
95
|
+
);
|
|
96
|
+
list.append(button);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
source.addEventListener("input", run);
|
|
100
|
+
|
|
101
|
+
const card = toolcard({
|
|
102
|
+
body: bench(
|
|
103
|
+
stack(panel("Text", source)),
|
|
104
|
+
stack(kv, panel("Cases", list))
|
|
105
|
+
),
|
|
106
|
+
foot: actions(copyButton(detectedValue, "Copy detected", "fill")),
|
|
107
|
+
});
|
|
108
|
+
slot.append(card.node);
|
|
109
|
+
run();
|
|
110
|
+
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default { mount };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Adds up notes and coins by denomination.
|
|
2
|
+
import { el, bench, stack, panel, numberInput, notice, copyButton, actions, toolcard, readPin, writePin } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const DENOMINATIONS = [100, 50, 20, 10, 5, 2, 1];
|
|
5
|
+
|
|
6
|
+
function fmt(n) {
|
|
7
|
+
return n.toLocaleString(undefined, { maximumFractionDigits: 2 });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function mount(slot) {
|
|
11
|
+
const PIN = "cash-denomination";
|
|
12
|
+
const saved = readPin(PIN, {});
|
|
13
|
+
const counts = new Map();
|
|
14
|
+
|
|
15
|
+
const listWrap = el("div", { class: "mvb-scroll" });
|
|
16
|
+
const stats = el("div", { class: "mvb-stats" });
|
|
17
|
+
const note = notice("note");
|
|
18
|
+
note.write("Values are generic. Match them to your currency.");
|
|
19
|
+
|
|
20
|
+
const tile = (key, value) => el("div", { class: "mvb-stat" },
|
|
21
|
+
el("span", { class: "mvb-stat__v" }, String(value)),
|
|
22
|
+
el("span", { class: "mvb-stat__k" }, key));
|
|
23
|
+
|
|
24
|
+
const readCount = (value) => {
|
|
25
|
+
const input = counts.get(value);
|
|
26
|
+
const n = Number(input.value);
|
|
27
|
+
return input.value.trim() === "" || !Number.isFinite(n) || n < 0 ? 0 : Math.floor(n);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const total = () => DENOMINATIONS.reduce((sum, value) => sum + value * readCount(value), 0);
|
|
31
|
+
|
|
32
|
+
function save() {
|
|
33
|
+
const data = {};
|
|
34
|
+
for (const value of DENOMINATIONS) data[value] = counts.get(value).value;
|
|
35
|
+
writePin(PIN, data);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function render() {
|
|
39
|
+
const rows = DENOMINATIONS.map((value) => el("tr", {},
|
|
40
|
+
el("th", {}, fmt(value)),
|
|
41
|
+
el("td", {}, fmt(readCount(value))),
|
|
42
|
+
el("td", {}, fmt(value * readCount(value)))));
|
|
43
|
+
listWrap.replaceChildren(el("table", { class: "mvb-ttable" },
|
|
44
|
+
el("thead", {}, el("tr", {}, el("th", {}, "Value"), el("th", {}, "Count"), el("th", {}, "Subtotal"))),
|
|
45
|
+
el("tbody", {}, ...rows)));
|
|
46
|
+
const pieces = DENOMINATIONS.reduce((sum, value) => sum + readCount(value), 0);
|
|
47
|
+
stats.replaceChildren(tile("Total", fmt(total())), tile("Pieces", String(pieces)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const inputs = DENOMINATIONS.map((value) => {
|
|
51
|
+
const input = numberInput({ value: String(saved[value] ?? ""), min: "0", "aria-label": fmt(value) });
|
|
52
|
+
input.addEventListener("input", () => { save(); render(); });
|
|
53
|
+
counts.set(value, input);
|
|
54
|
+
return input;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const card = toolcard({
|
|
58
|
+
body: bench(
|
|
59
|
+
stack(...DENOMINATIONS.map((value, i) => panel(fmt(value), inputs[i])), note),
|
|
60
|
+
stack(stats, panel("Subtotals", listWrap))
|
|
61
|
+
),
|
|
62
|
+
foot: actions(copyButton(
|
|
63
|
+
() => DENOMINATIONS.map((value) => `${fmt(value)} x ${readCount(value)} = ${fmt(value * readCount(value))}`).join("\n"),
|
|
64
|
+
"Copy count",
|
|
65
|
+
"fill"
|
|
66
|
+
)),
|
|
67
|
+
});
|
|
68
|
+
slot.append(card.node);
|
|
69
|
+
render();
|
|
70
|
+
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default { mount };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Money in and out from an opening balance, with a running total.
|
|
2
|
+
import { el, toast, blank, bench, stack, row, panel, textInput, numberInput, chipRow, button, copyButton, downloadButton, actions, toolcard, readPin, writePin } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
|
5
|
+
|
|
6
|
+
function fmt(n) {
|
|
7
|
+
return n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function mount(slot) {
|
|
11
|
+
const PIN = "cash-flow";
|
|
12
|
+
const saved = readPin(PIN, { opening: 0, entries: [] });
|
|
13
|
+
let entries = Array.isArray(saved.entries) ? saved.entries : [];
|
|
14
|
+
|
|
15
|
+
const opening = numberInput({ value: String(saved.opening ?? 0), "aria-label": "Opening balance" });
|
|
16
|
+
const label = textInput({ placeholder: "Label", "aria-label": "Label" });
|
|
17
|
+
const amount = numberInput({ min: "0", placeholder: "Amount", "aria-label": "Amount" });
|
|
18
|
+
let type = "in";
|
|
19
|
+
const typeChips = chipRow(
|
|
20
|
+
[{ value: "in", label: "In" }, { value: "out", label: "Out" }],
|
|
21
|
+
{ value: "in", label: "Direction", onChange: (next) => { type = next; } }
|
|
22
|
+
);
|
|
23
|
+
const listWrap = el("div", { class: "mvb-scroll" });
|
|
24
|
+
const stats = el("div", { class: "mvb-stats" });
|
|
25
|
+
|
|
26
|
+
const addButton = button("Add", { icon: "plus", kind: "fill" });
|
|
27
|
+
|
|
28
|
+
const save = () => writePin(PIN, { opening: Number(opening.value) || 0, entries });
|
|
29
|
+
const tile = (key, value) => el("div", { class: "mvb-stat" },
|
|
30
|
+
el("span", { class: "mvb-stat__v" }, String(value)),
|
|
31
|
+
el("span", { class: "mvb-stat__k" }, key));
|
|
32
|
+
|
|
33
|
+
function totals() {
|
|
34
|
+
let balance = Number(opening.value) || 0;
|
|
35
|
+
let moneyIn = 0;
|
|
36
|
+
let moneyOut = 0;
|
|
37
|
+
const lines = [`Opening ${fmt(balance)}`];
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (entry.type === "in") { balance += entry.amount; moneyIn += entry.amount; }
|
|
40
|
+
else { balance -= entry.amount; moneyOut += entry.amount; }
|
|
41
|
+
lines.push(`${entry.type === "in" ? "+" : "-"} ${fmt(entry.amount)} ${entry.label} balance ${fmt(balance)}`);
|
|
42
|
+
}
|
|
43
|
+
lines.push(`Closing ${fmt(balance)}`);
|
|
44
|
+
return { lines, moneyIn, moneyOut, closing: balance };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function removeButton(entry) {
|
|
48
|
+
const node = button("", { icon: "trash", "aria-label": `Remove ${entry.label}` });
|
|
49
|
+
node.addEventListener("click", () => {
|
|
50
|
+
entries = entries.filter((item) => item.id !== entry.id);
|
|
51
|
+
save();
|
|
52
|
+
render();
|
|
53
|
+
});
|
|
54
|
+
return node;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function render() {
|
|
58
|
+
if (!entries.length) {
|
|
59
|
+
listWrap.replaceChildren(blank("banknote", "No entries yet."));
|
|
60
|
+
} else {
|
|
61
|
+
let balance = Number(opening.value) || 0;
|
|
62
|
+
const rows = entries.map((entry) => {
|
|
63
|
+
if (entry.type === "in") balance += entry.amount;
|
|
64
|
+
else balance -= entry.amount;
|
|
65
|
+
return el("tr", {},
|
|
66
|
+
el("th", {}, entry.label),
|
|
67
|
+
el("td", {}, entry.type === "in" ? fmt(entry.amount) : ""),
|
|
68
|
+
el("td", {}, entry.type === "out" ? fmt(entry.amount) : ""),
|
|
69
|
+
el("td", {}, fmt(balance)),
|
|
70
|
+
el("td", {}, removeButton(entry)));
|
|
71
|
+
});
|
|
72
|
+
listWrap.replaceChildren(el("table", { class: "mvb-ttable" },
|
|
73
|
+
el("thead", {}, el("tr", {}, el("th", {}, "Label"), el("th", {}, "In"), el("th", {}, "Out"), el("th", {}, "Balance"), el("th", {}, ""))),
|
|
74
|
+
el("tbody", {}, ...rows)));
|
|
75
|
+
}
|
|
76
|
+
const { moneyIn, moneyOut, closing } = totals();
|
|
77
|
+
stats.replaceChildren(tile("In", fmt(moneyIn)), tile("Out", fmt(moneyOut)), tile("Closing", fmt(closing)));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function add() {
|
|
81
|
+
const text = label.value.trim();
|
|
82
|
+
const value = Number(amount.value);
|
|
83
|
+
if (!text) return toast("Add a label first");
|
|
84
|
+
if (amount.value.trim() === "" || !Number.isFinite(value) || value < 0) return toast("Add an amount");
|
|
85
|
+
entries.push({ id: uid(), label: text, type, amount: value });
|
|
86
|
+
label.value = "";
|
|
87
|
+
amount.value = "";
|
|
88
|
+
label.focus();
|
|
89
|
+
save();
|
|
90
|
+
render();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
label.addEventListener("keydown", (event) => { if (event.key === "Enter") add(); });
|
|
94
|
+
addButton.addEventListener("click", add);
|
|
95
|
+
opening.addEventListener("input", () => { save(); render(); });
|
|
96
|
+
|
|
97
|
+
function csv() {
|
|
98
|
+
const rows = [["Label", "Type", "Amount"]];
|
|
99
|
+
for (const entry of entries) rows.push([entry.label, entry.type, String(entry.amount)]);
|
|
100
|
+
return rows.map((cells) => cells.map((cell) => `"${String(cell).replaceAll('"', '""')}"`).join(",")).join("\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const card = toolcard({
|
|
104
|
+
body: bench(
|
|
105
|
+
stack(
|
|
106
|
+
panel("Opening balance", opening),
|
|
107
|
+
panel("Label", label),
|
|
108
|
+
row(panel("Direction", typeChips), panel("Amount", amount))
|
|
109
|
+
),
|
|
110
|
+
stack(stats, panel("Entries", listWrap))
|
|
111
|
+
),
|
|
112
|
+
foot: actions(
|
|
113
|
+
copyButton(() => totals().lines.join("\n"), "Copy summary"),
|
|
114
|
+
downloadButton(() => csv(), "cash-flow.csv", "Save CSV", "text/csv"),
|
|
115
|
+
addButton
|
|
116
|
+
),
|
|
117
|
+
});
|
|
118
|
+
slot.append(card.node);
|
|
119
|
+
render();
|
|
120
|
+
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default { mount };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Days left until a certificate expiry date, with a warning threshold.
|
|
2
|
+
import { el, textInput, numberInput, notice, copyButton, bench, stack, panel, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const DAY = 86400000;
|
|
5
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
6
|
+
|
|
7
|
+
function tile(label) {
|
|
8
|
+
const value = el("span", { class: "mvb-stat__v" }, "-");
|
|
9
|
+
const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
|
|
10
|
+
node.set = (text) => { value.textContent = text; };
|
|
11
|
+
return node;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function mount(slot) {
|
|
15
|
+
const soon = new Date(Date.now() + 90 * DAY);
|
|
16
|
+
const dateField = textInput({
|
|
17
|
+
type: "date",
|
|
18
|
+
value: `${soon.getFullYear()}-${pad(soon.getMonth() + 1)}-${pad(soon.getDate())}`,
|
|
19
|
+
"aria-label": "Expiry date",
|
|
20
|
+
});
|
|
21
|
+
const warnField = numberInput({ min: "1", max: "365", value: "30", "aria-label": "Warn threshold in days" });
|
|
22
|
+
|
|
23
|
+
const daysTile = tile("Days left");
|
|
24
|
+
const weeksTile = tile("Weeks left");
|
|
25
|
+
const statusTile = tile("Status");
|
|
26
|
+
const stats = el("div", { class: "mvb-stats" }, daysTile, weeksTile, statusTile);
|
|
27
|
+
const status = notice("note");
|
|
28
|
+
const error = notice("error");
|
|
29
|
+
let summary = "";
|
|
30
|
+
|
|
31
|
+
function run() {
|
|
32
|
+
error.write("");
|
|
33
|
+
status.write("");
|
|
34
|
+
summary = "";
|
|
35
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateField.value);
|
|
36
|
+
if (!match) {
|
|
37
|
+
daysTile.set("-");
|
|
38
|
+
weeksTile.set("-");
|
|
39
|
+
statusTile.set("-");
|
|
40
|
+
error.write("Enter an expiry date.");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const expiry = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
44
|
+
const now = new Date();
|
|
45
|
+
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
46
|
+
const days = Math.round((expiry - start) / DAY);
|
|
47
|
+
const threshold = Math.max(0, parseInt(warnField.value, 10) || 0);
|
|
48
|
+
|
|
49
|
+
daysTile.set(String(days));
|
|
50
|
+
weeksTile.set(String(Math.round(days / 7)));
|
|
51
|
+
const state = days < 0 ? "Expired" : days <= threshold ? "Soon" : "Valid";
|
|
52
|
+
statusTile.set(state);
|
|
53
|
+
|
|
54
|
+
if (days < 0) status.write(`Expired ${Math.abs(days)} days ago.`);
|
|
55
|
+
else if (days === 0) status.write("Expires today.");
|
|
56
|
+
else if (days <= threshold) status.write(`Expires in ${days} days, within the ${threshold} day warning window.`);
|
|
57
|
+
else status.write(`Expires in ${days} days.`);
|
|
58
|
+
|
|
59
|
+
summary = `Expires ${expiry.toLocaleDateString()}, ${days} days left, ${state}.`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
dateField.addEventListener("input", run);
|
|
63
|
+
warnField.addEventListener("input", run);
|
|
64
|
+
|
|
65
|
+
const card = toolcard({
|
|
66
|
+
body: bench(
|
|
67
|
+
stack(
|
|
68
|
+
panel("Expiry date", dateField),
|
|
69
|
+
panel("Warn within days", warnField),
|
|
70
|
+
el("p", { class: "mvb-note" }, "The date stays in this tab."),
|
|
71
|
+
error
|
|
72
|
+
),
|
|
73
|
+
stack(stats, status)
|
|
74
|
+
),
|
|
75
|
+
foot: actions(copyButton(() => summary, "Copy summary", "fill")),
|
|
76
|
+
});
|
|
77
|
+
slot.append(card.node);
|
|
78
|
+
run();
|
|
79
|
+
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default { mount };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Drafts a changelog entry grouped under added, changed, fixed and removed.
|
|
2
|
+
import { el, bench, stack, panel, textInput, textArea, output, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const GROUPS = [
|
|
5
|
+
{ key: "added", label: "Added" },
|
|
6
|
+
{ key: "changed", label: "Changed" },
|
|
7
|
+
{ key: "fixed", label: "Fixed" },
|
|
8
|
+
{ key: "removed", label: "Removed" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export function mount(slot) {
|
|
12
|
+
const version = textInput({ value: "1.0.0", class: "mvb-in mvb-in--mono", "aria-label": "Version" });
|
|
13
|
+
const date = el("input", { class: "mvb-in", type: "date", value: new Date().toISOString().slice(0, 10), "aria-label": "Date" });
|
|
14
|
+
const fields = GROUPS.map((group) => ({ ...group, area: textArea({ rows: 3, "aria-label": group.label }) }));
|
|
15
|
+
const out = output({ rows: 16 });
|
|
16
|
+
let text = "";
|
|
17
|
+
|
|
18
|
+
function run() {
|
|
19
|
+
const lines = [`## [${version.value.trim() || "Unreleased"}] - ${date.value}`, ""];
|
|
20
|
+
for (const group of fields) {
|
|
21
|
+
const items = group.area.value.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
22
|
+
if (!items.length) continue;
|
|
23
|
+
lines.push(`### ${group.label}`, "");
|
|
24
|
+
for (const item of items) lines.push(`- ${item}`);
|
|
25
|
+
lines.push("");
|
|
26
|
+
}
|
|
27
|
+
text = `${lines.join("\n").replace(/\n+$/, "")}\n`;
|
|
28
|
+
out.write(text);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
version.addEventListener("input", run);
|
|
32
|
+
date.addEventListener("input", run);
|
|
33
|
+
for (const group of fields) group.area.addEventListener("input", run);
|
|
34
|
+
|
|
35
|
+
const card = toolcard({
|
|
36
|
+
body: bench(
|
|
37
|
+
stack(panel("Version", version), panel("Date", date), ...fields.map((group) => panel(group.label, group.area))),
|
|
38
|
+
stack(panel("Changelog", out))
|
|
39
|
+
),
|
|
40
|
+
foot: actions(copyButton(() => text, "Copy Markdown", "fill")),
|
|
41
|
+
});
|
|
42
|
+
slot.append(card.node);
|
|
43
|
+
run();
|
|
44
|
+
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default { mount };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Character counts that follow the text, with an optional limit and the
|
|
2
|
+
// number of characters left.
|
|
3
|
+
import { el, bench, stack, panel, textArea, numberInput, notice, button, copyButton, actions, toolcard } from "../core.js";
|
|
4
|
+
|
|
5
|
+
const chars = (text) => [...text].length;
|
|
6
|
+
const withoutSpaces = (text) => [...text.replace(/\s/g, "")].length;
|
|
7
|
+
|
|
8
|
+
function tile(label) {
|
|
9
|
+
const value = el("span", { class: "mvb-stat__v" }, "0");
|
|
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 source = textArea({ rows: 10, "aria-label": "Text", placeholder: "Type or paste text" });
|
|
17
|
+
const limitField = numberInput({ min: "0", placeholder: "No limit", "aria-label": "Character limit" });
|
|
18
|
+
|
|
19
|
+
const totalTile = tile("Characters");
|
|
20
|
+
const noSpaceTile = tile("Without spaces");
|
|
21
|
+
const wordsTile = tile("Words");
|
|
22
|
+
const stats = el("div", { class: "mvb-stats" }, totalTile, noSpaceTile, wordsTile);
|
|
23
|
+
|
|
24
|
+
const meter = el("div", { class: "mvb-meter", role: "img", "aria-label": "Character limit" }, el("div", { class: "mvb-meter__fill" }));
|
|
25
|
+
const limitNote = notice("note");
|
|
26
|
+
const fill = meter.querySelector(".mvb-meter__fill");
|
|
27
|
+
|
|
28
|
+
let counts = { total: 0, noSpace: 0, words: 0 };
|
|
29
|
+
|
|
30
|
+
function summary() {
|
|
31
|
+
return [
|
|
32
|
+
`${counts.total} characters`,
|
|
33
|
+
`${counts.noSpace} without spaces`,
|
|
34
|
+
`${counts.words} words`,
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function run() {
|
|
39
|
+
const text = source.value;
|
|
40
|
+
counts = {
|
|
41
|
+
total: chars(text),
|
|
42
|
+
noSpace: withoutSpaces(text),
|
|
43
|
+
words: text.trim() ? text.trim().split(/\s+/).length : 0,
|
|
44
|
+
};
|
|
45
|
+
totalTile.set(String(counts.total));
|
|
46
|
+
noSpaceTile.set(String(counts.noSpace));
|
|
47
|
+
wordsTile.set(String(counts.words));
|
|
48
|
+
|
|
49
|
+
const limit = parseInt(limitField.value, 10) || 0;
|
|
50
|
+
if (limit > 0) {
|
|
51
|
+
const ratio = counts.total / limit;
|
|
52
|
+
fill.style.width = `${Math.min(100, ratio * 100)}%`;
|
|
53
|
+
fill.className = `mvb-meter__fill ${ratio > 1 ? "mvb-weak" : ratio > 0.8 ? "mvb-ok" : "mvb-strong"}`;
|
|
54
|
+
limitNote.write(ratio > 1 ? `${counts.total - limit} over the limit.` : `${limit - counts.total} characters left.`);
|
|
55
|
+
} else {
|
|
56
|
+
fill.style.width = "0%";
|
|
57
|
+
fill.className = "mvb-meter__fill";
|
|
58
|
+
limitNote.write("");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const clearButton = button("Clear", { icon: "trash" });
|
|
63
|
+
clearButton.addEventListener("click", () => { source.value = ""; run(); });
|
|
64
|
+
limitField.addEventListener("input", run);
|
|
65
|
+
source.addEventListener("input", run);
|
|
66
|
+
|
|
67
|
+
const card = toolcard({
|
|
68
|
+
body: bench(
|
|
69
|
+
stack(panel("Text", source), panel("Character limit", limitField)),
|
|
70
|
+
stack(stats, meter, limitNote)
|
|
71
|
+
),
|
|
72
|
+
foot: actions(clearButton, copyButton(summary, "Copy summary", "fill")),
|
|
73
|
+
});
|
|
74
|
+
slot.append(card.node);
|
|
75
|
+
run();
|
|
76
|
+
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default { mount };
|