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,114 @@
|
|
|
1
|
+
// Turns CSV rows into JSON records, the first row naming the properties.
|
|
2
|
+
import { el, bench, stack, panel, textArea, checkbox, notice, output, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function parseDelimited(text, delimiter) {
|
|
5
|
+
const rows = [];
|
|
6
|
+
let row = [];
|
|
7
|
+
let field = "";
|
|
8
|
+
let quoted = false;
|
|
9
|
+
for (let i = 0; i < text.length; i++) {
|
|
10
|
+
const char = text[i];
|
|
11
|
+
if (quoted) {
|
|
12
|
+
if (char === '"') {
|
|
13
|
+
if (text[i + 1] === '"') { field += '"'; i++; }
|
|
14
|
+
else quoted = false;
|
|
15
|
+
} else field += char;
|
|
16
|
+
} else if (char === '"') {
|
|
17
|
+
quoted = true;
|
|
18
|
+
} else if (char === delimiter) {
|
|
19
|
+
row.push(field);
|
|
20
|
+
field = "";
|
|
21
|
+
} else if (char === "\n") {
|
|
22
|
+
row.push(field);
|
|
23
|
+
rows.push(row);
|
|
24
|
+
row = [];
|
|
25
|
+
field = "";
|
|
26
|
+
} else if (char !== "\r") {
|
|
27
|
+
field += char;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (field !== "" || row.length) {
|
|
31
|
+
row.push(field);
|
|
32
|
+
rows.push(row);
|
|
33
|
+
}
|
|
34
|
+
return rows;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function detectDelimiter(text) {
|
|
38
|
+
const candidates = [",", ";", "\t", "|"];
|
|
39
|
+
const first = text.split(/\r?\n/)[0] ?? "";
|
|
40
|
+
const counts = { ",": 0, ";": 0, "\t": 0, "|": 0 };
|
|
41
|
+
let quoted = false;
|
|
42
|
+
for (let i = 0; i < first.length; i++) {
|
|
43
|
+
const char = first[i];
|
|
44
|
+
if (char === '"') quoted = !quoted;
|
|
45
|
+
else if (!quoted && char in counts) counts[char]++;
|
|
46
|
+
}
|
|
47
|
+
let best = ",";
|
|
48
|
+
let bestCount = 0;
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
if (counts[candidate] > bestCount) { bestCount = counts[candidate]; best = candidate; }
|
|
51
|
+
}
|
|
52
|
+
return best;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function coerce(value) {
|
|
56
|
+
const text = value ?? "";
|
|
57
|
+
if (text === "") return "";
|
|
58
|
+
if (/^-?\d+(\.\d+)?$/.test(text)) return Number(text);
|
|
59
|
+
if (text === "true") return true;
|
|
60
|
+
if (text === "false") return false;
|
|
61
|
+
return text;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function mount(slot) {
|
|
65
|
+
const source = textArea({ rows: 12, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "CSV", placeholder: "name,role\nAda,engineer" });
|
|
66
|
+
const typedBox = checkbox("Typed values", { checked: true });
|
|
67
|
+
const result = output({ rows: 12 });
|
|
68
|
+
const error = notice("error");
|
|
69
|
+
let current = "[]";
|
|
70
|
+
|
|
71
|
+
function run() {
|
|
72
|
+
error.write("");
|
|
73
|
+
const text = source.value;
|
|
74
|
+
if (!text.trim()) {
|
|
75
|
+
current = "[]";
|
|
76
|
+
result.write("[]");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const rows = parseDelimited(text, detectDelimiter(text));
|
|
80
|
+
const header = rows[0] ?? [];
|
|
81
|
+
const body = rows.slice(1);
|
|
82
|
+
if (body.some((row) => row.length !== header.length)) {
|
|
83
|
+
error.write("Some rows have a different number of columns.");
|
|
84
|
+
}
|
|
85
|
+
const typed = typedBox.input.checked;
|
|
86
|
+
const records = body.map((row) => {
|
|
87
|
+
const record = {};
|
|
88
|
+
header.forEach((key, i) => {
|
|
89
|
+
const name = key || `column${i + 1}`;
|
|
90
|
+
record[name] = typed ? coerce(row[i]) : (row[i] ?? "");
|
|
91
|
+
});
|
|
92
|
+
return record;
|
|
93
|
+
});
|
|
94
|
+
current = JSON.stringify(records, null, 2);
|
|
95
|
+
result.write(current);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
source.addEventListener("input", run);
|
|
99
|
+
typedBox.input.addEventListener("change", run);
|
|
100
|
+
|
|
101
|
+
const card = toolcard({
|
|
102
|
+
body: bench(
|
|
103
|
+
stack(panel("CSV", source), panel(null, typedBox), error),
|
|
104
|
+
stack(panel("JSON", result))
|
|
105
|
+
),
|
|
106
|
+
foot: actions(copyButton(() => current, "Copy JSON", "fill")),
|
|
107
|
+
});
|
|
108
|
+
slot.append(card.node);
|
|
109
|
+
run();
|
|
110
|
+
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default { mount };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Reads CSV or delimited text as a table, with row and column counts.
|
|
2
|
+
import { el, bench, stack, panel, textArea, selectInput, fileInput, notice, copyButton, button, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function parseDelimited(text, delimiter) {
|
|
5
|
+
const rows = [];
|
|
6
|
+
let row = [];
|
|
7
|
+
let field = "";
|
|
8
|
+
let quoted = false;
|
|
9
|
+
for (let i = 0; i < text.length; i++) {
|
|
10
|
+
const char = text[i];
|
|
11
|
+
if (quoted) {
|
|
12
|
+
if (char === '"') {
|
|
13
|
+
if (text[i + 1] === '"') { field += '"'; i++; }
|
|
14
|
+
else quoted = false;
|
|
15
|
+
} else field += char;
|
|
16
|
+
} else if (char === '"') {
|
|
17
|
+
quoted = true;
|
|
18
|
+
} else if (char === delimiter) {
|
|
19
|
+
row.push(field);
|
|
20
|
+
field = "";
|
|
21
|
+
} else if (char === "\n") {
|
|
22
|
+
row.push(field);
|
|
23
|
+
rows.push(row);
|
|
24
|
+
row = [];
|
|
25
|
+
field = "";
|
|
26
|
+
} else if (char !== "\r") {
|
|
27
|
+
field += char;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (field !== "" || row.length) {
|
|
31
|
+
row.push(field);
|
|
32
|
+
rows.push(row);
|
|
33
|
+
}
|
|
34
|
+
return rows;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function detectDelimiter(text) {
|
|
38
|
+
const candidates = [",", ";", "\t", "|"];
|
|
39
|
+
const first = text.split(/\r?\n/)[0] ?? "";
|
|
40
|
+
const counts = { ",": 0, ";": 0, "\t": 0, "|": 0 };
|
|
41
|
+
let quoted = false;
|
|
42
|
+
for (let i = 0; i < first.length; i++) {
|
|
43
|
+
const char = first[i];
|
|
44
|
+
if (char === '"') quoted = !quoted;
|
|
45
|
+
else if (!quoted && char in counts) counts[char]++;
|
|
46
|
+
}
|
|
47
|
+
let best = ",";
|
|
48
|
+
let bestCount = 0;
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
if (counts[candidate] > bestCount) { bestCount = counts[candidate]; best = candidate; }
|
|
51
|
+
}
|
|
52
|
+
return best;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function tile(label) {
|
|
56
|
+
const value = el("span", { class: "mvb-stat__v" }, "0");
|
|
57
|
+
const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
|
|
58
|
+
node.set = (text) => { value.textContent = text; };
|
|
59
|
+
return node;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function mount(slot) {
|
|
63
|
+
const source = textArea({ rows: 12, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "CSV", placeholder: "Paste CSV here" });
|
|
64
|
+
const delimiter = selectInput([
|
|
65
|
+
{ value: "auto", label: "Auto" },
|
|
66
|
+
{ value: ",", label: "Comma" },
|
|
67
|
+
{ value: ";", label: "Semicolon" },
|
|
68
|
+
{ value: "\t", label: "Tab" },
|
|
69
|
+
{ value: "|", label: "Pipe" },
|
|
70
|
+
], { value: "auto", "aria-label": "Delimiter" });
|
|
71
|
+
const file = fileInput({ accept: ".csv,.tsv,.txt,text/csv", "aria-label": "CSV file" });
|
|
72
|
+
|
|
73
|
+
const tableWrap = el("div", { class: "mvb-scroll" });
|
|
74
|
+
const rowsTile = tile("Rows");
|
|
75
|
+
const colsTile = tile("Columns");
|
|
76
|
+
const stats = el("div", { class: "mvb-stats" }, rowsTile, colsTile);
|
|
77
|
+
const error = notice("error");
|
|
78
|
+
let current = [];
|
|
79
|
+
|
|
80
|
+
function run() {
|
|
81
|
+
error.write("");
|
|
82
|
+
const text = source.value;
|
|
83
|
+
if (!text.trim()) {
|
|
84
|
+
current = [];
|
|
85
|
+
tableWrap.replaceChildren(el("p", { class: "mvb-note" }, "Paste CSV or choose a file."));
|
|
86
|
+
rowsTile.set("0");
|
|
87
|
+
colsTile.set("0");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const used = delimiter.value === "auto" ? detectDelimiter(text) : delimiter.value;
|
|
91
|
+
const rows = parseDelimited(text, used);
|
|
92
|
+
current = rows;
|
|
93
|
+
const header = rows[0] ?? [];
|
|
94
|
+
const body = rows.slice(1);
|
|
95
|
+
if (body.some((row) => row.length !== header.length)) {
|
|
96
|
+
error.write("Some rows have a different number of columns.");
|
|
97
|
+
}
|
|
98
|
+
rowsTile.set(String(body.length));
|
|
99
|
+
colsTile.set(String(header.length));
|
|
100
|
+
tableWrap.replaceChildren(el("table", { class: "mvb-ttable" },
|
|
101
|
+
el("thead", {}, el("tr", {}, ...header.map((cell, i) => el("th", {}, cell || `Column ${i + 1}`)))),
|
|
102
|
+
el("tbody", {}, ...body.map((row) => el("tr", {}, ...header.map((_, i) => el("td", {}, row[i] ?? "")))))
|
|
103
|
+
));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
file.addEventListener("change", async () => {
|
|
107
|
+
const picked = file.files[0];
|
|
108
|
+
if (!picked) return;
|
|
109
|
+
source.value = await picked.text();
|
|
110
|
+
file.value = "";
|
|
111
|
+
run();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const clearButton = button("Clear", { icon: "trash" });
|
|
115
|
+
clearButton.addEventListener("click", () => { source.value = ""; run(); });
|
|
116
|
+
delimiter.addEventListener("change", run);
|
|
117
|
+
source.addEventListener("input", run);
|
|
118
|
+
|
|
119
|
+
const card = toolcard({
|
|
120
|
+
body: bench(
|
|
121
|
+
stack(panel("CSV", source), panel("Delimiter", delimiter), panel("CSV file", file), error),
|
|
122
|
+
stack(stats, panel("Table", tableWrap))
|
|
123
|
+
),
|
|
124
|
+
foot: actions(clearButton, copyButton(() => current.map((row) => row.join("\t")).join("\n"), "Copy table", "fill")),
|
|
125
|
+
});
|
|
126
|
+
slot.append(card.node);
|
|
127
|
+
run();
|
|
128
|
+
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default { mount };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// A number shown as a currency for a chosen locale, with no rate applied.
|
|
2
|
+
import { el, bench, stack, panel, textInput, selectInput, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const CURRENCIES = ["USD", "EUR", "GBP", "JPY", "IDR", "INR", "AUD", "CAD", "CNY", "BRL", "SGD", "CHF"];
|
|
5
|
+
|
|
6
|
+
const LOCALES = [
|
|
7
|
+
{ value: "en-US", label: "English (US)" },
|
|
8
|
+
{ value: "en-GB", label: "English (UK)" },
|
|
9
|
+
{ value: "de-DE", label: "German" },
|
|
10
|
+
{ value: "fr-FR", label: "French" },
|
|
11
|
+
{ value: "es-ES", label: "Spanish" },
|
|
12
|
+
{ value: "id-ID", label: "Indonesian" },
|
|
13
|
+
{ value: "hi-IN", label: "Hindi" },
|
|
14
|
+
{ value: "ja-JP", label: "Japanese" },
|
|
15
|
+
{ value: "pt-BR", label: "Portuguese (Brazil)" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function format(locale, currency, value) {
|
|
19
|
+
try {
|
|
20
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency }).format(value);
|
|
21
|
+
} catch {
|
|
22
|
+
return String(value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function mount(slot) {
|
|
27
|
+
const value = textInput({ value: "1234.5", inputmode: "decimal", "aria-label": "Amount" });
|
|
28
|
+
const currency = selectInput(CURRENCIES, { value: "USD", "aria-label": "Currency" });
|
|
29
|
+
const locale = selectInput(LOCALES, { value: "en-US", "aria-label": "Locale" });
|
|
30
|
+
|
|
31
|
+
const amount = el("span", { class: "mvb-stat__v" }, "-");
|
|
32
|
+
const readout = el("div", { class: "mvb-stats" },
|
|
33
|
+
el("div", { class: "mvb-stat" }, amount, el("span", { class: "mvb-stat__k" }, "Formatted")));
|
|
34
|
+
|
|
35
|
+
const note = notice("note");
|
|
36
|
+
note.write("The amount is formatted, not converted. No exchange rate is applied.");
|
|
37
|
+
|
|
38
|
+
let formatted = "";
|
|
39
|
+
|
|
40
|
+
function run() {
|
|
41
|
+
const parsed = Number(value.value.trim());
|
|
42
|
+
formatted = value.value.trim() === "" || !Number.isFinite(parsed)
|
|
43
|
+
? ""
|
|
44
|
+
: format(locale.value, currency.value, parsed);
|
|
45
|
+
amount.textContent = formatted || "-";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
value.addEventListener("input", run);
|
|
49
|
+
currency.addEventListener("change", run);
|
|
50
|
+
locale.addEventListener("change", run);
|
|
51
|
+
|
|
52
|
+
const card = toolcard({
|
|
53
|
+
body: bench(
|
|
54
|
+
stack(
|
|
55
|
+
panel("Amount", value),
|
|
56
|
+
panel("Currency", currency),
|
|
57
|
+
panel("Locale", locale)
|
|
58
|
+
),
|
|
59
|
+
stack(readout, note)
|
|
60
|
+
),
|
|
61
|
+
foot: actions(copyButton(() => formatted, "Copy amount", "fill")),
|
|
62
|
+
});
|
|
63
|
+
slot.append(card.node);
|
|
64
|
+
run();
|
|
65
|
+
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default { mount };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Plans a day by the clock, sorting activities and flagging overlaps.
|
|
2
|
+
import { el, toast, blank, bench, stack, row, panel, textInput, button, notice, actions, toolcard, readPin, writePin } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
|
5
|
+
const toMin = (time) => {
|
|
6
|
+
const [h, m] = String(time).split(":").map(Number);
|
|
7
|
+
return (h || 0) * 60 + (m || 0);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function fmtDur(min) {
|
|
11
|
+
const h = Math.floor(min / 60);
|
|
12
|
+
const m = min % 60;
|
|
13
|
+
if (h && m) return `${h}h ${m}m`;
|
|
14
|
+
if (h) return `${h}h`;
|
|
15
|
+
return `${m}m`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function mount(slot) {
|
|
19
|
+
const PIN = "daily-planner";
|
|
20
|
+
const saved = readPin(PIN, []);
|
|
21
|
+
let list = Array.isArray(saved) ? saved : [];
|
|
22
|
+
|
|
23
|
+
const title = textInput({ placeholder: "Activity", "aria-label": "Activity" });
|
|
24
|
+
const start = el("input", { class: "mvb-in", type: "time", value: "09:00", "aria-label": "Start time" });
|
|
25
|
+
const end = el("input", { class: "mvb-in", type: "time", value: "10:00", "aria-label": "End time" });
|
|
26
|
+
|
|
27
|
+
const listWrap = el("div", { class: "mvb-scroll" });
|
|
28
|
+
const stats = el("div", { class: "mvb-stats" });
|
|
29
|
+
const overlapNote = notice("error");
|
|
30
|
+
const error = notice("error");
|
|
31
|
+
|
|
32
|
+
const addButton = button("Add", { icon: "plus", kind: "fill" });
|
|
33
|
+
const clearButton = button("Clear day", { icon: "trash" });
|
|
34
|
+
|
|
35
|
+
const tile = (key, value) => el("div", { class: "mvb-stat" },
|
|
36
|
+
el("span", { class: "mvb-stat__v" }, String(value)),
|
|
37
|
+
el("span", { class: "mvb-stat__k" }, key));
|
|
38
|
+
|
|
39
|
+
const save = () => writePin(PIN, list);
|
|
40
|
+
|
|
41
|
+
function removeButton(item) {
|
|
42
|
+
const node = button("", { icon: "trash", "aria-label": `Remove ${item.text}` });
|
|
43
|
+
node.addEventListener("click", () => {
|
|
44
|
+
list = list.filter((entry) => entry.id !== item.id);
|
|
45
|
+
save();
|
|
46
|
+
render();
|
|
47
|
+
});
|
|
48
|
+
return node;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function render() {
|
|
52
|
+
const sorted = list.slice().sort((a, b) => toMin(a.start) - toMin(b.start));
|
|
53
|
+
const flagged = new Set();
|
|
54
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
55
|
+
if (toMin(sorted[i].end) > toMin(sorted[i + 1].start)) {
|
|
56
|
+
flagged.add(sorted[i].id);
|
|
57
|
+
flagged.add(sorted[i + 1].id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!sorted.length) {
|
|
62
|
+
listWrap.replaceChildren(blank("calendar-check", "No activities yet."));
|
|
63
|
+
} else {
|
|
64
|
+
const rows = sorted.map((item) => el("tr", {},
|
|
65
|
+
el("th", {}, item.start),
|
|
66
|
+
el("td", {}, item.end),
|
|
67
|
+
el("td", {}, item.text),
|
|
68
|
+
el("td", {}, fmtDur(Math.max(0, toMin(item.end) - toMin(item.start)))),
|
|
69
|
+
el("td", {}, flagged.has(item.id) ? "Overlap" : ""),
|
|
70
|
+
el("td", {}, removeButton(item))
|
|
71
|
+
));
|
|
72
|
+
listWrap.replaceChildren(el("table", { class: "mvb-ttable" },
|
|
73
|
+
el("thead", {}, el("tr", {},
|
|
74
|
+
el("th", {}, "Start"), el("th", {}, "End"), el("th", {}, "Activity"),
|
|
75
|
+
el("th", {}, "Length"), el("th", {}, "Note"), el("th", {}, ""))),
|
|
76
|
+
el("tbody", {}, ...rows)
|
|
77
|
+
));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const planned = sorted.reduce((sum, item) => sum + Math.max(0, toMin(item.end) - toMin(item.start)), 0);
|
|
81
|
+
stats.replaceChildren(tile("Activities", sorted.length), tile("Planned", fmtDur(planned)));
|
|
82
|
+
overlapNote.write(flagged.size ? `${flagged.size} activities overlap.` : "");
|
|
83
|
+
clearButton.disabled = list.length === 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function add() {
|
|
87
|
+
const text = title.value.trim();
|
|
88
|
+
if (!text) return toast("Add an activity name");
|
|
89
|
+
if (!start.value || !end.value || toMin(end.value) <= toMin(start.value)) {
|
|
90
|
+
error.write("End time must be after the start.");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
error.write("");
|
|
94
|
+
list.push({ id: uid(), text, start: start.value, end: end.value });
|
|
95
|
+
title.value = "";
|
|
96
|
+
save();
|
|
97
|
+
render();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
addButton.addEventListener("click", add);
|
|
101
|
+
clearButton.addEventListener("click", () => {
|
|
102
|
+
list = [];
|
|
103
|
+
save();
|
|
104
|
+
render();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const card = toolcard({
|
|
108
|
+
body: bench(
|
|
109
|
+
stack(
|
|
110
|
+
panel("Activity", title),
|
|
111
|
+
row(panel("Start", start), panel("End", end)),
|
|
112
|
+
error
|
|
113
|
+
),
|
|
114
|
+
stack(stats, panel("Day", listWrap), overlapNote)
|
|
115
|
+
),
|
|
116
|
+
foot: actions(clearButton, addButton),
|
|
117
|
+
});
|
|
118
|
+
slot.append(card.node);
|
|
119
|
+
render();
|
|
120
|
+
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default { mount };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Masks emails, card numbers and phone numbers in pasted text.
|
|
2
|
+
import { el, textArea, output, toggleRow, copyButton, bench, stack, panel, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
5
|
+
const CARD = /\b\d[\d -]{11,}\d\b/g;
|
|
6
|
+
const PHONE = /\b\+?\d[\d ().-]{5,}\d\b/g;
|
|
7
|
+
|
|
8
|
+
function maskDigits(value, keep) {
|
|
9
|
+
const digits = value.replace(/\D/g, "");
|
|
10
|
+
if (digits.length <= keep) return value;
|
|
11
|
+
const cutoff = digits.length - keep;
|
|
12
|
+
let seen = 0;
|
|
13
|
+
return value.replace(/\d/g, (digit) => (seen++ < cutoff ? "*" : digit));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function maskEmail(value) {
|
|
17
|
+
return value.replace(EMAIL, (email) => {
|
|
18
|
+
const [local, domain] = email.split("@");
|
|
19
|
+
const head = local.slice(0, 1);
|
|
20
|
+
return `${head}${"*".repeat(Math.max(1, local.length - 1))}@${domain}`;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function mount(slot) {
|
|
25
|
+
const source = textArea({ rows: 12, "aria-label": "Values", placeholder: "Paste values, one per line" });
|
|
26
|
+
const result = output({ rows: 12 });
|
|
27
|
+
|
|
28
|
+
const kinds = toggleRow([
|
|
29
|
+
{ value: "email", label: "Email" },
|
|
30
|
+
{ value: "card", label: "Card number" },
|
|
31
|
+
{ value: "phone", label: "Phone number" },
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
function run() {
|
|
35
|
+
const on = kinds.values();
|
|
36
|
+
const lines = source.value.split("\n").map((line) => {
|
|
37
|
+
let text = line;
|
|
38
|
+
if (on.includes("email")) text = maskEmail(text);
|
|
39
|
+
if (on.includes("card")) text = text.replace(CARD, (value) => (value.replace(/\D/g, "").length >= 13 ? maskDigits(value, 4) : value));
|
|
40
|
+
if (on.includes("phone")) {
|
|
41
|
+
text = text.replace(PHONE, (value) => {
|
|
42
|
+
const digits = value.replace(/\D/g, "").length;
|
|
43
|
+
return digits >= 7 && digits <= 12 ? maskDigits(value, 2) : value;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return text;
|
|
47
|
+
});
|
|
48
|
+
result.write(lines.join("\n"));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
source.addEventListener("input", run);
|
|
52
|
+
kinds.addEventListener("change", run);
|
|
53
|
+
|
|
54
|
+
const card = toolcard({
|
|
55
|
+
body: bench(
|
|
56
|
+
stack(
|
|
57
|
+
panel("Values", source),
|
|
58
|
+
panel("Mask", kinds),
|
|
59
|
+
el("p", { class: "mvb-note" }, "Masking runs in this tab. Check the result before sharing.")
|
|
60
|
+
),
|
|
61
|
+
stack(panel("Masked", result))
|
|
62
|
+
),
|
|
63
|
+
foot: actions(copyButton(() => result.textContent, "Copy masked", "fill")),
|
|
64
|
+
});
|
|
65
|
+
slot.append(card.node);
|
|
66
|
+
run();
|
|
67
|
+
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default { mount };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Converts digital storage. Decimal units step by 1000, binary units by 1024.
|
|
2
|
+
import { el, bench, stack, panel, numberInput, selectInput, notice, copyButton, button, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const UNITS = [
|
|
5
|
+
{ value: "B", label: "Byte", factor: 1 },
|
|
6
|
+
{ value: "KB", label: "Kilobyte", factor: 1000 },
|
|
7
|
+
{ value: "MB", label: "Megabyte", factor: 1000000 },
|
|
8
|
+
{ value: "GB", label: "Gigabyte", factor: 1000000000 },
|
|
9
|
+
{ value: "TB", label: "Terabyte", factor: 1000000000000 },
|
|
10
|
+
{ value: "KiB", label: "Kibibyte", factor: 1024 },
|
|
11
|
+
{ value: "MiB", label: "Mebibyte", factor: 1048576 },
|
|
12
|
+
{ value: "GiB", label: "Gibibyte", factor: 1073741824 },
|
|
13
|
+
{ value: "TiB", label: "Tebibyte", factor: 1099511627776 },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const byId = Object.fromEntries(UNITS.map((unit) => [unit.value, unit]));
|
|
17
|
+
const options = UNITS.map(({ value, label }) => ({ value, label }));
|
|
18
|
+
|
|
19
|
+
function fmt(value) {
|
|
20
|
+
if (!Number.isFinite(value)) return "-";
|
|
21
|
+
if (value === 0) return "0";
|
|
22
|
+
const abs = Math.abs(value);
|
|
23
|
+
if (abs >= 1e-6 && abs < 1e15) return String(Number(value.toPrecision(8)));
|
|
24
|
+
return value.toExponential(4);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function mount(slot) {
|
|
28
|
+
const value = numberInput({ value: "1", step: "any", "aria-label": "Value" });
|
|
29
|
+
const from = selectInput(options, { value: "MB", "aria-label": "From unit" });
|
|
30
|
+
const to = selectInput(options, { value: "MiB", "aria-label": "To unit" });
|
|
31
|
+
const result = el("span", { class: "mvb-stat__v" }, "-");
|
|
32
|
+
const stats = el("div", { class: "mvb-stats" }, el("div", { class: "mvb-stat" }, el("span", { class: "mvb-stat__k" }, "Result"), result));
|
|
33
|
+
const factor = el("p", { class: "mvb-note" });
|
|
34
|
+
const scaleNote = notice("note");
|
|
35
|
+
scaleNote.write("KB is 1000 bytes, KiB is 1024 bytes.");
|
|
36
|
+
const error = notice("error");
|
|
37
|
+
const scroll = el("div", { class: "mvb-scroll" });
|
|
38
|
+
let text = "";
|
|
39
|
+
|
|
40
|
+
function run() {
|
|
41
|
+
error.write("");
|
|
42
|
+
const raw = value.value.trim();
|
|
43
|
+
const n = Number(raw);
|
|
44
|
+
if (!raw || !Number.isFinite(n)) {
|
|
45
|
+
result.textContent = "-";
|
|
46
|
+
factor.textContent = "";
|
|
47
|
+
scroll.replaceChildren();
|
|
48
|
+
text = "";
|
|
49
|
+
error.write("Enter a number.");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const a = byId[from.value];
|
|
53
|
+
const b = byId[to.value];
|
|
54
|
+
const out = (n * a.factor) / b.factor;
|
|
55
|
+
result.textContent = `${fmt(out)} ${b.value}`;
|
|
56
|
+
factor.textContent = `1 ${a.value} = ${fmt(a.factor / b.factor)} ${b.value}`;
|
|
57
|
+
scroll.replaceChildren(el("table", { class: "mvb-ttable" },
|
|
58
|
+
el("thead", {}, el("tr", {}, el("th", {}, "Unit"), el("th", {}, "Value"))),
|
|
59
|
+
el("tbody", {}, ...UNITS.map((unit) => el("tr", {},
|
|
60
|
+
el("td", {}, unit.label),
|
|
61
|
+
el("td", {}, fmt((n * a.factor) / unit.factor)))))));
|
|
62
|
+
text = `${fmt(out)} ${b.value}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
value.addEventListener("input", run);
|
|
66
|
+
from.addEventListener("change", run);
|
|
67
|
+
to.addEventListener("change", run);
|
|
68
|
+
|
|
69
|
+
const swap = button("Swap", { icon: "arrow-left-right", onclick: () => {
|
|
70
|
+
const held = from.value;
|
|
71
|
+
from.value = to.value;
|
|
72
|
+
to.value = held;
|
|
73
|
+
run();
|
|
74
|
+
}});
|
|
75
|
+
|
|
76
|
+
const card = toolcard({
|
|
77
|
+
body: bench(
|
|
78
|
+
stack(panel("Value", value), panel("From", from), panel("To", to), scaleNote, error),
|
|
79
|
+
stack(panel("Result", stats), factor, panel("All units", scroll))
|
|
80
|
+
),
|
|
81
|
+
foot: actions(swap, copyButton(() => text, "Copy result", "fill")),
|
|
82
|
+
});
|
|
83
|
+
slot.append(card.node);
|
|
84
|
+
run();
|
|
85
|
+
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default { mount };
|