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,137 @@
1
+ // Turns pasted table data into a Markdown table.
2
+ import { el, bench, stack, panel, textArea, selectInput, notice, output, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ function parseDelimited(text, delimiter) {
5
+ const rows = [];
6
+ let row = [];
7
+ let field = "";
8
+ let quoted = false;
9
+ for (let i = 0; i < text.length; i++) {
10
+ const char = text[i];
11
+ if (quoted) {
12
+ if (char === '"') {
13
+ if (text[i + 1] === '"') { field += '"'; i++; }
14
+ else quoted = false;
15
+ } else field += char;
16
+ } else if (char === '"') {
17
+ quoted = true;
18
+ } else if (char === delimiter) {
19
+ row.push(field);
20
+ field = "";
21
+ } else if (char === "\n") {
22
+ row.push(field);
23
+ rows.push(row);
24
+ row = [];
25
+ field = "";
26
+ } else if (char !== "\r") {
27
+ field += char;
28
+ }
29
+ }
30
+ if (field !== "" || row.length) {
31
+ row.push(field);
32
+ rows.push(row);
33
+ }
34
+ return rows;
35
+ }
36
+
37
+ function detectDelimiter(text) {
38
+ const candidates = [",", ";", "\t", "|"];
39
+ const first = text.split(/\r?\n/)[0] ?? "";
40
+ const counts = { ",": 0, ";": 0, "\t": 0, "|": 0 };
41
+ let quoted = false;
42
+ for (let i = 0; i < first.length; i++) {
43
+ const char = first[i];
44
+ if (char === '"') quoted = !quoted;
45
+ else if (!quoted && char in counts) counts[char]++;
46
+ }
47
+ let best = ",";
48
+ let bestCount = 0;
49
+ for (const candidate of candidates) {
50
+ if (counts[candidate] > bestCount) { bestCount = counts[candidate]; best = candidate; }
51
+ }
52
+ return best;
53
+ }
54
+
55
+ const escapeCell = (cell) => String(cell ?? "").replace(/\|/g, "\\|").replace(/\s*\n\s*/g, " ").trim();
56
+
57
+ function tile(label) {
58
+ const value = el("span", { class: "mvb-stat__v" }, "0");
59
+ const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
60
+ node.set = (text) => { value.textContent = text; };
61
+ return node;
62
+ }
63
+
64
+ export function mount(slot) {
65
+ const source = textArea({ rows: 12, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Table data", placeholder: "Paste rows copied from a spreadsheet" });
66
+ const delimiter = selectInput([
67
+ { value: "auto", label: "Auto" },
68
+ { value: ",", label: "Comma" },
69
+ { value: ";", label: "Semicolon" },
70
+ { value: "\t", label: "Tab" },
71
+ { value: "|", label: "Pipe" },
72
+ ], { value: "auto", "aria-label": "Delimiter" });
73
+ const alignment = selectInput([
74
+ { value: "none", label: "None" },
75
+ { value: "left", label: "Left" },
76
+ { value: "center", label: "Center" },
77
+ { value: "right", label: "Right" },
78
+ ], { value: "none", "aria-label": "Alignment" });
79
+
80
+ const result = output({ rows: 12 });
81
+ const rowsTile = tile("Rows");
82
+ const colsTile = tile("Columns");
83
+ const stats = el("div", { class: "mvb-stats" }, rowsTile, colsTile);
84
+ const error = notice("error");
85
+ let current = "";
86
+
87
+ function separator(count) {
88
+ const cell = alignment.value === "left" ? ":---" : alignment.value === "center" ? ":---:" : alignment.value === "right" ? "---:" : "---";
89
+ return Array.from({ length: count }, () => cell);
90
+ }
91
+
92
+ function run() {
93
+ error.write("");
94
+ const text = source.value;
95
+ if (!text.trim()) {
96
+ current = "";
97
+ result.write("");
98
+ rowsTile.set("0");
99
+ colsTile.set("0");
100
+ return;
101
+ }
102
+ const used = delimiter.value === "auto" ? detectDelimiter(text) : delimiter.value;
103
+ const rows = parseDelimited(text, used);
104
+ const header = rows[0] ?? [];
105
+ const body = rows.slice(1);
106
+ if (body.some((row) => row.length !== header.length)) {
107
+ error.write("Some rows have a different number of columns.");
108
+ }
109
+ rowsTile.set(String(rows.length));
110
+ colsTile.set(String(header.length));
111
+ const line = (cells) => `| ${cells.join(" | ")} |`;
112
+ current = [
113
+ line(header.map(escapeCell)),
114
+ line(separator(header.length)),
115
+ ...body.map((row) => line(header.map((_, i) => escapeCell(row[i])))),
116
+ ].join("\n");
117
+ result.write(current);
118
+ }
119
+
120
+ source.addEventListener("input", run);
121
+ delimiter.addEventListener("change", run);
122
+ alignment.addEventListener("change", run);
123
+
124
+ const card = toolcard({
125
+ body: bench(
126
+ stack(panel("Table data", source), panel("Delimiter", delimiter), panel("Alignment", alignment), error),
127
+ stack(stats, panel("Markdown", result))
128
+ ),
129
+ foot: actions(copyButton(() => current, "Copy Markdown", "fill")),
130
+ });
131
+ slot.append(card.node);
132
+ run();
133
+
134
+ return {};
135
+ }
136
+
137
+ export default { mount };
@@ -0,0 +1,84 @@
1
+ // Adds tax to an amount or takes it out of a tax-inclusive price.
2
+ import { el, bench, stack, panel, numberInput, chipRow, 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 amount = numberInput({ value: "100", min: "0", "aria-label": "Amount" });
17
+ const rate = numberInput({ value: "11", "aria-label": "Tax rate" });
18
+ let mode = "add";
19
+ const chips = chipRow(
20
+ [{ value: "add", label: "Add tax" }, { value: "included", label: "Tax included" }],
21
+ { value: "add", label: "Amount type", onChange: (next) => { mode = next; run(); } }
22
+ );
23
+ const taxTile = tile("Tax");
24
+ const netTile = tile("Net");
25
+ const grossTile = tile("Total");
26
+ const stats = el("div", { class: "mvb-stats" }, taxTile, netTile, grossTile);
27
+ const error = notice("error");
28
+ const note = notice("note");
29
+ note.write("You set the rate. No official rule is applied.");
30
+ let text = "";
31
+
32
+ function clear() {
33
+ taxTile.set("-");
34
+ netTile.set("-");
35
+ grossTile.set("-");
36
+ text = "";
37
+ }
38
+
39
+ function run() {
40
+ error.write("");
41
+ const a = Number(amount.value);
42
+ const r = Number(rate.value);
43
+ if (amount.value.trim() === "" || rate.value.trim() === "" || !Number.isFinite(a) || !Number.isFinite(r)) {
44
+ clear();
45
+ return;
46
+ }
47
+ if (a < 0) { clear(); error.write("The amount cannot be negative."); return; }
48
+ if (r <= -100) { clear(); error.write("The rate must be above -100%."); return; }
49
+
50
+ let net;
51
+ let tax;
52
+ let gross;
53
+ if (mode === "included") {
54
+ net = a / (1 + r / 100);
55
+ tax = a - net;
56
+ gross = a;
57
+ } else {
58
+ net = a;
59
+ tax = (a * r) / 100;
60
+ gross = a + tax;
61
+ }
62
+ taxTile.set(fmt(tax));
63
+ netTile.set(fmt(net));
64
+ grossTile.set(fmt(gross));
65
+ text = `Net ${fmt(net)}, tax ${fmt(tax)}, total ${fmt(gross)}`;
66
+ }
67
+
68
+ amount.addEventListener("input", run);
69
+ rate.addEventListener("input", run);
70
+
71
+ const card = toolcard({
72
+ body: bench(
73
+ stack(panel("Amount", amount), panel("Amount type", chips), panel("Tax rate %", rate), error),
74
+ stack(stats, note)
75
+ ),
76
+ foot: actions(copyButton(() => text, "Copy result", "fill")),
77
+ });
78
+ slot.append(card.node);
79
+ run();
80
+
81
+ return {};
82
+ }
83
+
84
+ export default { mount };
@@ -0,0 +1,94 @@
1
+ // Converts between Celsius, Fahrenheit and Kelvin, with the formula shown.
2
+ import { el, bench, stack, panel, numberInput, selectInput, notice, copyButton, button, actions, toolcard } from "../core.js";
3
+
4
+ const SCALES = [
5
+ { value: "c", label: "Celsius" },
6
+ { value: "f", label: "Fahrenheit" },
7
+ { value: "k", label: "Kelvin" },
8
+ ];
9
+
10
+ const byId = Object.fromEntries(SCALES.map((scale) => [scale.value, scale]));
11
+ const options = SCALES.map(({ value, label }) => ({ value, label }));
12
+
13
+ const toCelsius = (n, scale) => (scale === "c" ? n : scale === "f" ? ((n - 32) * 5) / 9 : n - 273.15);
14
+ const fromCelsius = (c, scale) => (scale === "c" ? c : scale === "f" ? (c * 9) / 5 + 32 : c + 273.15);
15
+
16
+ const FORMULAS = {
17
+ "c-f": "F = C x 9/5 + 32",
18
+ "c-k": "K = C + 273.15",
19
+ "f-c": "C = (F - 32) x 5/9",
20
+ "f-k": "K = (F - 32) x 5/9 + 273.15",
21
+ "k-c": "C = K - 273.15",
22
+ "k-f": "F = (K - 273.15) x 9/5 + 32",
23
+ };
24
+
25
+ function fmt(value) {
26
+ if (!Number.isFinite(value)) return "-";
27
+ if (value === 0) return "0";
28
+ const abs = Math.abs(value);
29
+ if (abs >= 1e-6 && abs < 1e15) return String(Number(value.toPrecision(8)));
30
+ return value.toExponential(4);
31
+ }
32
+
33
+ export function mount(slot) {
34
+ const value = numberInput({ value: "0", step: "any", "aria-label": "Value" });
35
+ const from = selectInput(options, { value: "c", "aria-label": "From scale" });
36
+ const to = selectInput(options, { value: "f", "aria-label": "To scale" });
37
+ const result = el("span", { class: "mvb-stat__v" }, "-");
38
+ const stats = el("div", { class: "mvb-stats" }, el("div", { class: "mvb-stat" }, el("span", { class: "mvb-stat__k" }, "Result"), result));
39
+ const formula = el("p", { class: "mvb-note" });
40
+ const error = notice("error");
41
+ const scroll = el("div", { class: "mvb-scroll" });
42
+ let text = "";
43
+
44
+ function run() {
45
+ error.write("");
46
+ const raw = value.value.trim();
47
+ const n = Number(raw);
48
+ if (!raw || !Number.isFinite(n)) {
49
+ result.textContent = "-";
50
+ formula.textContent = "";
51
+ scroll.replaceChildren();
52
+ text = "";
53
+ error.write("Enter a number.");
54
+ return;
55
+ }
56
+ const a = byId[from.value];
57
+ const b = byId[to.value];
58
+ const celsius = toCelsius(n, a.value);
59
+ const out = fromCelsius(celsius, b.value);
60
+ result.textContent = `${fmt(out)} ${b.value === "k" ? "K" : `°${b.value.toUpperCase()}`}`;
61
+ formula.textContent = a.value === b.value ? "" : FORMULAS[`${a.value}-${b.value}`];
62
+ scroll.replaceChildren(el("table", { class: "mvb-ttable" },
63
+ el("thead", {}, el("tr", {}, el("th", {}, "Scale"), el("th", {}, "Value"))),
64
+ el("tbody", {}, ...SCALES.map((scale) => el("tr", {},
65
+ el("td", {}, scale.label),
66
+ el("td", {}, fmt(fromCelsius(celsius, scale.value))))))));
67
+ text = `${fmt(out)} ${b.label}`;
68
+ }
69
+
70
+ value.addEventListener("input", run);
71
+ from.addEventListener("change", run);
72
+ to.addEventListener("change", run);
73
+
74
+ const swap = button("Swap", { icon: "arrow-left-right", onclick: () => {
75
+ const held = from.value;
76
+ from.value = to.value;
77
+ to.value = held;
78
+ run();
79
+ }});
80
+
81
+ const card = toolcard({
82
+ body: bench(
83
+ stack(panel("Value", value), panel("From", from), panel("To", to), error),
84
+ stack(panel("Result", stats), formula, panel("All scales", scroll))
85
+ ),
86
+ foot: actions(swap, copyButton(() => text, "Copy result", "fill")),
87
+ });
88
+ slot.append(card.node);
89
+ run();
90
+
91
+ return {};
92
+ }
93
+
94
+ export default { mount };
@@ -0,0 +1,104 @@
1
+ // Line by line comparison of two texts, marking added and removed lines.
2
+ import { el, bench, stack, panel, textArea, toggleRow, notice, button, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const MAX_LINES = 800;
5
+
6
+ export function mount(slot) {
7
+ const left = textArea({ rows: 9, "aria-label": "First text", placeholder: "First text" });
8
+ const right = textArea({ rows: 9, "aria-label": "Second text", placeholder: "Second text" });
9
+ const options = toggleRow([
10
+ { value: "ignore", label: "Ignore case", checked: false },
11
+ { value: "spaces", label: "Ignore spaces", checked: false },
12
+ ]);
13
+ const diffBox = el("div", { class: "mvb-diff" });
14
+ const note = notice("note");
15
+ let rows = [];
16
+
17
+ function normalize(line) {
18
+ let text = line;
19
+ if (options.values().includes("spaces")) text = text.replace(/\s+/g, " ").trim();
20
+ if (options.values().includes("ignore")) text = text.toLowerCase();
21
+ return text;
22
+ }
23
+
24
+ function diffLines(aNorm, bNorm, aRaw, bRaw) {
25
+ const n = aNorm.length;
26
+ const m = bNorm.length;
27
+ const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
28
+ for (let i = n - 1; i >= 0; i--) {
29
+ for (let j = m - 1; j >= 0; j--) {
30
+ dp[i][j] = aNorm[i] === bNorm[j]
31
+ ? dp[i + 1][j + 1] + 1
32
+ : Math.max(dp[i + 1][j], dp[i][j + 1]);
33
+ }
34
+ }
35
+ const out = [];
36
+ let i = 0;
37
+ let j = 0;
38
+ while (i < n && j < m) {
39
+ if (aNorm[i] === bNorm[j]) { out.push(["same", aRaw[i]]); i++; j++; }
40
+ else if (dp[i + 1][j] >= dp[i][j + 1]) { out.push(["del", aRaw[i]]); i++; }
41
+ else { out.push(["add", bRaw[j]]); j++; }
42
+ }
43
+ while (i < n) { out.push(["del", aRaw[i]]); i++; }
44
+ while (j < m) { out.push(["add", bRaw[j]]); j++; }
45
+ return out;
46
+ }
47
+
48
+ function diffText() {
49
+ return rows.map(([type, raw]) => `${type === "add" ? "+ " : type === "del" ? "- " : " "}${raw}`).join("\n");
50
+ }
51
+
52
+ function run() {
53
+ const aRaw = left.value === "" ? [] : left.value.split("\n");
54
+ const bRaw = right.value === "" ? [] : right.value.split("\n");
55
+ if (!aRaw.length && !bRaw.length) {
56
+ diffBox.replaceChildren();
57
+ rows = [];
58
+ note.write("Paste two texts to compare.");
59
+ return;
60
+ }
61
+ const truncated = aRaw.length > MAX_LINES || bRaw.length > MAX_LINES;
62
+ const a = aRaw.slice(0, MAX_LINES).map(normalize);
63
+ const b = bRaw.slice(0, MAX_LINES).map(normalize);
64
+ rows = diffLines(a, b, aRaw.slice(0, MAX_LINES), bRaw.slice(0, MAX_LINES));
65
+ const added = rows.filter(([type]) => type === "add").length;
66
+ const removed = rows.filter(([type]) => type === "del").length;
67
+ diffBox.replaceChildren(...rows.map(([type, raw]) => el(
68
+ "div",
69
+ { class: `mvb-diff__row${type === "add" ? " mvb-diff__row--add" : type === "del" ? " mvb-diff__row--del" : ""}` },
70
+ el("span", { class: "mvb-diff__sign" }, type === "add" ? "+" : type === "del" ? "-" : " "),
71
+ el("span", {}, raw)
72
+ )));
73
+ const counts = !added && !removed
74
+ ? "The texts are identical."
75
+ : `${added} added, ${removed} removed.`;
76
+ note.write(truncated ? `${counts} Compared the first ${MAX_LINES} lines.` : counts);
77
+ }
78
+
79
+ const swapButton = button("Swap texts", { icon: "arrow-left-right" });
80
+ swapButton.addEventListener("click", () => {
81
+ const value = left.value;
82
+ left.value = right.value;
83
+ right.value = value;
84
+ run();
85
+ });
86
+
87
+ left.addEventListener("input", run);
88
+ right.addEventListener("input", run);
89
+ options.addEventListener("change", run);
90
+
91
+ const card = toolcard({
92
+ body: bench(
93
+ stack(panel("First text", left), panel("Second text", right), options, note),
94
+ stack(panel("Difference", diffBox))
95
+ ),
96
+ foot: actions(swapButton, copyButton(diffText, "Copy diff", "fill")),
97
+ });
98
+ slot.append(card.node);
99
+ run();
100
+
101
+ return {};
102
+ }
103
+
104
+ export default { mount };
@@ -0,0 +1,55 @@
1
+ // Preview text under each direction, with the attribute to use.
2
+ import { el, bench, stack, panel, textArea, chipRow, output, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const RTL = /[\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0780-\u07BF\u08A0-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF]/;
5
+ const LTR = /[A-Za-z\u00C0-\u024F]/;
6
+
7
+ function detect(text) {
8
+ for (const char of text) {
9
+ if (RTL.test(char)) return "rtl";
10
+ if (LTR.test(char)) return "ltr";
11
+ }
12
+ return "ltr";
13
+ }
14
+
15
+ export function mount(slot) {
16
+ const source = textArea({ rows: 6, "aria-label": "Text", placeholder: "Type or paste text" });
17
+ const sample = el("div", { class: "mvb-preview__sample" }, "Sample text");
18
+ const code = output();
19
+
20
+ let mode = "auto";
21
+ const dirChips = chipRow(
22
+ [
23
+ { value: "auto", label: "Auto" },
24
+ { value: "ltr", label: "Left to right" },
25
+ { value: "rtl", label: "Right to left" },
26
+ ],
27
+ { value: "auto", label: "Direction", onChange: (next) => { mode = next; run(); } }
28
+ );
29
+
30
+ let snippet = "";
31
+
32
+ function run() {
33
+ const direction = mode === "auto" ? detect(source.value) : mode;
34
+ sample.dir = direction;
35
+ sample.textContent = source.value.trim() ? source.value : "Sample text";
36
+ snippet = `dir="${direction}"`;
37
+ code.write(snippet);
38
+ }
39
+
40
+ source.addEventListener("input", run);
41
+
42
+ const card = toolcard({
43
+ body: bench(
44
+ stack(panel("Text", source), panel("Direction", dirChips)),
45
+ stack(panel("Preview", sample), panel("Attribute", code))
46
+ ),
47
+ foot: actions(copyButton(() => snippet, "Copy attribute", "fill")),
48
+ });
49
+ slot.append(card.node);
50
+ run();
51
+
52
+ return {};
53
+ }
54
+
55
+ export default { mount };
@@ -0,0 +1,51 @@
1
+ // Reverses text by character, word or line. Character mode keeps emoji and
2
+ // accented letters whole when the browser exposes grapheme segmentation.
3
+ import { bench, stack, panel, textArea, chipRow, copyButton, actions, toolcard } from "../core.js";
4
+
5
+ function reverseChars(text) {
6
+ if (typeof Intl !== "undefined" && Intl.Segmenter) {
7
+ const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
8
+ return [...segmenter.segment(text)].map((part) => part.segment).reverse().join("");
9
+ }
10
+ return [...text].reverse().join("");
11
+ }
12
+
13
+ const reverseWords = (text) => text.trim() ? text.trim().split(/\s+/).reverse().join(" ") : "";
14
+ const reverseLines = (text) => text ? text.split("\n").reverse().join("\n") : "";
15
+
16
+ export function mount(slot) {
17
+ const source = textArea({ rows: 10, "aria-label": "Text", placeholder: "Type or paste text" });
18
+ const result = textArea({ rows: 10, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Result" });
19
+
20
+ let mode = "chars";
21
+ const chips = chipRow(
22
+ [
23
+ { value: "chars", label: "Characters" },
24
+ { value: "words", label: "Words" },
25
+ { value: "lines", label: "Lines" },
26
+ ],
27
+ { value: "chars", label: "Reverse by", onChange: (value) => { mode = value; run(); } }
28
+ );
29
+
30
+ function run() {
31
+ const text = source.value;
32
+ if (!text) { result.value = ""; return; }
33
+ result.value = mode === "chars" ? reverseChars(text) : mode === "words" ? reverseWords(text) : reverseLines(text);
34
+ }
35
+
36
+ source.addEventListener("input", run);
37
+
38
+ const card = toolcard({
39
+ body: bench(
40
+ stack(panel("Text", source), panel("Reverse by", chips)),
41
+ stack(panel("Result", result))
42
+ ),
43
+ foot: actions(copyButton(() => result.value, "Copy result", "fill")),
44
+ });
45
+ slot.append(card.node);
46
+ run();
47
+
48
+ return {};
49
+ }
50
+
51
+ export default { mount };
@@ -0,0 +1,83 @@
1
+ // Measures the time between two clock times, across midnight, minus a break.
2
+ import { el, bench, stack, panel, textInput, numberInput, notice, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ function parseTime(value) {
5
+ const match = /^(\d{1,2}):(\d{2})$/.exec(value);
6
+ if (!match) return null;
7
+ const hours = Number(match[1]);
8
+ const minutes = Number(match[2]);
9
+ if (hours > 23 || minutes > 59) return null;
10
+ return hours * 60 + minutes;
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 start = textInput({ type: "time", value: "09:00", "aria-label": "Start time" });
18
+ const end = textInput({ type: "time", value: "17:30", "aria-label": "End time" });
19
+ const breakMin = numberInput({ value: "30", min: "0", "aria-label": "Break in minutes" });
20
+
21
+ const result = el("span", { class: "mvb-stat__v" }, "-");
22
+ const tile = el("div", { class: "mvb-stat" }, result, el("span", { class: "mvb-stat__k" }, "Duration"));
23
+ const stats = el("div", { class: "mvb-stats" }, tile);
24
+ const minutesOut = el("span", { class: "mvb-kv__v" }, "-");
25
+ const decimalOut = el("span", { class: "mvb-kv__v" }, "-");
26
+ const kv = el("div", { class: "mvb-kv" }, kvRow("Total minutes", minutesOut), kvRow("Decimal hours", decimalOut));
27
+ const error = notice("error");
28
+ let text = "";
29
+
30
+ function clear() {
31
+ result.textContent = "-";
32
+ minutesOut.textContent = "-";
33
+ decimalOut.textContent = "-";
34
+ text = "";
35
+ }
36
+
37
+ function run() {
38
+ error.write("");
39
+ const from = parseTime(start.value);
40
+ const to = parseTime(end.value);
41
+ if (from == null || to == null) {
42
+ clear();
43
+ error.write("Enter a valid start and end time.");
44
+ return;
45
+ }
46
+ let span = to - from;
47
+ if (span < 0) span += 1440;
48
+ const pause = breakMin.value.trim() === "" ? 0 : Number(breakMin.value);
49
+ if (!Number.isFinite(pause) || pause < 0) {
50
+ clear();
51
+ error.write("Enter the break in minutes.");
52
+ return;
53
+ }
54
+ if (pause > span) {
55
+ clear();
56
+ error.write("The break is longer than the time between the two clocks.");
57
+ return;
58
+ }
59
+ const total = span - pause;
60
+ const hours = Math.floor(total / 60);
61
+ const minutes = total % 60;
62
+ result.textContent = `${hours}h ${minutes}m`;
63
+ minutesOut.textContent = String(total);
64
+ decimalOut.textContent = (total / 60).toFixed(2);
65
+ text = `${hours}h ${minutes}m`;
66
+ }
67
+
68
+ for (const input of [start, end, breakMin]) input.addEventListener("input", run);
69
+
70
+ const card = toolcard({
71
+ body: bench(
72
+ stack(panel("Start", start), panel("End", end), panel("Break in minutes", breakMin), error),
73
+ stack(stats, panel("Breakdown", kv))
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 };