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,78 @@
1
+ // Indents a SQL query clause by clause. Keywords can be upper-cased.
2
+ import { bench, stack, panel, textArea, notice, switchControl, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const CLAUSES = [
5
+ "SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "HAVING", "LIMIT", "OFFSET", "VALUES", "SET",
6
+ "INSERT INTO", "UPDATE", "DELETE FROM", "INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "FULL JOIN",
7
+ "CROSS JOIN", "JOIN", "UNION ALL", "UNION", "CREATE TABLE", "ALTER TABLE", "DROP TABLE", "WITH", "RETURNING",
8
+ ];
9
+ const WORDS = [...CLAUSES, "AND", "OR", "ON", "AS", "IN", "IS", "NOT", "NULL", "LIKE", "BETWEEN", "DISTINCT", "ASC", "DESC", "CASE", "WHEN", "THEN", "ELSE", "END"];
10
+
11
+ const toPattern = (word) => word.replace(/ /g, "\\s+");
12
+
13
+ function protect(sql) {
14
+ const store = [];
15
+ const text = sql.replace(/'(?:[^']|'')*'|"(?:[^"]|"")*"|`[^`]*`/g, (match) => {
16
+ store.push(match);
17
+ return `\u0001${store.length - 1}\u0001`;
18
+ });
19
+ return { text, store };
20
+ }
21
+
22
+ const restore = (text, store) => text.replace(/\u0001(\d+)\u0001/g, (_, index) => store[Number(index)]);
23
+
24
+ function format(sql, upper) {
25
+ const cleaned = sql.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/--[^\n]*/g, " ");
26
+ const { text, store } = protect(cleaned);
27
+ let out = text.replace(/\s+/g, " ").trim();
28
+ if (upper) {
29
+ const wordRe = new RegExp(`\\b(${[...WORDS].sort((a, b) => b.length - a.length).map(toPattern).join("|")})\\b`, "gi");
30
+ out = out.replace(wordRe, (match) => match.toUpperCase());
31
+ }
32
+ const clauseRe = new RegExp(`\\s*(${[...CLAUSES].sort((a, b) => b.length - a.length).map(toPattern).join("|")})\\b`, "gi");
33
+ out = out.replace(clauseRe, "\n$1");
34
+ out = out.replace(/\s*\b(AND|OR)\b/gi, "\n $1");
35
+ out = out.replace(/\(\s+/g, "(").replace(/\s+\)/g, ")");
36
+ out = out.replace(/\n{3,}/g, "\n\n").trim();
37
+ return restore(out, store);
38
+ }
39
+
40
+ export function mount(slot) {
41
+ const source = textArea({
42
+ rows: 12,
43
+ class: "mvb-in mvb-in--area mvb-in--mono",
44
+ "aria-label": "SQL",
45
+ value: "select id, name from users where active = true and age > 18 order by name",
46
+ });
47
+ const result = textArea({ rows: 12, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Formatted SQL" });
48
+ const error = notice("error");
49
+ let upper = true;
50
+
51
+ const casing = switchControl("Upper-case keywords", { checked: true });
52
+ casing.input.addEventListener("change", () => { upper = casing.input.checked; run(); });
53
+
54
+ function run() {
55
+ error.write("");
56
+ if (!source.value.trim()) {
57
+ result.value = "";
58
+ return;
59
+ }
60
+ result.value = format(source.value, upper);
61
+ }
62
+
63
+ source.addEventListener("input", run);
64
+
65
+ const card = toolcard({
66
+ body: bench(
67
+ stack(panel("Options", casing), panel("SQL", source), error),
68
+ stack(panel("Formatted", result))
69
+ ),
70
+ foot: actions(copyButton(() => result.value, "Copy query", "fill")),
71
+ });
72
+ slot.append(card.node);
73
+ run();
74
+
75
+ return {};
76
+ }
77
+
78
+ export default { mount };
@@ -0,0 +1,102 @@
1
+ // Compacts a SQL query. String literals are kept intact.
2
+ import { el, bench, stack, panel, textArea, switchControl, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const bytes = (text) => new TextEncoder().encode(text).length;
5
+
6
+ const tile = (key, value) =>
7
+ el("div", { class: "mvb-stat" }, el("span", { class: "mvb-stat__v" }, value), el("span", { class: "mvb-stat__k" }, key));
8
+
9
+ function minify(sql, stripComments) {
10
+ let out = "";
11
+ let pending = false;
12
+ let i = 0;
13
+ const n = sql.length;
14
+ while (i < n) {
15
+ const ch = sql[i];
16
+ const next = sql[i + 1];
17
+ if (stripComments && ch === "-" && next === "-") {
18
+ while (i < n && sql[i] !== "\n") i++;
19
+ continue;
20
+ }
21
+ if (stripComments && ch === "/" && next === "*") {
22
+ i += 2;
23
+ while (i < n && !(sql[i] === "*" && sql[i + 1] === "/")) i++;
24
+ i += 2;
25
+ continue;
26
+ }
27
+ if (ch === "'" || ch === '"' || ch === "`") {
28
+ if (pending && out) out += " ";
29
+ pending = false;
30
+ out += ch;
31
+ i++;
32
+ while (i < n) {
33
+ out += sql[i];
34
+ if (sql[i] === ch) {
35
+ if (sql[i + 1] === ch) { out += sql[i + 1]; i += 2; continue; }
36
+ i++;
37
+ break;
38
+ }
39
+ i++;
40
+ }
41
+ continue;
42
+ }
43
+ if (/\s/.test(ch)) {
44
+ pending = true;
45
+ i++;
46
+ continue;
47
+ }
48
+ if (pending && out) {
49
+ const prev = out[out.length - 1];
50
+ if (!" (,".includes(prev) && !"),;".includes(ch)) out += " ";
51
+ }
52
+ pending = false;
53
+ out += ch;
54
+ i++;
55
+ }
56
+ return out.trim().replace(/;}/g, "}");
57
+ }
58
+
59
+ export function mount(slot) {
60
+ const source = textArea({
61
+ rows: 12,
62
+ class: "mvb-in mvb-in--area mvb-in--mono",
63
+ "aria-label": "SQL",
64
+ value: "SELECT id,\n name\n FROM users\n WHERE active = true\n AND age > 18;",
65
+ });
66
+ const result = textArea({ rows: 12, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Minified SQL" });
67
+ const stats = el("div", { class: "mvb-stats" });
68
+ let stripComments = true;
69
+
70
+ const comments = switchControl("Remove comments", { checked: true });
71
+ comments.input.addEventListener("change", () => { stripComments = comments.input.checked; run(); });
72
+
73
+ function run() {
74
+ stats.replaceChildren();
75
+ if (!source.value.trim()) {
76
+ result.value = "";
77
+ return;
78
+ }
79
+ const compact = minify(source.value, stripComments);
80
+ const before = bytes(source.value);
81
+ const after = bytes(compact);
82
+ const saved = before ? Math.max(0, Math.round((1 - after / before) * 100)) : 0;
83
+ result.value = compact;
84
+ stats.append(tile("Before", `${before} B`), tile("After", `${after} B`), tile("Saved", `${saved}%`));
85
+ }
86
+
87
+ source.addEventListener("input", run);
88
+
89
+ const card = toolcard({
90
+ body: bench(
91
+ stack(panel("Options", comments), panel("SQL", source)),
92
+ stack(panel("Minified", result), stats)
93
+ ),
94
+ foot: actions(copyButton(() => result.value, "Copy query", "fill")),
95
+ });
96
+ slot.append(card.node);
97
+ run();
98
+
99
+ return {};
100
+ }
101
+
102
+ export default { mount };
@@ -0,0 +1,96 @@
1
+ // Variance and standard deviation for a population or a sample.
2
+ import { el, bench, stack, panel, textArea, notice, copyButton, chipRow, actions, toolcard } from "../core.js";
3
+
4
+ function fmt(n) {
5
+ return n.toLocaleString(undefined, { maximumFractionDigits: 6 });
6
+ }
7
+
8
+ function parseNumbers(text) {
9
+ const tokens = text.split(/[\s,;]+/).filter(Boolean);
10
+ const nums = tokens.map(Number);
11
+ return { nums, invalid: nums.some((n) => !Number.isFinite(n)) };
12
+ }
13
+
14
+ function tile(label) {
15
+ const value = el("span", { class: "mvb-stat__v" }, "-");
16
+ const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
17
+ node.set = (text) => { value.textContent = text; };
18
+ return node;
19
+ }
20
+
21
+ export function mount(slot) {
22
+ const source = textArea({ rows: 8, value: "2, 4, 4, 4, 5, 5, 7, 9", "aria-label": "Numbers", placeholder: "2, 4, 4, 4, 5, 5, 7, 9" });
23
+ const mean = tile("Mean");
24
+ const variance = tile("Variance");
25
+ const deviation = tile("Std deviation");
26
+ const count = tile("Count");
27
+ const stats = el("div", { class: "mvb-stats" }, mean, variance, deviation, count);
28
+ const error = notice("error");
29
+ let kind = "population";
30
+ let text = "";
31
+
32
+ function clear() {
33
+ for (const item of [mean, variance, deviation, count]) item.set("-");
34
+ text = "";
35
+ }
36
+
37
+ function run() {
38
+ error.write("");
39
+ const { nums, invalid } = parseNumbers(source.value);
40
+ if (!nums.length) {
41
+ clear();
42
+ return;
43
+ }
44
+ if (invalid) {
45
+ clear();
46
+ error.write("Use numbers only, separated by spaces, commas or new lines.");
47
+ return;
48
+ }
49
+ const n = nums.length;
50
+ if (kind === "sample" && n < 2) {
51
+ clear();
52
+ error.write("A sample needs at least two values.");
53
+ return;
54
+ }
55
+ const average = nums.reduce((acc, value) => acc + value, 0) / n;
56
+ const squares = nums.reduce((acc, value) => acc + (value - average) ** 2, 0);
57
+ const divisor = kind === "sample" ? n - 1 : n;
58
+ const spread = squares / divisor;
59
+ mean.set(fmt(average));
60
+ variance.set(fmt(spread));
61
+ deviation.set(fmt(Math.sqrt(spread)));
62
+ count.set(String(n));
63
+ text = `${kind === "sample" ? "Sample" : "Population"} of ${n}: mean ${fmt(average)}, variance ${fmt(spread)}, standard deviation ${fmt(Math.sqrt(spread))}`;
64
+ }
65
+
66
+ const chips = chipRow(
67
+ [
68
+ { value: "population", label: "Population" },
69
+ { value: "sample", label: "Sample" },
70
+ ],
71
+ {
72
+ value: kind,
73
+ label: "Data set",
74
+ onChange: (value) => {
75
+ kind = value;
76
+ run();
77
+ },
78
+ }
79
+ );
80
+
81
+ source.addEventListener("input", run);
82
+
83
+ const card = toolcard({
84
+ body: bench(
85
+ stack(panel("Numbers", source), panel("Data set", chips), error),
86
+ stack(stats)
87
+ ),
88
+ foot: actions(copyButton(() => text, "Copy summary", "fill")),
89
+ });
90
+ slot.append(card.node);
91
+ run();
92
+
93
+ return {};
94
+ }
95
+
96
+ export default { mount };
@@ -0,0 +1,133 @@
1
+ // Times an activity and records laps with millisecond precision.
2
+ import { el, blank, bench, stack, panel, button, output, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ function format(ms) {
5
+ const total = Math.floor(ms);
6
+ const h = Math.floor(total / 3600000);
7
+ const m = Math.floor((total % 3600000) / 60000);
8
+ const s = Math.floor((total % 60000) / 1000);
9
+ const cs = total % 1000;
10
+ const pad = (n, size = 2) => String(n).padStart(size, "0");
11
+ return h > 0
12
+ ? `${h}:${pad(m)}:${pad(s)}.${pad(cs, 3)}`
13
+ : `${pad(m)}:${pad(s)}.${pad(cs, 3)}`;
14
+ }
15
+
16
+ export function mount(slot) {
17
+ const display = output({ rows: 2 });
18
+ const listWrap = el("div", { class: "mvb-scroll" });
19
+
20
+ const startButton = button("Start", { icon: "play", kind: "fill" });
21
+ const lapButton = button("Lap", { icon: "plus" });
22
+ const resetButton = button("Reset", { icon: "refresh-cw" });
23
+ const copyLaps = copyButton(() => lapsText(), "Copy laps");
24
+
25
+ let running = false;
26
+ let startAt = 0;
27
+ let elapsed = 0;
28
+ let frame = 0;
29
+ let laps = [];
30
+
31
+ const label = () => startButton.querySelector("span");
32
+ const current = () => elapsed + (running ? performance.now() - startAt : 0);
33
+
34
+ function paint() {
35
+ display.write(format(current()));
36
+ }
37
+
38
+ function loop() {
39
+ paint();
40
+ frame = requestAnimationFrame(loop);
41
+ }
42
+
43
+ function updateButtons() {
44
+ lapButton.disabled = !running;
45
+ resetButton.disabled = !running && elapsed === 0 && laps.length === 0;
46
+ copyLaps.disabled = laps.length === 0;
47
+ }
48
+
49
+ function renderLaps() {
50
+ if (!laps.length) {
51
+ listWrap.replaceChildren(blank("list", "No laps yet."));
52
+ return;
53
+ }
54
+ const rows = laps.map((total, i) => {
55
+ const split = total - (laps[i - 1] ?? 0);
56
+ return el("tr", {},
57
+ el("th", {}, String(i + 1)),
58
+ el("td", {}, format(split)),
59
+ el("td", {}, format(total))
60
+ );
61
+ });
62
+ listWrap.replaceChildren(el("table", { class: "mvb-ttable" },
63
+ el("thead", {}, el("tr", {}, el("th", {}, "#"), el("th", {}, "Lap"), el("th", {}, "Total"))),
64
+ el("tbody", {}, ...rows)
65
+ ));
66
+ }
67
+
68
+ function lapsText() {
69
+ return laps.map((total, i) => {
70
+ const split = total - (laps[i - 1] ?? 0);
71
+ return `${i + 1}\t${format(split)}\t${format(total)}`;
72
+ }).join("\n");
73
+ }
74
+
75
+ function toggle() {
76
+ if (running) {
77
+ elapsed = current();
78
+ running = false;
79
+ cancelAnimationFrame(frame);
80
+ label().textContent = "Resume";
81
+ } else {
82
+ startAt = performance.now();
83
+ running = true;
84
+ label().textContent = "Pause";
85
+ frame = requestAnimationFrame(loop);
86
+ }
87
+ paint();
88
+ updateButtons();
89
+ }
90
+
91
+ function lap() {
92
+ if (!running) return;
93
+ laps.push(current());
94
+ renderLaps();
95
+ updateButtons();
96
+ }
97
+
98
+ function reset() {
99
+ running = false;
100
+ cancelAnimationFrame(frame);
101
+ elapsed = 0;
102
+ laps = [];
103
+ label().textContent = "Start";
104
+ paint();
105
+ renderLaps();
106
+ updateButtons();
107
+ }
108
+
109
+ startButton.addEventListener("click", toggle);
110
+ lapButton.addEventListener("click", lap);
111
+ resetButton.addEventListener("click", reset);
112
+
113
+ const card = toolcard({
114
+ body: bench(
115
+ stack(panel("Laps", listWrap)),
116
+ stack(display)
117
+ ),
118
+ foot: actions(copyLaps, resetButton, lapButton, startButton),
119
+ });
120
+ slot.append(card.node);
121
+ paint();
122
+ renderLaps();
123
+ updateButtons();
124
+
125
+ return {
126
+ unmount() {
127
+ cancelAnimationFrame(frame);
128
+ running = false;
129
+ },
130
+ };
131
+ }
132
+
133
+ export default { mount };
@@ -0,0 +1,88 @@
1
+ // Topics with a session date each, ticked off as you go.
2
+ import { el, toast, blank, bench, stack, panel, textInput, checkbox, button, copyButton, actions, toolcard, readPin, writePin } from "../core.js";
3
+
4
+ const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
5
+
6
+ export function mount(slot) {
7
+ const PIN = "study-schedule";
8
+ const saved = readPin(PIN, []);
9
+ let list = Array.isArray(saved) ? saved : [];
10
+
11
+ const name = textInput({ placeholder: "Topic", "aria-label": "Topic" });
12
+ const date = el("input", { class: "mvb-in", type: "date", "aria-label": "Session date" });
13
+ const listWrap = el("div", { class: "mvb-scroll" });
14
+ const stats = el("div", { class: "mvb-stats" });
15
+
16
+ const addButton = button("Add", { icon: "plus", kind: "fill" });
17
+
18
+ const save = () => writePin(PIN, list);
19
+ const tile = (key, value) => el("div", { class: "mvb-stat" },
20
+ el("span", { class: "mvb-stat__v" }, String(value)),
21
+ el("span", { class: "mvb-stat__k" }, key));
22
+
23
+ const sorted = () => list.slice().sort((a, b) => (a.date || "9999").localeCompare(b.date || "9999"));
24
+
25
+ function removeButton(item) {
26
+ const node = button("", { icon: "trash", "aria-label": `Remove ${item.name}` });
27
+ node.addEventListener("click", () => {
28
+ list = list.filter((entry) => entry.id !== item.id);
29
+ save();
30
+ render();
31
+ });
32
+ return node;
33
+ }
34
+
35
+ function rowNode(item) {
36
+ const box = checkbox(item.name, { checked: item.done });
37
+ box.input.addEventListener("change", () => {
38
+ item.done = box.input.checked;
39
+ save();
40
+ render();
41
+ });
42
+ return el("tr", {},
43
+ el("td", {}, box),
44
+ el("td", {}, item.date || "No date"),
45
+ el("td", {}, removeButton(item)));
46
+ }
47
+
48
+ function render() {
49
+ const rows = sorted();
50
+ if (!rows.length) {
51
+ listWrap.replaceChildren(blank("calendar-days", "No topics yet."));
52
+ } else {
53
+ listWrap.replaceChildren(el("table", { class: "mvb-ttable" },
54
+ el("thead", {}, el("tr", {}, el("th", {}, "Topic"), el("th", {}, "Date"), el("th", {}, ""))),
55
+ el("tbody", {}, ...rows.map(rowNode))));
56
+ }
57
+ const done = list.filter((item) => item.done).length;
58
+ stats.replaceChildren(tile("Topics", list.length), tile("Done", done), tile("Left", list.length - done));
59
+ }
60
+
61
+ function add() {
62
+ const text = name.value.trim();
63
+ if (!text) return toast("Add a topic first");
64
+ list.push({ id: uid(), name: text, date: date.value, done: false });
65
+ name.value = "";
66
+ date.value = "";
67
+ name.focus();
68
+ save();
69
+ render();
70
+ }
71
+
72
+ name.addEventListener("keydown", (event) => { if (event.key === "Enter") add(); });
73
+ addButton.addEventListener("click", add);
74
+
75
+ const card = toolcard({
76
+ body: bench(
77
+ stack(panel("Topic", name), panel("Session date", date)),
78
+ stack(stats, panel("Plan", listWrap))
79
+ ),
80
+ foot: actions(copyButton(() => sorted().map((item) => `${item.date || "No date"} ${item.done ? "[x]" : "[ ]"} ${item.name}`).join("\n"), "Copy plan"), addButton),
81
+ });
82
+ slot.append(card.node);
83
+ render();
84
+
85
+ return {};
86
+ }
87
+
88
+ export default { mount };
@@ -0,0 +1,74 @@
1
+ // Subtitle lines checked against a maximum length, with blank lines tidied.
2
+ import { el, blank, bench, stack, panel, textArea, numberInput, notice, output, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const isTiming = (line) => line.includes("-->");
5
+ const isIndex = (line) => /^\d+$/.test(line.trim());
6
+
7
+ function tile(label) {
8
+ const value = el("span", { class: "mvb-stat__v" }, "0");
9
+ const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
10
+ node.set = (text) => { value.textContent = text; };
11
+ return node;
12
+ }
13
+
14
+ export function mount(slot) {
15
+ const source = textArea({ rows: 12, "aria-label": "Subtitles", placeholder: "Paste subtitle text or SRT" });
16
+ const max = numberInput({ min: "10", max: "120", value: "42", "aria-label": "Maximum line length" });
17
+
18
+ const linesTile = tile("Lines");
19
+ const longTile = tile("Long lines");
20
+ const longestTile = tile("Longest");
21
+ const stats = el("div", { class: "mvb-stats" }, linesTile, longTile, longestTile);
22
+
23
+ const longWrap = el("div", { class: "mvb-scroll" });
24
+ const out = output({ rows: 10 });
25
+ const note = notice("note");
26
+
27
+ let tidied = "";
28
+
29
+ function run() {
30
+ const cap = Math.max(1, parseInt(max.value, 10) || 42);
31
+ const raw = source.value.replace(/\r\n?/g, "\n").split("\n");
32
+ const content = raw.filter((line) => line.trim() && !isIndex(line) && !isTiming(line));
33
+ const long = content.filter((line) => line.length > cap);
34
+ const longest = content.reduce((best, line) => Math.max(best, line.length), 0);
35
+
36
+ linesTile.set(String(content.length));
37
+ longTile.set(String(long.length));
38
+ longestTile.set(String(longest));
39
+ note.write(long.length ? `${long.length} lines run past ${cap} characters.` : "");
40
+
41
+ if (!long.length) {
42
+ longWrap.replaceChildren(blank("film", "No line runs past the limit."));
43
+ } else {
44
+ longWrap.replaceChildren(el("div", { class: "mvb-kv" },
45
+ ...long.map((line) => el("div", { class: "mvb-kv__row" },
46
+ el("span", { class: "mvb-kv__k" }, `${line.length}`),
47
+ el("span", { class: "mvb-kv__v" }, line)))));
48
+ }
49
+
50
+ tidied = raw
51
+ .map((line) => line.replace(/\s+$/, ""))
52
+ .join("\n")
53
+ .replace(/\n{3,}/g, "\n\n")
54
+ .replace(/^\n+|\n+$/g, "");
55
+ out.write(tidied);
56
+ }
57
+
58
+ source.addEventListener("input", run);
59
+ max.addEventListener("input", run);
60
+
61
+ const card = toolcard({
62
+ body: bench(
63
+ stack(panel("Subtitles", source), panel("Maximum line length", max), note),
64
+ stack(stats, panel("Long lines", longWrap), panel("Tidied", out))
65
+ ),
66
+ foot: actions(copyButton(() => tidied, "Copy text", "fill")),
67
+ });
68
+ slot.append(card.node);
69
+ run();
70
+
71
+ return {};
72
+ }
73
+
74
+ export default { mount };