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,71 @@
1
+ // A plain text preview of common LaTeX math, next to the source.
2
+ import { el, bench, stack, panel, textArea, notice, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const SUP = { "0": "⁰", "1": "¹", "2": "²", "3": "³", "4": "⁴", "5": "⁵", "6": "⁶", "7": "⁷", "8": "⁸", "9": "⁹", "+": "⁺", "-": "⁻", "n": "ⁿ" };
5
+ const SUB = { "0": "₀", "1": "₁", "2": "₂", "3": "₃", "4": "₄", "5": "₅", "6": "₆", "7": "₇", "8": "₈", "9": "₉", "+": "₊", "-": "₋" };
6
+ const GREEK = {
7
+ alpha: "α", beta: "β", gamma: "γ", delta: "δ", epsilon: "ε", zeta: "ζ", eta: "η",
8
+ theta: "θ", iota: "ι", kappa: "κ", lambda: "λ", mu: "μ", nu: "ν", xi: "ξ",
9
+ pi: "π", rho: "ρ", sigma: "σ", tau: "τ", phi: "φ", chi: "χ", psi: "ψ", omega: "ω",
10
+ Gamma: "Γ", Delta: "Δ", Theta: "Θ", Lambda: "Λ", Xi: "Ξ", Pi: "Π", Sigma: "Σ",
11
+ Phi: "Φ", Psi: "Ψ", Omega: "Ω",
12
+ };
13
+ const SYMBOLS = {
14
+ times: "×", cdot: "·", div: "÷", pm: "±", mp: "∓", le: "≤", leq: "≤",
15
+ ge: "≥", geq: "≥", ne: "≠", neq: "≠", approx: "≈", equiv: "≡", infty: "∞",
16
+ sum: "Σ", prod: "Π", int: "∫", partial: "∂", nabla: "∇", rightarrow: "→",
17
+ to: "→", leftarrow: "←", Rightarrow: "⇒", Leftarrow: "⇐", leftrightarrow: "↔",
18
+ in: "∈", notin: "∉", forall: "∀", exists: "∃", angle: "∠", degree: "°",
19
+ };
20
+
21
+ const superscript = (value) => [...value].map((char) => SUP[char] ?? char).join("");
22
+ const subscript = (value) => [...value].map((char) => SUB[char] ?? char).join("");
23
+
24
+ function preview(source) {
25
+ let text = source;
26
+ text = text.replace(/\\text\s*\{([^{}]*)\}/g, "$1");
27
+ text = text.replace(/\\sqrt\s*\{([^{}]*)\}/g, "√($1)");
28
+ text = text.replace(/\\(?:d|t)?frac\s*\{([^{}]*)\}\s*\{([^{}]*)\}/g, "($1)/($2)");
29
+ text = text.replace(/\\([A-Za-z]+)/g, (match, name) => SYMBOLS[name] ?? GREEK[name] ?? match);
30
+ text = text.replace(/\^\{([^{}]*)\}/g, (_, value) => superscript(value));
31
+ text = text.replace(/\^(\S)/g, (_, value) => superscript(value));
32
+ text = text.replace(/_\{([^{}]*)\}/g, (_, value) => subscript(value));
33
+ text = text.replace(/_(\S)/g, (_, value) => subscript(value));
34
+ text = text.replace(/\\(?:left|right)/g, "");
35
+ text = text.replace(/\\([ ,;:!])/g, " ");
36
+ text = text.replace(/\\/g, "");
37
+ text = text.replace(/[{}]/g, "");
38
+ return text;
39
+ }
40
+
41
+ export function mount(slot) {
42
+ const source = textArea({
43
+ rows: 12,
44
+ value: "x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}",
45
+ "aria-label": "LaTeX",
46
+ placeholder: "\\frac{a}{b}",
47
+ });
48
+ const result = el("pre", { class: "mvb-out" });
49
+ const note = notice("note");
50
+ note.write("Plain text preview, not rendered math.");
51
+
52
+ function run() {
53
+ result.textContent = source.value ? preview(source.value) : "";
54
+ }
55
+
56
+ source.addEventListener("input", run);
57
+
58
+ const card = toolcard({
59
+ body: bench(
60
+ stack(panel("LaTeX", source)),
61
+ stack(panel("Preview", result), note)
62
+ ),
63
+ foot: actions(copyButton(() => source.value, "Copy LaTeX", "fill")),
64
+ });
65
+ slot.append(card.node);
66
+ run();
67
+
68
+ return {};
69
+ }
70
+
71
+ export default { mount };
@@ -0,0 +1,90 @@
1
+ // Splits a total between people, equally or by custom shares.
2
+ import { el, bench, stack, panel, textArea, numberInput, chipRow, notice, copyButton, actions, toolcard, readPin, writePin } from "../core.js";
3
+
4
+ function fmt(n) {
5
+ return n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
6
+ }
7
+
8
+ export function mount(slot) {
9
+ const PIN = "expense-split";
10
+ const saved = readPin(PIN, { total: "120", people: "", shares: "", mode: "equal" });
11
+ let mode = saved.mode === "custom" ? "custom" : "equal";
12
+
13
+ const total = numberInput({ value: saved.total ?? "", min: "0", "aria-label": "Total" });
14
+ const people = textArea({ rows: 6, value: saved.people ?? "", "aria-label": "People", placeholder: "One name per line" });
15
+ const shares = textArea({ rows: 6, value: saved.shares ?? "", "aria-label": "Shares", placeholder: "One number per line" });
16
+ const modeChips = chipRow(
17
+ [{ value: "equal", label: "Equal" }, { value: "custom", label: "Custom" }],
18
+ { value: mode, label: "Mode", onChange: (next) => { mode = next; save(); run(); } }
19
+ );
20
+ const sharesField = panel("Shares", shares);
21
+ const result = el("div", { class: "mvb-scroll" });
22
+ const error = notice("error");
23
+ const remainder = notice("warn");
24
+ let text = "";
25
+
26
+ const save = () => writePin(PIN, { total: total.value, people: people.value, shares: shares.value, mode });
27
+
28
+ function run() {
29
+ error.write("");
30
+ remainder.write("");
31
+ text = "";
32
+ sharesField.hidden = mode !== "custom";
33
+
34
+ const names = people.value.split("\n").map((line) => line.trim()).filter(Boolean);
35
+ const amount = Number(total.value);
36
+ if (!names.length || total.value.trim() === "" || !Number.isFinite(amount) || amount < 0) {
37
+ result.replaceChildren(el("p", { class: "mvb-note" }, "Add a total and one name per line."));
38
+ return;
39
+ }
40
+
41
+ let weights;
42
+ if (mode === "custom") {
43
+ weights = shares.value.split("\n").map((line) => line.trim()).filter(Boolean).map(Number);
44
+ if (weights.length !== names.length || weights.some((w) => !Number.isFinite(w) || w < 0)) {
45
+ result.replaceChildren(el("p", { class: "mvb-note" }, "Add one share for each name."));
46
+ error.write("Add one share for each name.");
47
+ return;
48
+ }
49
+ if (weights.reduce((a, b) => a + b, 0) <= 0) {
50
+ result.replaceChildren(el("p", { class: "mvb-note" }, "Add one share for each name."));
51
+ error.write("The shares add up to zero.");
52
+ return;
53
+ }
54
+ } else {
55
+ weights = names.map(() => 1);
56
+ }
57
+
58
+ const weightSum = weights.reduce((a, b) => a + b, 0);
59
+ const rounded = weights.map((w) => Math.round(((amount * w) / weightSum) * 100) / 100);
60
+ const paid = rounded.reduce((a, b) => a + b, 0);
61
+ const leftover = Math.round((amount - paid) * 100) / 100;
62
+
63
+ result.replaceChildren(el("table", { class: "mvb-ttable" },
64
+ el("thead", {}, el("tr", {}, el("th", {}, "Person"), el("th", {}, "Amount"))),
65
+ el("tbody", {}, ...names.map((name, i) => el("tr", {},
66
+ el("th", {}, name),
67
+ el("td", {}, fmt(rounded[i])))))));
68
+
69
+ if (Math.abs(leftover) >= 0.01) remainder.write(`Rounding leaves ${fmt(leftover)} unassigned.`);
70
+ text = names.map((name, i) => `${name}: ${fmt(rounded[i])}`).join("\n");
71
+ }
72
+
73
+ total.addEventListener("input", () => { save(); run(); });
74
+ people.addEventListener("input", () => { save(); run(); });
75
+ shares.addEventListener("input", () => { save(); run(); });
76
+
77
+ const card = toolcard({
78
+ body: bench(
79
+ stack(panel("Total", total), panel("Mode", modeChips), panel("People", people), sharesField, error),
80
+ stack(panel("Each person", result), remainder)
81
+ ),
82
+ foot: actions(copyButton(() => text, "Copy split", "fill")),
83
+ });
84
+ slot.append(card.node);
85
+ run();
86
+
87
+ return {};
88
+ }
89
+
90
+ export default { mount };
@@ -0,0 +1,53 @@
1
+ // Builds the favicon and app icon link tags for a site, prefixed by its origin
2
+ // when one is given.
3
+ import { bench, stack, panel, textInput, output, notice, copyButton, actions, toolcard } from "../core.js";
4
+
5
+ const esc = (value) =>
6
+ String(value).replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[char]));
7
+
8
+ const isUrl = (value) => {
9
+ try {
10
+ new URL(value);
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
+ };
16
+
17
+ export function mount(slot) {
18
+ const site = textInput({ value: "https://example.com", "aria-label": "Site URL" });
19
+ const result = output({ rows: 6 });
20
+ const error = notice("error");
21
+
22
+ function build() {
23
+ error.write("");
24
+ let prefix = "";
25
+ const raw = site.value.trim();
26
+ if (raw) {
27
+ if (isUrl(raw)) prefix = new URL(raw).origin;
28
+ else error.write("That is not a valid URL.");
29
+ }
30
+ result.write([
31
+ `<link rel="icon" href="${esc(prefix)}/favicon.ico" sizes="any" />`,
32
+ `<link rel="icon" type="image/svg+xml" href="${esc(prefix)}/favicon.svg" />`,
33
+ `<link rel="apple-touch-icon" href="${esc(prefix)}/apple-touch-icon.png" />`,
34
+ `<link rel="manifest" href="${esc(prefix)}/site.webmanifest" />`,
35
+ ].join("\n"));
36
+ }
37
+
38
+ site.addEventListener("input", build);
39
+
40
+ const card = toolcard({
41
+ body: bench(
42
+ stack(panel("Site URL", site), error),
43
+ stack(panel("Favicon tags", result))
44
+ ),
45
+ foot: actions(copyButton(() => result.textContent, "Copy tags", "fill")),
46
+ });
47
+ slot.append(card.node);
48
+ build();
49
+
50
+ return {};
51
+ }
52
+
53
+ export default { mount };
@@ -0,0 +1,88 @@
1
+ // Find and replace, plain text or a regular expression, with a live match count.
2
+ import { bench, stack, panel, textArea, textInput, toggleRow, notice, button, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5
+
6
+ export function mount(slot) {
7
+ const source = textArea({ rows: 9, "aria-label": "Text", placeholder: "Paste your text" });
8
+ const findField = textInput({ placeholder: "Find", "aria-label": "Find" });
9
+ const replaceField = textInput({ placeholder: "Replace with", "aria-label": "Replace with" });
10
+ const options = toggleRow([
11
+ { value: "regex", label: "Regex", checked: false },
12
+ { value: "ignore", label: "Ignore case", checked: false },
13
+ ]);
14
+ const result = textArea({ rows: 9, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Result" });
15
+ const error = notice("error");
16
+ const countNote = notice("note");
17
+
18
+ function build(global) {
19
+ const find = findField.value;
20
+ if (!find) return null;
21
+ const opts = options.values();
22
+ const flags = (global ? "g" : "") + (opts.includes("ignore") ? "i" : "");
23
+ try {
24
+ return new RegExp(opts.includes("regex") ? find : escapeRegExp(find), flags);
25
+ } catch {
26
+ return "error";
27
+ }
28
+ }
29
+
30
+ function run() {
31
+ const pattern = build(true);
32
+ if (pattern === "error") {
33
+ error.write("That pattern is not valid.");
34
+ countNote.write("");
35
+ result.value = source.value;
36
+ return;
37
+ }
38
+ error.write("");
39
+ if (!pattern) {
40
+ countNote.write("");
41
+ result.value = source.value;
42
+ return;
43
+ }
44
+ const matches = source.value.match(pattern) || [];
45
+ result.value = source.value.replace(pattern, replaceField.value);
46
+ countNote.write(matches.length === 1 ? "1 match." : `${matches.length} matches.`);
47
+ }
48
+
49
+ function replaceOne() {
50
+ const pattern = build(false);
51
+ if (pattern === "error") return;
52
+ if (!pattern) return;
53
+ source.value = source.value.replace(pattern, replaceField.value);
54
+ run();
55
+ }
56
+
57
+ function replaceAll() {
58
+ const pattern = build(true);
59
+ if (pattern === "error") return;
60
+ if (!pattern) return;
61
+ source.value = source.value.replace(pattern, replaceField.value);
62
+ run();
63
+ }
64
+
65
+ const replaceOneButton = button("Replace one", { icon: "replace" });
66
+ replaceOneButton.addEventListener("click", replaceOne);
67
+ const replaceAllButton = button("Replace all", { icon: "replace", kind: "fill" });
68
+ replaceAllButton.addEventListener("click", replaceAll);
69
+
70
+ source.addEventListener("input", run);
71
+ findField.addEventListener("input", run);
72
+ replaceField.addEventListener("input", run);
73
+ options.addEventListener("change", run);
74
+
75
+ const card = toolcard({
76
+ body: bench(
77
+ stack(panel("Text", source), panel("Find", findField), panel("Replace with", replaceField), options, error, countNote),
78
+ stack(panel("Result", result))
79
+ ),
80
+ foot: actions(copyButton(() => result.value, "Copy result"), replaceOneButton, replaceAllButton),
81
+ });
82
+ slot.append(card.node);
83
+ run();
84
+
85
+ return {};
86
+ }
87
+
88
+ export default { mount };
@@ -0,0 +1,117 @@
1
+ // A question and answer set you can flip through and export.
2
+ import { el, toast, blank, bench, stack, row, panel, textInput, textArea, button, copyButton, downloadButton, 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 = "flashcard-creator";
8
+ const saved = readPin(PIN, []);
9
+ let cards = Array.isArray(saved) ? saved : [];
10
+ let index = 0;
11
+ let flipped = false;
12
+
13
+ const question = textInput({ placeholder: "Question", "aria-label": "Question" });
14
+ const answer = textArea({ rows: 3, placeholder: "Answer", "aria-label": "Answer" });
15
+ const listWrap = el("div", { class: "mvb-scroll" });
16
+ const stats = el("div", { class: "mvb-stats" });
17
+ const face = el("pre", { class: "mvb-out" });
18
+ const faceLabel = el("span", { class: "mvb-field__label" }, "Question");
19
+
20
+ const addButton = button("Add card", { icon: "plus", kind: "fill" });
21
+ const flipButton = button("Flip", { icon: "arrow-left-right" });
22
+ const prevButton = button("Previous");
23
+ const nextButton = button("Next");
24
+
25
+ const save = () => writePin(PIN, cards);
26
+ const tile = (key, value) => el("div", { class: "mvb-stat" },
27
+ el("span", { class: "mvb-stat__v" }, String(value)),
28
+ el("span", { class: "mvb-stat__k" }, key));
29
+
30
+ const textExport = () =>
31
+ cards.map((card, i) => `${i + 1}. Q: ${card.question}\n A: ${card.answer}`).join("\n\n");
32
+
33
+ const jsonExport = () =>
34
+ JSON.stringify(cards.map(({ question: q, answer: a }) => ({ question: q, answer: a })), null, 2);
35
+
36
+ function removeButton(card) {
37
+ const node = button("", { icon: "trash", "aria-label": `Remove ${card.question}` });
38
+ node.addEventListener("click", () => {
39
+ cards = cards.filter((entry) => entry.id !== card.id);
40
+ if (index >= cards.length) index = Math.max(0, cards.length - 1);
41
+ save();
42
+ render();
43
+ });
44
+ return node;
45
+ }
46
+
47
+ function render() {
48
+ if (!cards.length) {
49
+ face.textContent = "";
50
+ listWrap.replaceChildren(blank("layers", "No cards yet."));
51
+ } else {
52
+ const card = cards[index];
53
+ faceLabel.textContent = flipped ? "Answer" : "Question";
54
+ face.textContent = flipped ? card.answer : card.question;
55
+ listWrap.replaceChildren(el("table", { class: "mvb-ttable" },
56
+ el("tbody", {}, ...cards.map((entry, i) => el("tr", {},
57
+ el("th", {}, `${i + 1}. ${entry.question}`),
58
+ el("td", {}, removeButton(entry)))))));
59
+ }
60
+ stats.replaceChildren(tile("Cards", cards.length), tile("Card", cards.length ? `${index + 1} / ${cards.length}` : "-"));
61
+ flipButton.disabled = !cards.length;
62
+ prevButton.disabled = cards.length < 2;
63
+ nextButton.disabled = cards.length < 2;
64
+ }
65
+
66
+ function add() {
67
+ const q = question.value.trim();
68
+ const a = answer.value.trim();
69
+ if (!q || !a) return toast("Add a question and an answer");
70
+ cards.push({ id: uid(), question: q, answer: a });
71
+ question.value = "";
72
+ answer.value = "";
73
+ question.focus();
74
+ index = cards.length - 1;
75
+ flipped = false;
76
+ save();
77
+ render();
78
+ }
79
+
80
+ addButton.addEventListener("click", add);
81
+ flipButton.addEventListener("click", () => { flipped = !flipped; render(); });
82
+ prevButton.addEventListener("click", () => {
83
+ if (!cards.length) return;
84
+ index = (index - 1 + cards.length) % cards.length;
85
+ flipped = false;
86
+ render();
87
+ });
88
+ nextButton.addEventListener("click", () => {
89
+ if (!cards.length) return;
90
+ index = (index + 1) % cards.length;
91
+ flipped = false;
92
+ render();
93
+ });
94
+
95
+ const card = toolcard({
96
+ body: bench(
97
+ stack(panel("Question", question), panel("Answer", answer)),
98
+ stack(
99
+ stats,
100
+ panel(faceLabel, face),
101
+ row(prevButton, nextButton, flipButton),
102
+ panel("Cards", listWrap)
103
+ )
104
+ ),
105
+ foot: actions(
106
+ copyButton(() => textExport(), "Copy text"),
107
+ downloadButton(() => jsonExport(), "flashcards.json", "Save JSON", "application/json"),
108
+ addButton
109
+ ),
110
+ });
111
+ slot.append(card.node);
112
+ render();
113
+
114
+ return {};
115
+ }
116
+
117
+ export default { mount };
@@ -0,0 +1,88 @@
1
+ // Preview a heading and body font pair, then copy the CSS font stacks.
2
+ import { el, bench, stack, panel, selectInput, rangeInput, chipRow, output, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const FONTS = [
5
+ { value: "system-ui, sans-serif", label: "System UI" },
6
+ { value: "Arial, Helvetica, sans-serif", label: "Arial" },
7
+ { value: "Verdana, Geneva, sans-serif", label: "Verdana" },
8
+ { value: "Tahoma, Verdana, sans-serif", label: "Tahoma" },
9
+ { value: '"Trebuchet MS", sans-serif', label: "Trebuchet MS" },
10
+ { value: "Georgia, \"Times New Roman\", serif", label: "Georgia" },
11
+ { value: '"Times New Roman", Times, serif', label: "Times New Roman" },
12
+ { value: 'Palatino, "Palatino Linotype", serif', label: "Palatino" },
13
+ { value: "Garamond, Georgia, serif", label: "Garamond" },
14
+ { value: '"Courier New", Courier, monospace', label: "Courier New" },
15
+ { value: "ui-monospace, SFMono-Regular, Menlo, monospace", label: "Mono" },
16
+ ];
17
+
18
+ function slider(label, { min, max, step, value, unit }) {
19
+ const input = rangeInput({ min, max, step, value, "aria-label": label });
20
+ const val = el("span", { class: "mvb-slider__val" }, `${value}${unit}`);
21
+ const node = el(
22
+ "div",
23
+ { class: "mvb-slider" },
24
+ el("span", { class: "mvb-slider__label" }, label),
25
+ input,
26
+ val
27
+ );
28
+ node.input = input;
29
+ node.value = () => parseFloat(input.value) || 0;
30
+ node.setVal = (text) => { val.textContent = text; };
31
+ return node;
32
+ }
33
+
34
+ export function mount(slot) {
35
+ const headingFont = selectInput(FONTS, { value: "Georgia, \"Times New Roman\", serif", "aria-label": "Heading font" });
36
+ const bodyFont = selectInput(FONTS, { value: "system-ui, sans-serif", "aria-label": "Body font" });
37
+ const headingSize = slider("Heading size", { min: 20, max: 72, step: 1, value: 40, unit: "px" });
38
+ const bodySize = slider("Body size", { min: 12, max: 24, step: 1, value: 16, unit: "px" });
39
+ const lineHeight = slider("Line height", { min: 1, max: 2, step: 0.05, value: 1.5, unit: "" });
40
+ let weight = 700;
41
+
42
+ const weightChips = chipRow(
43
+ [400, 500, 600, 700, 800].map((n) => ({ value: n, label: String(n) })),
44
+ { value: weight, label: "Heading weight", onChange: (value) => { weight = value; update(); } }
45
+ );
46
+
47
+ const heading = el("h2", { style: "margin:0 0 8px" }, "Heading sample");
48
+ const para = el("p", { style: "margin:0" }, "Body text sample. The quick brown fox jumps over the lazy dog.");
49
+ const sample = el("div", { class: "mvb-preview__sample" }, heading, para);
50
+ const code = output();
51
+
52
+ function css() {
53
+ const hs = headingSize.value();
54
+ const bs = bodySize.value();
55
+ const lh = lineHeight.value();
56
+ return `h1 {\n font-family: ${headingFont.value};\n font-size: ${hs}px;\n font-weight: ${weight};\n}\nbody {\n font-family: ${bodyFont.value};\n font-size: ${bs}px;\n line-height: ${lh};\n}`;
57
+ }
58
+
59
+ function update() {
60
+ heading.style.fontFamily = headingFont.value;
61
+ heading.style.fontSize = `${headingSize.value()}px`;
62
+ heading.style.fontWeight = String(weight);
63
+ para.style.fontFamily = bodyFont.value;
64
+ para.style.fontSize = `${bodySize.value()}px`;
65
+ para.style.lineHeight = String(lineHeight.value());
66
+ code.write(css());
67
+ }
68
+
69
+ headingFont.addEventListener("change", update);
70
+ bodyFont.addEventListener("change", update);
71
+ headingSize.input.addEventListener("input", () => { headingSize.setVal(`${headingSize.value()}px`); update(); });
72
+ bodySize.input.addEventListener("input", () => { bodySize.setVal(`${bodySize.value()}px`); update(); });
73
+ lineHeight.input.addEventListener("input", () => { lineHeight.setVal(`${lineHeight.value()}`); update(); });
74
+
75
+ const card = toolcard({
76
+ body: bench(
77
+ stack(panel("Heading font", headingFont), panel("Body font", bodyFont), headingSize, bodySize, lineHeight, panel("Heading weight", weightChips)),
78
+ stack(sample, code)
79
+ ),
80
+ foot: actions(copyButton(css, "Copy CSS", "fill")),
81
+ });
82
+ slot.append(card.node);
83
+ update();
84
+
85
+ return {};
86
+ }
87
+
88
+ export default { mount };
@@ -0,0 +1,130 @@
1
+ // Fraction arithmetic, kept as a simplified fraction and a decimal.
2
+ import { el, bench, stack, panel, numberInput, notice, copyButton, chipRow, actions, toolcard } from "../core.js";
3
+
4
+ function gcd(a, b) {
5
+ a = Math.abs(a);
6
+ b = Math.abs(b);
7
+ while (b) {
8
+ const next = a % b;
9
+ a = b;
10
+ b = next;
11
+ }
12
+ return a;
13
+ }
14
+
15
+ function fmt(n) {
16
+ return n.toLocaleString(undefined, { maximumFractionDigits: 6 });
17
+ }
18
+
19
+ const kvRow = (key, value) =>
20
+ el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
21
+
22
+ export function mount(slot) {
23
+ const n1 = numberInput({ value: "1", "aria-label": "First numerator" });
24
+ const d1 = numberInput({ value: "2", "aria-label": "First denominator" });
25
+ const n2 = numberInput({ value: "1", "aria-label": "Second numerator" });
26
+ const d2 = numberInput({ value: "3", "aria-label": "Second denominator" });
27
+ const resultRow = el("span", { class: "mvb-kv__v" }, "-");
28
+ const decimalRow = el("span", { class: "mvb-kv__v" }, "-");
29
+ const mixedRow = el("span", { class: "mvb-kv__v" }, "-");
30
+ const kv = el(
31
+ "div",
32
+ { class: "mvb-kv" },
33
+ kvRow("Fraction", resultRow),
34
+ kvRow("Decimal", decimalRow),
35
+ kvRow("Mixed number", mixedRow)
36
+ );
37
+ const error = notice("error");
38
+ let op = "+";
39
+ let text = "";
40
+
41
+ function clear() {
42
+ resultRow.textContent = "-";
43
+ decimalRow.textContent = "-";
44
+ mixedRow.textContent = "-";
45
+ text = "";
46
+ }
47
+
48
+ function run() {
49
+ error.write("");
50
+ const A = Number(n1.value);
51
+ const B = Number(d1.value);
52
+ const C = Number(n2.value);
53
+ const D = Number(d2.value);
54
+ if ([n1, d1, n2, d2].some((input) => input.value.trim() === "") || ![A, B, C, D].every(Number.isFinite)) {
55
+ clear();
56
+ return;
57
+ }
58
+ if (B === 0 || D === 0) {
59
+ clear();
60
+ error.write("A denominator cannot be zero.");
61
+ return;
62
+ }
63
+ let num;
64
+ let den;
65
+ if (op === "+") { num = A * D + C * B; den = B * D; }
66
+ else if (op === "-") { num = A * D - C * B; den = B * D; }
67
+ else if (op === "x") { num = A * C; den = B * D; }
68
+ else {
69
+ if (C === 0) {
70
+ clear();
71
+ error.write("Cannot divide by a fraction with a zero numerator.");
72
+ return;
73
+ }
74
+ num = A * D;
75
+ den = B * C;
76
+ }
77
+ if (den < 0) { num = -num; den = -den; }
78
+ const divisor = gcd(num, den) || 1;
79
+ const simpleNum = num / divisor;
80
+ const simpleDen = den / divisor;
81
+ const fraction = simpleDen === 1 ? String(simpleNum) : `${simpleNum}/${simpleDen}`;
82
+ const decimal = num / den;
83
+ resultRow.textContent = fraction;
84
+ decimalRow.textContent = fmt(decimal);
85
+ const whole = Math.trunc(simpleNum / simpleDen);
86
+ const rest = Math.abs(simpleNum % simpleDen);
87
+ mixedRow.textContent = simpleDen !== 1 && Math.abs(simpleNum) > simpleDen && rest !== 0
88
+ ? `${whole} ${rest}/${simpleDen}`
89
+ : fraction;
90
+ text = fraction;
91
+ }
92
+
93
+ const chips = chipRow(
94
+ [
95
+ { value: "+", label: "+" },
96
+ { value: "-", label: "-" },
97
+ { value: "x", label: "x" },
98
+ { value: "/", label: "/" },
99
+ ],
100
+ {
101
+ value: op,
102
+ label: "Operation",
103
+ onChange: (value) => {
104
+ op = value;
105
+ run();
106
+ },
107
+ }
108
+ );
109
+
110
+ for (const input of [n1, d1, n2, d2]) input.addEventListener("input", run);
111
+
112
+ const card = toolcard({
113
+ body: bench(
114
+ stack(
115
+ panel("First fraction", el("div", { class: "mvb-row" }, n1, d1)),
116
+ panel("Operation", chips),
117
+ panel("Second fraction", el("div", { class: "mvb-row" }, n2, d2)),
118
+ error
119
+ ),
120
+ stack(panel("Result", kv))
121
+ ),
122
+ foot: actions(copyButton(() => text, "Copy fraction", "fill")),
123
+ });
124
+ slot.append(card.node);
125
+ run();
126
+
127
+ return {};
128
+ }
129
+
130
+ export default { mount };