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.
- package/README.md +24 -8
- package/mevebu/app/mevebu.js +3 -3
- 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,93 @@
|
|
|
1
|
+
// Check the contrast between text and background against WCAG.
|
|
2
|
+
import { el, bench, stack, panel, colorInput, button, 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
|
+
function luminance({ r, g, b }) {
|
|
14
|
+
const step = (value) => {
|
|
15
|
+
const channel = value / 255;
|
|
16
|
+
return channel <= 0.03928 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
|
|
17
|
+
};
|
|
18
|
+
return 0.2126 * step(r) + 0.7152 * step(g) + 0.0722 * step(b);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ratio(a, b) {
|
|
22
|
+
const la = luminance(a);
|
|
23
|
+
const lb = luminance(b);
|
|
24
|
+
const hi = Math.max(la, lb);
|
|
25
|
+
const lo = Math.min(la, lb);
|
|
26
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function mount(slot) {
|
|
30
|
+
const textPicker = colorInput({ value: "#111111", "aria-label": "Text color" });
|
|
31
|
+
const bgPicker = colorInput({ value: "#ffffff", "aria-label": "Background color" });
|
|
32
|
+
|
|
33
|
+
const kvRow = (key) => {
|
|
34
|
+
const value = el("span", { class: "mvb-kv__v" }, "-");
|
|
35
|
+
return { v: value, node: el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), value) };
|
|
36
|
+
};
|
|
37
|
+
const ratioRow = kvRow("Contrast ratio");
|
|
38
|
+
const normalAA = kvRow("Normal text AA");
|
|
39
|
+
const normalAAA = kvRow("Normal text AAA");
|
|
40
|
+
const largeAA = kvRow("Large text AA");
|
|
41
|
+
const largeAAA = kvRow("Large text AAA");
|
|
42
|
+
const list = el("div", { class: "mvb-kv" }, ratioRow.node, normalAA.node, normalAAA.node, largeAA.node, largeAAA.node);
|
|
43
|
+
|
|
44
|
+
const sample = el(
|
|
45
|
+
"div",
|
|
46
|
+
{ class: "mvb-preview__sample" },
|
|
47
|
+
el("p", { style: "margin:0 0 8px;font-size:20px;font-weight:700" }, "Large text sample"),
|
|
48
|
+
el("p", { style: "margin:0;font-size:15px" }, "Normal text sample for the contrast reading.")
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
let reading = "";
|
|
52
|
+
|
|
53
|
+
const pass = (ok) => (ok ? "Pass" : "Fail");
|
|
54
|
+
|
|
55
|
+
function update() {
|
|
56
|
+
const text = hexToRgb(textPicker.value);
|
|
57
|
+
const bg = hexToRgb(bgPicker.value);
|
|
58
|
+
const value = ratio(text, bg);
|
|
59
|
+
ratioRow.v.textContent = `${value.toFixed(2)}:1`;
|
|
60
|
+
normalAA.v.textContent = pass(value >= 4.5);
|
|
61
|
+
normalAAA.v.textContent = pass(value >= 7);
|
|
62
|
+
largeAA.v.textContent = pass(value >= 3);
|
|
63
|
+
largeAAA.v.textContent = pass(value >= 4.5);
|
|
64
|
+
sample.style.color = textPicker.value;
|
|
65
|
+
sample.style.background = bgPicker.value;
|
|
66
|
+
reading = `Contrast ${value.toFixed(2)}:1\nNormal text: AA ${pass(value >= 4.5)}, AAA ${pass(value >= 7)}\nLarge text: AA ${pass(value >= 3)}, AAA ${pass(value >= 4.5)}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
textPicker.addEventListener("input", update);
|
|
70
|
+
bgPicker.addEventListener("input", update);
|
|
71
|
+
|
|
72
|
+
const swap = button("Swap", { icon: "arrow-left-right" });
|
|
73
|
+
swap.addEventListener("click", () => {
|
|
74
|
+
const text = textPicker.value;
|
|
75
|
+
textPicker.value = bgPicker.value;
|
|
76
|
+
bgPicker.value = text;
|
|
77
|
+
update();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const card = toolcard({
|
|
81
|
+
body: bench(
|
|
82
|
+
stack(panel("Text color", textPicker), panel("Background color", bgPicker)),
|
|
83
|
+
stack(sample, list)
|
|
84
|
+
),
|
|
85
|
+
foot: actions(swap, copyButton(() => reading, "Copy result", "fill")),
|
|
86
|
+
});
|
|
87
|
+
slot.append(card.node);
|
|
88
|
+
update();
|
|
89
|
+
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default { mount };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Counts down from a duration you set, then beeps and flags the tab title.
|
|
2
|
+
import { toast, bench, stack, row, panel, numberInput, chipRow, button, output, notice, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const PRESETS = [
|
|
5
|
+
{ value: 5, label: "5 min" },
|
|
6
|
+
{ value: 10, label: "10 min" },
|
|
7
|
+
{ value: 15, label: "15 min" },
|
|
8
|
+
{ value: 25, label: "25 min" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
function format(ms) {
|
|
12
|
+
const total = Math.ceil(ms / 1000);
|
|
13
|
+
const h = Math.floor(total / 3600);
|
|
14
|
+
const m = Math.floor((total % 3600) / 60);
|
|
15
|
+
const s = total % 60;
|
|
16
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
17
|
+
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function beep() {
|
|
21
|
+
const Ctx = window.AudioContext || window.webkitAudioContext;
|
|
22
|
+
if (!Ctx) return;
|
|
23
|
+
try {
|
|
24
|
+
const context = new Ctx();
|
|
25
|
+
const osc = context.createOscillator();
|
|
26
|
+
const gain = context.createGain();
|
|
27
|
+
osc.type = "sine";
|
|
28
|
+
osc.frequency.value = 880;
|
|
29
|
+
gain.gain.setValueAtTime(0.15, context.currentTime);
|
|
30
|
+
gain.gain.exponentialRampToValueAtTime(0.0001, context.currentTime + 0.4);
|
|
31
|
+
osc.connect(gain).connect(context.destination);
|
|
32
|
+
osc.start();
|
|
33
|
+
osc.stop(context.currentTime + 0.4);
|
|
34
|
+
osc.onended = () => context.close();
|
|
35
|
+
} catch {
|
|
36
|
+
/* audio blocked, the title still changes */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function mount(slot) {
|
|
41
|
+
const baseTitle = document.title;
|
|
42
|
+
|
|
43
|
+
const hoursIn = numberInput({ min: 0, max: 99, value: 0, "aria-label": "Hours" });
|
|
44
|
+
const minsIn = numberInput({ min: 0, max: 59, value: 25, "aria-label": "Minutes" });
|
|
45
|
+
const secsIn = numberInput({ min: 0, max: 59, value: 0, "aria-label": "Seconds" });
|
|
46
|
+
|
|
47
|
+
const presets = chipRow(PRESETS, {
|
|
48
|
+
value: 25,
|
|
49
|
+
label: "Presets",
|
|
50
|
+
onChange: (minutes) => {
|
|
51
|
+
hoursIn.value = 0;
|
|
52
|
+
minsIn.value = minutes;
|
|
53
|
+
secsIn.value = 0;
|
|
54
|
+
reset();
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const display = output({ rows: 2 });
|
|
59
|
+
const status = notice("good");
|
|
60
|
+
const startButton = button("Start", { icon: "play", kind: "fill" });
|
|
61
|
+
const resetButton = button("Reset", { icon: "refresh-cw" });
|
|
62
|
+
|
|
63
|
+
let interval = 0;
|
|
64
|
+
let endAt = 0;
|
|
65
|
+
let remaining = 25 * 60 * 1000;
|
|
66
|
+
let mode = "idle";
|
|
67
|
+
|
|
68
|
+
const label = () => startButton.querySelector("span");
|
|
69
|
+
|
|
70
|
+
function readInputs() {
|
|
71
|
+
const h = Math.max(0, Number(hoursIn.value) || 0);
|
|
72
|
+
const m = Math.max(0, Number(minsIn.value) || 0);
|
|
73
|
+
const s = Math.max(0, Number(secsIn.value) || 0);
|
|
74
|
+
return ((h * 60 + m) * 60 + s) * 1000;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function paint() {
|
|
78
|
+
display.write(format(remaining));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function setInputsDisabled(off) {
|
|
82
|
+
hoursIn.disabled = off;
|
|
83
|
+
minsIn.disabled = off;
|
|
84
|
+
secsIn.disabled = off;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function stopTimer() {
|
|
88
|
+
clearInterval(interval);
|
|
89
|
+
interval = 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function tick() {
|
|
93
|
+
remaining = Math.max(0, endAt - performance.now());
|
|
94
|
+
paint();
|
|
95
|
+
if (remaining <= 0) finish();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function finish() {
|
|
99
|
+
stopTimer();
|
|
100
|
+
mode = "idle";
|
|
101
|
+
remaining = 0;
|
|
102
|
+
paint();
|
|
103
|
+
document.title = "Time is up";
|
|
104
|
+
status.write("Time is up.");
|
|
105
|
+
label().textContent = "Start";
|
|
106
|
+
setInputsDisabled(false);
|
|
107
|
+
beep();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function reset() {
|
|
111
|
+
stopTimer();
|
|
112
|
+
mode = "idle";
|
|
113
|
+
remaining = readInputs();
|
|
114
|
+
document.title = baseTitle;
|
|
115
|
+
status.write("");
|
|
116
|
+
label().textContent = "Start";
|
|
117
|
+
setInputsDisabled(false);
|
|
118
|
+
paint();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function start() {
|
|
122
|
+
if (mode === "running") {
|
|
123
|
+
remaining = Math.max(0, endAt - performance.now());
|
|
124
|
+
stopTimer();
|
|
125
|
+
mode = "paused";
|
|
126
|
+
label().textContent = "Resume";
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (remaining <= 0) {
|
|
130
|
+
remaining = readInputs();
|
|
131
|
+
if (remaining <= 0) return toast("Set a duration first");
|
|
132
|
+
}
|
|
133
|
+
endAt = performance.now() + remaining;
|
|
134
|
+
mode = "running";
|
|
135
|
+
setInputsDisabled(true);
|
|
136
|
+
status.write("");
|
|
137
|
+
document.title = baseTitle;
|
|
138
|
+
label().textContent = "Pause";
|
|
139
|
+
interval = setInterval(tick, 200);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
startButton.addEventListener("click", start);
|
|
143
|
+
resetButton.addEventListener("click", reset);
|
|
144
|
+
for (const input of [hoursIn, minsIn, secsIn]) {
|
|
145
|
+
input.addEventListener("input", () => {
|
|
146
|
+
if (mode === "idle") {
|
|
147
|
+
remaining = readInputs();
|
|
148
|
+
paint();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const card = toolcard({
|
|
154
|
+
body: bench(
|
|
155
|
+
stack(
|
|
156
|
+
panel("Presets", presets),
|
|
157
|
+
row(panel("Hours", hoursIn), panel("Minutes", minsIn), panel("Seconds", secsIn))
|
|
158
|
+
),
|
|
159
|
+
stack(display, status)
|
|
160
|
+
),
|
|
161
|
+
foot: actions(resetButton, startButton),
|
|
162
|
+
});
|
|
163
|
+
slot.append(card.node);
|
|
164
|
+
paint();
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
unmount() {
|
|
168
|
+
stopTimer();
|
|
169
|
+
document.title = baseTitle;
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export default { mount };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Builds a cron expression and reads it back in plain words.
|
|
2
|
+
import { el, bench, stack, panel, textInput, notice, button, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
5
|
+
const WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
6
|
+
|
|
7
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
8
|
+
|
|
9
|
+
function nameList(value, names, offset) {
|
|
10
|
+
return value.split(",").map((part) => {
|
|
11
|
+
if (/^\d+$/.test(part)) return names[(Number(part) - offset + names.length) % names.length] ?? part;
|
|
12
|
+
if (part.includes("-")) {
|
|
13
|
+
const [a, b] = part.split("-");
|
|
14
|
+
const from = names[(Number(a) - offset + names.length) % names.length] ?? a;
|
|
15
|
+
const to = names[(Number(b) - offset + names.length) % names.length] ?? b;
|
|
16
|
+
return `${from} through ${to}`;
|
|
17
|
+
}
|
|
18
|
+
return part;
|
|
19
|
+
}).join(", ");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function describe([minute, hour, day, month, weekday]) {
|
|
23
|
+
const parts = [];
|
|
24
|
+
const step = (value) => (value.startsWith("*/") ? value.slice(2) : null);
|
|
25
|
+
if (minute === "*" && hour === "*") parts.push("Every minute");
|
|
26
|
+
else if (step(minute) && hour === "*") parts.push(`Every ${step(minute)} minutes`);
|
|
27
|
+
else if (/^\d+$/.test(minute) && hour === "*") parts.push(`Every hour at minute ${Number(minute)}`);
|
|
28
|
+
else if (/^\d+$/.test(minute) && /^\d+$/.test(hour)) parts.push(`At ${pad(hour)}:${pad(minute)}`);
|
|
29
|
+
else parts.push(`Minute ${minute}, hour ${hour}`);
|
|
30
|
+
if (weekday !== "*") parts.push(`on ${nameList(weekday, WEEKDAYS, 0)}`);
|
|
31
|
+
else if (day !== "*") parts.push(`on day ${day} of the month`);
|
|
32
|
+
if (month !== "*") parts.push(`in ${nameList(month, MONTHS, 1)}`);
|
|
33
|
+
return `${parts.join(" ")}.`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const FIELDS = [
|
|
37
|
+
["minute", "Minute", "0-59", "*"],
|
|
38
|
+
["hour", "Hour", "0-23", "*"],
|
|
39
|
+
["day", "Day of month", "1-31", "*"],
|
|
40
|
+
["month", "Month", "1-12", "*"],
|
|
41
|
+
["weekday", "Day of week", "0-6", "*"],
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export function mount(slot) {
|
|
45
|
+
const inputs = {};
|
|
46
|
+
const controls = FIELDS.map(([key, label, hint, value]) => {
|
|
47
|
+
const input = textInput({ value, "aria-label": label, spellcheck: "false" });
|
|
48
|
+
inputs[key] = input;
|
|
49
|
+
return panel(label, input, hint);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const expression = textInput({ readonly: true, class: "mvb-in mvb-in--mono", "aria-label": "Cron expression" });
|
|
53
|
+
const reading = el("p", { class: "mvb-note" });
|
|
54
|
+
const error = notice("error");
|
|
55
|
+
|
|
56
|
+
function run() {
|
|
57
|
+
error.write("");
|
|
58
|
+
const values = FIELDS.map(([key]) => inputs[key].value.trim() || "*");
|
|
59
|
+
const invalid = values.find((value) => !/^[\d*,/\-\s]+$/.test(value));
|
|
60
|
+
if (invalid) {
|
|
61
|
+
error.write("Fields accept numbers, *, lists with commas, ranges with a hyphen and steps with a slash.");
|
|
62
|
+
expression.value = "";
|
|
63
|
+
reading.textContent = "";
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
expression.value = values.join(" ");
|
|
67
|
+
reading.textContent = describe(values);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const key of Object.keys(inputs)) inputs[key].addEventListener("input", run);
|
|
71
|
+
|
|
72
|
+
const reset = button("Reset to every minute", { icon: "refresh-cw" });
|
|
73
|
+
reset.addEventListener("click", () => {
|
|
74
|
+
for (const [key, , , value] of FIELDS) inputs[key].value = value;
|
|
75
|
+
run();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const card = toolcard({
|
|
79
|
+
body: bench(
|
|
80
|
+
stack(...controls, error),
|
|
81
|
+
stack(panel("Expression", expression), panel("Reading", reading))
|
|
82
|
+
),
|
|
83
|
+
foot: actions(reset, copyButton(() => expression.value, "Copy expression", "fill")),
|
|
84
|
+
});
|
|
85
|
+
slot.append(card.node);
|
|
86
|
+
run();
|
|
87
|
+
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default { mount };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Build a fluid size with the CSS clamp function.
|
|
2
|
+
import { el, bench, stack, rangeInput, output, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
function slider(label, { min, max, step, value, unit }) {
|
|
5
|
+
const input = rangeInput({ min, max, step, value, "aria-label": label });
|
|
6
|
+
const val = el("span", { class: "mvb-slider__val" }, `${value}${unit}`);
|
|
7
|
+
const node = el(
|
|
8
|
+
"div",
|
|
9
|
+
{ class: "mvb-slider" },
|
|
10
|
+
el("span", { class: "mvb-slider__label" }, label),
|
|
11
|
+
input,
|
|
12
|
+
val
|
|
13
|
+
);
|
|
14
|
+
node.input = input;
|
|
15
|
+
node.value = () => parseFloat(input.value) || 0;
|
|
16
|
+
node.setVal = (text) => { val.textContent = text; };
|
|
17
|
+
return node;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function mount(slot) {
|
|
21
|
+
const min = slider("Minimum", { min: 8, max: 96, step: 1, value: 16, unit: "px" });
|
|
22
|
+
const preferred = slider("Preferred", { min: 1, max: 10, step: 0.1, value: 2.5, unit: "vw" });
|
|
23
|
+
const max = slider("Maximum", { min: 8, max: 160, step: 1, value: 48, unit: "px" });
|
|
24
|
+
|
|
25
|
+
const sample = el(
|
|
26
|
+
"div",
|
|
27
|
+
{ class: "mvb-preview__sample" },
|
|
28
|
+
el("p", { style: "margin:0" }, "The quick brown fox jumps over the lazy dog.")
|
|
29
|
+
);
|
|
30
|
+
const code = output();
|
|
31
|
+
|
|
32
|
+
function clampValue() {
|
|
33
|
+
let lo = min.value();
|
|
34
|
+
let hi = max.value();
|
|
35
|
+
if (lo > hi) [lo, hi] = [hi, lo];
|
|
36
|
+
return `clamp(${lo}px, ${preferred.value()}vw, ${hi}px)`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function update() {
|
|
40
|
+
const value = clampValue();
|
|
41
|
+
sample.style.fontSize = value;
|
|
42
|
+
code.write(`font-size: ${value};`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const control of [min, preferred, max]) {
|
|
46
|
+
const unit = control === preferred ? "vw" : "px";
|
|
47
|
+
control.input.addEventListener("input", () => {
|
|
48
|
+
control.setVal(`${control.value()}${unit}`);
|
|
49
|
+
update();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const card = toolcard({
|
|
54
|
+
body: bench(
|
|
55
|
+
stack(min, preferred, max),
|
|
56
|
+
stack(sample, code)
|
|
57
|
+
),
|
|
58
|
+
foot: actions(copyButton(() => `font-size: ${clampValue()};`, "Copy CSS", "fill")),
|
|
59
|
+
});
|
|
60
|
+
slot.append(card.node);
|
|
61
|
+
update();
|
|
62
|
+
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default { mount };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Formats CSS: selectors on their own line, one property per line.
|
|
2
|
+
import { bench, stack, panel, textArea, chipRow, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const clean = (text) => text.trim().replace(/\s*\n\s*/g, " ");
|
|
5
|
+
|
|
6
|
+
function format(css, size) {
|
|
7
|
+
const pad = " ".repeat(size);
|
|
8
|
+
const lines = [];
|
|
9
|
+
let depth = 0;
|
|
10
|
+
let buffer = "";
|
|
11
|
+
let i = 0;
|
|
12
|
+
const push = (line) => { if (line) lines.push(pad.repeat(depth) + line); };
|
|
13
|
+
|
|
14
|
+
while (i < css.length) {
|
|
15
|
+
const ch = css[i];
|
|
16
|
+
if (ch === "/" && css[i + 1] === "*") {
|
|
17
|
+
const end = css.indexOf("*/", i + 2);
|
|
18
|
+
const comment = css.slice(i, end === -1 ? css.length : end + 2);
|
|
19
|
+
const before = clean(buffer);
|
|
20
|
+
if (before) { push(before); buffer = ""; }
|
|
21
|
+
push(comment);
|
|
22
|
+
i = end === -1 ? css.length : end + 2;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (ch === "{") {
|
|
26
|
+
push(`${clean(buffer)} {`);
|
|
27
|
+
buffer = "";
|
|
28
|
+
depth++;
|
|
29
|
+
} else if (ch === "}") {
|
|
30
|
+
const decl = clean(buffer);
|
|
31
|
+
if (decl) push(decl.endsWith(";") ? decl : `${decl};`);
|
|
32
|
+
buffer = "";
|
|
33
|
+
depth = Math.max(0, depth - 1);
|
|
34
|
+
push("}");
|
|
35
|
+
lines.push("");
|
|
36
|
+
} else if (ch === ";") {
|
|
37
|
+
const decl = clean(buffer);
|
|
38
|
+
if (decl) push(`${decl};`);
|
|
39
|
+
buffer = "";
|
|
40
|
+
} else {
|
|
41
|
+
buffer += ch;
|
|
42
|
+
}
|
|
43
|
+
i++;
|
|
44
|
+
}
|
|
45
|
+
const rest = clean(buffer);
|
|
46
|
+
if (rest) push(rest);
|
|
47
|
+
return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function mount(slot) {
|
|
51
|
+
const source = textArea({
|
|
52
|
+
rows: 12,
|
|
53
|
+
class: "mvb-in mvb-in--area mvb-in--mono",
|
|
54
|
+
"aria-label": "CSS",
|
|
55
|
+
value: ".card{color:red;padding:10px 12px}.card:hover{color:blue}",
|
|
56
|
+
});
|
|
57
|
+
const result = textArea({ rows: 12, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Formatted CSS" });
|
|
58
|
+
let size = "2";
|
|
59
|
+
|
|
60
|
+
const chips = chipRow(
|
|
61
|
+
[{ value: "2", label: "2 spaces" }, { value: "4", label: "4 spaces" }],
|
|
62
|
+
{ value: "2", label: "Indent", onChange: (value) => { size = value; run(); } }
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
function run() {
|
|
66
|
+
result.value = source.value.trim() ? format(source.value, Number(size)) : "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
source.addEventListener("input", run);
|
|
70
|
+
|
|
71
|
+
const card = toolcard({
|
|
72
|
+
body: bench(
|
|
73
|
+
stack(panel("Indent", chips), panel("CSS", source)),
|
|
74
|
+
stack(panel("Formatted", result))
|
|
75
|
+
),
|
|
76
|
+
foot: actions(copyButton(() => result.value, "Copy CSS", "fill")),
|
|
77
|
+
});
|
|
78
|
+
slot.append(card.node);
|
|
79
|
+
run();
|
|
80
|
+
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default { mount };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Lay out a CSS grid: columns, rows and gaps with a live preview.
|
|
2
|
+
import { el, bench, stack, panel, numberInput, rangeInput, output, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const CELL = "min-height:32px;background:var(--mvb-nebula);border:1px solid var(--mvb-comet);border-radius:6px";
|
|
5
|
+
|
|
6
|
+
function slider(label, value) {
|
|
7
|
+
const input = rangeInput({ min: 0, max: 48, value, "aria-label": label });
|
|
8
|
+
const val = el("span", { class: "mvb-slider__val" }, `${value}px`);
|
|
9
|
+
const node = el(
|
|
10
|
+
"div",
|
|
11
|
+
{ class: "mvb-slider" },
|
|
12
|
+
el("span", { class: "mvb-slider__label" }, label),
|
|
13
|
+
input,
|
|
14
|
+
val
|
|
15
|
+
);
|
|
16
|
+
node.input = input;
|
|
17
|
+
node.value = () => parseFloat(input.value) || 0;
|
|
18
|
+
node.setVal = (text) => { val.textContent = text; };
|
|
19
|
+
return node;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const clampInt = (value, min, max) => Math.max(min, Math.min(max, parseInt(value, 10) || min));
|
|
23
|
+
|
|
24
|
+
export function mount(slot) {
|
|
25
|
+
const columns = numberInput({ value: 3, min: "1", max: "8", "aria-label": "Columns" });
|
|
26
|
+
const rows = numberInput({ value: 2, min: "1", max: "8", "aria-label": "Rows" });
|
|
27
|
+
const columnGap = slider("Column gap", 12);
|
|
28
|
+
const rowGap = slider("Row gap", 12);
|
|
29
|
+
|
|
30
|
+
const grid = el("div");
|
|
31
|
+
const code = output();
|
|
32
|
+
|
|
33
|
+
function declarations() {
|
|
34
|
+
const c = clampInt(columns.value, 1, 8);
|
|
35
|
+
const r = clampInt(rows.value, 1, 8);
|
|
36
|
+
const cg = columnGap.value();
|
|
37
|
+
const rg = rowGap.value();
|
|
38
|
+
const gap = cg === rg ? `gap: ${cg}px;` : `gap: ${rg}px ${cg}px;`;
|
|
39
|
+
return { c, r, cg, rg, gap };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function update() {
|
|
43
|
+
const { c, r, cg, rg, gap } = declarations();
|
|
44
|
+
grid.style.display = "grid";
|
|
45
|
+
grid.style.width = "100%";
|
|
46
|
+
grid.style.gridTemplateColumns = `repeat(${c}, 1fr)`;
|
|
47
|
+
grid.style.gridTemplateRows = `repeat(${r}, 1fr)`;
|
|
48
|
+
grid.style.gap = `${rg}px ${cg}px`;
|
|
49
|
+
grid.replaceChildren(...Array.from({ length: c * r }, () => el("div", { style: CELL })));
|
|
50
|
+
code.write(`display: grid;\ngrid-template-columns: repeat(${c}, 1fr);\ngrid-template-rows: repeat(${r}, 1fr);\n${gap}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
columns.addEventListener("input", update);
|
|
54
|
+
rows.addEventListener("input", update);
|
|
55
|
+
for (const control of [columnGap, rowGap]) {
|
|
56
|
+
control.input.addEventListener("input", () => {
|
|
57
|
+
control.setVal(`${control.value()}px`);
|
|
58
|
+
update();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const card = toolcard({
|
|
63
|
+
body: bench(
|
|
64
|
+
stack(panel("Columns", columns), panel("Rows", rows), columnGap, rowGap),
|
|
65
|
+
stack(el("div", { class: "mvb-preview" }, grid), code)
|
|
66
|
+
),
|
|
67
|
+
foot: actions(copyButton(() => code.textContent, "Copy CSS", "fill")),
|
|
68
|
+
});
|
|
69
|
+
slot.append(card.node);
|
|
70
|
+
update();
|
|
71
|
+
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default { mount };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Compacts CSS and reports the size saved. Comments can be kept.
|
|
2
|
+
import { el, bench, stack, panel, textArea, switchControl, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
const bytes = (text) => new TextEncoder().encode(text).length;
|
|
5
|
+
|
|
6
|
+
const tile = (key, value) =>
|
|
7
|
+
el("div", { class: "mvb-stat" }, el("span", { class: "mvb-stat__v" }, value), el("span", { class: "mvb-stat__k" }, key));
|
|
8
|
+
|
|
9
|
+
function minify(css, stripComments) {
|
|
10
|
+
let out = "";
|
|
11
|
+
let pending = false;
|
|
12
|
+
let i = 0;
|
|
13
|
+
const n = css.length;
|
|
14
|
+
while (i < n) {
|
|
15
|
+
const ch = css[i];
|
|
16
|
+
const next = css[i + 1];
|
|
17
|
+
if (stripComments && ch === "/" && next === "*") {
|
|
18
|
+
i += 2;
|
|
19
|
+
while (i < n && !(css[i] === "*" && css[i + 1] === "/")) i++;
|
|
20
|
+
i += 2;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (ch === '"' || ch === "'") {
|
|
24
|
+
if (pending && out) out += " ";
|
|
25
|
+
pending = false;
|
|
26
|
+
out += ch;
|
|
27
|
+
i++;
|
|
28
|
+
while (i < n) {
|
|
29
|
+
out += css[i];
|
|
30
|
+
if (css[i] === ch) {
|
|
31
|
+
if (css[i + 1] === ch) { out += css[i + 1]; i += 2; continue; }
|
|
32
|
+
i++;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
i++;
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (/\s/.test(ch)) {
|
|
40
|
+
pending = true;
|
|
41
|
+
i++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (pending && out) {
|
|
45
|
+
const prev = out[out.length - 1];
|
|
46
|
+
if (!"{};,>+~(".includes(prev) && !"{};,>+~)".includes(ch)) out += " ";
|
|
47
|
+
}
|
|
48
|
+
pending = false;
|
|
49
|
+
out += ch;
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
return out.trim().replace(/;}/g, "}");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function mount(slot) {
|
|
56
|
+
const source = textArea({
|
|
57
|
+
rows: 12,
|
|
58
|
+
class: "mvb-in mvb-in--area mvb-in--mono",
|
|
59
|
+
"aria-label": "CSS",
|
|
60
|
+
value: "/* card */\n.card {\n color: red;\n padding: 10px 12px;\n}",
|
|
61
|
+
});
|
|
62
|
+
const result = textArea({ rows: 12, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Minified CSS" });
|
|
63
|
+
const stats = el("div", { class: "mvb-stats" });
|
|
64
|
+
let stripComments = true;
|
|
65
|
+
|
|
66
|
+
const comments = switchControl("Remove comments", { checked: true });
|
|
67
|
+
comments.input.addEventListener("change", () => { stripComments = comments.input.checked; run(); });
|
|
68
|
+
|
|
69
|
+
function run() {
|
|
70
|
+
stats.replaceChildren();
|
|
71
|
+
if (!source.value.trim()) {
|
|
72
|
+
result.value = "";
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const compact = minify(source.value, stripComments);
|
|
76
|
+
const before = bytes(source.value);
|
|
77
|
+
const after = bytes(compact);
|
|
78
|
+
const saved = before ? Math.max(0, Math.round((1 - after / before) * 100)) : 0;
|
|
79
|
+
result.value = compact;
|
|
80
|
+
stats.append(tile("Before", `${before} B`), tile("After", `${after} B`), tile("Saved", `${saved}%`));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
source.addEventListener("input", run);
|
|
84
|
+
|
|
85
|
+
const card = toolcard({
|
|
86
|
+
body: bench(
|
|
87
|
+
stack(panel("Options", comments), panel("CSS", source)),
|
|
88
|
+
stack(panel("Minified", result), stats)
|
|
89
|
+
),
|
|
90
|
+
foot: actions(copyButton(() => result.value, "Copy CSS", "fill")),
|
|
91
|
+
});
|
|
92
|
+
slot.append(card.node);
|
|
93
|
+
run();
|
|
94
|
+
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default { mount };
|