mevebu 0.3.1 → 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.
- 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,100 @@
|
|
|
1
|
+
// Builds a common git command from a few fields and marks the destructive ones.
|
|
2
|
+
import { el, bench, stack, panel, textInput, selectInput, notice, output, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const quote = (value) => (/^[A-Za-z0-9._/@:~^+=-]+$/.test(value) ? value : JSON.stringify(value));
|
|
5
|
+
|
|
6
|
+
const OPERATIONS = {
|
|
7
|
+
clone: {
|
|
8
|
+
label: "Clone a repository",
|
|
9
|
+
fields: [["url", "Repository URL", "https://github.com/user/repo.git"], ["directory", "Directory (optional)", ""]],
|
|
10
|
+
build: (v) => `git clone ${quote(v.url)}${v.directory ? ` ${quote(v.directory)}` : ""}`,
|
|
11
|
+
},
|
|
12
|
+
switch: {
|
|
13
|
+
label: "Switch branch",
|
|
14
|
+
fields: [["branch", "Branch", "main"]],
|
|
15
|
+
build: (v) => `git switch ${quote(v.branch)}`,
|
|
16
|
+
},
|
|
17
|
+
pull: {
|
|
18
|
+
label: "Pull",
|
|
19
|
+
fields: [["remote", "Remote", "origin"], ["branch", "Branch (optional)", ""]],
|
|
20
|
+
build: (v) => `git pull ${quote(v.remote)}${v.branch ? ` ${quote(v.branch)}` : ""}`,
|
|
21
|
+
},
|
|
22
|
+
commit: {
|
|
23
|
+
label: "Commit",
|
|
24
|
+
fields: [["message", "Message", "Fix the thing"], ["files", "Paths (optional)", ""]],
|
|
25
|
+
build: (v) => `git commit -m ${quote(v.message)}${v.files ? ` ${v.files}` : ""}`,
|
|
26
|
+
},
|
|
27
|
+
push: {
|
|
28
|
+
label: "Push",
|
|
29
|
+
fields: [["remote", "Remote", "origin"], ["branch", "Branch", "main"]],
|
|
30
|
+
build: (v) => `git push ${quote(v.remote)} ${quote(v.branch)}`,
|
|
31
|
+
},
|
|
32
|
+
"push-force": {
|
|
33
|
+
label: "Force push",
|
|
34
|
+
fields: [["remote", "Remote", "origin"], ["branch", "Branch", "main"]],
|
|
35
|
+
build: (v) => `git push --force-with-lease ${quote(v.remote)} ${quote(v.branch)}`,
|
|
36
|
+
destructive: "Force push can overwrite commits on the remote.",
|
|
37
|
+
},
|
|
38
|
+
merge: {
|
|
39
|
+
label: "Merge",
|
|
40
|
+
fields: [["branch", "Branch", "feature"]],
|
|
41
|
+
build: (v) => `git merge ${quote(v.branch)}`,
|
|
42
|
+
},
|
|
43
|
+
rebase: {
|
|
44
|
+
label: "Rebase",
|
|
45
|
+
fields: [["branch", "Branch", "main"]],
|
|
46
|
+
build: (v) => `git rebase ${quote(v.branch)}`,
|
|
47
|
+
},
|
|
48
|
+
reset: {
|
|
49
|
+
label: "Reset hard",
|
|
50
|
+
fields: [["ref", "Commit or ref", "HEAD~1"]],
|
|
51
|
+
build: (v) => `git reset --hard ${quote(v.ref)}`,
|
|
52
|
+
destructive: "Reset hard discards uncommitted work.",
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function mount(slot) {
|
|
57
|
+
const options = Object.entries(OPERATIONS).map(([value, config]) => ({ value, label: config.label }));
|
|
58
|
+
const operationSelect = selectInput(options, { value: "clone", "aria-label": "Operation" });
|
|
59
|
+
const fieldsHost = el("div", { class: "mvb-stack" });
|
|
60
|
+
const result = output({ rows: 4 });
|
|
61
|
+
const warn = notice("warn");
|
|
62
|
+
const values = {};
|
|
63
|
+
let inputs = [];
|
|
64
|
+
|
|
65
|
+
function paintFields() {
|
|
66
|
+
const config = OPERATIONS[operationSelect.value];
|
|
67
|
+
fieldsHost.replaceChildren();
|
|
68
|
+
inputs = [];
|
|
69
|
+
for (const [key, label, placeholder] of config.fields) {
|
|
70
|
+
if (!(key in values)) values[key] = "";
|
|
71
|
+
const input = textInput({ value: values[key], placeholder, "aria-label": label, spellcheck: "false" });
|
|
72
|
+
input.addEventListener("input", () => { values[key] = input.value; run(); });
|
|
73
|
+
inputs.push(input);
|
|
74
|
+
fieldsHost.append(panel(label, input));
|
|
75
|
+
}
|
|
76
|
+
run();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function run() {
|
|
80
|
+
const config = OPERATIONS[operationSelect.value];
|
|
81
|
+
warn.write(config.destructive ?? "");
|
|
82
|
+
result.write(config.build(values));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
operationSelect.addEventListener("change", paintFields);
|
|
86
|
+
|
|
87
|
+
const card = toolcard({
|
|
88
|
+
body: bench(
|
|
89
|
+
stack(panel("Operation", operationSelect), fieldsHost, warn),
|
|
90
|
+
stack(panel("Command", result))
|
|
91
|
+
),
|
|
92
|
+
foot: actions(copyButton(() => result.textContent, "Copy command", "fill")),
|
|
93
|
+
});
|
|
94
|
+
slot.append(card.node);
|
|
95
|
+
paintFields();
|
|
96
|
+
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default { mount };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Merges .gitignore templates for the stacks you pick.
|
|
2
|
+
import { bench, stack, panel, toggleRow, output, notice, copyButton, downloadButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const TEMPLATES = {
|
|
5
|
+
node: { label: "Node", lines: ["node_modules/", "npm-debug.log*", "yarn-debug.log*", "yarn-error.log*", ".pnpm-store/"] },
|
|
6
|
+
python: { label: "Python", lines: ["__pycache__/", "*.py[cod]", ".venv/", "venv/", "*.egg-info/", ".pytest_cache/"] },
|
|
7
|
+
laravel: { label: "Laravel", lines: ["/vendor/", "/node_modules/", "/public/build/", "/public/hot", "/storage/*.key", ".env", ".env.backup"] },
|
|
8
|
+
react: { label: "React", lines: ["/build/", "/dist/", ".env.local", ".env.development.local", ".env.production.local"] },
|
|
9
|
+
macos: { label: "macOS", lines: [".DS_Store", ".AppleDouble", ".LSConflict"] },
|
|
10
|
+
windows: { label: "Windows", lines: ["Thumbs.db", "ehthumbs.db", "Desktop.ini", "$RECYCLE.BIN/"] },
|
|
11
|
+
editor: { label: "Editor", lines: [".vscode/*", "!.vscode/extensions.json", ".idea/", "*.swp", "*.swo"] },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function mount(slot) {
|
|
15
|
+
const stacks = toggleRow(Object.entries(TEMPLATES).map(([value, template]) => ({ value, label: template.label })));
|
|
16
|
+
const out = output({ rows: 18 });
|
|
17
|
+
const error = notice("error");
|
|
18
|
+
let text = "";
|
|
19
|
+
|
|
20
|
+
function run() {
|
|
21
|
+
const picked = stacks.values();
|
|
22
|
+
if (!picked.length) {
|
|
23
|
+
text = "";
|
|
24
|
+
out.write("");
|
|
25
|
+
error.write("Pick at least one stack.");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
error.write("");
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
const lines = [];
|
|
31
|
+
for (const key of picked) {
|
|
32
|
+
const template = TEMPLATES[key];
|
|
33
|
+
lines.push(`# ${template.label}`);
|
|
34
|
+
for (const line of template.lines) {
|
|
35
|
+
if (seen.has(line)) continue;
|
|
36
|
+
seen.add(line);
|
|
37
|
+
lines.push(line);
|
|
38
|
+
}
|
|
39
|
+
lines.push("");
|
|
40
|
+
}
|
|
41
|
+
text = `${lines.join("\n").replace(/\n+$/, "")}\n`;
|
|
42
|
+
out.write(text);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
stacks.addEventListener("change", run);
|
|
46
|
+
|
|
47
|
+
const card = toolcard({
|
|
48
|
+
body: bench(
|
|
49
|
+
stack(panel("Stacks", stacks), error),
|
|
50
|
+
stack(panel(".gitignore", out))
|
|
51
|
+
),
|
|
52
|
+
foot: actions(downloadButton(() => text, ".gitignore"), copyButton(() => text, "Copy", "fill")),
|
|
53
|
+
});
|
|
54
|
+
slot.append(card.node);
|
|
55
|
+
run();
|
|
56
|
+
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default { mount };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Credit weighted GPA from courses, on a grade scale you pick.
|
|
2
|
+
import { el, toast, blank, bench, stack, row, panel, textInput, numberInput, selectInput, 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
|
+
const SCALE = [
|
|
7
|
+
["A+", 4.0], ["A", 4.0], ["A-", 3.7],
|
|
8
|
+
["B+", 3.3], ["B", 3.0], ["B-", 2.7],
|
|
9
|
+
["C+", 2.3], ["C", 2.0], ["C-", 1.7],
|
|
10
|
+
["D+", 1.3], ["D", 1.0], ["F", 0.0],
|
|
11
|
+
];
|
|
12
|
+
const POINTS = new Map(SCALE);
|
|
13
|
+
|
|
14
|
+
function fmt(n) {
|
|
15
|
+
return n.toLocaleString(undefined, { maximumFractionDigits: 3 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function mount(slot) {
|
|
19
|
+
const PIN = "gpa-calculator";
|
|
20
|
+
const saved = readPin(PIN, []);
|
|
21
|
+
let list = Array.isArray(saved) ? saved : [];
|
|
22
|
+
|
|
23
|
+
const name = textInput({ placeholder: "Course", "aria-label": "Course" });
|
|
24
|
+
const credits = numberInput({ min: "0", step: "0.5", placeholder: "Credits", "aria-label": "Credits" });
|
|
25
|
+
const grade = selectInput(SCALE.map(([label]) => ({ value: label, label })), { value: "A", "aria-label": "Grade" });
|
|
26
|
+
const listWrap = el("div", { class: "mvb-scroll" });
|
|
27
|
+
const stats = el("div", { class: "mvb-stats" });
|
|
28
|
+
|
|
29
|
+
const addButton = button("Add", { icon: "plus", kind: "fill" });
|
|
30
|
+
|
|
31
|
+
const save = () => writePin(PIN, list);
|
|
32
|
+
const tile = (key, value) => el("div", { class: "mvb-stat" },
|
|
33
|
+
el("span", { class: "mvb-stat__v" }, String(value)),
|
|
34
|
+
el("span", { class: "mvb-stat__k" }, key));
|
|
35
|
+
|
|
36
|
+
const totals = () => {
|
|
37
|
+
const totalCredits = list.reduce((sum, item) => sum + item.credits, 0);
|
|
38
|
+
const points = list.reduce((sum, item) => sum + item.credits * (POINTS.get(item.grade) ?? 0), 0);
|
|
39
|
+
return { totalCredits, gpa: totalCredits ? points / totalCredits : null };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function removeButton(item) {
|
|
43
|
+
const node = button("", { icon: "trash", "aria-label": `Remove ${item.name}` });
|
|
44
|
+
node.addEventListener("click", () => {
|
|
45
|
+
list = list.filter((entry) => entry.id !== item.id);
|
|
46
|
+
save();
|
|
47
|
+
render();
|
|
48
|
+
});
|
|
49
|
+
return node;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function render() {
|
|
53
|
+
if (!list.length) {
|
|
54
|
+
listWrap.replaceChildren(blank("graduation-cap", "No courses yet."));
|
|
55
|
+
} else {
|
|
56
|
+
listWrap.replaceChildren(el("table", { class: "mvb-ttable" },
|
|
57
|
+
el("thead", {}, el("tr", {}, el("th", {}, "Course"), el("th", {}, "Credits"), el("th", {}, "Grade"), el("th", {}, ""))),
|
|
58
|
+
el("tbody", {}, ...list.map((item) => el("tr", {},
|
|
59
|
+
el("th", {}, item.name),
|
|
60
|
+
el("td", {}, fmt(item.credits)),
|
|
61
|
+
el("td", {}, item.grade),
|
|
62
|
+
el("td", {}, removeButton(item)))))));
|
|
63
|
+
}
|
|
64
|
+
const { totalCredits, gpa } = totals();
|
|
65
|
+
stats.replaceChildren(tile("GPA", gpa == null ? "-" : fmt(gpa)), tile("Credits", fmt(totalCredits)));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function add() {
|
|
69
|
+
const text = name.value.trim();
|
|
70
|
+
if (!text) return toast("Name the course first");
|
|
71
|
+
const c = Number(credits.value);
|
|
72
|
+
if (credits.value.trim() === "" || !Number.isFinite(c) || c < 0) return toast("Add the credits");
|
|
73
|
+
list.push({ id: uid(), name: text, credits: c, grade: grade.value });
|
|
74
|
+
name.value = "";
|
|
75
|
+
credits.value = "";
|
|
76
|
+
name.focus();
|
|
77
|
+
save();
|
|
78
|
+
render();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
name.addEventListener("keydown", (event) => { if (event.key === "Enter") add(); });
|
|
82
|
+
addButton.addEventListener("click", add);
|
|
83
|
+
|
|
84
|
+
const card = toolcard({
|
|
85
|
+
body: bench(
|
|
86
|
+
stack(panel("Course", name), row(panel("Credits", credits), panel("Grade", grade))),
|
|
87
|
+
stack(stats, panel("Courses", listWrap))
|
|
88
|
+
),
|
|
89
|
+
foot: actions(copyButton(() => {
|
|
90
|
+
const { totalCredits, gpa } = totals();
|
|
91
|
+
return gpa == null ? "" : `GPA ${fmt(gpa)}, credits ${fmt(totalCredits)}`;
|
|
92
|
+
}, "Copy result"), addButton),
|
|
93
|
+
});
|
|
94
|
+
slot.append(card.node);
|
|
95
|
+
render();
|
|
96
|
+
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default { mount };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Weighted components with a check that the weights add up to 100.
|
|
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 fmt(n) {
|
|
7
|
+
return n.toLocaleString(undefined, { maximumFractionDigits: 2 });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function mount(slot) {
|
|
11
|
+
const PIN = "grade-calculator";
|
|
12
|
+
const saved = readPin(PIN, []);
|
|
13
|
+
let list = Array.isArray(saved) ? saved : [];
|
|
14
|
+
|
|
15
|
+
const name = textInput({ placeholder: "Component", "aria-label": "Component" });
|
|
16
|
+
const score = numberInput({ min: "0", max: "100", placeholder: "Score", "aria-label": "Score" });
|
|
17
|
+
const weight = numberInput({ min: "0", max: "100", placeholder: "Weight", "aria-label": "Weight" });
|
|
18
|
+
const listWrap = el("div", { class: "mvb-scroll" });
|
|
19
|
+
const stats = el("div", { class: "mvb-stats" });
|
|
20
|
+
const weightNote = notice("warn");
|
|
21
|
+
|
|
22
|
+
const addButton = button("Add", { icon: "plus", kind: "fill" });
|
|
23
|
+
|
|
24
|
+
const save = () => writePin(PIN, list);
|
|
25
|
+
const tile = (key, value) => el("div", { class: "mvb-stat" },
|
|
26
|
+
el("span", { class: "mvb-stat__v" }, String(value)),
|
|
27
|
+
el("span", { class: "mvb-stat__k" }, key));
|
|
28
|
+
|
|
29
|
+
const totalWeight = () => list.reduce((sum, item) => sum + item.weight, 0);
|
|
30
|
+
const grade = () => {
|
|
31
|
+
const weightSum = totalWeight();
|
|
32
|
+
if (!weightSum) return null;
|
|
33
|
+
return list.reduce((sum, item) => sum + item.score * item.weight, 0) / weightSum;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function removeButton(item) {
|
|
37
|
+
const node = button("", { icon: "trash", "aria-label": `Remove ${item.name}` });
|
|
38
|
+
node.addEventListener("click", () => {
|
|
39
|
+
list = list.filter((entry) => entry.id !== item.id);
|
|
40
|
+
save();
|
|
41
|
+
render();
|
|
42
|
+
});
|
|
43
|
+
return node;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function render() {
|
|
47
|
+
if (!list.length) {
|
|
48
|
+
listWrap.replaceChildren(blank("graduation-cap", "No components yet."));
|
|
49
|
+
} else {
|
|
50
|
+
listWrap.replaceChildren(el("table", { class: "mvb-ttable" },
|
|
51
|
+
el("thead", {}, el("tr", {}, el("th", {}, "Component"), el("th", {}, "Score"), el("th", {}, "Weight"), el("th", {}, ""))),
|
|
52
|
+
el("tbody", {}, ...list.map((item) => el("tr", {},
|
|
53
|
+
el("th", {}, item.name),
|
|
54
|
+
el("td", {}, fmt(item.score)),
|
|
55
|
+
el("td", {}, `${fmt(item.weight)}%`),
|
|
56
|
+
el("td", {}, removeButton(item)))))));
|
|
57
|
+
}
|
|
58
|
+
const weightSum = totalWeight();
|
|
59
|
+
const result = grade();
|
|
60
|
+
stats.replaceChildren(tile("Grade", result == null ? "-" : fmt(result)), tile("Weight", `${fmt(weightSum)}%`));
|
|
61
|
+
weightNote.write(list.length && Math.abs(weightSum - 100) > 0.001 ? `The weights add up to ${fmt(weightSum)}%, not 100.` : "");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function add() {
|
|
65
|
+
const text = name.value.trim();
|
|
66
|
+
if (!text) return toast("Name the component first");
|
|
67
|
+
const s = Number(score.value);
|
|
68
|
+
const w = Number(weight.value);
|
|
69
|
+
if (score.value.trim() === "" || weight.value.trim() === "" || !Number.isFinite(s) || !Number.isFinite(w)) {
|
|
70
|
+
return toast("Add a score and a weight");
|
|
71
|
+
}
|
|
72
|
+
if (s < 0 || w < 0) return toast("Score and weight cannot be negative");
|
|
73
|
+
list.push({ id: uid(), name: text, score: s, weight: w });
|
|
74
|
+
name.value = "";
|
|
75
|
+
score.value = "";
|
|
76
|
+
weight.value = "";
|
|
77
|
+
name.focus();
|
|
78
|
+
save();
|
|
79
|
+
render();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
name.addEventListener("keydown", (event) => { if (event.key === "Enter") add(); });
|
|
83
|
+
addButton.addEventListener("click", add);
|
|
84
|
+
|
|
85
|
+
const card = toolcard({
|
|
86
|
+
body: bench(
|
|
87
|
+
stack(panel("Component", name), row(panel("Score", score), panel("Weight", weight)), weightNote),
|
|
88
|
+
stack(stats, panel("Components", listWrap))
|
|
89
|
+
),
|
|
90
|
+
foot: actions(copyButton(() => {
|
|
91
|
+
const result = grade();
|
|
92
|
+
return result == null ? "" : `Grade ${fmt(result)}, total weight ${fmt(totalWeight())}%`;
|
|
93
|
+
}, "Copy result"), addButton),
|
|
94
|
+
});
|
|
95
|
+
slot.append(card.node);
|
|
96
|
+
render();
|
|
97
|
+
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default { mount };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Make a CSS gradient: linear or radial, with color stops you control.
|
|
2
|
+
import { el, bench, stack, panel, chipRow, colorInput, rangeInput, output, button, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
export function mount(slot) {
|
|
5
|
+
let type = "linear";
|
|
6
|
+
let angle = 90;
|
|
7
|
+
const stops = [
|
|
8
|
+
{ color: "#f5b942", pos: 0 },
|
|
9
|
+
{ color: "#8a2be2", pos: 100 },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const stopsBox = el("div", { class: "mvb-stack" });
|
|
13
|
+
const previewBox = el("div", { class: "mvb-preview__box" });
|
|
14
|
+
const code = output();
|
|
15
|
+
|
|
16
|
+
function gradientCss() {
|
|
17
|
+
const list = stops.map((stop) => `${stop.color} ${stop.pos}%`).join(", ");
|
|
18
|
+
return type === "linear" ? `linear-gradient(${angle}deg, ${list})` : `radial-gradient(circle, ${list})`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function update() {
|
|
22
|
+
const value = gradientCss();
|
|
23
|
+
previewBox.style.background = value;
|
|
24
|
+
code.write(`background: ${value};`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stopRow(stop, index) {
|
|
28
|
+
const picker = colorInput({ value: stop.color, "aria-label": "Stop color" });
|
|
29
|
+
picker.addEventListener("input", () => { stop.color = picker.value; update(); });
|
|
30
|
+
|
|
31
|
+
const range = rangeInput({ min: 0, max: 100, value: stop.pos, "aria-label": "Stop position" });
|
|
32
|
+
range.style.flex = "1";
|
|
33
|
+
const val = el("span", { class: "mvb-slider__val" }, `${stop.pos}%`);
|
|
34
|
+
range.addEventListener("input", () => {
|
|
35
|
+
stop.pos = parseInt(range.value, 10) || 0;
|
|
36
|
+
val.textContent = `${stop.pos}%`;
|
|
37
|
+
update();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const remove = button("", { icon: "minus", "aria-label": "Remove stop" });
|
|
41
|
+
remove.disabled = stops.length <= 2;
|
|
42
|
+
remove.addEventListener("click", () => {
|
|
43
|
+
stops.splice(index, 1);
|
|
44
|
+
paintStops();
|
|
45
|
+
update();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return el("div", { class: "mvb-row" }, picker, range, val, remove);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function paintStops() {
|
|
52
|
+
stopsBox.replaceChildren(...stops.map((stop, i) => stopRow(stop, i)));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const addStop = button("Add stop", { icon: "plus" });
|
|
56
|
+
addStop.addEventListener("click", () => {
|
|
57
|
+
const last = stops[stops.length - 1];
|
|
58
|
+
const prev = stops[stops.length - 2];
|
|
59
|
+
const pos = Math.round(((prev?.pos ?? 0) + last.pos) / 2);
|
|
60
|
+
stops.push({ color: "#ffffff", pos: Number.isFinite(pos) ? pos : 50 });
|
|
61
|
+
paintStops();
|
|
62
|
+
update();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const angleInput = rangeInput({ min: 0, max: 360, value: angle, "aria-label": "Angle" });
|
|
66
|
+
const angleVal = el("span", { class: "mvb-slider__val" }, `${angle}deg`);
|
|
67
|
+
const angleRow = el(
|
|
68
|
+
"div",
|
|
69
|
+
{ class: "mvb-slider" },
|
|
70
|
+
el("span", { class: "mvb-slider__label" }, "Angle"),
|
|
71
|
+
angleInput,
|
|
72
|
+
angleVal
|
|
73
|
+
);
|
|
74
|
+
angleInput.addEventListener("input", () => {
|
|
75
|
+
angle = parseInt(angleInput.value, 10) || 0;
|
|
76
|
+
angleVal.textContent = `${angle}deg`;
|
|
77
|
+
update();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const typeChips = chipRow(
|
|
81
|
+
[{ value: "linear", label: "Linear" }, { value: "radial", label: "Radial" }],
|
|
82
|
+
{ value: type, label: "Type", onChange: (value) => { type = value; angleRow.hidden = value !== "linear"; update(); } }
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const card = toolcard({
|
|
86
|
+
body: bench(
|
|
87
|
+
stack(panel("Type", typeChips), angleRow, panel("Color stops", stopsBox), addStop),
|
|
88
|
+
stack(el("div", { class: "mvb-preview" }, previewBox), code)
|
|
89
|
+
),
|
|
90
|
+
foot: actions(copyButton(() => `background: ${gradientCss()};`, "Copy CSS", "fill")),
|
|
91
|
+
});
|
|
92
|
+
slot.append(card.node);
|
|
93
|
+
paintStops();
|
|
94
|
+
update();
|
|
95
|
+
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default { mount };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Tracks a week of habits and shows the count and the current streak.
|
|
2
|
+
import { el, toast, bench, stack, panel, textInput, checkbox, button, actions, toolcard, readPin, writePin } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
|
5
|
+
const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
6
|
+
const todayIndex = () => (new Date().getDay() + 6) % 7;
|
|
7
|
+
|
|
8
|
+
export function mount(slot) {
|
|
9
|
+
const PIN = "habit-tracker";
|
|
10
|
+
const saved = readPin(PIN, []);
|
|
11
|
+
let list = Array.isArray(saved) ? saved : [];
|
|
12
|
+
|
|
13
|
+
const name = textInput({ placeholder: "Habit", "aria-label": "Habit" });
|
|
14
|
+
const tableWrap = el("div", { class: "mvb-scroll" });
|
|
15
|
+
const stats = el("div", { class: "mvb-stats" });
|
|
16
|
+
|
|
17
|
+
const addButton = button("Add", { icon: "plus", kind: "fill" });
|
|
18
|
+
const clearWeek = button("Clear week", { icon: "trash" });
|
|
19
|
+
|
|
20
|
+
const tile = (key, value) => el("div", { class: "mvb-stat" },
|
|
21
|
+
el("span", { class: "mvb-stat__v" }, String(value)),
|
|
22
|
+
el("span", { class: "mvb-stat__k" }, key));
|
|
23
|
+
|
|
24
|
+
const save = () => writePin(PIN, list);
|
|
25
|
+
const today = todayIndex();
|
|
26
|
+
|
|
27
|
+
function streak(habit) {
|
|
28
|
+
let i = today;
|
|
29
|
+
if (!habit.days[i]) i -= 1;
|
|
30
|
+
let count = 0;
|
|
31
|
+
while (i >= 0 && habit.days[i]) {
|
|
32
|
+
count++;
|
|
33
|
+
i--;
|
|
34
|
+
}
|
|
35
|
+
return count;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function removeButton(habit) {
|
|
39
|
+
const node = button("", { icon: "trash", "aria-label": `Remove ${habit.name}` });
|
|
40
|
+
node.addEventListener("click", () => {
|
|
41
|
+
list = list.filter((entry) => entry.id !== habit.id);
|
|
42
|
+
save();
|
|
43
|
+
render();
|
|
44
|
+
});
|
|
45
|
+
return node;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function dayCell(habit, i) {
|
|
49
|
+
const box = checkbox("", { checked: habit.days[i], "aria-label": `${habit.name} ${DAYS[i]}` });
|
|
50
|
+
box.input.addEventListener("change", () => {
|
|
51
|
+
habit.days[i] = box.input.checked;
|
|
52
|
+
save();
|
|
53
|
+
render();
|
|
54
|
+
});
|
|
55
|
+
return el("td", {}, box);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function render() {
|
|
59
|
+
if (!list.length) {
|
|
60
|
+
tableWrap.replaceChildren(el("p", { class: "mvb-note" }, "No habits yet."));
|
|
61
|
+
} else {
|
|
62
|
+
const head = el("tr", {},
|
|
63
|
+
el("th", {}, "Habit"),
|
|
64
|
+
...DAYS.map((label) => el("th", {}, label)),
|
|
65
|
+
el("th", {}, "Done"),
|
|
66
|
+
el("th", {}, "Streak"),
|
|
67
|
+
el("th", {}, "")
|
|
68
|
+
);
|
|
69
|
+
const rows = list.map((habit) => el("tr", {},
|
|
70
|
+
el("th", {}, habit.name),
|
|
71
|
+
...habit.days.map((_, i) => dayCell(habit, i)),
|
|
72
|
+
el("td", {}, String(habit.days.filter(Boolean).length)),
|
|
73
|
+
el("td", {}, String(streak(habit))),
|
|
74
|
+
el("td", {}, removeButton(habit))
|
|
75
|
+
));
|
|
76
|
+
tableWrap.replaceChildren(el("table", { class: "mvb-ttable" },
|
|
77
|
+
el("thead", {}, head),
|
|
78
|
+
el("tbody", {}, ...rows)
|
|
79
|
+
));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const doneToday = list.filter((habit) => habit.days[today]).length;
|
|
83
|
+
stats.replaceChildren(tile("Habits", list.length), tile("Done today", doneToday));
|
|
84
|
+
clearWeek.disabled = !list.some((habit) => habit.days.some(Boolean));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function add() {
|
|
88
|
+
const text = name.value.trim();
|
|
89
|
+
if (!text) return toast("Type a habit first");
|
|
90
|
+
list.push({ id: uid(), name: text, days: [false, false, false, false, false, false, false] });
|
|
91
|
+
name.value = "";
|
|
92
|
+
name.focus();
|
|
93
|
+
save();
|
|
94
|
+
render();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
name.addEventListener("keydown", (event) => {
|
|
98
|
+
if (event.key === "Enter") add();
|
|
99
|
+
});
|
|
100
|
+
addButton.addEventListener("click", add);
|
|
101
|
+
clearWeek.addEventListener("click", () => {
|
|
102
|
+
for (const habit of list) habit.days = [false, false, false, false, false, false, false];
|
|
103
|
+
save();
|
|
104
|
+
render();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const card = toolcard({
|
|
108
|
+
body: bench(
|
|
109
|
+
stack(panel("Habit", name)),
|
|
110
|
+
stack(stats, panel("Week", tableWrap))
|
|
111
|
+
),
|
|
112
|
+
foot: actions(clearWeek, addButton),
|
|
113
|
+
});
|
|
114
|
+
slot.append(card.node);
|
|
115
|
+
render();
|
|
116
|
+
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default { mount };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Builds a Harvard style reference and its in-text citation from source details.
|
|
2
|
+
import { bench, stack, panel, textInput, selectInput, output, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const TYPES = [
|
|
5
|
+
{ value: "book", label: "Book" },
|
|
6
|
+
{ value: "article", label: "Journal article" },
|
|
7
|
+
{ value: "web", label: "Web page" },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const parts = (raw) => raw.split(";").map((item) => item.trim()).filter(Boolean);
|
|
11
|
+
const surname = (author) => author.split(",")[0].trim();
|
|
12
|
+
|
|
13
|
+
function authorList(raw) {
|
|
14
|
+
const list = parts(raw);
|
|
15
|
+
if (!list.length) return "";
|
|
16
|
+
if (list.length === 1) return list[0];
|
|
17
|
+
if (list.length === 2) return `${list[0]} and ${list[1]}`;
|
|
18
|
+
return `${list[0]} et al.`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function inText(raw, year) {
|
|
22
|
+
const list = parts(raw);
|
|
23
|
+
const y = year.trim();
|
|
24
|
+
if (!list.length) return y ? `(${y})` : "";
|
|
25
|
+
if (list.length === 1) return `(${surname(list[0])}, ${y})`;
|
|
26
|
+
if (list.length === 2) return `(${surname(list[0])} and ${surname(list[1])}, ${y})`;
|
|
27
|
+
return `(${surname(list[0])} et al., ${y})`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function mount(slot) {
|
|
31
|
+
const type = selectInput(TYPES, { value: "book", "aria-label": "Source type" });
|
|
32
|
+
const authors = textInput({ placeholder: "Smith, J.; Doe, B.", "aria-label": "Authors" });
|
|
33
|
+
const year = textInput({ placeholder: "2020", "aria-label": "Year" });
|
|
34
|
+
const title = textInput({ placeholder: "Title of the work", "aria-label": "Title" });
|
|
35
|
+
const source = textInput({ placeholder: "Publisher, journal or site", "aria-label": "Source" });
|
|
36
|
+
const details = textInput({ placeholder: "Volume, issue, pages", "aria-label": "Details" });
|
|
37
|
+
const link = textInput({ placeholder: "https://example.com", "aria-label": "URL" });
|
|
38
|
+
|
|
39
|
+
const reference = output({ rows: 6 });
|
|
40
|
+
const citation = output({ rows: 2 });
|
|
41
|
+
const note = notice("note");
|
|
42
|
+
note.write("Harvard rules differ between institutions. Check yours and the source.");
|
|
43
|
+
let refText = "";
|
|
44
|
+
|
|
45
|
+
function build() {
|
|
46
|
+
const au = authorList(authors.value);
|
|
47
|
+
const yr = year.value.trim();
|
|
48
|
+
const ti = title.value.trim();
|
|
49
|
+
const src = source.value.trim();
|
|
50
|
+
const det = details.value.trim();
|
|
51
|
+
const url = link.value.trim();
|
|
52
|
+
const head = [au, yr ? `(${yr})` : ""].filter(Boolean).join(" ");
|
|
53
|
+
|
|
54
|
+
const tail = [src, det].filter(Boolean).join(", ");
|
|
55
|
+
const body = type.value === "article" ? [ti ? `'${ti}'` : "", tail, url] : [ti, tail, url];
|
|
56
|
+
refText = [head, ...body].filter(Boolean).join(" ").trim();
|
|
57
|
+
if (refText && !/[.!?]$/.test(refText)) refText += ".";
|
|
58
|
+
reference.write(refText);
|
|
59
|
+
citation.write(inText(authors.value, yr));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const input of [authors, year, title, source, details, link]) input.addEventListener("input", build);
|
|
63
|
+
type.addEventListener("change", build);
|
|
64
|
+
|
|
65
|
+
const card = toolcard({
|
|
66
|
+
body: bench(
|
|
67
|
+
stack(
|
|
68
|
+
panel("Source type", type),
|
|
69
|
+
panel("Authors", authors),
|
|
70
|
+
panel("Year", year),
|
|
71
|
+
panel("Title", title),
|
|
72
|
+
panel("Source", source),
|
|
73
|
+
panel("Details", details),
|
|
74
|
+
panel("URL", link),
|
|
75
|
+
note
|
|
76
|
+
),
|
|
77
|
+
stack(panel("Reference", reference), panel("In-text citation", citation))
|
|
78
|
+
),
|
|
79
|
+
foot: actions(
|
|
80
|
+
copyButton(() => citation.textContent, "Copy in-text"),
|
|
81
|
+
copyButton(() => refText, "Copy reference", "fill")
|
|
82
|
+
),
|
|
83
|
+
});
|
|
84
|
+
slot.append(card.node);
|
|
85
|
+
build();
|
|
86
|
+
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default { mount };
|