mevebu 0.3.1 → 1.0.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,54 @@
1
+ // Reference for the common MIME types, filterable by extension or type.
2
+ import { el, bench, stack, panel, textInput, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const TYPES = [
5
+ ["html", "text/html", "Text"], ["htm", "text/html", "Text"], ["css", "text/css", "Text"], ["js", "text/javascript", "Text"], ["mjs", "text/javascript", "Text"], ["json", "application/json", "Data"], ["xml", "application/xml", "Data"], ["txt", "text/plain", "Text"], ["csv", "text/csv", "Data"], ["md", "text/markdown", "Text"], ["yaml", "application/yaml", "Data"], ["yml", "application/yaml", "Data"], ["webmanifest", "application/manifest+json", "Data"],
6
+ ["pdf", "application/pdf", "Document"], ["rtf", "application/rtf", "Document"], ["doc", "application/msword", "Document"], ["docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "Document"], ["xls", "application/vnd.ms-excel", "Document"], ["xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "Document"], ["ppt", "application/vnd.ms-powerpoint", "Document"], ["pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "Document"], ["epub", "application/epub+zip", "Document"],
7
+ ["zip", "application/zip", "Archive"], ["gz", "application/gzip", "Archive"], ["tar", "application/x-tar", "Archive"], ["rar", "application/vnd.rar", "Archive"], ["7z", "application/x-7z-compressed", "Archive"], ["jar", "application/java-archive", "Archive"], ["apk", "application/vnd.android.package-archive", "Archive"],
8
+ ["png", "image/png", "Image"], ["jpg", "image/jpeg", "Image"], ["jpeg", "image/jpeg", "Image"], ["gif", "image/gif", "Image"], ["webp", "image/webp", "Image"], ["svg", "image/svg+xml", "Image"], ["ico", "image/x-icon", "Image"], ["avif", "image/avif", "Image"], ["bmp", "image/bmp", "Image"], ["tiff", "image/tiff", "Image"],
9
+ ["mp3", "audio/mpeg", "Audio"], ["wav", "audio/wav", "Audio"], ["ogg", "audio/ogg", "Audio"], ["oga", "audio/ogg", "Audio"], ["m4a", "audio/mp4", "Audio"], ["flac", "audio/flac", "Audio"], ["aac", "audio/aac", "Audio"],
10
+ ["mp4", "video/mp4", "Video"], ["webm", "video/webm", "Video"], ["ogv", "video/ogg", "Video"], ["mov", "video/quicktime", "Video"], ["avi", "video/x-msvideo", "Video"], ["mkv", "video/x-matroska", "Video"],
11
+ ["woff", "font/woff", "Font"], ["woff2", "font/woff2", "Font"], ["ttf", "font/ttf", "Font"], ["otf", "font/otf", "Font"], ["eot", "application/vnd.ms-fontobject", "Font"],
12
+ ["wasm", "application/wasm", "Binary"], ["bin", "application/octet-stream", "Binary"],
13
+ ];
14
+
15
+ export function mount(slot) {
16
+ const filter = textInput({ type: "search", placeholder: "Extension or type", "aria-label": "Filter" });
17
+ const scroll = el("div", { class: "mvb-scroll" });
18
+ let visible = TYPES;
19
+
20
+ function run() {
21
+ const query = filter.value.trim().toLowerCase();
22
+ visible = TYPES.filter(([ext, mime]) => !query || ext.includes(query) || mime.toLowerCase().includes(query));
23
+ if (!visible.length) {
24
+ scroll.replaceChildren(el("p", { class: "mvb-note" }, "No type matches."));
25
+ return;
26
+ }
27
+ scroll.replaceChildren(el(
28
+ "table",
29
+ { class: "mvb-ttable" },
30
+ el("thead", {}, el("tr", {}, el("th", {}, "Extension"), el("th", {}, "MIME type"), el("th", {}, "Kind"))),
31
+ el("tbody", {}, ...visible.map(([ext, mime, kind]) => el("tr", {},
32
+ el("td", {}, `.${ext}`),
33
+ el("td", {}, mime),
34
+ el("td", {}, kind)
35
+ )))
36
+ ));
37
+ }
38
+
39
+ filter.addEventListener("input", run);
40
+
41
+ const card = toolcard({
42
+ body: bench(
43
+ stack(panel("Filter", filter)),
44
+ stack(panel("MIME types", scroll))
45
+ ),
46
+ foot: actions(copyButton(() => visible.map(([ext, mime]) => `.${ext} ${mime}`).join("\n"), "Copy list", "fill")),
47
+ });
48
+ slot.append(card.node);
49
+ run();
50
+
51
+ return {};
52
+ }
53
+
54
+ export default { mount };
@@ -0,0 +1,91 @@
1
+ // Builds an MLA 9 works cited entry from source details.
2
+ import { bench, stack, panel, textInput, selectInput, output, notice, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const TYPES = [
5
+ { value: "book", label: "Book" },
6
+ { value: "article", label: "Journal article" },
7
+ { value: "web", label: "Web page" },
8
+ ];
9
+
10
+ const parts = (raw) => raw.split(";").map((item) => item.trim()).filter(Boolean);
11
+
12
+ function flip(author) {
13
+ const bits = author.split(",").map((item) => item.trim());
14
+ return bits.length >= 2 ? `${bits[1]} ${bits[0]}` : author;
15
+ }
16
+
17
+ function authorList(raw) {
18
+ const list = parts(raw);
19
+ if (!list.length) return "";
20
+ if (list.length === 1) return list[0];
21
+ if (list.length === 2) return `${list[0]}, and ${flip(list[1])}`;
22
+ return `${list[0]}, et al.`;
23
+ }
24
+
25
+ export function mount(slot) {
26
+ const type = selectInput(TYPES, { value: "book", "aria-label": "Source type" });
27
+ const authors = textInput({ placeholder: "Smith, John; Doe, Jane", "aria-label": "Authors" });
28
+ const title = textInput({ placeholder: "Title of the work", "aria-label": "Title" });
29
+ const source = textInput({ placeholder: "Publisher, journal or site", "aria-label": "Source" });
30
+ const details = textInput({ placeholder: "Volume, issue, pages", "aria-label": "Details" });
31
+ const year = textInput({ placeholder: "2020", "aria-label": "Year" });
32
+ const link = textInput({ placeholder: "https://example.com", "aria-label": "URL" });
33
+
34
+ const entry = output({ rows: 6 });
35
+ const note = notice("note");
36
+ note.write("Check the details against the source and the MLA 9 guide.");
37
+ let refText = "";
38
+
39
+ function build() {
40
+ const pieces = [];
41
+ const au = authorList(authors.value);
42
+ if (au) pieces.push(`${au}.`);
43
+ const ti = title.value.trim();
44
+ const src = source.value.trim();
45
+ const det = details.value.trim();
46
+ const yr = year.value.trim();
47
+ const url = link.value.trim();
48
+
49
+ if (type.value === "book") {
50
+ if (ti) pieces.push(`${ti}.`);
51
+ const tail = [src, yr].filter(Boolean).join(", ");
52
+ if (tail) pieces.push(`${tail}.`);
53
+ } else if (type.value === "article") {
54
+ if (ti) pieces.push(`"${ti}."`);
55
+ const tail = [src, det, yr].filter(Boolean).join(", ");
56
+ if (tail) pieces.push(`${tail}.`);
57
+ } else {
58
+ if (ti) pieces.push(`"${ti}."`);
59
+ const tail = [src, yr, url].filter(Boolean).join(", ");
60
+ if (tail) pieces.push(`${tail}.`);
61
+ }
62
+ refText = pieces.join(" ");
63
+ entry.write(refText);
64
+ }
65
+
66
+ for (const input of [authors, title, source, details, year, link]) input.addEventListener("input", build);
67
+ type.addEventListener("change", build);
68
+
69
+ const card = toolcard({
70
+ body: bench(
71
+ stack(
72
+ panel("Source type", type),
73
+ panel("Authors", authors),
74
+ panel("Title", title),
75
+ panel("Source", source),
76
+ panel("Details", details),
77
+ panel("Year", year),
78
+ panel("URL", link),
79
+ note
80
+ ),
81
+ stack(panel("Works cited entry", entry))
82
+ ),
83
+ foot: actions(copyButton(() => refText, "Copy entry", "fill")),
84
+ });
85
+ slot.append(card.node);
86
+ build();
87
+
88
+ return {};
89
+ }
90
+
91
+ export default { mount };
@@ -0,0 +1,97 @@
1
+ // Converts a number between binary, octal, decimal and hexadecimal.
2
+ import { el, bench, stack, panel, textInput, notice, copyButton, chipRow, actions, toolcard } from "../core.js";
3
+
4
+ const BASES = [
5
+ { value: "2", label: "Binary" },
6
+ { value: "8", label: "Octal" },
7
+ { value: "10", label: "Decimal" },
8
+ { value: "16", label: "Hex" },
9
+ ];
10
+
11
+ const PATTERNS = { 2: /^[01]+$/, 8: /^[0-7]+$/, 10: /^[0-9]+$/, 16: /^[0-9a-f]+$/ };
12
+ const PREFIX = { 2: "0b", 8: "0o", 10: "", 16: "0x" };
13
+
14
+ function parseInBase(text, base) {
15
+ let clean = text.trim().toLowerCase().replace(/[\s_]/g, "");
16
+ if (!clean) return null;
17
+ let sign = 1n;
18
+ if (clean.startsWith("-")) { sign = -1n; clean = clean.slice(1); }
19
+ else if (clean.startsWith("+")) clean = clean.slice(1);
20
+ clean = clean.replace(/^0[bxo]/, "");
21
+ if (!PATTERNS[base].test(clean)) return null;
22
+ return sign * BigInt(PREFIX[base] + clean);
23
+ }
24
+
25
+ const kvRow = (key, value) =>
26
+ el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
27
+
28
+ export function mount(slot) {
29
+ const source = textInput({ value: "255", "aria-label": "Number", placeholder: "255" });
30
+ const binary = el("span", { class: "mvb-kv__v" }, "-");
31
+ const octal = el("span", { class: "mvb-kv__v" }, "-");
32
+ const decimal = el("span", { class: "mvb-kv__v" }, "-");
33
+ const hex = el("span", { class: "mvb-kv__v" }, "-");
34
+ const kv = el(
35
+ "div",
36
+ { class: "mvb-kv" },
37
+ kvRow("Binary", binary),
38
+ kvRow("Octal", octal),
39
+ kvRow("Decimal", decimal),
40
+ kvRow("Hex", hex)
41
+ );
42
+ const error = notice("error");
43
+ let base = "10";
44
+ let text = "";
45
+
46
+ function clear() {
47
+ binary.textContent = "-";
48
+ octal.textContent = "-";
49
+ decimal.textContent = "-";
50
+ hex.textContent = "-";
51
+ text = "";
52
+ }
53
+
54
+ function run() {
55
+ error.write("");
56
+ if (!source.value.trim()) {
57
+ clear();
58
+ return;
59
+ }
60
+ const value = parseInBase(source.value, base);
61
+ if (value === null) {
62
+ clear();
63
+ error.write(`That is not a valid ${BASES.find((entry) => entry.value === base).label.toLowerCase()} number.`);
64
+ return;
65
+ }
66
+ binary.textContent = value.toString(2);
67
+ octal.textContent = value.toString(8);
68
+ decimal.textContent = value.toString(10);
69
+ hex.textContent = value.toString(16).toUpperCase();
70
+ text = `Binary ${value.toString(2)}\nOctal ${value.toString(8)}\nDecimal ${value.toString(10)}\nHex ${value.toString(16).toUpperCase()}`;
71
+ }
72
+
73
+ const chips = chipRow(BASES, {
74
+ value: base,
75
+ label: "Input base",
76
+ onChange: (value) => {
77
+ base = value;
78
+ run();
79
+ },
80
+ });
81
+
82
+ source.addEventListener("input", run);
83
+
84
+ const card = toolcard({
85
+ body: bench(
86
+ stack(panel("Number", source), panel("Input base", chips), error),
87
+ stack(panel("In every base", kv))
88
+ ),
89
+ foot: actions(copyButton(() => text, "Copy all", "fill")),
90
+ });
91
+ slot.append(card.node);
92
+ run();
93
+
94
+ return {};
95
+ }
96
+
97
+ export default { mount };
@@ -0,0 +1,62 @@
1
+ // Builds Open Graph and Twitter card tags from a page title, description,
2
+ // address, image and type.
3
+ import { bench, stack, panel, textInput, textArea, selectInput, output, notice, copyButton, actions, toolcard } from "../core.js";
4
+
5
+ const esc = (value) =>
6
+ String(value).replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[char]));
7
+
8
+ export function mount(slot) {
9
+ const title = textInput({ value: "mevebu", "aria-label": "Title" });
10
+ const description = textArea({ rows: 3, value: "Small privacy first tools that run entirely in the browser.", "aria-label": "Description" });
11
+ const url = textInput({ value: "https://example.com", "aria-label": "URL" });
12
+ const image = textInput({ value: "https://example.com/card.png", "aria-label": "Image URL" });
13
+ const site = textInput({ value: "mevebu", "aria-label": "Site name" });
14
+ const type = selectInput(["website", "article", "product", "profile"], { value: "website", "aria-label": "Type" });
15
+ const result = output({ rows: 12 });
16
+ const note = notice("note");
17
+
18
+ function build() {
19
+ const lines = [];
20
+ const t = title.value.trim();
21
+ const d = description.value.trim();
22
+ const u = url.value.trim();
23
+ const img = image.value.trim();
24
+ const s = site.value.trim();
25
+ if (t) lines.push(`<meta property="og:title" content="${esc(t)}" />`);
26
+ if (d) lines.push(`<meta property="og:description" content="${esc(d)}" />`);
27
+ lines.push(`<meta property="og:type" content="${esc(type.value)}" />`);
28
+ if (u) lines.push(`<meta property="og:url" content="${esc(u)}" />`);
29
+ if (img) lines.push(`<meta property="og:image" content="${esc(img)}" />`);
30
+ if (s) lines.push(`<meta property="og:site_name" content="${esc(s)}" />`);
31
+ lines.push(`<meta name="twitter:card" content="${img ? "summary_large_image" : "summary"}" />`);
32
+ if (t) lines.push(`<meta name="twitter:title" content="${esc(t)}" />`);
33
+ if (d) lines.push(`<meta name="twitter:description" content="${esc(d)}" />`);
34
+ if (img) lines.push(`<meta name="twitter:image" content="${esc(img)}" />`);
35
+ result.write(lines.join("\n"));
36
+ note.write("Platforms read different tags, so previews vary.");
37
+ }
38
+
39
+ for (const input of [title, description, url, image, site]) input.addEventListener("input", build);
40
+ type.addEventListener("change", build);
41
+
42
+ const card = toolcard({
43
+ body: bench(
44
+ stack(
45
+ panel("Title", title),
46
+ panel("Description", description),
47
+ panel("URL", url),
48
+ panel("Image URL", image),
49
+ panel("Site name", site),
50
+ panel("Type", type)
51
+ ),
52
+ stack(panel("Open Graph tags", result), note)
53
+ ),
54
+ foot: actions(copyButton(() => result.textContent, "Copy tags", "fill")),
55
+ });
56
+ slot.append(card.node);
57
+ build();
58
+
59
+ return {};
60
+ }
61
+
62
+ export default { mount };
@@ -0,0 +1,118 @@
1
+ // Estimates password strength from length and patterns, all in this tab.
2
+ import { el, icon, passwordInput, button, bench, stack, panel, notice, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const COMMON = new Set([
5
+ "password", "123456", "123456789", "qwerty", "letmein", "admin", "welcome",
6
+ "iloveyou", "monkey", "dragon", "abc123", "111111", "000000", "passw0rd",
7
+ ]);
8
+
9
+ const GROUPS = [
10
+ { test: (value) => /[a-z]/.test(value) },
11
+ { test: (value) => /[A-Z]/.test(value) },
12
+ { test: (value) => /[0-9]/.test(value) },
13
+ { test: (value) => /[^A-Za-z0-9]/.test(value) },
14
+ ];
15
+
16
+ const LEVELS = [
17
+ { name: "very weak", cls: "mvb-weak" },
18
+ { name: "weak", cls: "mvb-weak" },
19
+ { name: "fair", cls: "mvb-ok" },
20
+ { name: "good", cls: "mvb-ok" },
21
+ { name: "strong", cls: "mvb-strong" },
22
+ { name: "very strong", cls: "mvb-strong" },
23
+ ];
24
+
25
+ function analyze(value) {
26
+ const length = value.length;
27
+ const groups = GROUPS.filter((group) => group.test(value)).length;
28
+ let score = 0;
29
+ if (length >= 8) score += 1;
30
+ if (length >= 12) score += 1;
31
+ if (length >= 16) score += 1;
32
+ if (groups >= 3) score += 1;
33
+ if (length >= 20 && groups === 4) score += 1;
34
+
35
+ const hints = [];
36
+ if (length < 12) hints.push("Use at least 12 characters.");
37
+ if (groups < 3) hints.push("Mix upper case, lower case, digits and symbols.");
38
+ if (/(.)\1{2,}/.test(value)) {
39
+ score -= 1;
40
+ hints.push("Repeated characters are easy to guess.");
41
+ }
42
+ if (COMMON.has(value.toLowerCase())) {
43
+ score = 0;
44
+ hints.unshift("This is one of the most common passwords.");
45
+ }
46
+ return { score: Math.max(0, Math.min(5, score)), groups, length, hints };
47
+ }
48
+
49
+ const kvRow = (key, value) =>
50
+ el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
51
+
52
+ export function mount(slot) {
53
+ const field = passwordInput({ autocomplete: "off", "aria-label": "Password", placeholder: "Type a password" });
54
+ const showButton = button("Show", { icon: "eye" });
55
+ const meter = el("div", { class: "mvb-meter", role: "img", "aria-label": "Strength" }, el("div", { class: "mvb-meter__fill" }));
56
+ const label = el("p", { class: "note" });
57
+ const lengthValue = el("span", { class: "mvb-kv__v" }, "-");
58
+ const groupsValue = el("span", { class: "mvb-kv__v" }, "-");
59
+ const kv = el("div", { class: "mvb-kv" }, kvRow("Length", lengthValue), kvRow("Character groups", groupsValue));
60
+ const hints = el("div", { class: "mvb-stack" });
61
+ const status = notice("note");
62
+ let report = "";
63
+
64
+ showButton.addEventListener("click", () => {
65
+ const visible = field.type === "text";
66
+ field.type = visible ? "password" : "text";
67
+ showButton.replaceChildren(icon(visible ? "eye" : "eye-off"), el("span", {}, visible ? "Show" : "Hide"));
68
+ });
69
+
70
+ function run() {
71
+ const value = field.value;
72
+ const fill = meter.querySelector(".mvb-meter__fill");
73
+ hints.replaceChildren();
74
+ if (!value) {
75
+ fill.style.width = "0%";
76
+ fill.className = "mvb-meter__fill";
77
+ label.textContent = "";
78
+ lengthValue.textContent = "-";
79
+ groupsValue.textContent = "-";
80
+ status.write("");
81
+ report = "";
82
+ return;
83
+ }
84
+ const { score, groups, length, hints: why } = analyze(value);
85
+ const level = LEVELS[score];
86
+ fill.style.width = `${(score / 5) * 100}%`;
87
+ fill.className = `mvb-meter__fill ${level.cls}`;
88
+ label.textContent = `${level.name} by length and patterns.`;
89
+ lengthValue.textContent = `${length} characters`;
90
+ groupsValue.textContent = `${groups} of 4`;
91
+ for (const text of why) {
92
+ const node = notice("note");
93
+ node.write(text);
94
+ hints.append(node);
95
+ }
96
+ status.write("A score is not a guarantee. Length and randomness matter most.");
97
+ report = [`Strength: ${level.name}`, `Length: ${length}`, `Character groups: ${groups} of 4`, ...why].join("\n");
98
+ }
99
+
100
+ field.addEventListener("input", run);
101
+
102
+ const card = toolcard({
103
+ body: bench(
104
+ stack(
105
+ panel("Password", el("div", { class: "mvb-secret" }, field, showButton)),
106
+ el("p", { class: "mvb-note" }, "The check runs in this tab. Do not type a real password into a site you do not trust.")
107
+ ),
108
+ stack(meter, label, kv, hints, status)
109
+ ),
110
+ foot: actions(copyButton(() => report, "Copy report", "fill")),
111
+ });
112
+ slot.append(card.node);
113
+ run();
114
+
115
+ return {};
116
+ }
117
+
118
+ export default { mount };
@@ -0,0 +1,96 @@
1
+ // Turns a percent into a simplified fraction, with the decimal and mixed form.
2
+ import { el, bench, stack, panel, textInput, notice, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ function gcd(a, b) {
5
+ a = Math.abs(a);
6
+ b = Math.abs(b);
7
+ while (b) {
8
+ const next = a % b;
9
+ a = b;
10
+ b = next;
11
+ }
12
+ return a;
13
+ }
14
+
15
+ function fmt(n) {
16
+ return n.toLocaleString(undefined, { maximumFractionDigits: 10 });
17
+ }
18
+
19
+ const kvRow = (key, value) =>
20
+ el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
21
+
22
+ export function mount(slot) {
23
+ const source = textInput({ value: "12.5", "aria-label": "Percent", placeholder: "12.5" });
24
+ const fraction = el("span", { class: "mvb-kv__v" }, "-");
25
+ const decimal = el("span", { class: "mvb-kv__v" }, "-");
26
+ const mixed = el("span", { class: "mvb-kv__v" }, "-");
27
+ const kv = el(
28
+ "div",
29
+ { class: "mvb-kv" },
30
+ kvRow("Fraction", fraction),
31
+ kvRow("Decimal", decimal),
32
+ kvRow("Mixed number", mixed)
33
+ );
34
+ const error = notice("error");
35
+ let text = "";
36
+
37
+ function clear() {
38
+ fraction.textContent = "-";
39
+ decimal.textContent = "-";
40
+ mixed.textContent = "-";
41
+ text = "";
42
+ }
43
+
44
+ function run() {
45
+ error.write("");
46
+ const raw = source.value.trim();
47
+ if (!raw) {
48
+ clear();
49
+ return;
50
+ }
51
+ if (!/^[+-]?\d*\.?\d+$/.test(raw)) {
52
+ clear();
53
+ error.write("Enter a percent, for example 12.5.");
54
+ return;
55
+ }
56
+ const negative = raw.startsWith("-");
57
+ const clean = raw.replace(/^[+-]/, "");
58
+ const [whole, decimals = ""] = clean.split(".");
59
+ const numerator = Number(whole + decimals);
60
+ const denominator = 100 * 10 ** decimals.length;
61
+ if (!Number.isFinite(numerator)) {
62
+ clear();
63
+ error.write("That percent is too large to convert exactly.");
64
+ return;
65
+ }
66
+ const divisor = gcd(numerator, denominator) || 1;
67
+ let simpleNum = numerator / divisor;
68
+ const simpleDen = denominator / divisor;
69
+ if (negative) simpleNum = -simpleNum;
70
+ const fractionText = simpleDen === 1 ? String(simpleNum) : `${simpleNum}/${simpleDen}`;
71
+ fraction.textContent = fractionText;
72
+ decimal.textContent = fmt(simpleNum / simpleDen);
73
+ const wholePart = Math.trunc(simpleNum / simpleDen);
74
+ const rest = Math.abs(simpleNum % simpleDen);
75
+ mixed.textContent = simpleDen !== 1 && Math.abs(simpleNum) > simpleDen && rest !== 0
76
+ ? `${wholePart} ${rest}/${simpleDen}`
77
+ : fractionText;
78
+ text = fractionText;
79
+ }
80
+
81
+ source.addEventListener("input", run);
82
+
83
+ const card = toolcard({
84
+ body: bench(
85
+ stack(panel("Percent", source), error),
86
+ stack(panel("Result", kv))
87
+ ),
88
+ foot: actions(copyButton(() => text, "Copy fraction", "fill")),
89
+ });
90
+ slot.append(card.node);
91
+ run();
92
+
93
+ return {};
94
+ }
95
+
96
+ export default { mount };
@@ -0,0 +1,86 @@
1
+ // A percent of a value, or what percent one value is of another.
2
+ import { el, bench, stack, panel, numberInput, notice, copyButton, chipRow, actions, toolcard } from "../core.js";
3
+
4
+ function fmt(n) {
5
+ if (!Number.isFinite(n)) return "";
6
+ return n.toLocaleString(undefined, { maximumFractionDigits: 6 });
7
+ }
8
+
9
+ export function mount(slot) {
10
+ const modes = [
11
+ { value: "of", label: "Percent of a value" },
12
+ { value: "is", label: "Value as a percent" },
13
+ ];
14
+ const labelA = el("span", {}, "Percent");
15
+ const labelB = el("span", {}, "Value");
16
+ const a = numberInput({ value: "15", "aria-label": "First value" });
17
+ const b = numberInput({ value: "200", "aria-label": "Second value" });
18
+ const result = el("span", { class: "mvb-stat__v" }, "-");
19
+ const stats = el(
20
+ "div",
21
+ { class: "mvb-stats" },
22
+ el("div", { class: "mvb-stat" }, result, el("span", { class: "mvb-stat__k" }, "Result"))
23
+ );
24
+ const error = notice("error");
25
+ let mode = "of";
26
+ let text = "";
27
+
28
+ function run() {
29
+ error.write("");
30
+ const x = Number(a.value);
31
+ const y = Number(b.value);
32
+ if (a.value.trim() === "" || b.value.trim() === "" || !Number.isFinite(x) || !Number.isFinite(y)) {
33
+ result.textContent = "-";
34
+ text = "";
35
+ return;
36
+ }
37
+ if (mode === "of") {
38
+ const value = (x / 100) * y;
39
+ result.textContent = fmt(value);
40
+ text = `${fmt(x)}% of ${fmt(y)} is ${fmt(value)}`;
41
+ return;
42
+ }
43
+ if (y === 0) {
44
+ result.textContent = "-";
45
+ text = "";
46
+ error.write("The total cannot be zero.");
47
+ return;
48
+ }
49
+ const value = (x / y) * 100;
50
+ result.textContent = `${fmt(value)}%`;
51
+ text = `${fmt(x)} is ${fmt(value)}% of ${fmt(y)}`;
52
+ }
53
+
54
+ function relabel() {
55
+ labelA.textContent = mode === "of" ? "Percent" : "Value";
56
+ labelB.textContent = mode === "of" ? "Value" : "Total";
57
+ }
58
+
59
+ const chips = chipRow(modes, {
60
+ value: mode,
61
+ label: "Mode",
62
+ onChange: (value) => {
63
+ mode = value;
64
+ relabel();
65
+ run();
66
+ },
67
+ });
68
+
69
+ a.addEventListener("input", run);
70
+ b.addEventListener("input", run);
71
+
72
+ const card = toolcard({
73
+ body: bench(
74
+ stack(panel("Mode", chips), panel(labelA, a), panel(labelB, b), error),
75
+ stack(stats)
76
+ ),
77
+ foot: actions(copyButton(() => text, "Copy result", "fill")),
78
+ });
79
+ slot.append(card.node);
80
+ relabel();
81
+ run();
82
+
83
+ return {};
84
+ }
85
+
86
+ export default { mount };
@@ -0,0 +1,70 @@
1
+ // Percent change between two values, with the direction and the difference.
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: 6 });
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 start = numberInput({ value: "80", "aria-label": "Starting value" });
17
+ const end = numberInput({ value: "100", "aria-label": "Ending value" });
18
+ const change = tile("Change");
19
+ const difference = tile("Difference");
20
+ const direction = tile("Direction");
21
+ const stats = el("div", { class: "mvb-stats" }, change, difference, direction);
22
+ const error = notice("error");
23
+ let text = "";
24
+
25
+ function clear() {
26
+ change.set("-");
27
+ difference.set("-");
28
+ direction.set("-");
29
+ text = "";
30
+ }
31
+
32
+ function run() {
33
+ error.write("");
34
+ const a = Number(start.value);
35
+ const b = Number(end.value);
36
+ if (start.value.trim() === "" || end.value.trim() === "" || !Number.isFinite(a) || !Number.isFinite(b)) {
37
+ clear();
38
+ return;
39
+ }
40
+ if (a === 0) {
41
+ clear();
42
+ error.write("The starting value cannot be zero.");
43
+ return;
44
+ }
45
+ const diff = b - a;
46
+ const pct = (diff / Math.abs(a)) * 100;
47
+ const label = pct > 0 ? "Increase" : pct < 0 ? "Decrease" : "No change";
48
+ change.set(`${fmt(pct)}%`);
49
+ difference.set(fmt(diff));
50
+ direction.set(label);
51
+ text = `${fmt(a)} to ${fmt(b)}: ${label.toLowerCase()} of ${fmt(Math.abs(pct))}%, difference ${fmt(diff)}`;
52
+ }
53
+
54
+ start.addEventListener("input", run);
55
+ end.addEventListener("input", run);
56
+
57
+ const card = toolcard({
58
+ body: bench(
59
+ stack(panel("Starting value", start), panel("Ending value", end), error),
60
+ stack(stats)
61
+ ),
62
+ foot: actions(copyButton(() => text, "Copy result", "fill")),
63
+ });
64
+ slot.append(card.node);
65
+ run();
66
+
67
+ return {};
68
+ }
69
+
70
+ export default { mount };