mevebu 0.3.1 → 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.
Files changed (178) hide show
  1. package/README.md +24 -8
  2. package/mevebu/app/mevebu.js +2 -2
  3. package/mevebu/libs/css/tools.css +103 -1
  4. package/mevebu/libs/js/core.js +1 -1
  5. package/mevebu/libs/js/icons.js +115 -1
  6. package/mevebu/libs/js/registry.js +3102 -0
  7. package/mevebu/libs/js/tools/abstract-checklist.js +54 -0
  8. package/mevebu/libs/js/tools/add-subtract-days.js +105 -0
  9. package/mevebu/libs/js/tools/age-calculator.js +108 -0
  10. package/mevebu/libs/js/tools/apa-reference.js +98 -0
  11. package/mevebu/libs/js/tools/api-request-builder.js +76 -0
  12. package/mevebu/libs/js/tools/area-converter.js +87 -0
  13. package/mevebu/libs/js/tools/aspect-ratio.js +81 -0
  14. package/mevebu/libs/js/tools/average-calculator.js +83 -0
  15. package/mevebu/libs/js/tools/bibliography-sorter.js +43 -0
  16. package/mevebu/libs/js/tools/bibtex-generator.js +98 -0
  17. package/mevebu/libs/js/tools/binary-calculator.js +58 -0
  18. package/mevebu/libs/js/tools/border-radius.js +88 -0
  19. package/mevebu/libs/js/tools/box-shadow.js +71 -0
  20. package/mevebu/libs/js/tools/branch-name.js +44 -0
  21. package/mevebu/libs/js/tools/break-even.js +68 -0
  22. package/mevebu/libs/js/tools/business-days.js +120 -0
  23. package/mevebu/libs/js/tools/canonical-tag.js +63 -0
  24. package/mevebu/libs/js/tools/caption-counter.js +66 -0
  25. package/mevebu/libs/js/tools/case-inspector.js +114 -0
  26. package/mevebu/libs/js/tools/cash-denomination.js +74 -0
  27. package/mevebu/libs/js/tools/cash-flow.js +124 -0
  28. package/mevebu/libs/js/tools/cert-expiry.js +83 -0
  29. package/mevebu/libs/js/tools/changelog-generator.js +48 -0
  30. package/mevebu/libs/js/tools/character-counter.js +80 -0
  31. package/mevebu/libs/js/tools/checklist.js +86 -0
  32. package/mevebu/libs/js/tools/citation-metadata.js +89 -0
  33. package/mevebu/libs/js/tools/citation-number-check.js +107 -0
  34. package/mevebu/libs/js/tools/color-palette.js +110 -0
  35. package/mevebu/libs/js/tools/color-picker.js +93 -0
  36. package/mevebu/libs/js/tools/commit-message.js +62 -0
  37. package/mevebu/libs/js/tools/content-calendar.js +111 -0
  38. package/mevebu/libs/js/tools/contrast-checker.js +93 -0
  39. package/mevebu/libs/js/tools/countdown-timer.js +174 -0
  40. package/mevebu/libs/js/tools/cron-builder.js +91 -0
  41. package/mevebu/libs/js/tools/css-clamp.js +66 -0
  42. package/mevebu/libs/js/tools/css-formatter.js +84 -0
  43. package/mevebu/libs/js/tools/css-grid.js +75 -0
  44. package/mevebu/libs/js/tools/css-minifier.js +98 -0
  45. package/mevebu/libs/js/tools/csv-to-json.js +114 -0
  46. package/mevebu/libs/js/tools/csv-viewer.js +132 -0
  47. package/mevebu/libs/js/tools/currency-format.js +69 -0
  48. package/mevebu/libs/js/tools/daily-planner.js +124 -0
  49. package/mevebu/libs/js/tools/data-masker.js +71 -0
  50. package/mevebu/libs/js/tools/data-storage.js +89 -0
  51. package/mevebu/libs/js/tools/data-validator.js +111 -0
  52. package/mevebu/libs/js/tools/date-difference.js +100 -0
  53. package/mevebu/libs/js/tools/date-format.js +107 -0
  54. package/mevebu/libs/js/tools/delimiter-converter.js +109 -0
  55. package/mevebu/libs/js/tools/discount-calculator.js +83 -0
  56. package/mevebu/libs/js/tools/doi-resolver.js +67 -0
  57. package/mevebu/libs/js/tools/env-formatter.js +72 -0
  58. package/mevebu/libs/js/tools/equation-formatter.js +71 -0
  59. package/mevebu/libs/js/tools/expense-split.js +90 -0
  60. package/mevebu/libs/js/tools/favicon-markup.js +53 -0
  61. package/mevebu/libs/js/tools/find-replace.js +88 -0
  62. package/mevebu/libs/js/tools/flashcard-creator.js +117 -0
  63. package/mevebu/libs/js/tools/font-pairing.js +88 -0
  64. package/mevebu/libs/js/tools/fraction-calculator.js +130 -0
  65. package/mevebu/libs/js/tools/git-command-builder.js +100 -0
  66. package/mevebu/libs/js/tools/gitignore-generator.js +60 -0
  67. package/mevebu/libs/js/tools/gpa-calculator.js +100 -0
  68. package/mevebu/libs/js/tools/grade-calculator.js +101 -0
  69. package/mevebu/libs/js/tools/gradient-generator.js +99 -0
  70. package/mevebu/libs/js/tools/habit-tracker.js +120 -0
  71. package/mevebu/libs/js/tools/harvard-citation.js +90 -0
  72. package/mevebu/libs/js/tools/hashtag-formatter.js +62 -0
  73. package/mevebu/libs/js/tools/heading-formatter.js +61 -0
  74. package/mevebu/libs/js/tools/hmac-generator.js +63 -0
  75. package/mevebu/libs/js/tools/hpp-calculator.js +70 -0
  76. package/mevebu/libs/js/tools/html-entity.js +49 -0
  77. package/mevebu/libs/js/tools/html-formatter.js +72 -0
  78. package/mevebu/libs/js/tools/html-to-text.js +47 -0
  79. package/mevebu/libs/js/tools/http-status.js +63 -0
  80. package/mevebu/libs/js/tools/ieee-reference.js +102 -0
  81. package/mevebu/libs/js/tools/intext-citation.js +75 -0
  82. package/mevebu/libs/js/tools/inventory-quantity.js +103 -0
  83. package/mevebu/libs/js/tools/invoice-number.js +64 -0
  84. package/mevebu/libs/js/tools/json-escape.js +76 -0
  85. package/mevebu/libs/js/tools/json-formatter.js +51 -0
  86. package/mevebu/libs/js/tools/json-minifier.js +58 -0
  87. package/mevebu/libs/js/tools/json-to-csv.js +72 -0
  88. package/mevebu/libs/js/tools/json-to-typescript.js +102 -0
  89. package/mevebu/libs/js/tools/json-validator.js +49 -0
  90. package/mevebu/libs/js/tools/jwt-decoder.js +74 -0
  91. package/mevebu/libs/js/tools/jwt-expiry.js +97 -0
  92. package/mevebu/libs/js/tools/length-converter.js +86 -0
  93. package/mevebu/libs/js/tools/license-reference.js +58 -0
  94. package/mevebu/libs/js/tools/line-break-formatter.js +48 -0
  95. package/mevebu/libs/js/tools/line-counter.js +55 -0
  96. package/mevebu/libs/js/tools/list-difference.js +86 -0
  97. package/mevebu/libs/js/tools/list-merger.js +67 -0
  98. package/mevebu/libs/js/tools/locale-number.js +79 -0
  99. package/mevebu/libs/js/tools/markdown-preview.js +143 -0
  100. package/mevebu/libs/js/tools/markdown-table.js +111 -0
  101. package/mevebu/libs/js/tools/markdown-to-text.js +48 -0
  102. package/mevebu/libs/js/tools/markup-calculator.js +69 -0
  103. package/mevebu/libs/js/tools/meeting-agenda.js +137 -0
  104. package/mevebu/libs/js/tools/meta-preview.js +51 -0
  105. package/mevebu/libs/js/tools/meta-tags.js +62 -0
  106. package/mevebu/libs/js/tools/mime-types.js +54 -0
  107. package/mevebu/libs/js/tools/mla-citation.js +91 -0
  108. package/mevebu/libs/js/tools/number-base-converter.js +97 -0
  109. package/mevebu/libs/js/tools/open-graph.js +62 -0
  110. package/mevebu/libs/js/tools/password-strength.js +118 -0
  111. package/mevebu/libs/js/tools/percent-to-fraction.js +96 -0
  112. package/mevebu/libs/js/tools/percentage-calculator.js +86 -0
  113. package/mevebu/libs/js/tools/percentage-change.js +70 -0
  114. package/mevebu/libs/js/tools/permutation-combination.js +106 -0
  115. package/mevebu/libs/js/tools/prefix-suffix.js +46 -0
  116. package/mevebu/libs/js/tools/priority-matrix.js +100 -0
  117. package/mevebu/libs/js/tools/profit-margin.js +69 -0
  118. package/mevebu/libs/js/tools/quadratic-solver.js +93 -0
  119. package/mevebu/libs/js/tools/query-builder.js +71 -0
  120. package/mevebu/libs/js/tools/quiz-formatter.js +70 -0
  121. package/mevebu/libs/js/tools/quotation-formatter.js +77 -0
  122. package/mevebu/libs/js/tools/ratio-calculator.js +103 -0
  123. package/mevebu/libs/js/tools/readability.js +112 -0
  124. package/mevebu/libs/js/tools/reading-time.js +76 -0
  125. package/mevebu/libs/js/tools/readme-generator.js +50 -0
  126. package/mevebu/libs/js/tools/reference-completeness.js +62 -0
  127. package/mevebu/libs/js/tools/regex-tester.js +86 -0
  128. package/mevebu/libs/js/tools/remove-duplicate-lines.js +46 -0
  129. package/mevebu/libs/js/tools/remove-extra-spaces.js +41 -0
  130. package/mevebu/libs/js/tools/robots-txt.js +60 -0
  131. package/mevebu/libs/js/tools/roman-numeral-converter.js +106 -0
  132. package/mevebu/libs/js/tools/sales-commission.js +96 -0
  133. package/mevebu/libs/js/tools/scientific-calculator.js +247 -0
  134. package/mevebu/libs/js/tools/scientific-notation.js +83 -0
  135. package/mevebu/libs/js/tools/script-planner.js +72 -0
  136. package/mevebu/libs/js/tools/secure-token.js +60 -0
  137. package/mevebu/libs/js/tools/selling-price.js +74 -0
  138. package/mevebu/libs/js/tools/semver-helper.js +54 -0
  139. package/mevebu/libs/js/tools/sentence-length.js +80 -0
  140. package/mevebu/libs/js/tools/sequence-analyzer.js +90 -0
  141. package/mevebu/libs/js/tools/sitemap-xml.js +82 -0
  142. package/mevebu/libs/js/tools/slug-converter.js +48 -0
  143. package/mevebu/libs/js/tools/slug-preview.js +83 -0
  144. package/mevebu/libs/js/tools/sort-lines.js +54 -0
  145. package/mevebu/libs/js/tools/speed-converter.js +83 -0
  146. package/mevebu/libs/js/tools/sql-formatter.js +78 -0
  147. package/mevebu/libs/js/tools/sql-minifier.js +102 -0
  148. package/mevebu/libs/js/tools/standard-deviation.js +96 -0
  149. package/mevebu/libs/js/tools/stopwatch.js +133 -0
  150. package/mevebu/libs/js/tools/study-schedule.js +88 -0
  151. package/mevebu/libs/js/tools/subtitle-formatter.js +74 -0
  152. package/mevebu/libs/js/tools/table-to-markdown.js +137 -0
  153. package/mevebu/libs/js/tools/tax-calculator.js +84 -0
  154. package/mevebu/libs/js/tools/temperature-converter.js +94 -0
  155. package/mevebu/libs/js/tools/text-diff.js +104 -0
  156. package/mevebu/libs/js/tools/text-direction.js +55 -0
  157. package/mevebu/libs/js/tools/text-reverser.js +51 -0
  158. package/mevebu/libs/js/tools/time-duration.js +83 -0
  159. package/mevebu/libs/js/tools/time-format-converter.js +86 -0
  160. package/mevebu/libs/js/tools/timestamp-converter.js +85 -0
  161. package/mevebu/libs/js/tools/timezone-converter.js +163 -0
  162. package/mevebu/libs/js/tools/title-length.js +67 -0
  163. package/mevebu/libs/js/tools/unicode-escape.js +80 -0
  164. package/mevebu/libs/js/tools/unicode-inspector.js +58 -0
  165. package/mevebu/libs/js/tools/url-inspector.js +98 -0
  166. package/mevebu/libs/js/tools/url-parser.js +83 -0
  167. package/mevebu/libs/js/tools/utf8-bytes.js +67 -0
  168. package/mevebu/libs/js/tools/version-compare.js +70 -0
  169. package/mevebu/libs/js/tools/video-duration.js +72 -0
  170. package/mevebu/libs/js/tools/volume-converter.js +88 -0
  171. package/mevebu/libs/js/tools/weekly-planner.js +101 -0
  172. package/mevebu/libs/js/tools/weight-converter.js +85 -0
  173. package/mevebu/libs/js/tools/weighted-average.js +86 -0
  174. package/mevebu/libs/js/tools/word-counter.js +90 -0
  175. package/mevebu/libs/js/tools/word-limit.js +60 -0
  176. package/mevebu/libs/js/tools/xml-formatter.js +85 -0
  177. package/mevebu/libs/js/tools/yaml-formatter.js +71 -0
  178. package/package.json +1 -1
@@ -0,0 +1,111 @@
1
+ // Checks each value against a rule and lists the rows that fail.
2
+ import { el, bench, stack, panel, textArea, selectInput, textInput, 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: 12, "aria-label": "Values, one per line", placeholder: "One value per line" });
13
+ const rule = selectInput([
14
+ { value: "notempty", label: "Not empty" },
15
+ { value: "email", label: "Email" },
16
+ { value: "url", label: "URL" },
17
+ { value: "number", label: "Number" },
18
+ { value: "minlength", label: "Minimum length" },
19
+ { value: "maxlength", label: "Maximum length" },
20
+ { value: "regex", label: "Regex" },
21
+ ], { value: "email", "aria-label": "Rule" });
22
+ const setting = textInput({ "aria-label": "Rule setting", placeholder: "Number" });
23
+ const settingField = panel("Setting", setting);
24
+
25
+ const tableWrap = el("div", { class: "mvb-scroll" });
26
+ const totalTile = tile("Total");
27
+ const validTile = tile("Valid");
28
+ const invalidTile = tile("Invalid");
29
+ const stats = el("div", { class: "mvb-stats" }, totalTile, validTile, invalidTile);
30
+ const error = notice("error");
31
+ let failing = [];
32
+
33
+ function needsSetting() {
34
+ return ["minlength", "maxlength", "regex"].includes(rule.value);
35
+ }
36
+
37
+ function check(value) {
38
+ switch (rule.value) {
39
+ case "notempty":
40
+ return value.trim() ? null : "Empty";
41
+ case "email":
42
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : "Not an email";
43
+ case "url":
44
+ try { new URL(value); return null; } catch { return "Not a URL"; }
45
+ case "number":
46
+ return value.trim() !== "" && !Number.isNaN(Number(value)) ? null : "Not a number";
47
+ case "minlength":
48
+ return value.length >= Number(setting.value) ? null : `Shorter than ${setting.value}`;
49
+ case "maxlength":
50
+ return value.length <= Number(setting.value) ? null : `Longer than ${setting.value}`;
51
+ case "regex":
52
+ try { return new RegExp(setting.value).test(value) ? null : "No match"; } catch { return "Invalid pattern"; }
53
+ default:
54
+ return null;
55
+ }
56
+ }
57
+
58
+ function run() {
59
+ error.write("");
60
+ settingField.hidden = !needsSetting();
61
+ setting.placeholder = rule.value === "regex" ? "Pattern" : "Number";
62
+
63
+ const raw = source.value.split(/\r?\n/);
64
+ if (raw.length && raw[raw.length - 1] === "") raw.pop();
65
+ const values = raw;
66
+
67
+ const failures = [];
68
+ values.forEach((value, i) => {
69
+ const reason = check(value);
70
+ if (reason) failures.push({ line: i + 1, value, reason });
71
+ });
72
+ failing = failures;
73
+
74
+ if (!values.length) {
75
+ tableWrap.replaceChildren(el("p", { class: "mvb-note" }, "Paste one value per line."));
76
+ } else if (!failures.length) {
77
+ tableWrap.replaceChildren(el("p", { class: "mvb-note" }, "Every value passes."));
78
+ } else {
79
+ tableWrap.replaceChildren(el("table", { class: "mvb-ttable" },
80
+ el("thead", {}, el("tr", {}, el("th", {}, "#"), el("th", {}, "Value"), el("th", {}, "Reason"))),
81
+ el("tbody", {}, ...failures.map((row) => el("tr", {},
82
+ el("th", {}, String(row.line)),
83
+ el("td", {}, row.value),
84
+ el("td", {}, row.reason)
85
+ )))
86
+ ));
87
+ }
88
+
89
+ totalTile.set(String(values.length));
90
+ validTile.set(String(values.length - failures.length));
91
+ invalidTile.set(String(failures.length));
92
+ }
93
+
94
+ source.addEventListener("input", run);
95
+ rule.addEventListener("change", run);
96
+ setting.addEventListener("input", run);
97
+
98
+ const card = toolcard({
99
+ body: bench(
100
+ stack(panel("Values", source), panel("Rule", rule), settingField, error),
101
+ stack(stats, panel("Failed rows", tableWrap))
102
+ ),
103
+ foot: actions(copyButton(() => failing.map((row) => row.value).join("\n"), "Copy failing values", "fill")),
104
+ });
105
+ slot.append(card.node);
106
+ run();
107
+
108
+ return {};
109
+ }
110
+
111
+ export default { mount };
@@ -0,0 +1,100 @@
1
+ // Counts the gap between two dates in days, weeks and calendar units.
2
+ import { el, bench, stack, panel, textInput, checkbox, notice, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const DAY = 86400000;
5
+
6
+ function parseDate(value) {
7
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
8
+ if (!match) return null;
9
+ const year = Number(match[1]);
10
+ const month = Number(match[2]) - 1;
11
+ const day = Number(match[3]);
12
+ const date = new Date(year, month, day);
13
+ if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return null;
14
+ return date;
15
+ }
16
+
17
+ function calendarDiff(from, to) {
18
+ let years = to.getFullYear() - from.getFullYear();
19
+ let months = to.getMonth() - from.getMonth();
20
+ let days = to.getDate() - from.getDate();
21
+ if (days < 0) {
22
+ months -= 1;
23
+ days += new Date(to.getFullYear(), to.getMonth(), 0).getDate();
24
+ }
25
+ if (months < 0) {
26
+ years -= 1;
27
+ months += 12;
28
+ }
29
+ return { years, months, days };
30
+ }
31
+
32
+ const kvRow = (key, value) =>
33
+ el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
34
+
35
+ export function mount(slot) {
36
+ const start = textInput({ type: "date", value: "2026-01-01", "aria-label": "Start date" });
37
+ const end = textInput({ type: "date", value: "2026-12-31", "aria-label": "End date" });
38
+ const includeEnd = checkbox("Include the end date", { checked: false });
39
+ const days = el("span", { class: "mvb-kv__v" }, "-");
40
+ const weeks = el("span", { class: "mvb-kv__v" }, "-");
41
+ const calendar = el("span", { class: "mvb-kv__v" }, "-");
42
+ const kv = el(
43
+ "div",
44
+ { class: "mvb-kv" },
45
+ kvRow("Total days", days),
46
+ kvRow("Weeks", weeks),
47
+ kvRow("Calendar gap", calendar)
48
+ );
49
+ const error = notice("error");
50
+ let text = "";
51
+
52
+ function clear() {
53
+ days.textContent = "-";
54
+ weeks.textContent = "-";
55
+ calendar.textContent = "-";
56
+ text = "";
57
+ }
58
+
59
+ function run() {
60
+ error.write("");
61
+ const a = parseDate(start.value);
62
+ const b = parseDate(end.value);
63
+ if (!a || !b) {
64
+ clear();
65
+ error.write("Pick a valid start and end date.");
66
+ return;
67
+ }
68
+ if (b < a) {
69
+ clear();
70
+ error.write("The end date is before the start date.");
71
+ return;
72
+ }
73
+ const to = includeEnd.input.checked ? new Date(b.getFullYear(), b.getMonth(), b.getDate() + 1) : b;
74
+ const total = Math.round((to - a) / DAY);
75
+ const diff = calendarDiff(a, to);
76
+ const weekCount = Math.floor(total / 7);
77
+ const rest = total % 7;
78
+ days.textContent = String(total);
79
+ weeks.textContent = rest ? `${weekCount} weeks ${rest} days` : `${weekCount} weeks`;
80
+ calendar.textContent = `${diff.years} years ${diff.months} months ${diff.days} days`;
81
+ text = `${total} days`;
82
+ }
83
+
84
+ for (const input of [start, end]) input.addEventListener("input", run);
85
+ includeEnd.input.addEventListener("change", run);
86
+
87
+ const card = toolcard({
88
+ body: bench(
89
+ stack(panel("Start", start), panel("End", end), includeEnd, error),
90
+ stack(panel("Difference", kv))
91
+ ),
92
+ foot: actions(copyButton(() => text, "Copy result", "fill")),
93
+ });
94
+ slot.append(card.node);
95
+ run();
96
+
97
+ return {};
98
+ }
99
+
100
+ export default { mount };
@@ -0,0 +1,107 @@
1
+ // A date typed in one format, written out in the others.
2
+ import { el, bench, stack, panel, textInput, selectInput, notice, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const FORMATS = [
5
+ { value: "iso", label: "ISO (yyyy-mm-dd)" },
6
+ { value: "dmy", label: "Day first (dd/mm/yyyy)" },
7
+ { value: "mdy", label: "Month first (mm/dd/yyyy)" },
8
+ ];
9
+
10
+ const pad = (n) => String(n).padStart(2, "0");
11
+
12
+ function parseDate(text, format) {
13
+ const raw = text.trim();
14
+ let year;
15
+ let month;
16
+ let day;
17
+ if (format === "iso") {
18
+ const match = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(raw);
19
+ if (!match) return null;
20
+ [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];
21
+ } else {
22
+ const match = /^(\d{1,2})[/.\-](\d{1,2})[/.\-](\d{2,4})$/.exec(raw);
23
+ if (!match) return null;
24
+ const first = Number(match[1]);
25
+ const second = Number(match[2]);
26
+ let rest = Number(match[3]);
27
+ if (rest < 100) rest += rest < 70 ? 2000 : 1900;
28
+ if (format === "dmy") [day, month, year] = [first, second, rest];
29
+ else [month, day, year] = [first, second, rest];
30
+ }
31
+ const date = new Date(year, month - 1, day);
32
+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) return null;
33
+ return date;
34
+ }
35
+
36
+ function intlFormat(date, options, fallback) {
37
+ try {
38
+ return new Intl.DateTimeFormat(undefined, options).format(date);
39
+ } catch {
40
+ return fallback;
41
+ }
42
+ }
43
+
44
+ const kvRow = (key, value) => el("div", { class: "mvb-kv__row" },
45
+ el("span", { class: "mvb-kv__k" }, key),
46
+ el("span", { class: "mvb-kv__v" }, value));
47
+
48
+ export function mount(slot) {
49
+ const format = selectInput(FORMATS, { value: "iso", "aria-label": "Input format" });
50
+ const entry = textInput({ placeholder: "2026-10-15", "aria-label": "Date" });
51
+
52
+ const iso = el("span", { class: "mvb-kv__v" }, "-");
53
+ const long = el("span", { class: "mvb-kv__v" }, "-");
54
+ const medium = el("span", { class: "mvb-kv__v" }, "-");
55
+ const weekday = el("span", { class: "mvb-kv__v" }, "-");
56
+ const unix = el("span", { class: "mvb-kv__v" }, "-");
57
+ const out = el("div", { class: "mvb-kv" },
58
+ kvRow("ISO", iso),
59
+ kvRow("Long", long),
60
+ kvRow("Medium", medium),
61
+ kvRow("Weekday", weekday),
62
+ kvRow("Unix seconds", unix));
63
+
64
+ const error = notice("error");
65
+ const note = notice("note");
66
+ note.write("Pick the input format so the date is read the way you meant.");
67
+
68
+ let isoText = "";
69
+
70
+ function run() {
71
+ const date = parseDate(entry.value, format.value);
72
+ if (!date) {
73
+ iso.textContent = "-";
74
+ long.textContent = "-";
75
+ medium.textContent = "-";
76
+ weekday.textContent = "-";
77
+ unix.textContent = "-";
78
+ isoText = "";
79
+ error.write(entry.value.trim() ? "That date does not match the chosen format." : "");
80
+ return;
81
+ }
82
+ error.write("");
83
+ isoText = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
84
+ iso.textContent = isoText;
85
+ long.textContent = intlFormat(date, { dateStyle: "long" }, date.toDateString());
86
+ medium.textContent = intlFormat(date, { dateStyle: "medium" }, date.toDateString());
87
+ weekday.textContent = intlFormat(date, { weekday: "long" }, "");
88
+ unix.textContent = String(Math.floor(date.getTime() / 1000));
89
+ }
90
+
91
+ format.addEventListener("change", run);
92
+ entry.addEventListener("input", run);
93
+
94
+ const card = toolcard({
95
+ body: bench(
96
+ stack(panel("Input format", format), panel("Date", entry), error),
97
+ stack(out, note)
98
+ ),
99
+ foot: actions(copyButton(() => isoText, "Copy ISO date", "fill")),
100
+ });
101
+ slot.append(card.node);
102
+ run();
103
+
104
+ return {};
105
+ }
106
+
107
+ export default { mount };
@@ -0,0 +1,109 @@
1
+ // Swaps one delimiter for another, keeping quoted fields intact.
2
+ import { el, bench, stack, panel, textArea, selectInput, notice, output, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const OPTIONS = [
5
+ { value: ",", label: "Comma" },
6
+ { value: ";", label: "Semicolon" },
7
+ { value: "\t", label: "Tab" },
8
+ { value: "|", label: "Pipe" },
9
+ ];
10
+
11
+ function parseDelimited(text, delimiter) {
12
+ const rows = [];
13
+ let row = [];
14
+ let field = "";
15
+ let quoted = false;
16
+ for (let i = 0; i < text.length; i++) {
17
+ const char = text[i];
18
+ if (quoted) {
19
+ if (char === '"') {
20
+ if (text[i + 1] === '"') { field += '"'; i++; }
21
+ else quoted = false;
22
+ } else field += char;
23
+ } else if (char === '"') {
24
+ quoted = true;
25
+ } else if (char === delimiter) {
26
+ row.push(field);
27
+ field = "";
28
+ } else if (char === "\n") {
29
+ row.push(field);
30
+ rows.push(row);
31
+ row = [];
32
+ field = "";
33
+ } else if (char !== "\r") {
34
+ field += char;
35
+ }
36
+ }
37
+ if (field !== "" || row.length) {
38
+ row.push(field);
39
+ rows.push(row);
40
+ }
41
+ return rows;
42
+ }
43
+
44
+ function detectDelimiter(text) {
45
+ const candidates = [",", ";", "\t", "|"];
46
+ const first = text.split(/\r?\n/)[0] ?? "";
47
+ const counts = { ",": 0, ";": 0, "\t": 0, "|": 0 };
48
+ let quoted = false;
49
+ for (let i = 0; i < first.length; i++) {
50
+ const char = first[i];
51
+ if (char === '"') quoted = !quoted;
52
+ else if (!quoted && char in counts) counts[char]++;
53
+ }
54
+ let best = ",";
55
+ let bestCount = 0;
56
+ for (const candidate of candidates) {
57
+ if (counts[candidate] > bestCount) { bestCount = counts[candidate]; best = candidate; }
58
+ }
59
+ return best;
60
+ }
61
+
62
+ function stringifyCSV(rows, delimiter) {
63
+ return rows.map((row) => row.map((cell) => {
64
+ const value = cell == null ? "" : String(cell);
65
+ const needsQuote = value.includes(delimiter) || /["\n\r]/.test(value) || value !== value.trim();
66
+ return needsQuote ? `"${value.replace(/"/g, '""')}"` : value;
67
+ }).join(delimiter)).join("\n");
68
+ }
69
+
70
+ export function mount(slot) {
71
+ const source = textArea({ rows: 12, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Data", placeholder: "Paste delimited data here" });
72
+ const from = selectInput([{ value: "auto", label: "Auto" }, ...OPTIONS], { value: "auto", "aria-label": "Current delimiter" });
73
+ const to = selectInput(OPTIONS, { value: ";", "aria-label": "New delimiter" });
74
+ const result = output({ rows: 12 });
75
+ const error = notice("error");
76
+ let current = "";
77
+
78
+ function run() {
79
+ error.write("");
80
+ const text = source.value;
81
+ if (!text.trim()) {
82
+ current = "";
83
+ result.write("");
84
+ return;
85
+ }
86
+ const used = from.value === "auto" ? detectDelimiter(text) : from.value;
87
+ const rows = parseDelimited(text, used);
88
+ current = stringifyCSV(rows, to.value);
89
+ result.write(current);
90
+ }
91
+
92
+ source.addEventListener("input", run);
93
+ from.addEventListener("change", run);
94
+ to.addEventListener("change", run);
95
+
96
+ const card = toolcard({
97
+ body: bench(
98
+ stack(panel("Data", source), panel("From", from), panel("To", to), error),
99
+ stack(panel("Result", result))
100
+ ),
101
+ foot: actions(copyButton(() => current, "Copy result", "fill")),
102
+ });
103
+ slot.append(card.node);
104
+ run();
105
+
106
+ return {};
107
+ }
108
+
109
+ export default { mount };
@@ -0,0 +1,83 @@
1
+ // One or two discounts on a price, with optional tax added after.
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 price = numberInput({ value: "120", "aria-label": "Original price" });
17
+ const first = numberInput({ value: "20", "aria-label": "First discount percent" });
18
+ const second = numberInput({ placeholder: "None", "aria-label": "Second discount percent" });
19
+ const tax = numberInput({ placeholder: "None", "aria-label": "Tax percent" });
20
+ const finalTile = tile("Final price");
21
+ const savedTile = tile("Saved");
22
+ const effectiveTile = tile("Effective discount");
23
+ const stats = el("div", { class: "mvb-stats" }, finalTile, savedTile, effectiveTile);
24
+ const error = notice("error");
25
+ let text = "";
26
+
27
+ function clear() {
28
+ finalTile.set("-");
29
+ savedTile.set("-");
30
+ effectiveTile.set("-");
31
+ text = "";
32
+ }
33
+
34
+ function run() {
35
+ error.write("");
36
+ const p = Number(price.value);
37
+ const d1 = first.value.trim() === "" ? 0 : Number(first.value);
38
+ const d2 = second.value.trim() === "" ? 0 : Number(second.value);
39
+ const t = tax.value.trim() === "" ? 0 : Number(tax.value);
40
+ if (price.value.trim() === "" || ![p, d1, d2, t].every(Number.isFinite)) {
41
+ clear();
42
+ return;
43
+ }
44
+ if (p < 0 || d1 < 0 || d2 < 0 || t < 0) {
45
+ clear();
46
+ error.write("The price, discounts and tax cannot be negative.");
47
+ return;
48
+ }
49
+ const afterDiscounts = p * (1 - d1 / 100) * (1 - d2 / 100);
50
+ const finalPrice = afterDiscounts * (1 + t / 100);
51
+ const saved = p - afterDiscounts;
52
+ const effective = p === 0 ? 0 : (saved / p) * 100;
53
+ finalTile.set(fmt(finalPrice));
54
+ savedTile.set(fmt(saved));
55
+ effectiveTile.set(`${fmt(effective)}%`);
56
+ text = `Final price ${fmt(finalPrice)}, saved ${fmt(saved)}, effective discount ${fmt(effective)}%`;
57
+ }
58
+
59
+ price.addEventListener("input", run);
60
+ first.addEventListener("input", run);
61
+ second.addEventListener("input", run);
62
+ tax.addEventListener("input", run);
63
+
64
+ const card = toolcard({
65
+ body: bench(
66
+ stack(
67
+ panel("Original price", price),
68
+ panel("First discount %", first),
69
+ panel("Second discount %", second),
70
+ panel("Tax %", tax),
71
+ error
72
+ ),
73
+ stack(stats)
74
+ ),
75
+ foot: actions(copyButton(() => text, "Copy result", "fill")),
76
+ });
77
+ slot.append(card.node);
78
+ run();
79
+
80
+ return {};
81
+ }
82
+
83
+ export default { mount };
@@ -0,0 +1,67 @@
1
+ // Cleans a DOI and writes its resolver link.
2
+ import { el, bench, stack, panel, textInput, notice, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ function kvRow(key) {
5
+ const value = el("span", { class: "mvb-kv__v" }, "-");
6
+ const row = el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), value);
7
+ return { row, value };
8
+ }
9
+
10
+ const clean = (raw) =>
11
+ raw
12
+ .trim()
13
+ .replace(/^https?:\/\/(dx\.)?doi\.org\//i, "")
14
+ .replace(/^doi:\s*/i, "")
15
+ .trim();
16
+
17
+ export function mount(slot) {
18
+ const input = textInput({ placeholder: "10.1000/xyz or https://doi.org/10.1000/xyz", "aria-label": "DOI" });
19
+ const doiRow = kvRow("Cleaned DOI");
20
+ const linkRow = kvRow("Resolver URL");
21
+ const kv = el("div", { class: "mvb-kv" }, doiRow.row, linkRow.row);
22
+ const error = notice("error");
23
+ const note = notice("note");
24
+ note.write("A matching pattern does not prove the DOI is registered.");
25
+ let link = "";
26
+
27
+ function run() {
28
+ const cleaned = clean(input.value);
29
+ if (!input.value.trim()) {
30
+ error.write("");
31
+ doiRow.value.textContent = "-";
32
+ linkRow.value.textContent = "-";
33
+ link = "";
34
+ return;
35
+ }
36
+ if (!/^10\.\d{4,9}\/\S+$/.test(cleaned)) {
37
+ error.write("That does not look like a DOI.");
38
+ doiRow.value.textContent = cleaned || "-";
39
+ linkRow.value.textContent = "-";
40
+ link = "";
41
+ return;
42
+ }
43
+ error.write("");
44
+ doiRow.value.textContent = cleaned;
45
+ link = `https://doi.org/${cleaned}`;
46
+ linkRow.value.textContent = link;
47
+ }
48
+
49
+ input.addEventListener("input", run);
50
+
51
+ const card = toolcard({
52
+ body: bench(
53
+ stack(panel("DOI", input), note, error),
54
+ stack(panel("Result", kv))
55
+ ),
56
+ foot: actions(
57
+ copyButton(() => doiRow.value.textContent, "Copy DOI"),
58
+ copyButton(() => link, "Copy link", "fill")
59
+ ),
60
+ });
61
+ slot.append(card.node);
62
+ run();
63
+
64
+ return {};
65
+ }
66
+
67
+ export default { mount };
@@ -0,0 +1,72 @@
1
+ // Tidies KEY=value lines: sorts the names and flags duplicates.
2
+ import { el, bench, stack, panel, textArea, notice, switchControl, output, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ function parse(text) {
5
+ const entries = [];
6
+ const comments = [];
7
+ const seen = new Set();
8
+ const duplicates = [];
9
+ for (const raw of text.replace(/\r\n?/g, "\n").split("\n")) {
10
+ const line = raw.trim();
11
+ if (!line) continue;
12
+ if (line.startsWith("#")) { comments.push(line); continue; }
13
+ const at = line.indexOf("=");
14
+ const key = at === -1 ? line : line.slice(0, at).trim();
15
+ const value = at === -1 ? null : line.slice(at + 1).trim();
16
+ if (seen.has(key)) duplicates.push(key);
17
+ else seen.add(key);
18
+ entries.push({ key, value });
19
+ }
20
+ return { entries, comments, duplicates };
21
+ }
22
+
23
+ export function mount(slot) {
24
+ const source = textArea({
25
+ rows: 12,
26
+ class: "mvb-in mvb-in--area mvb-in--mono",
27
+ "aria-label": "Environment variables",
28
+ placeholder: "KEY=value",
29
+ value: "API_URL=https://example.com\n# a note\nPORT=3000\nAPI_URL=https://other.com",
30
+ });
31
+ const result = output({ rows: 12 });
32
+ const warn = notice("warn");
33
+ let sort = true;
34
+ let keepComments = true;
35
+
36
+ const sortSwitch = switchControl("Sort names", { checked: true });
37
+ sortSwitch.input.addEventListener("change", () => { sort = sortSwitch.input.checked; run(); });
38
+
39
+ const commentSwitch = switchControl("Keep comments", { checked: true });
40
+ commentSwitch.input.addEventListener("change", () => { keepComments = commentSwitch.input.checked; run(); });
41
+
42
+ function run() {
43
+ warn.write("");
44
+ if (!source.value.trim()) {
45
+ result.write("");
46
+ return;
47
+ }
48
+ const { entries, comments, duplicates } = parse(source.value);
49
+ const ordered = sort ? entries.slice().sort((a, b) => a.key.localeCompare(b.key)) : entries;
50
+ const lines = [];
51
+ if (keepComments) lines.push(...comments);
52
+ for (const entry of ordered) lines.push(entry.value === null ? entry.key : `${entry.key}=${entry.value}`);
53
+ result.write(lines.join("\n"));
54
+ if (duplicates.length) warn.write(`Duplicate names: ${[...new Set(duplicates)].join(", ")}.`);
55
+ }
56
+
57
+ source.addEventListener("input", run);
58
+
59
+ const card = toolcard({
60
+ body: bench(
61
+ stack(panel("Options", el("div", { class: "mvb-stack" }, sortSwitch, commentSwitch)), panel("Input", source), warn),
62
+ stack(panel("Tidied", result))
63
+ ),
64
+ foot: actions(copyButton(() => result.textContent, "Copy list", "fill")),
65
+ });
66
+ slot.append(card.node);
67
+ run();
68
+
69
+ return {};
70
+ }
71
+
72
+ export default { mount };