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,97 @@
1
+ // Reads the time claims of a JWT and says whether it looks expired.
2
+ import { el, textArea, notice, copyButton, bench, stack, panel, actions, toolcard } from "../core.js";
3
+
4
+ const CLAIMS = [["iat", "Issued at"], ["nbf", "Not before"], ["exp", "Expires"]];
5
+
6
+ function decodeSegment(segment) {
7
+ const base = segment.replace(/-/g, "+").replace(/_/g, "/");
8
+ const padded = base.padEnd(base.length + ((4 - (base.length % 4)) % 4), "=");
9
+ const binary = atob(padded);
10
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
11
+ return JSON.parse(new TextDecoder().decode(bytes));
12
+ }
13
+
14
+ function relative(seconds) {
15
+ const diff = Math.round(seconds - Date.now() / 1000);
16
+ const abs = Math.abs(diff);
17
+ const [unit, size] = abs < 60 ? ["second", 1] : abs < 3600 ? ["minute", 60] : abs < 86400 ? ["hour", 3600] : ["day", 86400];
18
+ const value = Math.round(abs / size);
19
+ const text = `${value} ${unit}${value === 1 ? "" : "s"}`;
20
+ return diff >= 0 ? `in ${text}` : `${text} ago`;
21
+ }
22
+
23
+ const kvRow = (key, value) =>
24
+ el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
25
+
26
+ export function mount(slot) {
27
+ const source = textArea({ rows: 4, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Token", placeholder: "Paste a JWT" });
28
+ const claims = el("div", { class: "mvb-kv" });
29
+ const status = notice("note");
30
+ const error = notice("error");
31
+ let summary = "";
32
+
33
+ function run() {
34
+ claims.replaceChildren();
35
+ status.write("");
36
+ error.write("");
37
+ summary = "";
38
+ const token = source.value.trim();
39
+ if (!token) return;
40
+ const parts = token.split(".");
41
+ if (parts.length < 2) {
42
+ error.write("A JWT has three parts separated by dots.");
43
+ return;
44
+ }
45
+ let payload;
46
+ try {
47
+ payload = decodeSegment(parts[1]);
48
+ } catch {
49
+ error.write("The payload is not readable base64url JSON.");
50
+ return;
51
+ }
52
+
53
+ const lines = [];
54
+ for (const [key, label] of CLAIMS) {
55
+ if (payload[key] === undefined) continue;
56
+ const value = Number(payload[key]);
57
+ const when = Number.isFinite(value) ? new Date(value * 1000).toLocaleString() : String(payload[key]);
58
+ const extra = Number.isFinite(value) ? ` (${relative(value)})` : "";
59
+ claims.append(kvRow(label, `${when}${extra}`));
60
+ lines.push(`${label}: ${when}${extra}`);
61
+ }
62
+
63
+ if (payload.exp === undefined) {
64
+ status.write("No exp claim. This token has no stated expiry.");
65
+ } else {
66
+ const exp = Number(payload.exp);
67
+ if (!Number.isFinite(exp)) {
68
+ status.write("The exp claim is not a number.");
69
+ } else if (exp * 1000 < Date.now()) {
70
+ status.write(`Looks expired, ${relative(exp)}.`);
71
+ } else {
72
+ status.write(`Looks valid, expires ${relative(exp)}.`);
73
+ }
74
+ }
75
+ summary = lines.join("\n");
76
+ }
77
+
78
+ source.addEventListener("input", run);
79
+
80
+ const card = toolcard({
81
+ body: bench(
82
+ stack(
83
+ panel("Token", source),
84
+ error,
85
+ el("p", { class: "mvb-note" }, "Only the payload is read in this tab. The signature is not checked, so a valid look is not proof the token is genuine.")
86
+ ),
87
+ stack(panel("Time claims", claims), status)
88
+ ),
89
+ foot: actions(copyButton(() => summary, "Copy claims", "fill")),
90
+ });
91
+ slot.append(card.node);
92
+ run();
93
+
94
+ return {};
95
+ }
96
+
97
+ export default { mount };
@@ -0,0 +1,86 @@
1
+ // Converts a length between metric and imperial units, with the factor used.
2
+ import { el, bench, stack, panel, numberInput, selectInput, notice, copyButton, button, actions, toolcard } from "../core.js";
3
+
4
+ const UNITS = [
5
+ { value: "mm", label: "Millimeter", factor: 0.001 },
6
+ { value: "cm", label: "Centimeter", factor: 0.01 },
7
+ { value: "m", label: "Meter", factor: 1 },
8
+ { value: "km", label: "Kilometer", factor: 1000 },
9
+ { value: "in", label: "Inch", factor: 0.0254 },
10
+ { value: "ft", label: "Foot", factor: 0.3048 },
11
+ { value: "yd", label: "Yard", factor: 0.9144 },
12
+ { value: "mi", label: "Mile", factor: 1609.344 },
13
+ ];
14
+
15
+ const byId = Object.fromEntries(UNITS.map((unit) => [unit.value, unit]));
16
+ const options = UNITS.map(({ value, label }) => ({ value, label }));
17
+
18
+ function fmt(value) {
19
+ if (!Number.isFinite(value)) return "-";
20
+ if (value === 0) return "0";
21
+ const abs = Math.abs(value);
22
+ if (abs >= 1e-6 && abs < 1e15) return String(Number(value.toPrecision(8)));
23
+ return value.toExponential(4);
24
+ }
25
+
26
+ export function mount(slot) {
27
+ const value = numberInput({ value: "1", step: "any", "aria-label": "Value" });
28
+ const from = selectInput(options, { value: "m", "aria-label": "From unit" });
29
+ const to = selectInput(options, { value: "ft", "aria-label": "To unit" });
30
+ const result = el("span", { class: "mvb-stat__v" }, "-");
31
+ const stats = el("div", { class: "mvb-stats" }, el("div", { class: "mvb-stat" }, el("span", { class: "mvb-stat__k" }, "Result"), result));
32
+ const factor = el("p", { class: "mvb-note" });
33
+ const error = notice("error");
34
+ const scroll = el("div", { class: "mvb-scroll" });
35
+ let text = "";
36
+
37
+ function run() {
38
+ error.write("");
39
+ const raw = value.value.trim();
40
+ const n = Number(raw);
41
+ if (!raw || !Number.isFinite(n)) {
42
+ result.textContent = "-";
43
+ factor.textContent = "";
44
+ scroll.replaceChildren();
45
+ text = "";
46
+ error.write("Enter a number.");
47
+ return;
48
+ }
49
+ const a = byId[from.value];
50
+ const b = byId[to.value];
51
+ const out = (n * a.factor) / b.factor;
52
+ result.textContent = `${fmt(out)} ${b.value}`;
53
+ factor.textContent = `1 ${a.value} = ${fmt(a.factor / b.factor)} ${b.value}`;
54
+ scroll.replaceChildren(el("table", { class: "mvb-ttable" },
55
+ el("thead", {}, el("tr", {}, el("th", {}, "Unit"), el("th", {}, "Value"))),
56
+ el("tbody", {}, ...UNITS.map((unit) => el("tr", {},
57
+ el("td", {}, unit.label),
58
+ el("td", {}, fmt((n * a.factor) / unit.factor)))))));
59
+ text = `${fmt(out)} ${b.value}`;
60
+ }
61
+
62
+ value.addEventListener("input", run);
63
+ from.addEventListener("change", run);
64
+ to.addEventListener("change", run);
65
+
66
+ const swap = button("Swap", { icon: "arrow-left-right", onclick: () => {
67
+ const held = from.value;
68
+ from.value = to.value;
69
+ to.value = held;
70
+ run();
71
+ }});
72
+
73
+ const card = toolcard({
74
+ body: bench(
75
+ stack(panel("Value", value), panel("From", from), panel("To", to), error),
76
+ stack(panel("Result", stats), factor, panel("All units", scroll))
77
+ ),
78
+ foot: actions(swap, copyButton(() => text, "Copy result", "fill")),
79
+ });
80
+ slot.append(card.node);
81
+ run();
82
+
83
+ return {};
84
+ }
85
+
86
+ export default { mount };
@@ -0,0 +1,58 @@
1
+ // Short summaries of common open source licenses. Not legal advice.
2
+ import { el, bench, stack, panel, chipRow, notice, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const LICENSES = [
5
+ { value: "MIT", label: "MIT", can: "Use, modify and distribute, including commercially.", must: "Include the copyright notice and the license text.", cannot: "Hold the author liable." },
6
+ { value: "Apache-2.0", label: "Apache-2.0", can: "Use, modify and distribute, including commercially.", must: "Include the license, state changes and keep the NOTICE file.", cannot: "Use the project trademarks without permission." },
7
+ { value: "GPL-3.0", label: "GPL-3.0", can: "Use, modify and distribute.", must: "Release derivative works under the same license and share the source.", cannot: "Add restrictions beyond the license." },
8
+ { value: "BSD-3-Clause", label: "BSD-3-Clause", can: "Use, modify and distribute, including commercially.", must: "Keep the copyright notice and disclaimer.", cannot: "Use contributor names to endorse derivatives." },
9
+ { value: "MPL-2.0", label: "MPL-2.0", can: "Use, modify and distribute.", must: "Share changes to MPL-licensed files under MPL.", cannot: "Hold contributors liable." },
10
+ { value: "Unlicense", label: "Unlicense", can: "Use, modify and distribute with no conditions.", must: "Nothing.", cannot: "Hold the author liable." },
11
+ ];
12
+
13
+ const kvRow = (key, value) =>
14
+ el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
15
+
16
+ export function mount(slot) {
17
+ const name = el("span", { class: "mvb-stat__v" }, LICENSES[0].label);
18
+ const stats = el("div", { class: "mvb-stats" }, el("div", { class: "mvb-stat" }, el("span", { class: "mvb-stat__k" }, "License"), name));
19
+ const can = el("span", { class: "mvb-kv__v" });
20
+ const must = el("span", { class: "mvb-kv__v" });
21
+ const cannot = el("span", { class: "mvb-kv__v" });
22
+ const kv = el("div", { class: "mvb-kv" }, kvRow("Can", can), kvRow("Must", must), kvRow("Cannot", cannot));
23
+ const disclaimer = notice("note");
24
+ disclaimer.write("Short summary, not legal advice.");
25
+ let current = LICENSES[0];
26
+ let text = "";
27
+
28
+ const chips = chipRow(LICENSES.map(({ value, label }) => ({ value, label })), {
29
+ value: current.value,
30
+ label: "License",
31
+ onChange: (value) => {
32
+ current = LICENSES.find((entry) => entry.value === value);
33
+ run();
34
+ },
35
+ });
36
+
37
+ function run() {
38
+ name.textContent = current.label;
39
+ can.textContent = current.can;
40
+ must.textContent = current.must;
41
+ cannot.textContent = current.cannot;
42
+ text = `${current.label}\nCan: ${current.can}\nMust: ${current.must}\nCannot: ${current.cannot}`;
43
+ }
44
+
45
+ const card = toolcard({
46
+ body: bench(
47
+ stack(panel("License", chips), disclaimer),
48
+ stack(panel("Summary", stats), panel("Terms", kv))
49
+ ),
50
+ foot: actions(copyButton(() => text, "Copy summary", "fill")),
51
+ });
52
+ slot.append(card.node);
53
+ run();
54
+
55
+ return {};
56
+ }
57
+
58
+ export default { mount };
@@ -0,0 +1,48 @@
1
+ // Tidies the spacing in a caption, with even gaps between paragraphs.
2
+ import { el, bench, stack, panel, textArea, numberInput, switchControl, output, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ export function mount(slot) {
5
+ const source = textArea({ rows: 10, "aria-label": "Text", placeholder: "Type or paste text" });
6
+ const blankLines = numberInput({ min: "0", max: "4", value: "1", "aria-label": "Blank lines between paragraphs" });
7
+ const trim = switchControl("Trim trailing spaces", { checked: true });
8
+ const out = output({ rows: 10 });
9
+
10
+ let result = "";
11
+
12
+ function run() {
13
+ const normalized = source.value.replace(/\r\n?/g, "\n");
14
+ const paragraphs = normalized
15
+ .split(/\n{2,}/)
16
+ .map((block) => block
17
+ .split("\n")
18
+ .map((line) => (trim.input.checked ? line.replace(/\s+$/, "") : line))
19
+ .join("\n"))
20
+ .map((block) => block.replace(/^\n+|\n+$/g, ""))
21
+ .filter((block) => block.trim() !== "");
22
+ const gap = "\n".repeat(1 + Math.max(0, parseInt(blankLines.value, 10) || 0));
23
+ result = paragraphs.join(gap);
24
+ out.write(result);
25
+ }
26
+
27
+ source.addEventListener("input", run);
28
+ blankLines.addEventListener("input", run);
29
+ trim.input.addEventListener("change", run);
30
+
31
+ const card = toolcard({
32
+ body: bench(
33
+ stack(
34
+ panel("Text", source),
35
+ panel("Blank lines between paragraphs", blankLines),
36
+ panel("Spacing", trim)
37
+ ),
38
+ stack(panel("Result", out))
39
+ ),
40
+ foot: actions(copyButton(() => result, "Copy text", "fill")),
41
+ });
42
+ slot.append(card.node);
43
+ run();
44
+
45
+ return {};
46
+ }
47
+
48
+ export default { mount };
@@ -0,0 +1,55 @@
1
+ // Line totals for pasted lists, logs and data: total, filled and blank.
2
+ import { el, bench, stack, panel, textArea, button, 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: 14, "aria-label": "Text", placeholder: "Paste text, one entry per line" });
13
+
14
+ const totalTile = tile("Total lines");
15
+ const filledTile = tile("Filled");
16
+ const blankTile = tile("Blank");
17
+ const stats = el("div", { class: "mvb-stats" }, totalTile, filledTile, blankTile);
18
+
19
+ let counts = { total: 0, filled: 0, blank: 0 };
20
+
21
+ function summary() {
22
+ return [
23
+ `${counts.total} total lines`,
24
+ `${counts.filled} filled`,
25
+ `${counts.blank} blank`,
26
+ ].join("\n");
27
+ }
28
+
29
+ function run() {
30
+ const lines = source.value === "" ? [] : source.value.split("\n");
31
+ const blank = lines.filter((line) => !line.trim()).length;
32
+ counts = { total: lines.length, filled: lines.length - blank, blank };
33
+ totalTile.set(String(counts.total));
34
+ filledTile.set(String(counts.filled));
35
+ blankTile.set(String(counts.blank));
36
+ }
37
+
38
+ const clearButton = button("Clear", { icon: "trash" });
39
+ clearButton.addEventListener("click", () => { source.value = ""; run(); });
40
+ source.addEventListener("input", run);
41
+
42
+ const card = toolcard({
43
+ body: bench(
44
+ stack(panel("Text", source)),
45
+ stack(stats)
46
+ ),
47
+ foot: actions(clearButton, copyButton(summary, "Copy summary", "fill")),
48
+ });
49
+ slot.append(card.node);
50
+ run();
51
+
52
+ return {};
53
+ }
54
+
55
+ export default { mount };
@@ -0,0 +1,86 @@
1
+ // Compares two lists and shows what is only in each and what they share.
2
+ import { el, bench, stack, panel, textArea, checkbox, output, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const linesOf = (text) => text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
5
+
6
+ function uniqueLines(text, ignoreCase) {
7
+ const seen = new Set();
8
+ const out = [];
9
+ for (const line of linesOf(text)) {
10
+ const key = ignoreCase ? line.toLowerCase() : line;
11
+ if (seen.has(key)) continue;
12
+ seen.add(key);
13
+ out.push(line);
14
+ }
15
+ return out;
16
+ }
17
+
18
+ function tile(label) {
19
+ const value = el("span", { class: "mvb-stat__v" }, "0");
20
+ const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
21
+ node.set = (text) => { value.textContent = text; };
22
+ return node;
23
+ }
24
+
25
+ export function mount(slot) {
26
+ const first = textArea({ rows: 8, "aria-label": "List A", placeholder: "One entry per line" });
27
+ const second = textArea({ rows: 8, "aria-label": "List B", placeholder: "One entry per line" });
28
+ const caseBox = checkbox("Ignore case");
29
+
30
+ const onlyAOut = output({ rows: 5 });
31
+ const onlyBOut = output({ rows: 5 });
32
+ const bothOut = output({ rows: 5 });
33
+ const onlyATile = tile("Only in A");
34
+ const onlyBTile = tile("Only in B");
35
+ const bothTile = tile("In both");
36
+ const stats = el("div", { class: "mvb-stats" }, onlyATile, onlyBTile, bothTile);
37
+ let report = "";
38
+
39
+ function run() {
40
+ const ignoreCase = caseBox.input.checked;
41
+ const key = (line) => (ignoreCase ? line.toLowerCase() : line);
42
+ const a = uniqueLines(first.value, ignoreCase);
43
+ const b = uniqueLines(second.value, ignoreCase);
44
+ const keyB = new Set(b.map(key));
45
+ const keyA = new Set(a.map(key));
46
+ const onlyA = a.filter((line) => !keyB.has(key(line)));
47
+ const onlyB = b.filter((line) => !keyA.has(key(line)));
48
+ const both = a.filter((line) => keyB.has(key(line)));
49
+
50
+ onlyAOut.write(onlyA.join("\n"));
51
+ onlyBOut.write(onlyB.join("\n"));
52
+ bothOut.write(both.join("\n"));
53
+ onlyATile.set(String(onlyA.length));
54
+ onlyBTile.set(String(onlyB.length));
55
+ bothTile.set(String(both.length));
56
+
57
+ report = [
58
+ `Only in A (${onlyA.length}):`,
59
+ onlyA.join("\n"),
60
+ "",
61
+ `Only in B (${onlyB.length}):`,
62
+ onlyB.join("\n"),
63
+ "",
64
+ `In both (${both.length}):`,
65
+ both.join("\n"),
66
+ ].join("\n");
67
+ }
68
+
69
+ first.addEventListener("input", run);
70
+ second.addEventListener("input", run);
71
+ caseBox.input.addEventListener("change", run);
72
+
73
+ const card = toolcard({
74
+ body: bench(
75
+ stack(panel("List A", first), panel("List B", second), panel(null, caseBox)),
76
+ stack(stats, panel("Only in A", onlyAOut), panel("Only in B", onlyBOut), panel("In both", bothOut))
77
+ ),
78
+ foot: actions(copyButton(() => report, "Copy report", "fill")),
79
+ });
80
+ slot.append(card.node);
81
+ run();
82
+
83
+ return {};
84
+ }
85
+
86
+ export default { mount };
@@ -0,0 +1,67 @@
1
+ // Joins two lists into one, optionally dropping duplicates.
2
+ import { el, bench, stack, panel, textArea, checkbox, output, copyButton, button, actions, toolcard } from "../core.js";
3
+
4
+ const linesOf = (text) => text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
5
+
6
+ function tile(label) {
7
+ const value = el("span", { class: "mvb-stat__v" }, "0");
8
+ const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
9
+ node.set = (text) => { value.textContent = text; };
10
+ return node;
11
+ }
12
+
13
+ export function mount(slot) {
14
+ const first = textArea({ rows: 8, "aria-label": "List A", placeholder: "One entry per line" });
15
+ const second = textArea({ rows: 8, "aria-label": "List B", placeholder: "One entry per line" });
16
+ const dedupeBox = checkbox("Remove duplicates", { checked: true });
17
+ const caseBox = checkbox("Ignore case");
18
+
19
+ const result = output({ rows: 12 });
20
+ const listATile = tile("List A");
21
+ const listBTile = tile("List B");
22
+ const mergedTile = tile("Merged");
23
+ const stats = el("div", { class: "mvb-stats" }, listATile, listBTile, mergedTile);
24
+ let current = [];
25
+
26
+ function run() {
27
+ const a = linesOf(first.value);
28
+ const b = linesOf(second.value);
29
+ let merged = [...a, ...b];
30
+ if (dedupeBox.input.checked) {
31
+ const seen = new Set();
32
+ const ignoreCase = caseBox.input.checked;
33
+ merged = merged.filter((line) => {
34
+ const key = ignoreCase ? line.toLowerCase() : line;
35
+ if (seen.has(key)) return false;
36
+ seen.add(key);
37
+ return true;
38
+ });
39
+ }
40
+ current = merged;
41
+ result.write(merged.join("\n"));
42
+ listATile.set(String(a.length));
43
+ listBTile.set(String(b.length));
44
+ mergedTile.set(String(merged.length));
45
+ }
46
+
47
+ const clearButton = button("Clear", { icon: "trash" });
48
+ clearButton.addEventListener("click", () => { first.value = ""; second.value = ""; run(); });
49
+ first.addEventListener("input", run);
50
+ second.addEventListener("input", run);
51
+ dedupeBox.input.addEventListener("change", run);
52
+ caseBox.input.addEventListener("change", run);
53
+
54
+ const card = toolcard({
55
+ body: bench(
56
+ stack(panel("List A", first), panel("List B", second), panel(null, dedupeBox), panel(null, caseBox)),
57
+ stack(stats, panel("Merged", result))
58
+ ),
59
+ foot: actions(clearButton, copyButton(() => current.join("\n"), "Copy merged", "fill")),
60
+ });
61
+ slot.append(card.node);
62
+ run();
63
+
64
+ return {};
65
+ }
66
+
67
+ export default { mount };
@@ -0,0 +1,79 @@
1
+ // One number shown in several locales, with decimal and grouping options.
2
+ import { el, bench, stack, panel, textInput, numberInput, switchControl, notice, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const LOCALES = [
5
+ ["en-US", "English (US)"],
6
+ ["en-GB", "English (UK)"],
7
+ ["de-DE", "German"],
8
+ ["fr-FR", "French"],
9
+ ["es-ES", "Spanish"],
10
+ ["id-ID", "Indonesian"],
11
+ ["hi-IN", "Hindi"],
12
+ ["ar-EG", "Arabic"],
13
+ ["ja-JP", "Japanese"],
14
+ ];
15
+
16
+ function format(locale, value, options) {
17
+ try {
18
+ return new Intl.NumberFormat(locale, options).format(value);
19
+ } catch {
20
+ return String(value);
21
+ }
22
+ }
23
+
24
+ export function mount(slot) {
25
+ const value = textInput({ value: "1234567.89", inputmode: "decimal", "aria-label": "Number" });
26
+ const decimals = numberInput({ min: "0", max: "20", value: "2", "aria-label": "Fraction digits" });
27
+ const grouping = switchControl("Group thousands", { checked: true });
28
+
29
+ const table = el("table", { class: "mvb-ttable" });
30
+ const tableWrap = el("div", { class: "mvb-scroll" }, table);
31
+ const note = notice("note");
32
+ note.write("Formatting only. The number is not converted.");
33
+
34
+ let tableText = "";
35
+
36
+ function run() {
37
+ const raw = value.value.trim();
38
+ const parsed = Number(raw);
39
+ const digits = Math.min(20, Math.max(0, parseInt(decimals.value, 10) || 0));
40
+ const options = {
41
+ minimumFractionDigits: digits,
42
+ maximumFractionDigits: digits,
43
+ useGrouping: grouping.input.checked,
44
+ };
45
+ const rows = LOCALES.map(([code, name]) => {
46
+ const shown = raw === "" || !Number.isFinite(parsed) ? "-" : format(code, parsed, options);
47
+ return el("tr", {}, el("th", {}, name), el("td", {}, shown));
48
+ });
49
+ table.replaceChildren(
50
+ el("thead", {}, el("tr", {}, el("th", {}, "Locale"), el("th", {}, "Number"))),
51
+ el("tbody", {}, ...rows));
52
+ tableText = LOCALES.map(([code, name]) => {
53
+ const shown = raw === "" || !Number.isFinite(parsed) ? "-" : format(code, parsed, options);
54
+ return `${name}: ${shown}`;
55
+ }).join("\n");
56
+ }
57
+
58
+ value.addEventListener("input", run);
59
+ decimals.addEventListener("input", run);
60
+ grouping.input.addEventListener("change", run);
61
+
62
+ const card = toolcard({
63
+ body: bench(
64
+ stack(
65
+ panel("Number", value),
66
+ panel("Fraction digits", decimals),
67
+ panel("Grouping", grouping)
68
+ ),
69
+ stack(note, panel("By locale", tableWrap))
70
+ ),
71
+ foot: actions(copyButton(() => tableText, "Copy list", "fill")),
72
+ });
73
+ slot.append(card.node);
74
+ run();
75
+
76
+ return {};
77
+ }
78
+
79
+ export default { mount };