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.
- package/README.md +24 -8
- package/mevebu/app/mevebu.js +2 -2
- package/mevebu/libs/css/tools.css +103 -1
- package/mevebu/libs/js/core.js +1 -1
- package/mevebu/libs/js/icons.js +115 -1
- package/mevebu/libs/js/registry.js +3102 -0
- package/mevebu/libs/js/tools/abstract-checklist.js +54 -0
- package/mevebu/libs/js/tools/add-subtract-days.js +105 -0
- package/mevebu/libs/js/tools/age-calculator.js +108 -0
- package/mevebu/libs/js/tools/apa-reference.js +98 -0
- package/mevebu/libs/js/tools/api-request-builder.js +76 -0
- package/mevebu/libs/js/tools/area-converter.js +87 -0
- package/mevebu/libs/js/tools/aspect-ratio.js +81 -0
- package/mevebu/libs/js/tools/average-calculator.js +83 -0
- package/mevebu/libs/js/tools/bibliography-sorter.js +43 -0
- package/mevebu/libs/js/tools/bibtex-generator.js +98 -0
- package/mevebu/libs/js/tools/binary-calculator.js +58 -0
- package/mevebu/libs/js/tools/border-radius.js +88 -0
- package/mevebu/libs/js/tools/box-shadow.js +71 -0
- package/mevebu/libs/js/tools/branch-name.js +44 -0
- package/mevebu/libs/js/tools/break-even.js +68 -0
- package/mevebu/libs/js/tools/business-days.js +120 -0
- package/mevebu/libs/js/tools/canonical-tag.js +63 -0
- package/mevebu/libs/js/tools/caption-counter.js +66 -0
- package/mevebu/libs/js/tools/case-inspector.js +114 -0
- package/mevebu/libs/js/tools/cash-denomination.js +74 -0
- package/mevebu/libs/js/tools/cash-flow.js +124 -0
- package/mevebu/libs/js/tools/cert-expiry.js +83 -0
- package/mevebu/libs/js/tools/changelog-generator.js +48 -0
- package/mevebu/libs/js/tools/character-counter.js +80 -0
- package/mevebu/libs/js/tools/checklist.js +86 -0
- package/mevebu/libs/js/tools/citation-metadata.js +89 -0
- package/mevebu/libs/js/tools/citation-number-check.js +107 -0
- package/mevebu/libs/js/tools/color-palette.js +110 -0
- package/mevebu/libs/js/tools/color-picker.js +93 -0
- package/mevebu/libs/js/tools/commit-message.js +62 -0
- package/mevebu/libs/js/tools/content-calendar.js +111 -0
- package/mevebu/libs/js/tools/contrast-checker.js +93 -0
- package/mevebu/libs/js/tools/countdown-timer.js +174 -0
- package/mevebu/libs/js/tools/cron-builder.js +91 -0
- package/mevebu/libs/js/tools/css-clamp.js +66 -0
- package/mevebu/libs/js/tools/css-formatter.js +84 -0
- package/mevebu/libs/js/tools/css-grid.js +75 -0
- package/mevebu/libs/js/tools/css-minifier.js +98 -0
- package/mevebu/libs/js/tools/csv-to-json.js +114 -0
- package/mevebu/libs/js/tools/csv-viewer.js +132 -0
- package/mevebu/libs/js/tools/currency-format.js +69 -0
- package/mevebu/libs/js/tools/daily-planner.js +124 -0
- package/mevebu/libs/js/tools/data-masker.js +71 -0
- package/mevebu/libs/js/tools/data-storage.js +89 -0
- package/mevebu/libs/js/tools/data-validator.js +111 -0
- package/mevebu/libs/js/tools/date-difference.js +100 -0
- package/mevebu/libs/js/tools/date-format.js +107 -0
- package/mevebu/libs/js/tools/delimiter-converter.js +109 -0
- package/mevebu/libs/js/tools/discount-calculator.js +83 -0
- package/mevebu/libs/js/tools/doi-resolver.js +67 -0
- package/mevebu/libs/js/tools/env-formatter.js +72 -0
- package/mevebu/libs/js/tools/equation-formatter.js +71 -0
- package/mevebu/libs/js/tools/expense-split.js +90 -0
- package/mevebu/libs/js/tools/favicon-markup.js +53 -0
- package/mevebu/libs/js/tools/find-replace.js +88 -0
- package/mevebu/libs/js/tools/flashcard-creator.js +117 -0
- package/mevebu/libs/js/tools/font-pairing.js +88 -0
- package/mevebu/libs/js/tools/fraction-calculator.js +130 -0
- package/mevebu/libs/js/tools/git-command-builder.js +100 -0
- package/mevebu/libs/js/tools/gitignore-generator.js +60 -0
- package/mevebu/libs/js/tools/gpa-calculator.js +100 -0
- package/mevebu/libs/js/tools/grade-calculator.js +101 -0
- package/mevebu/libs/js/tools/gradient-generator.js +99 -0
- package/mevebu/libs/js/tools/habit-tracker.js +120 -0
- package/mevebu/libs/js/tools/harvard-citation.js +90 -0
- package/mevebu/libs/js/tools/hashtag-formatter.js +62 -0
- package/mevebu/libs/js/tools/heading-formatter.js +61 -0
- package/mevebu/libs/js/tools/hmac-generator.js +63 -0
- package/mevebu/libs/js/tools/hpp-calculator.js +70 -0
- package/mevebu/libs/js/tools/html-entity.js +49 -0
- package/mevebu/libs/js/tools/html-formatter.js +72 -0
- package/mevebu/libs/js/tools/html-to-text.js +47 -0
- package/mevebu/libs/js/tools/http-status.js +63 -0
- package/mevebu/libs/js/tools/ieee-reference.js +102 -0
- package/mevebu/libs/js/tools/intext-citation.js +75 -0
- package/mevebu/libs/js/tools/inventory-quantity.js +103 -0
- package/mevebu/libs/js/tools/invoice-number.js +64 -0
- package/mevebu/libs/js/tools/json-escape.js +76 -0
- package/mevebu/libs/js/tools/json-formatter.js +51 -0
- package/mevebu/libs/js/tools/json-minifier.js +58 -0
- package/mevebu/libs/js/tools/json-to-csv.js +72 -0
- package/mevebu/libs/js/tools/json-to-typescript.js +102 -0
- package/mevebu/libs/js/tools/json-validator.js +49 -0
- package/mevebu/libs/js/tools/jwt-decoder.js +74 -0
- package/mevebu/libs/js/tools/jwt-expiry.js +97 -0
- package/mevebu/libs/js/tools/length-converter.js +86 -0
- package/mevebu/libs/js/tools/license-reference.js +58 -0
- package/mevebu/libs/js/tools/line-break-formatter.js +48 -0
- package/mevebu/libs/js/tools/line-counter.js +55 -0
- package/mevebu/libs/js/tools/list-difference.js +86 -0
- package/mevebu/libs/js/tools/list-merger.js +67 -0
- package/mevebu/libs/js/tools/locale-number.js +79 -0
- package/mevebu/libs/js/tools/markdown-preview.js +143 -0
- package/mevebu/libs/js/tools/markdown-table.js +111 -0
- package/mevebu/libs/js/tools/markdown-to-text.js +48 -0
- package/mevebu/libs/js/tools/markup-calculator.js +69 -0
- package/mevebu/libs/js/tools/meeting-agenda.js +137 -0
- package/mevebu/libs/js/tools/meta-preview.js +51 -0
- package/mevebu/libs/js/tools/meta-tags.js +62 -0
- package/mevebu/libs/js/tools/mime-types.js +54 -0
- package/mevebu/libs/js/tools/mla-citation.js +91 -0
- package/mevebu/libs/js/tools/number-base-converter.js +97 -0
- package/mevebu/libs/js/tools/open-graph.js +62 -0
- package/mevebu/libs/js/tools/password-strength.js +118 -0
- package/mevebu/libs/js/tools/percent-to-fraction.js +96 -0
- package/mevebu/libs/js/tools/percentage-calculator.js +86 -0
- package/mevebu/libs/js/tools/percentage-change.js +70 -0
- package/mevebu/libs/js/tools/permutation-combination.js +106 -0
- package/mevebu/libs/js/tools/prefix-suffix.js +46 -0
- package/mevebu/libs/js/tools/priority-matrix.js +100 -0
- package/mevebu/libs/js/tools/profit-margin.js +69 -0
- package/mevebu/libs/js/tools/quadratic-solver.js +93 -0
- package/mevebu/libs/js/tools/query-builder.js +71 -0
- package/mevebu/libs/js/tools/quiz-formatter.js +70 -0
- package/mevebu/libs/js/tools/quotation-formatter.js +77 -0
- package/mevebu/libs/js/tools/ratio-calculator.js +103 -0
- package/mevebu/libs/js/tools/readability.js +112 -0
- package/mevebu/libs/js/tools/reading-time.js +76 -0
- package/mevebu/libs/js/tools/readme-generator.js +50 -0
- package/mevebu/libs/js/tools/reference-completeness.js +62 -0
- package/mevebu/libs/js/tools/regex-tester.js +86 -0
- package/mevebu/libs/js/tools/remove-duplicate-lines.js +46 -0
- package/mevebu/libs/js/tools/remove-extra-spaces.js +41 -0
- package/mevebu/libs/js/tools/robots-txt.js +60 -0
- package/mevebu/libs/js/tools/roman-numeral-converter.js +106 -0
- package/mevebu/libs/js/tools/sales-commission.js +96 -0
- package/mevebu/libs/js/tools/scientific-calculator.js +247 -0
- package/mevebu/libs/js/tools/scientific-notation.js +83 -0
- package/mevebu/libs/js/tools/script-planner.js +72 -0
- package/mevebu/libs/js/tools/secure-token.js +60 -0
- package/mevebu/libs/js/tools/selling-price.js +74 -0
- package/mevebu/libs/js/tools/semver-helper.js +54 -0
- package/mevebu/libs/js/tools/sentence-length.js +80 -0
- package/mevebu/libs/js/tools/sequence-analyzer.js +90 -0
- package/mevebu/libs/js/tools/sitemap-xml.js +82 -0
- package/mevebu/libs/js/tools/slug-converter.js +48 -0
- package/mevebu/libs/js/tools/slug-preview.js +83 -0
- package/mevebu/libs/js/tools/sort-lines.js +54 -0
- package/mevebu/libs/js/tools/speed-converter.js +83 -0
- package/mevebu/libs/js/tools/sql-formatter.js +78 -0
- package/mevebu/libs/js/tools/sql-minifier.js +102 -0
- package/mevebu/libs/js/tools/standard-deviation.js +96 -0
- package/mevebu/libs/js/tools/stopwatch.js +133 -0
- package/mevebu/libs/js/tools/study-schedule.js +88 -0
- package/mevebu/libs/js/tools/subtitle-formatter.js +74 -0
- package/mevebu/libs/js/tools/table-to-markdown.js +137 -0
- package/mevebu/libs/js/tools/tax-calculator.js +84 -0
- package/mevebu/libs/js/tools/temperature-converter.js +94 -0
- package/mevebu/libs/js/tools/text-diff.js +104 -0
- package/mevebu/libs/js/tools/text-direction.js +55 -0
- package/mevebu/libs/js/tools/text-reverser.js +51 -0
- package/mevebu/libs/js/tools/time-duration.js +83 -0
- package/mevebu/libs/js/tools/time-format-converter.js +86 -0
- package/mevebu/libs/js/tools/timestamp-converter.js +85 -0
- package/mevebu/libs/js/tools/timezone-converter.js +163 -0
- package/mevebu/libs/js/tools/title-length.js +67 -0
- package/mevebu/libs/js/tools/unicode-escape.js +80 -0
- package/mevebu/libs/js/tools/unicode-inspector.js +58 -0
- package/mevebu/libs/js/tools/url-inspector.js +98 -0
- package/mevebu/libs/js/tools/url-parser.js +83 -0
- package/mevebu/libs/js/tools/utf8-bytes.js +67 -0
- package/mevebu/libs/js/tools/version-compare.js +70 -0
- package/mevebu/libs/js/tools/video-duration.js +72 -0
- package/mevebu/libs/js/tools/volume-converter.js +88 -0
- package/mevebu/libs/js/tools/weekly-planner.js +101 -0
- package/mevebu/libs/js/tools/weight-converter.js +85 -0
- package/mevebu/libs/js/tools/weighted-average.js +86 -0
- package/mevebu/libs/js/tools/word-counter.js +90 -0
- package/mevebu/libs/js/tools/word-limit.js +60 -0
- package/mevebu/libs/js/tools/xml-formatter.js +85 -0
- package/mevebu/libs/js/tools/yaml-formatter.js +71 -0
- package/package.json +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// A list you can tick off, with the remaining count kept in view.
|
|
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 = "checklist";
|
|
8
|
+
const saved = readPin(PIN, []);
|
|
9
|
+
let list = Array.isArray(saved) ? saved : [];
|
|
10
|
+
|
|
11
|
+
const entry = textInput({ placeholder: "New item", "aria-label": "New item" });
|
|
12
|
+
const listWrap = el("div", { class: "mvb-scroll" });
|
|
13
|
+
const stats = el("div", { class: "mvb-stats" });
|
|
14
|
+
|
|
15
|
+
const addButton = button("Add", { icon: "plus", kind: "fill" });
|
|
16
|
+
const clearDone = button("Clear finished", { icon: "trash" });
|
|
17
|
+
const copyList = copyButton(() => listText(), "Copy list");
|
|
18
|
+
|
|
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 save = () => writePin(PIN, list);
|
|
24
|
+
|
|
25
|
+
function listText() {
|
|
26
|
+
return list.map((item) => `${item.done ? "[x]" : "[ ]"} ${item.text}`).join("\n");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function itemNode(item) {
|
|
30
|
+
const box = checkbox(item.text, { checked: item.done });
|
|
31
|
+
box.input.addEventListener("change", () => {
|
|
32
|
+
item.done = box.input.checked;
|
|
33
|
+
save();
|
|
34
|
+
renderStats();
|
|
35
|
+
});
|
|
36
|
+
return box;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function renderStats() {
|
|
40
|
+
const done = list.filter((item) => item.done).length;
|
|
41
|
+
stats.replaceChildren(tile("Done", done), tile("Left", list.length - done));
|
|
42
|
+
clearDone.disabled = done === 0;
|
|
43
|
+
copyList.disabled = list.length === 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function renderList() {
|
|
47
|
+
listWrap.replaceChildren(
|
|
48
|
+
list.length ? stack(...list.map(itemNode)) : blank("list-checks", "No items yet.")
|
|
49
|
+
);
|
|
50
|
+
renderStats();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function add() {
|
|
54
|
+
const text = entry.value.trim();
|
|
55
|
+
if (!text) return toast("Type an item first");
|
|
56
|
+
list.push({ id: uid(), text, done: false });
|
|
57
|
+
entry.value = "";
|
|
58
|
+
entry.focus();
|
|
59
|
+
save();
|
|
60
|
+
renderList();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
entry.addEventListener("keydown", (event) => {
|
|
64
|
+
if (event.key === "Enter") add();
|
|
65
|
+
});
|
|
66
|
+
addButton.addEventListener("click", add);
|
|
67
|
+
clearDone.addEventListener("click", () => {
|
|
68
|
+
list = list.filter((item) => !item.done);
|
|
69
|
+
save();
|
|
70
|
+
renderList();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const card = toolcard({
|
|
74
|
+
body: bench(
|
|
75
|
+
stack(panel("New item", entry)),
|
|
76
|
+
stack(stats, panel("Items", listWrap))
|
|
77
|
+
),
|
|
78
|
+
foot: actions(copyList, clearDone, addButton),
|
|
79
|
+
});
|
|
80
|
+
slot.append(card.node);
|
|
81
|
+
renderList();
|
|
82
|
+
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default { mount };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Collects the details of one source in a single form.
|
|
2
|
+
import { bench, stack, panel, textInput, selectInput, output, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const TYPES = [
|
|
5
|
+
{ value: "journal", label: "Journal article" },
|
|
6
|
+
{ value: "book", label: "Book" },
|
|
7
|
+
{ value: "chapter", label: "Chapter" },
|
|
8
|
+
{ value: "conference", label: "Conference paper" },
|
|
9
|
+
{ value: "web", label: "Web page" },
|
|
10
|
+
{ value: "report", label: "Report" },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function mount(slot) {
|
|
14
|
+
const type = selectInput(TYPES, { value: "journal", "aria-label": "Source type" });
|
|
15
|
+
const authors = textInput({ placeholder: "Smith, J. A.; Doe, B.", "aria-label": "Authors" });
|
|
16
|
+
const year = textInput({ placeholder: "2020", "aria-label": "Year" });
|
|
17
|
+
const title = textInput({ placeholder: "Title", "aria-label": "Title" });
|
|
18
|
+
const container = textInput({ placeholder: "Journal, book or site", "aria-label": "Container" });
|
|
19
|
+
const volume = textInput({ placeholder: "12", "aria-label": "Volume" });
|
|
20
|
+
const issue = textInput({ placeholder: "3", "aria-label": "Issue" });
|
|
21
|
+
const pages = textInput({ placeholder: "34-45", "aria-label": "Pages" });
|
|
22
|
+
const publisher = textInput({ placeholder: "Publisher", "aria-label": "Publisher" });
|
|
23
|
+
const doi = textInput({ placeholder: "10.0000/abc", "aria-label": "DOI" });
|
|
24
|
+
const url = textInput({ placeholder: "https://example.com", "aria-label": "URL" });
|
|
25
|
+
|
|
26
|
+
const result = output({ rows: 14 });
|
|
27
|
+
const note = notice("note");
|
|
28
|
+
note.write("Check the details against the source before you cite it.");
|
|
29
|
+
let jsonText = "";
|
|
30
|
+
|
|
31
|
+
function collect() {
|
|
32
|
+
const fields = {
|
|
33
|
+
type: type.value,
|
|
34
|
+
authors: authors.value.trim(),
|
|
35
|
+
year: year.value.trim(),
|
|
36
|
+
title: title.value.trim(),
|
|
37
|
+
container: container.value.trim(),
|
|
38
|
+
volume: volume.value.trim(),
|
|
39
|
+
issue: issue.value.trim(),
|
|
40
|
+
pages: pages.value.trim(),
|
|
41
|
+
publisher: publisher.value.trim(),
|
|
42
|
+
doi: doi.value.trim(),
|
|
43
|
+
url: url.value.trim(),
|
|
44
|
+
};
|
|
45
|
+
return Object.fromEntries(Object.entries(fields).filter(([, value]) => value));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function build() {
|
|
49
|
+
jsonText = JSON.stringify(collect(), null, 2);
|
|
50
|
+
result.write(jsonText);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const plain = () => Object.entries(collect()).map(([key, value]) => `${key}: ${value}`).join("\n");
|
|
54
|
+
|
|
55
|
+
for (const input of [authors, year, title, container, volume, issue, pages, publisher, doi, url]) {
|
|
56
|
+
input.addEventListener("input", build);
|
|
57
|
+
}
|
|
58
|
+
type.addEventListener("change", build);
|
|
59
|
+
|
|
60
|
+
const card = toolcard({
|
|
61
|
+
body: bench(
|
|
62
|
+
stack(
|
|
63
|
+
panel("Source type", type),
|
|
64
|
+
panel("Authors", authors),
|
|
65
|
+
panel("Year", year),
|
|
66
|
+
panel("Title", title),
|
|
67
|
+
panel("Container", container),
|
|
68
|
+
panel("Volume", volume),
|
|
69
|
+
panel("Issue", issue),
|
|
70
|
+
panel("Pages", pages),
|
|
71
|
+
panel("Publisher", publisher),
|
|
72
|
+
panel("DOI", doi),
|
|
73
|
+
panel("URL", url),
|
|
74
|
+
note
|
|
75
|
+
),
|
|
76
|
+
stack(panel("Details", result))
|
|
77
|
+
),
|
|
78
|
+
foot: actions(
|
|
79
|
+
copyButton(plain, "Copy as text"),
|
|
80
|
+
copyButton(() => jsonText, "Copy JSON", "fill")
|
|
81
|
+
),
|
|
82
|
+
});
|
|
83
|
+
slot.append(card.node);
|
|
84
|
+
build();
|
|
85
|
+
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default { mount };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Checks numbered citations against a reference list.
|
|
2
|
+
import { el, bench, stack, panel, textArea, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function tile(label) {
|
|
5
|
+
const value = el("span", { class: "mvb-stat__v" }, "0");
|
|
6
|
+
const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
|
|
7
|
+
node.set = (text) => { value.textContent = text; };
|
|
8
|
+
return node;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function citedNumbers(text) {
|
|
12
|
+
const set = new Set();
|
|
13
|
+
const re = /\[([0-9,\s-]+)\]/g;
|
|
14
|
+
let match;
|
|
15
|
+
while ((match = re.exec(text))) {
|
|
16
|
+
for (const chunk of match[1].split(",")) {
|
|
17
|
+
const trimmed = chunk.trim();
|
|
18
|
+
if (!trimmed) continue;
|
|
19
|
+
const range = trimmed.split(/\s*-\s*/);
|
|
20
|
+
if (range.length === 2) {
|
|
21
|
+
const a = Number(range[0]);
|
|
22
|
+
const b = Number(range[1]);
|
|
23
|
+
if (Number.isInteger(a) && Number.isInteger(b) && b >= a && b - a < 500) {
|
|
24
|
+
for (let i = a; i <= b; i++) set.add(i);
|
|
25
|
+
}
|
|
26
|
+
} else if (Number.isInteger(Number(trimmed))) {
|
|
27
|
+
set.add(Number(trimmed));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return set;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function referenceNumbers(text) {
|
|
35
|
+
const set = new Set();
|
|
36
|
+
for (const line of text.split(/\r?\n/)) {
|
|
37
|
+
const match = line.trim().match(/^\[?(\d+)\]?[.)\]]?/);
|
|
38
|
+
if (match) set.add(Number(match[1]));
|
|
39
|
+
}
|
|
40
|
+
return set;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function mount(slot) {
|
|
44
|
+
const body = textArea({ rows: 10, "aria-label": "Text with citations", placeholder: "The text, with citations like [1]." });
|
|
45
|
+
const list = textArea({ rows: 10, "aria-label": "Reference list", placeholder: "One reference per line" });
|
|
46
|
+
const citedTile = tile("Cited");
|
|
47
|
+
const refsTile = tile("References");
|
|
48
|
+
const missingTile = tile("Missing");
|
|
49
|
+
const unusedTile = tile("Unused");
|
|
50
|
+
const stats = el("div", { class: "mvb-stats" }, citedTile, refsTile, missingTile, unusedTile);
|
|
51
|
+
const tableWrap = el("div", { class: "mvb-scroll" });
|
|
52
|
+
const note = notice("note");
|
|
53
|
+
note.write("A number match does not prove the reference is correct.");
|
|
54
|
+
let report = "";
|
|
55
|
+
|
|
56
|
+
function run() {
|
|
57
|
+
const cited = citedNumbers(body.value);
|
|
58
|
+
const refs = referenceNumbers(list.value);
|
|
59
|
+
const missing = [...cited].filter((n) => !refs.has(n)).sort((a, b) => a - b);
|
|
60
|
+
const unused = [...refs].filter((n) => !cited.has(n)).sort((a, b) => a - b);
|
|
61
|
+
|
|
62
|
+
citedTile.set(String(cited.size));
|
|
63
|
+
refsTile.set(String(refs.size));
|
|
64
|
+
missingTile.set(String(missing.length));
|
|
65
|
+
unusedTile.set(String(unused.length));
|
|
66
|
+
|
|
67
|
+
const rows = [
|
|
68
|
+
...missing.map((n) => [n, "Cited, no reference"]),
|
|
69
|
+
...unused.map((n) => [n, "Reference, not cited"]),
|
|
70
|
+
];
|
|
71
|
+
if (!body.value.trim() && !list.value.trim()) {
|
|
72
|
+
tableWrap.replaceChildren(el("p", { class: "mvb-note" }, "Paste the text and the reference list."));
|
|
73
|
+
} else if (!rows.length) {
|
|
74
|
+
tableWrap.replaceChildren(el("p", { class: "mvb-note" }, "Every number matches."));
|
|
75
|
+
} else {
|
|
76
|
+
tableWrap.replaceChildren(el("table", { class: "mvb-ttable" },
|
|
77
|
+
el("thead", {}, el("tr", {}, el("th", {}, "Number"), el("th", {}, "Problem"))),
|
|
78
|
+
el("tbody", {}, ...rows.map((row) => el("tr", {},
|
|
79
|
+
el("th", {}, String(row[0])),
|
|
80
|
+
el("td", {}, row[1])
|
|
81
|
+
)))
|
|
82
|
+
));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
report = [
|
|
86
|
+
...missing.map((n) => `${n}: cited, no reference`),
|
|
87
|
+
...unused.map((n) => `${n}: reference, not cited`),
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
body.addEventListener("input", run);
|
|
92
|
+
list.addEventListener("input", run);
|
|
93
|
+
|
|
94
|
+
const card = toolcard({
|
|
95
|
+
body: bench(
|
|
96
|
+
stack(panel("Text with citations", body), panel("Reference list", list), note),
|
|
97
|
+
stack(stats, panel("Problems", tableWrap))
|
|
98
|
+
),
|
|
99
|
+
foot: actions(copyButton(() => report, "Copy problems", "fill")),
|
|
100
|
+
});
|
|
101
|
+
slot.append(card.node);
|
|
102
|
+
run();
|
|
103
|
+
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default { mount };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// A matching palette from one base color: analogous, complementary or monochrome.
|
|
2
|
+
import { el, toast, copyText, bench, stack, panel, chipRow, colorInput, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function hexToRgb(hex) {
|
|
5
|
+
const clean = String(hex).trim().replace(/^#/, "");
|
|
6
|
+
let value;
|
|
7
|
+
if (/^[0-9a-f]{3}$/i.test(clean)) value = parseInt(clean.split("").map((c) => c + c).join(""), 16);
|
|
8
|
+
else if (/^[0-9a-f]{6}$/i.test(clean)) value = parseInt(clean, 16);
|
|
9
|
+
else return null;
|
|
10
|
+
return { r: (value >> 16) & 255, g: (value >> 8) & 255, b: value & 255 };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const clamp = (n) => Math.max(0, Math.min(255, Math.round(n)));
|
|
14
|
+
const toHex = ({ r, g, b }) => `#${[r, g, b].map((v) => clamp(v).toString(16).padStart(2, "0")).join("")}`;
|
|
15
|
+
|
|
16
|
+
function toHsl({ r, g, b }) {
|
|
17
|
+
const rn = r / 255;
|
|
18
|
+
const gn = g / 255;
|
|
19
|
+
const bn = b / 255;
|
|
20
|
+
const max = Math.max(rn, gn, bn);
|
|
21
|
+
const min = Math.min(rn, gn, bn);
|
|
22
|
+
const l = (max + min) / 2;
|
|
23
|
+
if (max === min) return { h: 0, s: 0, l };
|
|
24
|
+
const d = max - min;
|
|
25
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
26
|
+
let h;
|
|
27
|
+
if (max === rn) h = (gn - bn) / d + (gn < bn ? 6 : 0);
|
|
28
|
+
else if (max === gn) h = (bn - rn) / d + 2;
|
|
29
|
+
else h = (rn - gn) / d + 4;
|
|
30
|
+
return { h: h * 60, s, l };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hslToRgb(h, s, l) {
|
|
34
|
+
const hue = ((h % 360) + 360) % 360;
|
|
35
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
36
|
+
const x = c * (1 - Math.abs(((hue / 60) % 2) - 1));
|
|
37
|
+
const m = l - c / 2;
|
|
38
|
+
let r;
|
|
39
|
+
let g;
|
|
40
|
+
let b;
|
|
41
|
+
if (hue < 60) [r, g, b] = [c, x, 0];
|
|
42
|
+
else if (hue < 120) [r, g, b] = [x, c, 0];
|
|
43
|
+
else if (hue < 180) [r, g, b] = [0, c, x];
|
|
44
|
+
else if (hue < 240) [r, g, b] = [0, x, c];
|
|
45
|
+
else if (hue < 300) [r, g, b] = [x, 0, c];
|
|
46
|
+
else [r, g, b] = [c, 0, x];
|
|
47
|
+
return { r: Math.round((r + m) * 255), g: Math.round((g + m) * 255), b: Math.round((b + m) * 255) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const hslToHex = (h, s, l) => toHex(hslToRgb(h, s / 100, Math.max(0.06, Math.min(0.94, l / 100))));
|
|
51
|
+
|
|
52
|
+
function palette(hex, scheme) {
|
|
53
|
+
const { h, s, l } = toHsl(hexToRgb(hex));
|
|
54
|
+
const mix = (dh, dl) => hslToHex(h + dh, s * 100, l * 100 + dl);
|
|
55
|
+
if (scheme === "monochrome") return [-30, -15, 0, 15, 30].map((d) => mix(0, d));
|
|
56
|
+
if (scheme === "complementary") return [mix(0, 0), mix(180, 0), mix(0, -22), mix(180, -22), mix(0, 22)];
|
|
57
|
+
return [-40, -20, 0, 20, 40].map((d) => mix(d, 0));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function mount(slot) {
|
|
61
|
+
const picker = colorInput({ value: "#f5b942", "aria-label": "Base color" });
|
|
62
|
+
const swatches = el("div", { class: "mvb-swatches" });
|
|
63
|
+
let colors = [];
|
|
64
|
+
let scheme = "analogous";
|
|
65
|
+
|
|
66
|
+
const chips = chipRow(
|
|
67
|
+
[
|
|
68
|
+
{ value: "analogous", label: "Analogous" },
|
|
69
|
+
{ value: "complementary", label: "Complementary" },
|
|
70
|
+
{ value: "monochrome", label: "Monochrome" },
|
|
71
|
+
],
|
|
72
|
+
{ value: scheme, label: "Scheme", onChange: (value) => { scheme = value; paint(); } }
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
async function copyOne(hex) {
|
|
76
|
+
toast((await copyText(hex)) ? "Copied" : "Copy failed");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function paint() {
|
|
80
|
+
colors = palette(picker.value, scheme);
|
|
81
|
+
swatches.replaceChildren(
|
|
82
|
+
...colors.map((hex) =>
|
|
83
|
+
el(
|
|
84
|
+
"button",
|
|
85
|
+
{ class: "mvb-swatchcard", type: "button", onclick: () => copyOne(hex) },
|
|
86
|
+
el("span", { class: "mvb-swatchcard__chip", style: `background:${hex}` }),
|
|
87
|
+
el("span", { class: "mvb-swatchcard__code" }, hex)
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
picker.addEventListener("input", paint);
|
|
94
|
+
|
|
95
|
+
const card = toolcard({
|
|
96
|
+
body: bench(
|
|
97
|
+
stack(panel("Base color", picker), panel("Scheme", chips)),
|
|
98
|
+
stack(swatches)
|
|
99
|
+
),
|
|
100
|
+
foot: actions(
|
|
101
|
+
copyButton(() => colors.map((hex, i) => `--color-${i + 1}: ${hex};`).join("\n"), "Copy CSS variables", "fill")
|
|
102
|
+
),
|
|
103
|
+
});
|
|
104
|
+
slot.append(card.node);
|
|
105
|
+
paint();
|
|
106
|
+
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default { mount };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Pick a color and read its hex, rgb and hsl codes.
|
|
2
|
+
import { el, toast, copyText, bench, stack, panel, row, textInput, colorInput, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function hexToRgb(hex) {
|
|
5
|
+
const clean = String(hex).trim().replace(/^#/, "");
|
|
6
|
+
let value;
|
|
7
|
+
if (/^[0-9a-f]{3}$/i.test(clean)) value = parseInt(clean.split("").map((c) => c + c).join(""), 16);
|
|
8
|
+
else if (/^[0-9a-f]{6}$/i.test(clean)) value = parseInt(clean, 16);
|
|
9
|
+
else return null;
|
|
10
|
+
return { r: (value >> 16) & 255, g: (value >> 8) & 255, b: value & 255 };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const clamp = (n) => Math.max(0, Math.min(255, Math.round(n)));
|
|
14
|
+
const toHex = ({ r, g, b }) => `#${[r, g, b].map((v) => clamp(v).toString(16).padStart(2, "0")).join("")}`;
|
|
15
|
+
|
|
16
|
+
function toHsl({ r, g, b }) {
|
|
17
|
+
const rn = r / 255;
|
|
18
|
+
const gn = g / 255;
|
|
19
|
+
const bn = b / 255;
|
|
20
|
+
const max = Math.max(rn, gn, bn);
|
|
21
|
+
const min = Math.min(rn, gn, bn);
|
|
22
|
+
const l = (max + min) / 2;
|
|
23
|
+
if (max === min) return { h: 0, s: 0, l: Math.round(l * 100) };
|
|
24
|
+
const d = max - min;
|
|
25
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
26
|
+
let h;
|
|
27
|
+
if (max === rn) h = (gn - bn) / d + (gn < bn ? 6 : 0);
|
|
28
|
+
else if (max === gn) h = (bn - rn) / d + 2;
|
|
29
|
+
else h = (rn - gn) / d + 4;
|
|
30
|
+
return { h: Math.round(h * 60), s: Math.round(s * 100), l: Math.round(l * 100) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function mount(slot) {
|
|
34
|
+
const picker = colorInput({ value: "#f5b942", "aria-label": "Pick a color" });
|
|
35
|
+
const hexInput = textInput({ value: "#F5B942", class: "mvb-in mvb-in--mono", "aria-label": "Hex value" });
|
|
36
|
+
const swatch = el("div", { class: "mvb-swatch" });
|
|
37
|
+
const error = notice("error");
|
|
38
|
+
let current = "#f5b942";
|
|
39
|
+
|
|
40
|
+
const readonly = (label) => {
|
|
41
|
+
const field = textInput({ readonly: true, class: "mvb-in mvb-in--mono", "aria-label": `${label}, press to copy` });
|
|
42
|
+
const wrap = panel(label, field);
|
|
43
|
+
wrap.style.cursor = "pointer";
|
|
44
|
+
wrap.addEventListener("click", async () => {
|
|
45
|
+
if (!field.value) return;
|
|
46
|
+
field.select();
|
|
47
|
+
toast((await copyText(field.value)) ? "Copied" : "Copy failed");
|
|
48
|
+
});
|
|
49
|
+
return wrap;
|
|
50
|
+
};
|
|
51
|
+
const hexField = readonly("Hex");
|
|
52
|
+
const rgbField = readonly("RGB");
|
|
53
|
+
const hslField = readonly("HSL");
|
|
54
|
+
|
|
55
|
+
function show(text, fromPicker) {
|
|
56
|
+
const rgb = hexToRgb(text);
|
|
57
|
+
if (!rgb) return false;
|
|
58
|
+
current = toHex(rgb);
|
|
59
|
+
const { h, s, l } = toHsl(rgb);
|
|
60
|
+
hexField.querySelector("input").value = current.toUpperCase();
|
|
61
|
+
rgbField.querySelector("input").value = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
|
|
62
|
+
hslField.querySelector("input").value = `hsl(${h}, ${s}%, ${l}%)`;
|
|
63
|
+
swatch.style.background = current;
|
|
64
|
+
if (fromPicker) hexInput.value = current.toUpperCase();
|
|
65
|
+
else picker.value = current;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function read(text, fromPicker) {
|
|
70
|
+
if (show(text, fromPicker)) error.write("");
|
|
71
|
+
else error.write("That color was not recognized.");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
hexInput.addEventListener("input", () => read(hexInput.value, false));
|
|
75
|
+
picker.addEventListener("input", () => read(picker.value, true));
|
|
76
|
+
|
|
77
|
+
const card = toolcard({
|
|
78
|
+
body: bench(
|
|
79
|
+
stack(panel("Color", row(picker, hexInput)), error, panel(null, swatch)),
|
|
80
|
+
stack(hexField, rgbField, hslField)
|
|
81
|
+
),
|
|
82
|
+
foot: actions(
|
|
83
|
+
copyButton(() => `--color: ${current};`, "Copy CSS"),
|
|
84
|
+
copyButton(() => current, "Copy hex", "fill")
|
|
85
|
+
),
|
|
86
|
+
});
|
|
87
|
+
slot.append(card.node);
|
|
88
|
+
read("#F5B942", false);
|
|
89
|
+
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default { mount };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Builds a Conventional Commit message and checks the header length.
|
|
2
|
+
import { bench, stack, panel, textInput, textArea, selectInput, checkbox, notice, output, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const TYPES = [
|
|
5
|
+
{ value: "feat", label: "feat" },
|
|
6
|
+
{ value: "fix", label: "fix" },
|
|
7
|
+
{ value: "docs", label: "docs" },
|
|
8
|
+
{ value: "style", label: "style" },
|
|
9
|
+
{ value: "refactor", label: "refactor" },
|
|
10
|
+
{ value: "perf", label: "perf" },
|
|
11
|
+
{ value: "test", label: "test" },
|
|
12
|
+
{ value: "build", label: "build" },
|
|
13
|
+
{ value: "ci", label: "ci" },
|
|
14
|
+
{ value: "chore", label: "chore" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function mount(slot) {
|
|
18
|
+
const type = selectInput(TYPES, { value: "feat", "aria-label": "Type" });
|
|
19
|
+
const scope = textInput({ placeholder: "core", "aria-label": "Scope" });
|
|
20
|
+
const description = textInput({ value: "add dark mode", "aria-label": "Description" });
|
|
21
|
+
const body = textArea({ rows: 3, "aria-label": "Body" });
|
|
22
|
+
const breaking = checkbox("Breaking change");
|
|
23
|
+
const check = notice("warn");
|
|
24
|
+
const out = output({ rows: 8 });
|
|
25
|
+
let text = "";
|
|
26
|
+
|
|
27
|
+
function run() {
|
|
28
|
+
const desc = description.value.trim();
|
|
29
|
+
const scoped = scope.value.trim();
|
|
30
|
+
const header = `${type.value}${scoped ? `(${scoped})` : ""}${breaking.input.checked ? "!" : ""}: ${desc}`;
|
|
31
|
+
const lines = [header];
|
|
32
|
+
if (body.value.trim()) lines.push("", body.value.trim());
|
|
33
|
+
if (breaking.input.checked) lines.push("", `BREAKING CHANGE: ${desc || "describe the change"}`);
|
|
34
|
+
text = lines.join("\n");
|
|
35
|
+
out.write(text);
|
|
36
|
+
|
|
37
|
+
if (!desc) check.write("Add a short description.");
|
|
38
|
+
else if (header.length > 72) check.write(`Header is ${header.length} characters, keep it at 72 or less.`);
|
|
39
|
+
else if (/\.$/.test(desc)) check.write("Drop the trailing period.");
|
|
40
|
+
else check.write("");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type.addEventListener("change", run);
|
|
44
|
+
scope.addEventListener("input", run);
|
|
45
|
+
description.addEventListener("input", run);
|
|
46
|
+
body.addEventListener("input", run);
|
|
47
|
+
breaking.input.addEventListener("change", run);
|
|
48
|
+
|
|
49
|
+
const card = toolcard({
|
|
50
|
+
body: bench(
|
|
51
|
+
stack(panel("Type", type), panel("Scope", scope), panel("Description", description), panel("Body", body), breaking, check),
|
|
52
|
+
stack(panel("Message", out))
|
|
53
|
+
),
|
|
54
|
+
foot: actions(copyButton(() => text, "Copy message", "fill")),
|
|
55
|
+
});
|
|
56
|
+
slot.append(card.node);
|
|
57
|
+
run();
|
|
58
|
+
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default { mount };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Posts on a calendar, kept in local storage, exportable as CSV or Markdown.
|
|
2
|
+
import { el, toast, blank, bench, stack, row, panel, textInput, chipRow, button, 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
|
+
const STATUS = ["Idea", "Draft", "Scheduled", "Published"];
|
|
7
|
+
|
|
8
|
+
export function mount(slot) {
|
|
9
|
+
const PIN = "content-calendar";
|
|
10
|
+
const saved = readPin(PIN, { entries: [] });
|
|
11
|
+
let entries = Array.isArray(saved.entries) ? saved.entries : [];
|
|
12
|
+
|
|
13
|
+
const date = textInput({ type: "date", "aria-label": "Date" });
|
|
14
|
+
const topic = textInput({ placeholder: "Topic", "aria-label": "Topic" });
|
|
15
|
+
const platform = textInput({ placeholder: "Platform", "aria-label": "Platform" });
|
|
16
|
+
let status = "Idea";
|
|
17
|
+
const statusChips = chipRow(
|
|
18
|
+
STATUS.map((value) => ({ value, label: value })),
|
|
19
|
+
{ value: "Idea", label: "Status", onChange: (next) => { status = next; } }
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const listWrap = el("div", { class: "mvb-scroll" });
|
|
23
|
+
const addButton = button("Add", { icon: "plus", kind: "fill" });
|
|
24
|
+
|
|
25
|
+
const save = () => writePin(PIN, { entries });
|
|
26
|
+
|
|
27
|
+
function removeButton(entry) {
|
|
28
|
+
const node = button("", { icon: "trash", "aria-label": `Remove ${entry.topic}` });
|
|
29
|
+
node.addEventListener("click", () => {
|
|
30
|
+
entries = entries.filter((item) => item.id !== entry.id);
|
|
31
|
+
save();
|
|
32
|
+
render();
|
|
33
|
+
});
|
|
34
|
+
return node;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function render() {
|
|
38
|
+
if (!entries.length) {
|
|
39
|
+
listWrap.replaceChildren(blank("calendar-days", "No posts planned yet."));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const sorted = entries.slice().sort((a, b) => a.date.localeCompare(b.date));
|
|
43
|
+
const rows = sorted.map((entry) => el("tr", {},
|
|
44
|
+
el("th", {}, entry.date || "-"),
|
|
45
|
+
el("td", {}, entry.topic),
|
|
46
|
+
el("td", {}, entry.platform || ""),
|
|
47
|
+
el("td", {}, entry.status),
|
|
48
|
+
el("td", {}, removeButton(entry))));
|
|
49
|
+
listWrap.replaceChildren(el("table", { class: "mvb-ttable" },
|
|
50
|
+
el("thead", {}, el("tr", {},
|
|
51
|
+
el("th", {}, "Date"), el("th", {}, "Topic"), el("th", {}, "Platform"), el("th", {}, "Status"), el("th", {}, ""))),
|
|
52
|
+
el("tbody", {}, ...rows)));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function add() {
|
|
56
|
+
const when = date.value.trim();
|
|
57
|
+
const text = topic.value.trim();
|
|
58
|
+
if (!when) return toast("Pick a date");
|
|
59
|
+
if (!text) return toast("Add a topic");
|
|
60
|
+
entries.push({ id: uid(), date: when, topic: text, platform: platform.value.trim(), status });
|
|
61
|
+
topic.value = "";
|
|
62
|
+
platform.value = "";
|
|
63
|
+
topic.focus();
|
|
64
|
+
save();
|
|
65
|
+
render();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
addButton.addEventListener("click", add);
|
|
69
|
+
|
|
70
|
+
function csv() {
|
|
71
|
+
const rows = [["Date", "Topic", "Platform", "Status"]];
|
|
72
|
+
for (const entry of entries) rows.push([entry.date, entry.topic, entry.platform, entry.status]);
|
|
73
|
+
return rows.map((cells) => cells.map((cell) => `"${String(cell).replaceAll('"', '""')}"`).join(",")).join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function markdown() {
|
|
77
|
+
const sorted = entries.slice().sort((a, b) => a.date.localeCompare(b.date));
|
|
78
|
+
const lines = ["# Content calendar", ""];
|
|
79
|
+
let current = null;
|
|
80
|
+
for (const entry of sorted) {
|
|
81
|
+
if (entry.date !== current) {
|
|
82
|
+
current = entry.date;
|
|
83
|
+
lines.push(`## ${entry.date}`);
|
|
84
|
+
}
|
|
85
|
+
lines.push(`- ${entry.topic}${entry.platform ? ` (${entry.platform})` : ""} - ${entry.status}`);
|
|
86
|
+
}
|
|
87
|
+
return lines.join("\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const card = toolcard({
|
|
91
|
+
body: bench(
|
|
92
|
+
stack(
|
|
93
|
+
panel("Date", date),
|
|
94
|
+
panel("Topic", topic),
|
|
95
|
+
row(panel("Platform", platform), panel("Status", statusChips))
|
|
96
|
+
),
|
|
97
|
+
stack(panel("Plan", listWrap))
|
|
98
|
+
),
|
|
99
|
+
foot: actions(
|
|
100
|
+
downloadButton(() => csv(), "content-calendar.csv", "Save CSV", "text/csv"),
|
|
101
|
+
downloadButton(() => markdown(), "content-calendar.md", "Save Markdown", "text/markdown"),
|
|
102
|
+
addButton
|
|
103
|
+
),
|
|
104
|
+
});
|
|
105
|
+
slot.append(card.node);
|
|
106
|
+
render();
|
|
107
|
+
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default { mount };
|