mevebu 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +24 -8
  2. package/mevebu/app/mevebu.js +3 -3
  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
+ // Checks the usual parts of a research abstract.
2
+ import { el, bench, stack, panel, textArea, toggleRow, notice, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const PARTS = [
5
+ { value: "background", label: "Background" },
6
+ { value: "aim", label: "Aim" },
7
+ { value: "method", label: "Method" },
8
+ { value: "results", label: "Results" },
9
+ { value: "conclusion", label: "Conclusion" },
10
+ ];
11
+
12
+ function tile(label) {
13
+ const value = el("span", { class: "mvb-stat__v" }, "0");
14
+ const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
15
+ node.set = (text) => { value.textContent = text; };
16
+ return node;
17
+ }
18
+
19
+ const countWords = (text) => (text.trim() ? text.trim().split(/\s+/).length : 0);
20
+
21
+ export function mount(slot) {
22
+ const source = textArea({ rows: 12, "aria-label": "Abstract", placeholder: "Paste the abstract" });
23
+ const parts = toggleRow(PARTS.map((part) => ({ ...part, checked: false })));
24
+ const wordsTile = tile("Words");
25
+ const tickedTile = tile("Ticked");
26
+ const stats = el("div", { class: "mvb-stats" }, wordsTile, tickedTile);
27
+ const note = notice("note");
28
+ note.write("This checks structure only, not quality.");
29
+ let summary = "";
30
+
31
+ function run() {
32
+ const ticked = parts.values();
33
+ wordsTile.set(String(countWords(source.value)));
34
+ tickedTile.set(String(ticked.length));
35
+ summary = PARTS.map((part) => `${part.label}: ${ticked.includes(part.value) ? "yes" : "no"}`).join("\n");
36
+ }
37
+
38
+ source.addEventListener("input", run);
39
+ parts.addEventListener("change", run);
40
+
41
+ const card = toolcard({
42
+ body: bench(
43
+ stack(panel("Abstract", source), note),
44
+ stack(stats, panel("Parts found", parts))
45
+ ),
46
+ foot: actions(copyButton(() => summary, "Copy checklist", "fill")),
47
+ });
48
+ slot.append(card.node);
49
+ run();
50
+
51
+ return {};
52
+ }
53
+
54
+ export default { mount };
@@ -0,0 +1,105 @@
1
+ // Moves a date forward or back by days, weeks, months or years.
2
+ import { el, bench, stack, panel, textInput, numberInput, selectInput, chipRow, notice, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const UNITS = [
5
+ { value: "days", label: "Days" },
6
+ { value: "weeks", label: "Weeks" },
7
+ { value: "months", label: "Months" },
8
+ { value: "years", label: "Years" },
9
+ ];
10
+
11
+ const pad = (n) => String(n).padStart(2, "0");
12
+
13
+ function parseDate(value) {
14
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
15
+ if (!match) return null;
16
+ const year = Number(match[1]);
17
+ const month = Number(match[2]) - 1;
18
+ const day = Number(match[3]);
19
+ const date = new Date(year, month, day);
20
+ if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return null;
21
+ return date;
22
+ }
23
+
24
+ const iso = (date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
25
+
26
+ const kvRow = (key, value) =>
27
+ el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
28
+
29
+ export function mount(slot) {
30
+ const date = textInput({ type: "date", value: "2026-01-01", "aria-label": "Start date" });
31
+ const amount = numberInput({ value: "30", min: "0", "aria-label": "Amount" });
32
+ const unit = selectInput(UNITS, { value: "days", "aria-label": "Unit" });
33
+ let mode = "add";
34
+ const direction = chipRow(
35
+ [{ value: "add", label: "Add" }, { value: "subtract", label: "Subtract" }],
36
+ { value: "add", label: "Direction", onChange: (value) => { mode = value; run(); } }
37
+ );
38
+ const result = el("span", { class: "mvb-stat__v" }, "-");
39
+ const tile = el("div", { class: "mvb-stat" }, result, el("span", { class: "mvb-stat__k" }, "Result"));
40
+ const stats = el("div", { class: "mvb-stats" }, tile);
41
+ const isoOut = el("span", { class: "mvb-kv__v" }, "-");
42
+ const weekday = el("span", { class: "mvb-kv__v" }, "-");
43
+ const kv = el("div", { class: "mvb-kv" }, kvRow("ISO date", isoOut), kvRow("Weekday", weekday));
44
+ const error = notice("error");
45
+ let text = "";
46
+
47
+ function clear() {
48
+ result.textContent = "-";
49
+ isoOut.textContent = "-";
50
+ weekday.textContent = "-";
51
+ text = "";
52
+ }
53
+
54
+ function run() {
55
+ error.write("");
56
+ const base = parseDate(date.value);
57
+ const count = Number(amount.value);
58
+ if (!base) {
59
+ clear();
60
+ error.write("Pick a valid date.");
61
+ return;
62
+ }
63
+ if (amount.value.trim() === "" || !Number.isFinite(count) || count < 0) {
64
+ clear();
65
+ error.write("Enter a number of days, weeks, months or years.");
66
+ return;
67
+ }
68
+ const sign = mode === "subtract" ? -1 : 1;
69
+ const step = sign * Math.floor(count);
70
+ const out = new Date(base.getFullYear(), base.getMonth(), base.getDate());
71
+ if (unit.value === "days") out.setDate(out.getDate() + step);
72
+ else if (unit.value === "weeks") out.setDate(out.getDate() + step * 7);
73
+ else if (unit.value === "months") out.setMonth(out.getMonth() + step);
74
+ else out.setFullYear(out.getFullYear() + step);
75
+
76
+ result.textContent = out.toLocaleDateString(undefined, { day: "numeric", month: "short", year: "numeric" });
77
+ isoOut.textContent = iso(out);
78
+ weekday.textContent = out.toLocaleDateString(undefined, { weekday: "long" });
79
+ text = `${out.toLocaleDateString(undefined, { weekday: "long", day: "numeric", month: "long", year: "numeric" })} (${iso(out)})`;
80
+ }
81
+
82
+ date.addEventListener("input", run);
83
+ amount.addEventListener("input", run);
84
+ unit.addEventListener("change", run);
85
+
86
+ const card = toolcard({
87
+ body: bench(
88
+ stack(
89
+ panel("Date", date),
90
+ panel("Amount", amount),
91
+ panel("Unit", unit),
92
+ panel("Direction", direction),
93
+ error
94
+ ),
95
+ stack(stats, panel("Result", kv))
96
+ ),
97
+ foot: actions(copyButton(() => text, "Copy result", "fill")),
98
+ });
99
+ slot.append(card.node);
100
+ run();
101
+
102
+ return {};
103
+ }
104
+
105
+ export default { mount };
@@ -0,0 +1,108 @@
1
+ // Age in years, months and days on any date, plus the next birthday.
2
+ import { el, bench, stack, panel, textInput, notice, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const DAY = 86400000;
5
+
6
+ const pad = (n) => String(n).padStart(2, "0");
7
+
8
+ function parseDate(value) {
9
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
10
+ if (!match) return null;
11
+ const year = Number(match[1]);
12
+ const month = Number(match[2]) - 1;
13
+ const day = Number(match[3]);
14
+ const date = new Date(year, month, day);
15
+ if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return null;
16
+ return date;
17
+ }
18
+
19
+ function calendarDiff(from, to) {
20
+ let years = to.getFullYear() - from.getFullYear();
21
+ let months = to.getMonth() - from.getMonth();
22
+ let days = to.getDate() - from.getDate();
23
+ if (days < 0) {
24
+ months -= 1;
25
+ days += new Date(to.getFullYear(), to.getMonth(), 0).getDate();
26
+ }
27
+ if (months < 0) {
28
+ years -= 1;
29
+ months += 12;
30
+ }
31
+ return { years, months, days };
32
+ }
33
+
34
+ const kvRow = (key, value) =>
35
+ el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
36
+
37
+ export function mount(slot) {
38
+ const today = new Date();
39
+ const birth = textInput({ type: "date", value: "1990-01-01", "aria-label": "Birth date" });
40
+ const at = textInput({ type: "date", value: `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`, "aria-label": "Age at date" });
41
+
42
+ const age = el("span", { class: "mvb-kv__v" }, "-");
43
+ const totalDays = el("span", { class: "mvb-kv__v" }, "-");
44
+ const nextDate = el("span", { class: "mvb-kv__v" }, "-");
45
+ const nextIn = el("span", { class: "mvb-kv__v" }, "-");
46
+ const kv = el(
47
+ "div",
48
+ { class: "mvb-kv" },
49
+ kvRow("Age", age),
50
+ kvRow("Total days", totalDays),
51
+ kvRow("Next birthday", nextDate),
52
+ kvRow("Until next birthday", nextIn)
53
+ );
54
+ const error = notice("error");
55
+ let text = "";
56
+
57
+ function clear() {
58
+ age.textContent = "-";
59
+ totalDays.textContent = "-";
60
+ nextDate.textContent = "-";
61
+ nextIn.textContent = "-";
62
+ text = "";
63
+ }
64
+
65
+ function run() {
66
+ error.write("");
67
+ const born = parseDate(birth.value);
68
+ const measure = parseDate(at.value);
69
+ if (!born || !measure) {
70
+ clear();
71
+ error.write("Enter a valid birth date and measure date.");
72
+ return;
73
+ }
74
+ if (measure < born) {
75
+ clear();
76
+ error.write("The measure date is before the birth date.");
77
+ return;
78
+ }
79
+ const diff = calendarDiff(born, measure);
80
+ const total = Math.round((measure - born) / DAY);
81
+
82
+ let next = new Date(measure.getFullYear(), born.getMonth(), born.getDate());
83
+ if (next < measure) next = new Date(measure.getFullYear() + 1, born.getMonth(), born.getDate());
84
+ const untilNext = Math.round((next - measure) / DAY);
85
+
86
+ age.textContent = `${diff.years} years ${diff.months} months ${diff.days} days`;
87
+ totalDays.textContent = String(total);
88
+ nextDate.textContent = next.toLocaleDateString(undefined, { weekday: "long", day: "numeric", month: "long", year: "numeric" });
89
+ nextIn.textContent = untilNext === 0 ? "Today" : `${untilNext} days`;
90
+ text = `${diff.years} years ${diff.months} months ${diff.days} days`;
91
+ }
92
+
93
+ for (const input of [birth, at]) input.addEventListener("input", run);
94
+
95
+ const card = toolcard({
96
+ body: bench(
97
+ stack(panel("Birth date", birth), panel("Age at", at), error),
98
+ stack(panel("Result", kv))
99
+ ),
100
+ foot: actions(copyButton(() => text, "Copy age", "fill")),
101
+ });
102
+ slot.append(card.node);
103
+ run();
104
+
105
+ return {};
106
+ }
107
+
108
+ export default { mount };
@@ -0,0 +1,98 @@
1
+ // Builds an APA 7 reference and its in-text citation 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: "chapter", label: "Chapter in a book" },
8
+ { value: "web", label: "Web page" },
9
+ ];
10
+
11
+ const parts = (raw) => raw.split(";").map((item) => item.trim()).filter(Boolean);
12
+ const surname = (author) => author.split(",")[0].trim();
13
+
14
+ function authorList(raw) {
15
+ const list = parts(raw);
16
+ if (!list.length) return "";
17
+ if (list.length === 1) return list[0];
18
+ if (list.length === 2) return `${list[0]}, & ${list[1]}`;
19
+ return `${list.slice(0, -1).join(", ")}, & ${list[list.length - 1]}`;
20
+ }
21
+
22
+ function inText(raw, year) {
23
+ const list = parts(raw);
24
+ const y = year.trim();
25
+ if (!list.length) return y ? `(${y})` : "";
26
+ if (list.length === 1) return `(${surname(list[0])}, ${y})`;
27
+ if (list.length === 2) return `(${surname(list[0])} & ${surname(list[1])}, ${y})`;
28
+ return `(${surname(list[0])} et al., ${y})`;
29
+ }
30
+
31
+ const endDot = (value) => (/[.!?]$/.test(value) ? value : `${value}.`);
32
+
33
+ export function mount(slot) {
34
+ const type = selectInput(TYPES, { value: "book", "aria-label": "Source type" });
35
+ const authors = textInput({ placeholder: "Smith, J. A.; Doe, B.", "aria-label": "Authors" });
36
+ const year = textInput({ placeholder: "2020", "aria-label": "Year" });
37
+ const title = textInput({ placeholder: "Title of the work", "aria-label": "Title" });
38
+ const source = textInput({ placeholder: "Journal, publisher or site", "aria-label": "Source" });
39
+ const details = textInput({ placeholder: "Volume, issue, pages or edition", "aria-label": "Details" });
40
+ const link = textInput({ placeholder: "https://doi.org/10.0000/abc", "aria-label": "DOI or URL" });
41
+
42
+ const reference = output({ rows: 6 });
43
+ const citation = output({ rows: 2 });
44
+ const note = notice("note");
45
+ note.write("Check the details against the source and the APA 7 guide.");
46
+ let refText = "";
47
+
48
+ function build() {
49
+ const pieces = [];
50
+ const au = authorList(authors.value);
51
+ const yr = year.value.trim();
52
+ if (au) pieces.push(au);
53
+ if (yr) pieces.push(`(${yr}).`);
54
+ const ti = title.value.trim();
55
+ if (ti) pieces.push(endDot(ti));
56
+ const src = source.value.trim();
57
+ const det = details.value.trim();
58
+ if (type.value === "article") {
59
+ if (src) pieces.push(det ? `${src}, ${det}.` : endDot(src));
60
+ } else {
61
+ if (src) pieces.push(endDot(src));
62
+ if (det) pieces.push(endDot(det));
63
+ }
64
+ if (link.value.trim()) pieces.push(link.value.trim());
65
+ refText = pieces.join(" ");
66
+ reference.write(refText);
67
+ citation.write(inText(authors.value, yr));
68
+ }
69
+
70
+ for (const input of [authors, year, title, source, details, link]) input.addEventListener("input", build);
71
+ type.addEventListener("change", build);
72
+
73
+ const card = toolcard({
74
+ body: bench(
75
+ stack(
76
+ panel("Source type", type),
77
+ panel("Authors", authors),
78
+ panel("Year", year),
79
+ panel("Title", title),
80
+ panel("Source", source),
81
+ panel("Details", details),
82
+ panel("DOI or URL", link),
83
+ note
84
+ ),
85
+ stack(panel("Reference", reference), panel("In-text citation", citation))
86
+ ),
87
+ foot: actions(
88
+ copyButton(() => citation.textContent, "Copy in-text"),
89
+ copyButton(() => refText, "Copy reference", "fill")
90
+ ),
91
+ });
92
+ slot.append(card.node);
93
+ build();
94
+
95
+ return {};
96
+ }
97
+
98
+ export default { mount };
@@ -0,0 +1,76 @@
1
+ // Builds an HTTP request and writes it out as a fetch snippet.
2
+ import { bench, stack, panel, textInput, textArea, selectInput, notice, output, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
5
+ const BODYLESS = new Set(["GET", "HEAD"]);
6
+
7
+ function parseHeaders(text) {
8
+ const headers = {};
9
+ for (const raw of text.replace(/\r\n?/g, "\n").split("\n")) {
10
+ const line = raw.trim();
11
+ if (!line) continue;
12
+ const at = line.indexOf(":");
13
+ if (at === -1) continue;
14
+ headers[line.slice(0, at).trim()] = line.slice(at + 1).trim();
15
+ }
16
+ return headers;
17
+ }
18
+
19
+ function build(method, url, headers, body) {
20
+ const lines = [];
21
+ lines.push(`const response = await fetch(${JSON.stringify(url)}, {`);
22
+ lines.push(` method: ${JSON.stringify(method)},`);
23
+ const keys = Object.keys(headers);
24
+ if (keys.length) {
25
+ lines.push(" headers: {");
26
+ for (const key of keys) lines.push(` ${JSON.stringify(key)}: ${JSON.stringify(headers[key])},`);
27
+ lines.push(" },");
28
+ }
29
+ if (!BODYLESS.has(method) && body) lines.push(` body: ${JSON.stringify(body)},`);
30
+ lines.push("});");
31
+ lines.push("");
32
+ lines.push("const data = await response.json();");
33
+ return lines.join("\n");
34
+ }
35
+
36
+ export function mount(slot) {
37
+ const methodSelect = selectInput(METHODS, { value: "GET", "aria-label": "Method" });
38
+ const urlInput = textInput({ value: "https://api.example.com/users", "aria-label": "URL", spellcheck: "false" });
39
+ const headerInput = textArea({ rows: 5, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Headers", placeholder: "Content-Type: application/json", value: "Accept: application/json" });
40
+ const bodyInput = textArea({ rows: 6, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Body", placeholder: '{"name":"Ada"}', value: '{\n "name": "Ada"\n}' });
41
+ const result = output({ rows: 14 });
42
+ const hint = notice("note");
43
+
44
+ function run() {
45
+ const method = methodSelect.value;
46
+ const bodyless = BODYLESS.has(method);
47
+ bodyInput.disabled = bodyless;
48
+ hint.write(bodyless ? `${method} requests carry no body.` : "");
49
+ result.write(build(method, urlInput.value.trim() || "https://api.example.com/resource", parseHeaders(headerInput.value), bodyInput.value.trim()));
50
+ }
51
+
52
+ methodSelect.addEventListener("change", run);
53
+ urlInput.addEventListener("input", run);
54
+ headerInput.addEventListener("input", run);
55
+ bodyInput.addEventListener("input", run);
56
+
57
+ const card = toolcard({
58
+ body: bench(
59
+ stack(
60
+ panel("Method", methodSelect),
61
+ panel("URL", urlInput),
62
+ panel("Headers", headerInput),
63
+ panel("Body", bodyInput),
64
+ hint
65
+ ),
66
+ stack(panel("fetch", result))
67
+ ),
68
+ foot: actions(copyButton(() => result.textContent, "Copy snippet", "fill")),
69
+ });
70
+ slot.append(card.node);
71
+ run();
72
+
73
+ return {};
74
+ }
75
+
76
+ export default { mount };
@@ -0,0 +1,87 @@
1
+ // Converts an area 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: "mm2", label: "Square millimeter", factor: 0.000001 },
6
+ { value: "cm2", label: "Square centimeter", factor: 0.0001 },
7
+ { value: "m2", label: "Square meter", factor: 1 },
8
+ { value: "ha", label: "Hectare", factor: 10000 },
9
+ { value: "km2", label: "Square kilometer", factor: 1000000 },
10
+ { value: "in2", label: "Square inch", factor: 0.00064516 },
11
+ { value: "ft2", label: "Square foot", factor: 0.09290304 },
12
+ { value: "yd2", label: "Square yard", factor: 0.83612736 },
13
+ { value: "acre", label: "Acre", factor: 4046.8564224 },
14
+ ];
15
+
16
+ const byId = Object.fromEntries(UNITS.map((unit) => [unit.value, unit]));
17
+ const options = UNITS.map(({ value, label }) => ({ value, label }));
18
+
19
+ function fmt(value) {
20
+ if (!Number.isFinite(value)) return "-";
21
+ if (value === 0) return "0";
22
+ const abs = Math.abs(value);
23
+ if (abs >= 1e-6 && abs < 1e15) return String(Number(value.toPrecision(8)));
24
+ return value.toExponential(4);
25
+ }
26
+
27
+ export function mount(slot) {
28
+ const value = numberInput({ value: "1", step: "any", "aria-label": "Value" });
29
+ const from = selectInput(options, { value: "m2", "aria-label": "From unit" });
30
+ const to = selectInput(options, { value: "ft2", "aria-label": "To unit" });
31
+ const result = el("span", { class: "mvb-stat__v" }, "-");
32
+ const stats = el("div", { class: "mvb-stats" }, el("div", { class: "mvb-stat" }, el("span", { class: "mvb-stat__k" }, "Result"), result));
33
+ const factor = el("p", { class: "mvb-note" });
34
+ const error = notice("error");
35
+ const scroll = el("div", { class: "mvb-scroll" });
36
+ let text = "";
37
+
38
+ function run() {
39
+ error.write("");
40
+ const raw = value.value.trim();
41
+ const n = Number(raw);
42
+ if (!raw || !Number.isFinite(n)) {
43
+ result.textContent = "-";
44
+ factor.textContent = "";
45
+ scroll.replaceChildren();
46
+ text = "";
47
+ error.write("Enter a number.");
48
+ return;
49
+ }
50
+ const a = byId[from.value];
51
+ const b = byId[to.value];
52
+ const out = (n * a.factor) / b.factor;
53
+ result.textContent = `${fmt(out)} ${b.value}`;
54
+ factor.textContent = `1 ${a.value} = ${fmt(a.factor / b.factor)} ${b.value}`;
55
+ scroll.replaceChildren(el("table", { class: "mvb-ttable" },
56
+ el("thead", {}, el("tr", {}, el("th", {}, "Unit"), el("th", {}, "Value"))),
57
+ el("tbody", {}, ...UNITS.map((unit) => el("tr", {},
58
+ el("td", {}, unit.label),
59
+ el("td", {}, fmt((n * a.factor) / unit.factor)))))));
60
+ text = `${fmt(out)} ${b.value}`;
61
+ }
62
+
63
+ value.addEventListener("input", run);
64
+ from.addEventListener("change", run);
65
+ to.addEventListener("change", run);
66
+
67
+ const swap = button("Swap", { icon: "arrow-left-right", onclick: () => {
68
+ const held = from.value;
69
+ from.value = to.value;
70
+ to.value = held;
71
+ run();
72
+ }});
73
+
74
+ const card = toolcard({
75
+ body: bench(
76
+ stack(panel("Value", value), panel("From", from), panel("To", to), error),
77
+ stack(panel("Result", stats), factor, panel("All units", scroll))
78
+ ),
79
+ foot: actions(swap, copyButton(() => text, "Copy result", "fill")),
80
+ });
81
+ slot.append(card.node);
82
+ run();
83
+
84
+ return {};
85
+ }
86
+
87
+ export default { mount };
@@ -0,0 +1,81 @@
1
+ // Simplify an aspect ratio and find a missing width or height.
2
+ import { el, bench, stack, panel, numberInput, output, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const gcd = (a, b) => (b ? gcd(b, a % b) : a);
5
+ const round2 = (n) => Math.round(n * 100) / 100;
6
+
7
+ export function mount(slot) {
8
+ const width = numberInput({ value: 1920, min: "1", "aria-label": "Width" });
9
+ const height = numberInput({ value: 1080, min: "1", "aria-label": "Height" });
10
+
11
+ const ratioValue = el("span", { class: "mvb-kv__v" }, "-");
12
+ const decimalValue = el("span", { class: "mvb-kv__v" }, "-");
13
+ const list = el(
14
+ "div",
15
+ { class: "mvb-kv" },
16
+ el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, "Ratio"), ratioValue),
17
+ el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, "Decimal"), decimalValue)
18
+ );
19
+
20
+ const newWidth = numberInput({ value: 800, min: "1", "aria-label": "New width" });
21
+ const newHeight = numberInput({ value: 450, min: "1", "aria-label": "New height" });
22
+ const box = el("div", { class: "mvb-preview__box" });
23
+ const code = output();
24
+ let ratioText = "16 / 9";
25
+
26
+ function readSides() {
27
+ const w = parseFloat(width.value) || 0;
28
+ const h = parseFloat(height.value) || 0;
29
+ return { w, h };
30
+ }
31
+
32
+ function update() {
33
+ const { w, h } = readSides();
34
+ if (w > 0 && h > 0) {
35
+ const g = gcd(Math.round(w), Math.round(h)) || 1;
36
+ const sw = Math.round(w) / g;
37
+ const sh = Math.round(h) / g;
38
+ ratioText = `${sw} / ${sh}`;
39
+ ratioValue.textContent = `${sw}:${sh}`;
40
+ decimalValue.textContent = (w / h).toFixed(2);
41
+ box.style.height = "auto";
42
+ box.style.aspectRatio = `${w} / ${h}`;
43
+ box.textContent = `${sw}:${sh}`;
44
+ code.write(`aspect-ratio: ${ratioText};`);
45
+ const nw = parseFloat(newWidth.value) || 0;
46
+ if (nw > 0) newHeight.value = round2((nw * h) / w);
47
+ } else {
48
+ ratioValue.textContent = "-";
49
+ decimalValue.textContent = "-";
50
+ code.write("");
51
+ }
52
+ }
53
+
54
+ width.addEventListener("input", update);
55
+ height.addEventListener("input", update);
56
+
57
+ newWidth.addEventListener("input", () => {
58
+ const { w, h } = readSides();
59
+ const nw = parseFloat(newWidth.value) || 0;
60
+ if (w > 0 && nw > 0) newHeight.value = round2((nw * h) / w);
61
+ });
62
+ newHeight.addEventListener("input", () => {
63
+ const { w, h } = readSides();
64
+ const nh = parseFloat(newHeight.value) || 0;
65
+ if (h > 0 && nh > 0) newWidth.value = round2((nh * w) / h);
66
+ });
67
+
68
+ const card = toolcard({
69
+ body: bench(
70
+ stack(panel("Width", width), panel("Height", height), list),
71
+ stack(el("div", { class: "mvb-preview" }, box), panel("New width", newWidth), panel("New height", newHeight), code)
72
+ ),
73
+ foot: actions(copyButton(() => `aspect-ratio: ${ratioText};`, "Copy CSS", "fill")),
74
+ });
75
+ slot.append(card.node);
76
+ update();
77
+
78
+ return {};
79
+ }
80
+
81
+ export default { mount };