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,143 @@
1
+ // Renders a subset of Markdown into a preview pane. The source is turned into
2
+ // DOM nodes, so nothing in it can inject HTML.
3
+ import { el, bench, stack, panel, textArea, copyButton, actions, toolcard } from "../core.js";
4
+
5
+ const SAFE_URL = /^(https?:|mailto:|#|\/|\.)/i;
6
+
7
+ const safeUrl = (url) => (SAFE_URL.test(url.trim()) ? url.trim() : null);
8
+
9
+ function inline(text) {
10
+ const out = [];
11
+ const pattern = /(`([^`]+)`)|(\*\*([^*]+)\*\*)|(\*([^*]+)\*)|(\[([^\]]+)\]\(([^)\s]+)\))/g;
12
+ let last = 0;
13
+ let match;
14
+ while ((match = pattern.exec(text))) {
15
+ if (match.index > last) out.push(document.createTextNode(text.slice(last, match.index)));
16
+ if (match[2] != null) out.push(el("code", {}, match[2]));
17
+ else if (match[4] != null) out.push(el("strong", {}, match[4]));
18
+ else if (match[6] != null) out.push(el("em", {}, match[6]));
19
+ else if (match[8] != null) {
20
+ const href = safeUrl(match[9]);
21
+ out.push(href
22
+ ? el("a", { href, target: "_blank", rel: "noopener noreferrer" }, match[8])
23
+ : document.createTextNode(match[8]));
24
+ }
25
+ last = pattern.lastIndex;
26
+ }
27
+ if (last < text.length) out.push(document.createTextNode(text.slice(last)));
28
+ return out;
29
+ }
30
+
31
+ const splitRow = (row) => row.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => cell.trim());
32
+
33
+ function blocks(source) {
34
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
35
+ const nodes = [];
36
+ let i = 0;
37
+
38
+ const isSeparator = (value) => /^\s*\|?[\s:|-]*-[\s:|-]*\|?\s*$/.test(value);
39
+ const isTableStart = (index) => index + 1 < lines.length && lines[index].includes("|") && isSeparator(lines[index + 1]);
40
+ const isBlockStart = (index) => {
41
+ const line = lines[index];
42
+ return !line.trim() || /^```/.test(line) || /^#{1,6}\s/.test(line) || /^\s*>/.test(line) ||
43
+ /^\s*([-*+]|\d+[.)])\s+/.test(line) || /^\s*([-*_])(\s*\1){2,}\s*$/.test(line) || isTableStart(index);
44
+ };
45
+
46
+ while (i < lines.length) {
47
+ const line = lines[i];
48
+ if (!line.trim()) { i++; continue; }
49
+
50
+ const fence = line.match(/^```(\w*)\s*$/);
51
+ if (fence) {
52
+ const code = [];
53
+ i++;
54
+ while (i < lines.length && !/^```\s*$/.test(lines[i])) { code.push(lines[i]); i++; }
55
+ i++;
56
+ nodes.push(el("pre", {}, el("code", { class: fence[1] ? `language-${fence[1]}` : null }, code.join("\n"))));
57
+ continue;
58
+ }
59
+
60
+ const heading = line.match(/^(#{1,6})\s+(.*)$/);
61
+ if (heading) {
62
+ nodes.push(el(`h${heading[1].length}`, {}, ...inline(heading[2].trim())));
63
+ i++;
64
+ continue;
65
+ }
66
+
67
+ if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
68
+ nodes.push(el("hr"));
69
+ i++;
70
+ continue;
71
+ }
72
+
73
+ if (/^\s*>/.test(line)) {
74
+ const quote = [];
75
+ while (i < lines.length && /^\s*>/.test(lines[i])) { quote.push(lines[i].replace(/^\s*>\s?/, "")); i++; }
76
+ nodes.push(el("blockquote", {}, el("p", {}, ...inline(quote.join(" ")))));
77
+ continue;
78
+ }
79
+
80
+ if (isTableStart(i)) {
81
+ const head = splitRow(line);
82
+ i += 2;
83
+ const table = el("table", {}, el("thead", {}, el("tr", {}, ...head.map((cell) => el("th", {}, ...inline(cell))))));
84
+ const body = el("tbody");
85
+ while (i < lines.length && lines[i].includes("|") && lines[i].trim()) {
86
+ body.append(el("tr", {}, ...splitRow(lines[i]).map((cell) => el("td", {}, ...inline(cell)))));
87
+ i++;
88
+ }
89
+ table.append(body);
90
+ nodes.push(table);
91
+ continue;
92
+ }
93
+
94
+ if (/^\s*([-*+]|\d+[.)])\s+/.test(line)) {
95
+ const ordered = /^\s*\d+[.)]\s+/.test(line);
96
+ const items = [];
97
+ while (i < lines.length && /^\s*([-*+]|\d+[.)])\s+/.test(lines[i])) {
98
+ items.push(lines[i].replace(/^\s*([-*+]|\d+[.)])\s+/, ""));
99
+ i++;
100
+ }
101
+ const list = el(ordered ? "ol" : "ul");
102
+ for (const item of items) list.append(el("li", {}, ...inline(item)));
103
+ nodes.push(list);
104
+ continue;
105
+ }
106
+
107
+ const paragraph = [line];
108
+ i++;
109
+ while (i < lines.length && !isBlockStart(i)) { paragraph.push(lines[i]); i++; }
110
+ nodes.push(el("p", {}, ...inline(paragraph.join(" "))));
111
+ }
112
+ return nodes;
113
+ }
114
+
115
+ export function mount(slot) {
116
+ const source = textArea({
117
+ rows: 16,
118
+ class: "mvb-in mvb-in--area mvb-in--mono",
119
+ "aria-label": "Markdown",
120
+ value: "# Title\n\nSome **bold**, *italic* and `code`.\n\n- one\n- two\n\n> A quote.\n\n| a | b |\n| --- | --- |\n| 1 | 2 |\n\n```js\nconst x = 1;\n```\n",
121
+ });
122
+ const preview = el("div", { class: "mvb-pane" });
123
+
124
+ function run() {
125
+ preview.replaceChildren(...blocks(source.value));
126
+ }
127
+
128
+ source.addEventListener("input", run);
129
+
130
+ const card = toolcard({
131
+ body: bench(
132
+ stack(panel("Markdown", source)),
133
+ stack(panel("Preview", preview))
134
+ ),
135
+ foot: actions(copyButton(() => source.value, "Copy markdown", "fill")),
136
+ });
137
+ slot.append(card.node);
138
+ run();
139
+
140
+ return {};
141
+ }
142
+
143
+ export default { mount };
@@ -0,0 +1,111 @@
1
+ // Builds a Markdown table cell by cell, with per-column alignment.
2
+ import { el, bench, stack, panel, row, textInput, selectInput, button, output, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ const cell = (text) => String(text ?? "").replace(/\|/g, "\\|").trim();
5
+
6
+ export function mount(slot) {
7
+ const columns = ["left", "left", "left"];
8
+ const rows = [["Name", "Role", "City"], ["Ada", "Engineer", "London"]];
9
+ const editor = el("div", { class: "mvb-scroll" });
10
+ const alignment = el("div", { class: "mvb-stack" });
11
+ const result = output({ rows: 8 });
12
+
13
+ const toMarkdown = () => {
14
+ const head = rows[0] ?? [];
15
+ const marks = columns.map((align) => (align === "center" ? ":---:" : align === "right" ? "---:" : ":---"));
16
+ const lines = [
17
+ `| ${columns.map((_, index) => cell(head[index])).join(" | ")} |`,
18
+ `| ${marks.join(" | ")} |`,
19
+ ];
20
+ for (let r = 1; r < rows.length; r++) {
21
+ lines.push(`| ${columns.map((_, index) => cell(rows[r][index])).join(" | ")} |`);
22
+ }
23
+ return lines.join("\n");
24
+ };
25
+
26
+ const update = () => { result.write(toMarkdown()); };
27
+
28
+ function paintEditor() {
29
+ const table = el("table", { class: "mvb-ttable" });
30
+ const headRow = el("tr");
31
+ for (let c = 0; c < columns.length; c++) {
32
+ const input = textInput({ value: rows[0][c] ?? "", "aria-label": `Column ${c + 1} header` });
33
+ input.addEventListener("input", () => { rows[0][c] = input.value; update(); });
34
+ headRow.append(el("th", {}, input));
35
+ }
36
+ table.append(el("thead", {}, headRow));
37
+ const body = el("tbody");
38
+ for (let r = 1; r < rows.length; r++) {
39
+ const bodyRow = el("tr");
40
+ for (let c = 0; c < columns.length; c++) {
41
+ const input = textInput({ value: rows[r][c] ?? "", "aria-label": `Row ${r} column ${c + 1}` });
42
+ input.addEventListener("input", () => { rows[r][c] = input.value; update(); });
43
+ bodyRow.append(el("td", {}, input));
44
+ }
45
+ body.append(bodyRow);
46
+ }
47
+ table.append(body);
48
+ editor.replaceChildren(table);
49
+ update();
50
+ }
51
+
52
+ function paintAlignment() {
53
+ alignment.replaceChildren(
54
+ ...columns.map((align, index) => {
55
+ const select = selectInput(
56
+ [{ value: "left", label: "Left" }, { value: "center", label: "Center" }, { value: "right", label: "Right" }],
57
+ { value: align, "aria-label": `Column ${index + 1} alignment` }
58
+ );
59
+ select.addEventListener("change", () => { columns[index] = select.value; update(); });
60
+ return panel(`Column ${index + 1}`, select);
61
+ })
62
+ );
63
+ }
64
+
65
+ function paint() {
66
+ paintAlignment();
67
+ paintEditor();
68
+ }
69
+
70
+ const addColumn = button("Add column", { icon: "plus" });
71
+ addColumn.addEventListener("click", () => {
72
+ columns.push("left");
73
+ for (const entry of rows) entry.push("");
74
+ paint();
75
+ });
76
+
77
+ const removeColumn = button("Remove column", { icon: "minus" });
78
+ removeColumn.addEventListener("click", () => {
79
+ if (columns.length <= 1) return;
80
+ columns.pop();
81
+ for (const entry of rows) entry.pop();
82
+ paint();
83
+ });
84
+
85
+ const addRow = button("Add row", { icon: "plus" });
86
+ addRow.addEventListener("click", () => {
87
+ rows.push(columns.map(() => ""));
88
+ paintEditor();
89
+ });
90
+
91
+ const removeRow = button("Remove row", { icon: "minus" });
92
+ removeRow.addEventListener("click", () => {
93
+ if (rows.length <= 2) return;
94
+ rows.pop();
95
+ paintEditor();
96
+ });
97
+
98
+ const card = toolcard({
99
+ body: bench(
100
+ stack(panel("Columns", row(addColumn, removeColumn)), panel("Rows", row(addRow, removeRow)), panel("Alignment", alignment)),
101
+ stack(panel("Cells", editor), panel("Markdown", result))
102
+ ),
103
+ foot: actions(copyButton(() => result.textContent, "Copy table", "fill")),
104
+ });
105
+ slot.append(card.node);
106
+ paint();
107
+
108
+ return {};
109
+ }
110
+
111
+ export default { mount };
@@ -0,0 +1,48 @@
1
+ // Removes Markdown syntax and keeps the words.
2
+ import { bench, stack, panel, textArea, output, notice, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ function toText(markdown) {
5
+ let text = markdown;
6
+ text = text.replace(/```[^\n]*\n?/g, "");
7
+ text = text.replace(/`([^`]+)`/g, "$1");
8
+ text = text.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
9
+ text = text.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1");
10
+ text = text.replace(/^\s{0,3}#{1,6}\s+/gm, "");
11
+ text = text.replace(/^\s{0,3}>\s?/gm, "");
12
+ text = text.replace(/^\s{0,3}[-*+]\s+/gm, "");
13
+ text = text.replace(/^\s{0,3}\d+\.\s+/gm, "");
14
+ text = text.replace(/^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/gm, "");
15
+ text = text.replace(/(\*\*|__)(.*?)\1/g, "$2");
16
+ text = text.replace(/(\*|_)(.*?)\1/g, "$2");
17
+ text = text.replace(/~~(.*?)~~/g, "$1");
18
+ return text.replace(/\s+/g, " ").trim();
19
+ }
20
+
21
+ export function mount(slot) {
22
+ const source = textArea({ rows: 9, value: "## Title\n\nSome **bold** and [a link](https://example.com).", "aria-label": "Markdown" });
23
+ const result = output({ value: "", rows: 9 });
24
+ const error = notice("error");
25
+ let text = "";
26
+
27
+ function run() {
28
+ error.write("");
29
+ text = source.value ? toText(source.value) : "";
30
+ result.write(text);
31
+ }
32
+
33
+ source.addEventListener("input", run);
34
+
35
+ const card = toolcard({
36
+ body: bench(
37
+ stack(panel("Markdown", source)),
38
+ stack(panel("Plain text", result), error)
39
+ ),
40
+ foot: actions(copyButton(() => text, "Copy text", "fill")),
41
+ });
42
+ slot.append(card.node);
43
+ run();
44
+
45
+ return {};
46
+ }
47
+
48
+ export default { mount };
@@ -0,0 +1,69 @@
1
+ // Turns a cost and a target markup into a selling price.
2
+ import { el, bench, stack, panel, numberInput, notice, copyButton, actions, toolcard } from "../core.js";
3
+
4
+ function fmt(n) {
5
+ return n.toLocaleString(undefined, { maximumFractionDigits: 2 });
6
+ }
7
+
8
+ function tile(label) {
9
+ const value = el("span", { class: "mvb-stat__v" }, "-");
10
+ const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
11
+ node.set = (text) => { value.textContent = text; };
12
+ return node;
13
+ }
14
+
15
+ export function mount(slot) {
16
+ const cost = numberInput({ value: "40", "aria-label": "Cost" });
17
+ const markup = numberInput({ value: "150", "aria-label": "Markup percent" });
18
+ const price = tile("Selling price");
19
+ const profit = tile("Profit");
20
+ const margin = tile("Margin");
21
+ const stats = el("div", { class: "mvb-stats" }, price, profit, margin);
22
+ const error = notice("error");
23
+ let text = "";
24
+
25
+ function clear() {
26
+ price.set("-");
27
+ profit.set("-");
28
+ margin.set("-");
29
+ text = "";
30
+ }
31
+
32
+ function run() {
33
+ error.write("");
34
+ const c = Number(cost.value);
35
+ const m = Number(markup.value);
36
+ if (cost.value.trim() === "" || markup.value.trim() === "" || !Number.isFinite(c) || !Number.isFinite(m)) {
37
+ clear();
38
+ return;
39
+ }
40
+ if (c < 0 || m < -100) {
41
+ clear();
42
+ error.write("The cost cannot be negative and the markup cannot drop below -100%.");
43
+ return;
44
+ }
45
+ const selling = c * (1 + m / 100);
46
+ const gain = selling - c;
47
+ price.set(fmt(selling));
48
+ profit.set(fmt(gain));
49
+ margin.set(selling === 0 ? "-" : `${fmt((gain / selling) * 100)}%`);
50
+ text = `Selling price ${fmt(selling)}, profit ${fmt(gain)}, margin ${selling === 0 ? "n/a" : `${fmt((gain / selling) * 100)}%`}`;
51
+ }
52
+
53
+ cost.addEventListener("input", run);
54
+ markup.addEventListener("input", run);
55
+
56
+ const card = toolcard({
57
+ body: bench(
58
+ stack(panel("Cost", cost), panel("Markup %", markup), error),
59
+ stack(stats)
60
+ ),
61
+ foot: actions(copyButton(() => text, "Copy result", "fill")),
62
+ });
63
+ slot.append(card.node);
64
+ run();
65
+
66
+ return {};
67
+ }
68
+
69
+ export default { mount };
@@ -0,0 +1,137 @@
1
+ // Lays out a meeting with owners and time budgets, totalled for you.
2
+ import { el, toast, blank, bench, stack, row, panel, textInput, numberInput, button, copyButton, notice, actions, toolcard, readPin, writePin } from "../core.js";
3
+
4
+ const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
5
+
6
+ function hhmm(mins) {
7
+ const h = Math.floor(mins / 60) % 24;
8
+ const m = mins % 60;
9
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
10
+ }
11
+
12
+ function fmtDur(min) {
13
+ const h = Math.floor(min / 60);
14
+ const m = min % 60;
15
+ if (h && m) return `${h}h ${m}m`;
16
+ if (h) return `${h}h`;
17
+ return `${m}m`;
18
+ }
19
+
20
+ export function mount(slot) {
21
+ const PIN = "meeting-agenda";
22
+ const saved = readPin(PIN, { start: "09:00", items: [] });
23
+ let list = Array.isArray(saved.items) ? saved.items : [];
24
+
25
+ const startInput = el("input", { class: "mvb-in", type: "time", value: saved.start || "09:00", "aria-label": "Meeting start" });
26
+ const topic = textInput({ placeholder: "Topic", "aria-label": "Topic" });
27
+ const owner = textInput({ placeholder: "Owner", "aria-label": "Owner" });
28
+ const minutes = numberInput({ min: 1, max: 600, value: 10, "aria-label": "Minutes" });
29
+
30
+ const listWrap = el("div", { class: "mvb-scroll" });
31
+ const stats = el("div", { class: "mvb-stats" });
32
+ const error = notice("error");
33
+
34
+ const addButton = button("Add", { icon: "plus", kind: "fill" });
35
+ const copyAgenda = copyButton(() => agendaText(), "Copy agenda");
36
+
37
+ const tile = (key, value) => el("div", { class: "mvb-stat" },
38
+ el("span", { class: "mvb-stat__v" }, String(value)),
39
+ el("span", { class: "mvb-stat__k" }, key));
40
+
41
+ const base = () => {
42
+ const [h, m] = String(startInput.value || "00:00").split(":").map(Number);
43
+ return (h || 0) * 60 + (m || 0);
44
+ };
45
+ const save = () => writePin(PIN, { start: startInput.value, items: list });
46
+
47
+ function startTimes() {
48
+ const out = [];
49
+ let cursor = base();
50
+ for (const item of list) {
51
+ out.push(hhmm(cursor));
52
+ cursor += Math.max(0, Number(item.minutes) || 0);
53
+ }
54
+ return out;
55
+ }
56
+
57
+ function agendaText() {
58
+ const starts = startTimes();
59
+ return list.map((item, i) =>
60
+ `${starts[i]} ${item.topic}${item.owner ? ` (${item.owner})` : ""} ${item.minutes} min`
61
+ ).join("\n");
62
+ }
63
+
64
+ function removeButton(item) {
65
+ const node = button("", { icon: "trash", "aria-label": `Remove ${item.topic}` });
66
+ node.addEventListener("click", () => {
67
+ list = list.filter((entry) => entry.id !== item.id);
68
+ save();
69
+ render();
70
+ });
71
+ return node;
72
+ }
73
+
74
+ function render() {
75
+ const starts = startTimes();
76
+ if (!list.length) {
77
+ listWrap.replaceChildren(blank("clipboard-list", "No topics yet."));
78
+ } else {
79
+ const rows = list.map((item, i) => el("tr", {},
80
+ el("th", {}, starts[i]),
81
+ el("td", {}, item.topic),
82
+ el("td", {}, item.owner),
83
+ el("td", {}, `${item.minutes} min`),
84
+ el("td", {}, removeButton(item))
85
+ ));
86
+ listWrap.replaceChildren(el("table", { class: "mvb-ttable" },
87
+ el("thead", {}, el("tr", {},
88
+ el("th", {}, "Start"), el("th", {}, "Topic"), el("th", {}, "Owner"),
89
+ el("th", {}, "Time"), el("th", {}, ""))),
90
+ el("tbody", {}, ...rows)
91
+ ));
92
+ }
93
+ const total = list.reduce((sum, item) => sum + Math.max(0, Number(item.minutes) || 0), 0);
94
+ stats.replaceChildren(tile("Topics", list.length), tile("Total", fmtDur(total)));
95
+ copyAgenda.disabled = list.length === 0;
96
+ }
97
+
98
+ function add() {
99
+ const text = topic.value.trim();
100
+ if (!text) return toast("Add a topic first");
101
+ const value = Math.max(1, Number(minutes.value) || 0);
102
+ error.write("");
103
+ list.push({ id: uid(), topic: text, owner: owner.value.trim(), minutes: value });
104
+ topic.value = "";
105
+ owner.value = "";
106
+ save();
107
+ render();
108
+ }
109
+
110
+ topic.addEventListener("keydown", (event) => {
111
+ if (event.key === "Enter") add();
112
+ });
113
+ startInput.addEventListener("change", () => {
114
+ save();
115
+ render();
116
+ });
117
+ addButton.addEventListener("click", add);
118
+
119
+ const card = toolcard({
120
+ body: bench(
121
+ stack(
122
+ panel("Meeting start", startInput),
123
+ panel("Topic", topic),
124
+ row(panel("Owner", owner), panel("Minutes", minutes)),
125
+ error
126
+ ),
127
+ stack(stats, panel("Agenda", listWrap))
128
+ ),
129
+ foot: actions(copyAgenda, addButton),
130
+ });
131
+ slot.append(card.node);
132
+ render();
133
+
134
+ return {};
135
+ }
136
+
137
+ export default { mount };
@@ -0,0 +1,51 @@
1
+ // Shows how a title, address and description look as a search result, with
2
+ // length counts for the title and description.
3
+ import { el, bench, stack, panel, textInput, textArea, notice, copyButton, actions, toolcard } from "../core.js";
4
+
5
+ export function mount(slot) {
6
+ const title = textInput({ value: "mevebu", "aria-label": "Title" });
7
+ const url = textInput({ value: "https://example.com", "aria-label": "URL" });
8
+ const description = textArea({ rows: 3, value: "Small privacy first tools that run entirely in the browser.", "aria-label": "Description" });
9
+
10
+ const urlLine = el("span", { class: "mvb-note" }, "");
11
+ const titleLine = el("span", { class: "mvb-stat__v" }, "");
12
+ const descLine = el("p", { class: "mvb-note" }, "");
13
+ const sample = el("div", { class: "mvb-preview__sample" },
14
+ el("div", { class: "mvb-stack" }, urlLine, titleLine, descLine));
15
+
16
+ const titleCount = el("span", { class: "mvb-stat__v" }, "0");
17
+ const descCount = el("span", { class: "mvb-stat__v" }, "0");
18
+ const stats = el("div", { class: "mvb-stats" },
19
+ el("div", { class: "mvb-stat" }, titleCount, el("span", { class: "mvb-stat__k" }, "Title chars")),
20
+ el("div", { class: "mvb-stat" }, descCount, el("span", { class: "mvb-stat__k" }, "Description chars"))
21
+ );
22
+ const note = notice("note");
23
+
24
+ function run() {
25
+ urlLine.textContent = url.value.trim() || "example.com";
26
+ titleLine.textContent = title.value.trim() || "Page title";
27
+ descLine.textContent = description.value.trim() || "Page description.";
28
+ titleCount.textContent = String(title.value.trim().length);
29
+ descCount.textContent = String(description.value.trim().length);
30
+ note.write("Search engines trim long titles and descriptions, and previews vary.");
31
+ }
32
+
33
+ for (const input of [title, url, description]) input.addEventListener("input", run);
34
+
35
+ const card = toolcard({
36
+ body: bench(
37
+ stack(panel("Title", title), panel("URL", url), panel("Description", description)),
38
+ stack(sample, stats, note)
39
+ ),
40
+ foot: actions(
41
+ copyButton(() => title.value.trim(), "Copy title"),
42
+ copyButton(() => description.value.trim(), "Copy description", "fill")
43
+ ),
44
+ });
45
+ slot.append(card.node);
46
+ run();
47
+
48
+ return {};
49
+ }
50
+
51
+ export default { mount };
@@ -0,0 +1,62 @@
1
+ // Builds the standard meta tags for a page: title, description, keywords,
2
+ // author and robots. The result is plain HTML for the head.
3
+ import { el, bench, stack, panel, textInput, textArea, selectInput, output, copyButton, actions, toolcard } from "../core.js";
4
+
5
+ const esc = (value) =>
6
+ String(value).replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[char]));
7
+
8
+ export function mount(slot) {
9
+ const title = textInput({ value: "mevebu", "aria-label": "Title" });
10
+ const description = textArea({ rows: 3, value: "Small privacy first tools that run entirely in the browser.", "aria-label": "Description" });
11
+ const keywords = textInput({ placeholder: "tools, privacy, browser", "aria-label": "Keywords" });
12
+ const author = textInput({ "aria-label": "Author" });
13
+ const robots = selectInput(
14
+ ["index, follow", "noindex, follow", "index, nofollow", "noindex, nofollow"],
15
+ { value: "index, follow", "aria-label": "Robots" }
16
+ );
17
+ const result = output({ rows: 8 });
18
+
19
+ const titleCount = el("span", { class: "mvb-stat__v" }, "0");
20
+ const descCount = el("span", { class: "mvb-stat__v" }, "0");
21
+ const stats = el("div", { class: "mvb-stats" },
22
+ el("div", { class: "mvb-stat" }, titleCount, el("span", { class: "mvb-stat__k" }, "Title chars")),
23
+ el("div", { class: "mvb-stat" }, descCount, el("span", { class: "mvb-stat__k" }, "Description chars"))
24
+ );
25
+
26
+ function build() {
27
+ const lines = [];
28
+ const t = title.value.trim();
29
+ const d = description.value.trim();
30
+ if (t) lines.push(`<title>${esc(t)}</title>`);
31
+ if (d) lines.push(`<meta name="description" content="${esc(d)}" />`);
32
+ if (keywords.value.trim()) lines.push(`<meta name="keywords" content="${esc(keywords.value.trim())}" />`);
33
+ if (author.value.trim()) lines.push(`<meta name="author" content="${esc(author.value.trim())}" />`);
34
+ lines.push(`<meta name="robots" content="${esc(robots.value)}" />`);
35
+ result.write(lines.join("\n"));
36
+ titleCount.textContent = String(t.length);
37
+ descCount.textContent = String(d.length);
38
+ }
39
+
40
+ for (const input of [title, description, keywords, author]) input.addEventListener("input", build);
41
+ robots.addEventListener("change", build);
42
+
43
+ const card = toolcard({
44
+ body: bench(
45
+ stack(
46
+ panel("Title", title),
47
+ panel("Description", description),
48
+ panel("Keywords", keywords),
49
+ panel("Author", author),
50
+ panel("Robots", robots)
51
+ ),
52
+ stack(stats, panel("Meta tags", result))
53
+ ),
54
+ foot: actions(copyButton(() => result.textContent, "Copy tags", "fill")),
55
+ });
56
+ slot.append(card.node);
57
+ build();
58
+
59
+ return {};
60
+ }
61
+
62
+ export default { mount };