mevebu 1.3.0 → 1.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/mevebu/app/mevebu.js +260 -208
- package/mevebu/app/tailwind.css +1635 -2079
- package/mevebu/app/tailwind.src.css +155 -1127
- package/mevebu/index.html +2 -2
- package/mevebu/libs/js/core.js +235 -95
- package/mevebu/libs/js/tools/abstract-checklist.js +5 -7
- package/mevebu/libs/js/tools/add-subtract-days.js +9 -11
- package/mevebu/libs/js/tools/age-calculator.js +7 -8
- package/mevebu/libs/js/tools/api-request-builder.js +3 -4
- package/mevebu/libs/js/tools/area-converter.js +7 -9
- package/mevebu/libs/js/tools/aspect-ratio.js +8 -9
- package/mevebu/libs/js/tools/average-calculator.js +4 -5
- package/mevebu/libs/js/tools/base64.js +2 -3
- package/mevebu/libs/js/tools/binary-calculator.js +6 -7
- package/mevebu/libs/js/tools/border-radius.js +7 -9
- package/mevebu/libs/js/tools/box-shadow.js +6 -7
- package/mevebu/libs/js/tools/break-even.js +4 -5
- package/mevebu/libs/js/tools/business-days.js +7 -8
- package/mevebu/libs/js/tools/canonical-tag.js +5 -6
- package/mevebu/libs/js/tools/caption-counter.js +5 -7
- package/mevebu/libs/js/tools/case-converter.js +10 -11
- package/mevebu/libs/js/tools/case-inspector.js +14 -16
- package/mevebu/libs/js/tools/cash-denomination.js +7 -8
- package/mevebu/libs/js/tools/cash-flow.js +7 -8
- package/mevebu/libs/js/tools/cert-expiry.js +5 -7
- package/mevebu/libs/js/tools/changelog-generator.js +4 -6
- package/mevebu/libs/js/tools/character-counter.js +10 -13
- package/mevebu/libs/js/tools/checklist.js +6 -7
- package/mevebu/libs/js/tools/citation-number-check.js +10 -13
- package/mevebu/libs/js/tools/coin-flip.js +9 -11
- package/mevebu/libs/js/tools/color-converter.js +6 -7
- package/mevebu/libs/js/tools/color-palette.js +6 -8
- package/mevebu/libs/js/tools/color-picker.js +4 -5
- package/mevebu/libs/js/tools/content-calendar.js +3 -4
- package/mevebu/libs/js/tools/contrast-checker.js +5 -6
- package/mevebu/libs/js/tools/cron-builder.js +4 -6
- package/mevebu/libs/js/tools/css-clamp.js +5 -6
- package/mevebu/libs/js/tools/css-formatter.js +4 -7
- package/mevebu/libs/js/tools/css-grid.js +6 -7
- package/mevebu/libs/js/tools/css-minifier.js +5 -8
- package/mevebu/libs/js/tools/csv-to-json.js +2 -3
- package/mevebu/libs/js/tools/csv-viewer.js +9 -11
- package/mevebu/libs/js/tools/currency-format.js +5 -7
- package/mevebu/libs/js/tools/daily-planner.js +9 -10
- package/mevebu/libs/js/tools/data-masker.js +6 -8
- package/mevebu/libs/js/tools/data-storage.js +7 -9
- package/mevebu/libs/js/tools/data-validator.js +9 -11
- package/mevebu/libs/js/tools/date-difference.js +6 -7
- package/mevebu/libs/js/tools/date-format.js +11 -13
- package/mevebu/libs/js/tools/delimiter-converter.js +3 -5
- package/mevebu/libs/js/tools/discount-calculator.js +4 -5
- package/mevebu/libs/js/tools/doi-resolver.js +4 -5
- package/mevebu/libs/js/tools/env-formatter.js +3 -6
- package/mevebu/libs/js/tools/equation-formatter.js +5 -9
- package/mevebu/libs/js/tools/expense-split.js +6 -7
- package/mevebu/libs/js/tools/fake-data.js +8 -13
- package/mevebu/libs/js/tools/find-replace.js +3 -5
- package/mevebu/libs/js/tools/flashcard-creator.js +9 -10
- package/mevebu/libs/js/tools/font-pairing.js +6 -8
- package/mevebu/libs/js/tools/fraction-calculator.js +10 -13
- package/mevebu/libs/js/tools/git-command-builder.js +11 -22
- package/mevebu/libs/js/tools/gpa-calculator.js +8 -10
- package/mevebu/libs/js/tools/grade-calculator.js +7 -8
- package/mevebu/libs/js/tools/gradient-generator.js +10 -12
- package/mevebu/libs/js/tools/habit-tracker.js +8 -9
- package/mevebu/libs/js/tools/hash-generator.js +3 -5
- package/mevebu/libs/js/tools/hashtag-formatter.js +6 -8
- package/mevebu/libs/js/tools/hmac-generator.js +3 -5
- package/mevebu/libs/js/tools/hpp-calculator.js +4 -5
- package/mevebu/libs/js/tools/html-entity.js +2 -3
- package/mevebu/libs/js/tools/html-formatter.js +3 -6
- package/mevebu/libs/js/tools/http-status.js +5 -7
- package/mevebu/libs/js/tools/inventory-quantity.js +7 -8
- package/mevebu/libs/js/tools/json-formatter.js +3 -6
- package/mevebu/libs/js/tools/json-minifier.js +5 -8
- package/mevebu/libs/js/tools/json-to-csv.js +3 -5
- package/mevebu/libs/js/tools/json-to-typescript.js +2 -5
- package/mevebu/libs/js/tools/json-validator.js +2 -3
- package/mevebu/libs/js/tools/jwt-decoder.js +6 -7
- package/mevebu/libs/js/tools/jwt-expiry.js +5 -6
- package/mevebu/libs/js/tools/jwt-generator.js +4 -6
- package/mevebu/libs/js/tools/length-converter.js +7 -9
- package/mevebu/libs/js/tools/license-reference.js +10 -13
- package/mevebu/libs/js/tools/line-counter.js +5 -7
- package/mevebu/libs/js/tools/list-difference.js +5 -7
- package/mevebu/libs/js/tools/list-merger.js +4 -5
- package/mevebu/libs/js/tools/locale-number.js +5 -8
- package/mevebu/libs/js/tools/markdown-preview.js +5 -8
- package/mevebu/libs/js/tools/markdown-table.js +5 -7
- package/mevebu/libs/js/tools/markup-calculator.js +4 -5
- package/mevebu/libs/js/tools/meeting-agenda.js +8 -9
- package/mevebu/libs/js/tools/meta-preview.js +12 -13
- package/mevebu/libs/js/tools/meta-tags.js +6 -7
- package/mevebu/libs/js/tools/mime-types.js +5 -7
- package/mevebu/libs/js/tools/name-picker.js +2 -3
- package/mevebu/libs/js/tools/number-base-converter.js +9 -12
- package/mevebu/libs/js/tools/password-generator.js +9 -12
- package/mevebu/libs/js/tools/password-strength.js +21 -25
- package/mevebu/libs/js/tools/percent-to-fraction.js +6 -7
- package/mevebu/libs/js/tools/percentage-calculator.js +6 -9
- package/mevebu/libs/js/tools/percentage-change.js +4 -5
- package/mevebu/libs/js/tools/permutation-combination.js +9 -12
- package/mevebu/libs/js/tools/prefix-suffix.js +2 -3
- package/mevebu/libs/js/tools/priority-matrix.js +6 -10
- package/mevebu/libs/js/tools/profit-margin.js +4 -5
- package/mevebu/libs/js/tools/qr-generator.js +3 -4
- package/mevebu/libs/js/tools/qr-scanner.js +11 -12
- package/mevebu/libs/js/tools/quadratic-solver.js +7 -8
- package/mevebu/libs/js/tools/query-builder.js +2 -3
- package/mevebu/libs/js/tools/quiz-formatter.js +4 -6
- package/mevebu/libs/js/tools/ratio-calculator.js +7 -8
- package/mevebu/libs/js/tools/readability.js +12 -15
- package/mevebu/libs/js/tools/reading-time.js +5 -7
- package/mevebu/libs/js/tools/reference-completeness.js +6 -8
- package/mevebu/libs/js/tools/regex-tester.js +7 -10
- package/mevebu/libs/js/tools/remove-duplicate-lines.js +2 -3
- package/mevebu/libs/js/tools/remove-extra-spaces.js +3 -5
- package/mevebu/libs/js/tools/robots-txt.js +3 -4
- package/mevebu/libs/js/tools/roman-numeral-converter.js +6 -8
- package/mevebu/libs/js/tools/sales-commission.js +8 -9
- package/mevebu/libs/js/tools/scientific-calculator.js +10 -14
- package/mevebu/libs/js/tools/scientific-notation.js +8 -9
- package/mevebu/libs/js/tools/script-planner.js +8 -10
- package/mevebu/libs/js/tools/selling-price.js +4 -5
- package/mevebu/libs/js/tools/semver-helper.js +3 -5
- package/mevebu/libs/js/tools/sentence-length.js +9 -10
- package/mevebu/libs/js/tools/sequence-analyzer.js +7 -8
- package/mevebu/libs/js/tools/sitemap-xml.js +4 -7
- package/mevebu/libs/js/tools/slug-converter.js +3 -5
- package/mevebu/libs/js/tools/slug-preview.js +6 -7
- package/mevebu/libs/js/tools/sort-lines.js +3 -5
- package/mevebu/libs/js/tools/speed-converter.js +7 -9
- package/mevebu/libs/js/tools/spin-wheel.js +19 -17
- package/mevebu/libs/js/tools/sql-formatter.js +5 -9
- package/mevebu/libs/js/tools/sql-minifier.js +5 -8
- package/mevebu/libs/js/tools/standard-deviation.js +6 -9
- package/mevebu/libs/js/tools/stopwatch.js +4 -6
- package/mevebu/libs/js/tools/study-schedule.js +8 -9
- package/mevebu/libs/js/tools/subtitle-formatter.js +10 -11
- package/mevebu/libs/js/tools/table-to-markdown.js +8 -12
- package/mevebu/libs/js/tools/tax-calculator.js +4 -5
- package/mevebu/libs/js/tools/temperature-converter.js +8 -11
- package/mevebu/libs/js/tools/text-diff.js +11 -7
- package/mevebu/libs/js/tools/text-direction.js +3 -5
- package/mevebu/libs/js/tools/text-reverser.js +3 -5
- package/mevebu/libs/js/tools/time-duration.js +8 -9
- package/mevebu/libs/js/tools/timestamp-converter.js +4 -6
- package/mevebu/libs/js/tools/timezone-converter.js +9 -13
- package/mevebu/libs/js/tools/title-length.js +6 -7
- package/mevebu/libs/js/tools/unicode-inspector.js +4 -5
- package/mevebu/libs/js/tools/url-inspector.js +8 -9
- package/mevebu/libs/js/tools/url-parser.js +12 -13
- package/mevebu/libs/js/tools/utf8-bytes.js +6 -9
- package/mevebu/libs/js/tools/uuid-generator.js +5 -9
- package/mevebu/libs/js/tools/version-compare.js +3 -4
- package/mevebu/libs/js/tools/video-duration.js +5 -7
- package/mevebu/libs/js/tools/volume-converter.js +7 -9
- package/mevebu/libs/js/tools/weekly-planner.js +8 -10
- package/mevebu/libs/js/tools/weight-converter.js +7 -9
- package/mevebu/libs/js/tools/weighted-average.js +4 -5
- package/mevebu/libs/js/tools/word-counter.js +10 -13
- package/mevebu/libs/js/tools/word-limit.js +4 -5
- package/mevebu/libs/js/tools/yaml-formatter.js +6 -7
- package/package.json +1 -1
package/mevebu/app/mevebu.js
CHANGED
|
@@ -5,7 +5,7 @@ import { CATEGORIES, TOOLS, getTool, getCategory, toolsInCategory, toolHref } fr
|
|
|
5
5
|
import { route, start, match, currentPath, go } from "../libs/js/router.js";
|
|
6
6
|
import {
|
|
7
7
|
$, $$, el, icon, getFavorites, isFavorite, toggleFavorite, setTheme,
|
|
8
|
-
modal, button, copyButton, openPalette, shuffle, toolcard,
|
|
8
|
+
modal, modalHead, button, copyButton, openPalette, shuffle, toolcard, blank,
|
|
9
9
|
VERSION, PACKAGE_NAME, checkForUpdate, updateCommand, compareVersions,
|
|
10
10
|
} from "../libs/js/core.js";
|
|
11
11
|
|
|
@@ -21,39 +21,72 @@ const NAV = [
|
|
|
21
21
|
|
|
22
22
|
// ---------- shell ----------
|
|
23
23
|
|
|
24
|
+
const KBD = "rounded-[5px] border border-orbit-hard px-[7px] py-px font-sans text-2xs leading-[1.45] text-ink-3";
|
|
25
|
+
const ICOBTN =
|
|
26
|
+
"size-[var(--ctl)] cursor-pointer items-center justify-center rounded-r1 border border-transparent bg-transparent text-ink-2 transition-colors duration-150 hover:bg-deck-2 hover:text-ink focus-visible:border-jade";
|
|
27
|
+
/* Link shaped like a button. `button()` returns a <button>, and the three actions
|
|
28
|
+
that leave the page are anchors, so they share the same two skins here. */
|
|
29
|
+
const LINK_BASE = "inline-flex min-h-[var(--ctl)] items-center justify-center gap-2 rounded-r1 border px-4 text-sm font-semibold whitespace-nowrap no-underline transition-colors duration-150";
|
|
30
|
+
const LINK_FILL = `${LINK_BASE} border-jade bg-jade text-on-jade hover:border-jade-deep hover:bg-jade-deep`;
|
|
31
|
+
const LINK_PLAIN = `${LINK_BASE} border-orbit bg-deck text-ink hover:border-orbit-hard hover:bg-deck-2`;
|
|
32
|
+
|
|
33
|
+
/* Page furniture. These strings are the layout rhythm of the whole site, so they
|
|
34
|
+
are named once here instead of spelled out on every page. */
|
|
35
|
+
const PAGE = "px-[var(--margin)]";
|
|
36
|
+
const PAGE_NARROW = "mx-auto max-w-[var(--bench)] px-[var(--margin)]";
|
|
37
|
+
const GRID = "grid grid-cols-1 gap-[var(--gutter)] sm:grid-cols-2 lg:grid-cols-3";
|
|
38
|
+
const SECTION = "pt-[clamp(32px,5vw,60px)]";
|
|
39
|
+
const SECTION_HEAD = "mb-4 flex items-baseline gap-3";
|
|
40
|
+
const SECTION_TITLE = "flex items-center gap-2.5 text-lg font-bold tracking-[-0.02em]";
|
|
41
|
+
const SECTION_MORE = "ms-auto text-sm font-semibold text-jade no-underline hover:underline";
|
|
42
|
+
const SECTION_COUNT = "ms-1.5 rounded-full bg-deck-2 px-2 py-px text-2xs font-semibold text-ink-3";
|
|
43
|
+
const LEAD = "mt-[clamp(28px,5vw,56px)] mb-0 max-w-[20ch] text-balance text-lead font-bold tracking-[-0.025em]";
|
|
44
|
+
const LEDE = "mt-3 mb-0 max-w-[60ch] text-pretty text-md text-ink-2";
|
|
45
|
+
const TEXT_LINK = "text-jade underline-offset-[3px]";
|
|
46
|
+
const ABOUT_BLOCK = "mt-8 grid max-w-[62ch] gap-2.5 border-t border-orbit pt-6";
|
|
47
|
+
const CODE = "rounded-[4px] bg-deck-2 px-[5px] py-px font-mono text-[0.92em]";
|
|
48
|
+
|
|
24
49
|
function searchButton() {
|
|
25
|
-
|
|
50
|
+
return el(
|
|
26
51
|
"button",
|
|
27
52
|
{
|
|
28
|
-
class:
|
|
53
|
+
class:
|
|
54
|
+
"me-1 inline-flex h-[var(--ctl)] min-w-[180px] cursor-pointer items-center gap-2 rounded-r1 border border-orbit bg-deck-2 ps-3 pe-2.5 text-sm font-medium text-ink-2 transition-colors duration-150 hover:border-orbit-hard hover:bg-deck hover:text-ink max-[60rem]:hidden",
|
|
29
55
|
type: "button",
|
|
30
56
|
"aria-label": "Search tools (Ctrl K)",
|
|
31
57
|
onclick: () => openPalette(),
|
|
32
58
|
},
|
|
33
|
-
icon("search"),
|
|
34
|
-
el("span", { class: "
|
|
35
|
-
el("kbd", { class:
|
|
59
|
+
icon("search", "size-[15px] flex-none text-ink-3"),
|
|
60
|
+
el("span", { class: "flex-1 text-start text-sm" }, "Search"),
|
|
61
|
+
el("kbd", { class: KBD }, "⌘K")
|
|
36
62
|
);
|
|
37
|
-
return btn;
|
|
38
63
|
}
|
|
39
64
|
|
|
40
65
|
function masthead() {
|
|
41
66
|
const menuBtn = el(
|
|
42
67
|
"button",
|
|
43
|
-
{
|
|
68
|
+
{
|
|
69
|
+
class: `hidden max-[45rem]:inline-flex ${ICOBTN}`,
|
|
70
|
+
type: "button",
|
|
71
|
+
"aria-expanded": "false",
|
|
72
|
+
"aria-controls": "navsheet",
|
|
73
|
+
"aria-label": "Menu",
|
|
74
|
+
},
|
|
44
75
|
icon("menu")
|
|
45
76
|
);
|
|
46
77
|
|
|
47
78
|
const sheet = el(
|
|
48
79
|
"div",
|
|
49
|
-
{ class: "
|
|
80
|
+
{ class: "grid border-t border-orbit bg-deck px-[var(--margin)] pt-1.5 pb-3.5", id: "navsheet", hidden: true },
|
|
50
81
|
el(
|
|
51
82
|
"nav",
|
|
52
|
-
{ class: "
|
|
53
|
-
...NAV.map((item) =>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
83
|
+
{ class: "grid", "aria-label": "Site" },
|
|
84
|
+
...NAV.map((item) =>
|
|
85
|
+
el("a", { class: "flex items-center gap-2 border-b border-orbit py-[13px] text-start text-md font-medium text-ink no-underline last:border-b-0", href: `#${item.path}` }, item.label)
|
|
86
|
+
),
|
|
87
|
+
el("a", { class: "flex items-center gap-2 border-b border-orbit py-[13px] text-md font-medium text-ink no-underline last:border-b-0", href: REPO_URL, target: "_blank", rel: "noopener" }, "GitHub"),
|
|
88
|
+
el("button", { class: "flex items-center gap-2 border-b-0 border-orbit py-[13px] text-start text-md font-medium text-jade", type: "button", onclick: () => openPalette() },
|
|
89
|
+
icon("search", "size-[18px] flex-none text-ink-3"), el("span", {}, "Search tools"))
|
|
57
90
|
)
|
|
58
91
|
);
|
|
59
92
|
|
|
@@ -67,7 +100,7 @@ function masthead() {
|
|
|
67
100
|
if (event.key === "Escape" && !sheet.hidden) setMenu(false);
|
|
68
101
|
});
|
|
69
102
|
|
|
70
|
-
const themeBtn = el("button", { class:
|
|
103
|
+
const themeBtn = el("button", { class: `inline-flex ${ICOBTN}`, type: "button", "aria-label": "Switch theme" }, icon("moon"));
|
|
71
104
|
themeBtn.addEventListener("click", () => {
|
|
72
105
|
const next = document.documentElement.dataset.theme === "dark" ? "light" : "dark";
|
|
73
106
|
setTheme(next);
|
|
@@ -76,45 +109,52 @@ function masthead() {
|
|
|
76
109
|
|
|
77
110
|
const nav = el(
|
|
78
111
|
"nav",
|
|
79
|
-
{ class: "
|
|
112
|
+
{ class: "ms-auto hidden flex-none gap-1 rounded-r2 border border-orbit bg-deck-2 p-1 min-[45rem]:flex", "aria-label": "Site" },
|
|
80
113
|
...NAV.map((item) =>
|
|
81
|
-
el(
|
|
114
|
+
el(
|
|
115
|
+
"a",
|
|
116
|
+
{
|
|
117
|
+
class:
|
|
118
|
+
"whitespace-nowrap rounded-lg px-3 py-[7px] text-sm font-semibold text-ink-2 no-underline transition-colors duration-150 hover:bg-ink/6 hover:text-ink data-[on]:bg-deck data-[on]:text-ink data-[on]:shadow-[0_0_0_1px_var(--orbit)]",
|
|
119
|
+
href: `#${item.path}`,
|
|
120
|
+
dataset: { path: item.path },
|
|
121
|
+
},
|
|
122
|
+
item.label
|
|
123
|
+
)
|
|
82
124
|
)
|
|
83
125
|
);
|
|
84
126
|
|
|
85
127
|
return el(
|
|
86
128
|
"header",
|
|
87
|
-
{ class: "
|
|
129
|
+
{ class: "sticky top-0 z-30 border-b border-orbit bg-deck" },
|
|
88
130
|
el(
|
|
89
131
|
"div",
|
|
90
|
-
{ class: "
|
|
91
|
-
el("a", { class: "
|
|
92
|
-
el("img", { src: "./icon.png", alt: "", width: "30", height: "30" }), "mevebu"),
|
|
132
|
+
{ class: "mx-[var(--margin)] flex h-[var(--bar)] items-center gap-[clamp(14px,3vw,32px)]" },
|
|
133
|
+
el("a", { class: "inline-flex flex-none items-center gap-2.5 text-lg font-bold tracking-[-0.01em] text-ink no-underline", href: "#/" },
|
|
134
|
+
el("img", { class: "block size-[30px] rounded-[9px] shadow-[0_0_0_1px_var(--orbit)]", src: "./icon.png", alt: "", width: "30", height: "30" }), "mevebu"),
|
|
93
135
|
nav,
|
|
94
|
-
el("div", { class: "
|
|
136
|
+
el("div", { class: "flex items-center gap-0.5 max-[45rem]:ms-auto" }, searchButton(), themeBtn, menuBtn)
|
|
95
137
|
),
|
|
96
138
|
sheet
|
|
97
139
|
);
|
|
98
140
|
}
|
|
99
141
|
|
|
100
142
|
function openHelp(tool) {
|
|
101
|
-
const category = getCategory(tool.category);
|
|
102
|
-
const body = [
|
|
103
|
-
el("div", { class: "mvb-help__head" },
|
|
104
|
-
el("span", { class: "mvb-help__sigil" }, icon(tool.icon)),
|
|
105
|
-
el("h2", { class: "mvb-help__title" }, tool.name)),
|
|
106
|
-
el("p", { class: "mvb-help__note" }, tool.tutorial?.intro ?? tool.about),
|
|
107
|
-
el("ol", { class: "mvb-help__steps" },
|
|
108
|
-
...(tool.tutorial?.steps ?? []).map((step) => el("li", {}, step))),
|
|
109
|
-
];
|
|
110
143
|
modal({
|
|
111
144
|
label: `${tool.name} how to use it`,
|
|
112
|
-
body
|
|
145
|
+
body: [
|
|
146
|
+
modalHead(tool.icon, tool.name),
|
|
147
|
+
el("p", {}, tool.tutorial?.intro ?? tool.about),
|
|
148
|
+
el(
|
|
149
|
+
"ol",
|
|
150
|
+
{ class: "m-0 grid list-none gap-2.5 p-0 [counter-reset:step] [&>li]:grid [&>li]:grid-cols-[22px_1fr] [&>li]:items-start [&>li]:gap-2 [&>li]:text-sm [&>li]:text-ink [&>li]:[counter-increment:step] [&>li::before]:grid [&>li::before]:size-[22px] [&>li::before]:place-items-center [&>li::before]:rounded-full [&>li::before]:bg-jade-soft [&>li::before]:text-2xs [&>li::before]:font-bold [&>li::before]:tabular-nums [&>li::before]:text-jade [&>li::before]:content-[counter(step)]" },
|
|
151
|
+
...(tool.tutorial?.steps ?? []).map((step) => el("li", {}, step))
|
|
152
|
+
),
|
|
153
|
+
],
|
|
113
154
|
actions: [
|
|
114
|
-
el("a", { class:
|
|
155
|
+
el("a", { class: LINK_FILL, href: toolHref(tool) },
|
|
115
156
|
icon("play"), el("span", {}, `Open ${tool.name}`)),
|
|
116
157
|
],
|
|
117
|
-
veilClass: "mvb-veil--help",
|
|
118
158
|
});
|
|
119
159
|
}
|
|
120
160
|
|
|
@@ -122,15 +162,14 @@ function openDonate() {
|
|
|
122
162
|
const close = modal({
|
|
123
163
|
label: "Support mevebu",
|
|
124
164
|
body: [
|
|
125
|
-
|
|
126
|
-
el("h2", { class: "mvb-modal__title" }, "Keep mevebu flying"),
|
|
165
|
+
modalHead("coffee", "Keep mevebu flying"),
|
|
127
166
|
el("p", {}, "mevebu is free and runs entirely in your browser. No accounts, no ads, nothing to sell."),
|
|
128
167
|
el("p", {}, "If a tool here saved you time, a coffee keeps the next one coming."),
|
|
129
168
|
],
|
|
130
169
|
actions: [
|
|
131
|
-
el("a", { class: "mvb-btn mvb-btn--fill", href: DONATE_URL, target: "_blank", rel: "noopener" },
|
|
132
|
-
icon("coffee"), el("span", {}, "Buy me a coffee")),
|
|
133
170
|
button("Not now", { onclick: () => close() }),
|
|
171
|
+
el("a", { class: LINK_FILL, href: DONATE_URL, target: "_blank", rel: "noopener" },
|
|
172
|
+
icon("coffee"), el("span", {}, "Buy me a coffee")),
|
|
134
173
|
],
|
|
135
174
|
});
|
|
136
175
|
}
|
|
@@ -142,33 +181,35 @@ function openUpdate(latest) {
|
|
|
142
181
|
|
|
143
182
|
const commandBox = el(
|
|
144
183
|
"div",
|
|
145
|
-
{ class: "
|
|
146
|
-
el("code", { class: "
|
|
184
|
+
{ class: "mt-2.5 flex flex-wrap items-center gap-2 rounded-r2 border border-orbit bg-deck-2 py-2.5 pe-2.5 ps-4" },
|
|
185
|
+
el("code", { class: "min-w-0 flex-1 font-mono text-sm [overflow-wrap:anywhere]" }, command),
|
|
147
186
|
copyButton(() => command, "Copy command", "fill")
|
|
148
187
|
);
|
|
149
188
|
|
|
189
|
+
const versionRow = (label, value, isNew) =>
|
|
190
|
+
el(
|
|
191
|
+
"div",
|
|
192
|
+
{ class: "flex items-center justify-between gap-3 px-4 py-3 [&:not(:first-child)]:border-t [&:not(:first-child)]:border-orbit" },
|
|
193
|
+
el("span", { class: "text-sm text-ink-3" }, label),
|
|
194
|
+
el("span", { class: `font-mono text-sm font-semibold ${isNew ? "text-jade" : ""}` }, value)
|
|
195
|
+
);
|
|
196
|
+
|
|
150
197
|
modal({
|
|
151
198
|
label: "Update mevebu",
|
|
152
199
|
body: [
|
|
153
|
-
|
|
154
|
-
el("h2", { class: "mvb-modal__title" }, updatable ? `mevebu ${latest} is out` : "mevebu is up to date"),
|
|
200
|
+
modalHead("circle-arrow-up", updatable ? `mevebu ${latest} is out` : "mevebu is up to date"),
|
|
155
201
|
updatable
|
|
156
|
-
? el("p", {}, "A newer version is published. Update your global install, then run ", el("code", {}, PACKAGE_NAME), " again.")
|
|
202
|
+
? el("p", {}, "A newer version is published. Update your global install, then run ", el("code", { class: "rounded-[4px] bg-deck-2 px-[5px] py-px font-mono text-[0.92em]" }, PACKAGE_NAME), " again.")
|
|
157
203
|
: el("p", {}, "You are on the latest version. Nothing to do."),
|
|
158
|
-
el("div", { class: "
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
el("div", { class: "mvb-vers__row" + (updatable ? " mvb-vers__row--new" : "") },
|
|
163
|
-
el("span", { class: "mvb-vers__label" }, "Latest"),
|
|
164
|
-
el("span", { class: "mvb-vers__val" }, latest ?? "-"))),
|
|
165
|
-
updatable ? el("p", { class: "mvb-vers__hint" }, "Copy the command below and run it in your terminal.") : null,
|
|
204
|
+
el("div", { class: "mt-[18px] overflow-hidden rounded-r2 border border-orbit" },
|
|
205
|
+
versionRow("Installed", VERSION, false),
|
|
206
|
+
versionRow("Latest", latest ?? "-", Boolean(updatable))),
|
|
207
|
+
updatable ? el("p", { class: "mt-3.5 text-sm" }, "Copy the command below and run it in your terminal.") : null,
|
|
166
208
|
updatable ? commandBox : null,
|
|
167
209
|
],
|
|
168
210
|
actions: updatable
|
|
169
|
-
? [el("a", { class:
|
|
211
|
+
? [el("a", { class: LINK_PLAIN, href: `${REPO_URL}/releases/tag/v${latest}`, target: "_blank", rel: "noopener" }, "What changed")]
|
|
170
212
|
: [],
|
|
171
|
-
veilClass: "mvb-veil--help",
|
|
172
213
|
});
|
|
173
214
|
}
|
|
174
215
|
|
|
@@ -177,7 +218,7 @@ function openUpdate(latest) {
|
|
|
177
218
|
// The footer chip shows the current version. When npm reports a newer one, the
|
|
178
219
|
// chip turns into an "update" button and the modal explains how to upgrade.
|
|
179
220
|
async function startVersionChip() {
|
|
180
|
-
const chip = $("#
|
|
221
|
+
const chip = $("#version");
|
|
181
222
|
if (!chip) return;
|
|
182
223
|
chip.textContent = `v${VERSION}`;
|
|
183
224
|
|
|
@@ -191,7 +232,7 @@ async function startVersionChip() {
|
|
|
191
232
|
|
|
192
233
|
const outdated = latest && compareVersions(latest, VERSION) > 0;
|
|
193
234
|
if (outdated) {
|
|
194
|
-
chip.
|
|
235
|
+
chip.dataset.new = "";
|
|
195
236
|
chip.setAttribute("aria-label", `Update available: version ${latest}`);
|
|
196
237
|
chip.replaceChildren(icon("circle-arrow-up"), el("span", {}, `Update to v${latest}`));
|
|
197
238
|
}
|
|
@@ -211,70 +252,71 @@ async function startVersionChip() {
|
|
|
211
252
|
function footer() {
|
|
212
253
|
const donate = button("Donate", {
|
|
213
254
|
icon: "coffee",
|
|
255
|
+
kind: "quiet",
|
|
214
256
|
onclick: (event) => {
|
|
215
257
|
event.preventDefault();
|
|
216
258
|
openDonate();
|
|
217
259
|
},
|
|
218
260
|
});
|
|
219
261
|
|
|
262
|
+
const FOOT_LINK = "text-sm text-ink-2 no-underline hover:text-jade";
|
|
263
|
+
const group = (title, links) =>
|
|
264
|
+
el(
|
|
265
|
+
"div",
|
|
266
|
+
{},
|
|
267
|
+
el("h4", { class: "m-0 mb-2.5 text-2xs font-bold tracking-[0.08em] text-ink-3 uppercase" }, title),
|
|
268
|
+
el("ul", { class: "m-0 grid list-none gap-1.5 p-0" }, ...links.map((link) => el("li", {}, link)))
|
|
269
|
+
);
|
|
270
|
+
|
|
220
271
|
return el(
|
|
221
272
|
"footer",
|
|
222
|
-
{ class: "
|
|
273
|
+
{ class: "mt-[clamp(40px,8vw,88px)] border-t border-orbit bg-deck" },
|
|
223
274
|
el(
|
|
224
275
|
"div",
|
|
225
|
-
{ class: "
|
|
276
|
+
{ class: "mx-[var(--margin)] pt-[clamp(30px,5vw,48px)] pb-7" },
|
|
226
277
|
el(
|
|
227
278
|
"div",
|
|
228
|
-
{ class: "
|
|
279
|
+
{ class: "flex flex-wrap items-start justify-between gap-[clamp(24px,5vw,64px)]" },
|
|
229
280
|
el(
|
|
230
281
|
"div",
|
|
231
|
-
{ class: "
|
|
232
|
-
el("a", { class: "
|
|
233
|
-
el("img", { src: "./icon.png", alt: "", width: "34", height: "34" }), "mevebu"),
|
|
234
|
-
el("p", { class: "
|
|
282
|
+
{ class: "grid gap-1.5" },
|
|
283
|
+
el("a", { class: "inline-flex items-center gap-[9px] text-md font-bold tracking-[-0.01em] text-ink no-underline", href: "#/" },
|
|
284
|
+
el("img", { class: "block size-[34px] rounded-[11px] shadow-[0_0_0_1px_var(--orbit)]", src: "./icon.png", alt: "", width: "34", height: "34" }), "mevebu"),
|
|
285
|
+
el("p", { class: "m-0 max-w-[30ch] text-sm leading-[1.5] text-ink-3" }, "Small tools that run in your browser. No accounts, no uploads, no tracking.")
|
|
235
286
|
),
|
|
236
287
|
el(
|
|
237
288
|
"nav",
|
|
238
|
-
{ class: "
|
|
239
|
-
|
|
240
|
-
"
|
|
241
|
-
{},
|
|
242
|
-
el("
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
)
|
|
249
|
-
el(
|
|
250
|
-
"div",
|
|
251
|
-
{},
|
|
252
|
-
el("h4", {}, "Project"),
|
|
253
|
-
el("ul", {},
|
|
254
|
-
el("li", {}, el("a", { href: "#/about" }, "About")),
|
|
255
|
-
el("li", {}, el("a", { href: REPO_URL, target: "_blank", rel: "noopener" }, "GitHub")),
|
|
256
|
-
el("li", {}, donate)
|
|
257
|
-
)
|
|
258
|
-
)
|
|
289
|
+
{ class: "flex flex-wrap gap-[clamp(28px,6vw,72px)]", "aria-label": "Site" },
|
|
290
|
+
group("Browse", [
|
|
291
|
+
el("a", { class: FOOT_LINK, href: "#/tools" }, "All tools"),
|
|
292
|
+
el("a", { class: FOOT_LINK, href: "#/categories" }, "Categories"),
|
|
293
|
+
el("a", { class: FOOT_LINK, href: "#/favorites" }, "Favorites"),
|
|
294
|
+
]),
|
|
295
|
+
group("Project", [
|
|
296
|
+
el("a", { class: FOOT_LINK, href: "#/about" }, "About"),
|
|
297
|
+
el("a", { class: FOOT_LINK, href: REPO_URL, target: "_blank", rel: "noopener" }, "GitHub"),
|
|
298
|
+
donate,
|
|
299
|
+
])
|
|
259
300
|
)
|
|
260
301
|
),
|
|
261
302
|
el(
|
|
262
303
|
"div",
|
|
263
|
-
{ class: "
|
|
264
|
-
el("p", { class: "
|
|
304
|
+
{ class: "mt-[clamp(26px,4vw,44px)] flex flex-wrap items-center gap-x-5 gap-y-2 border-t border-orbit pt-5 text-2xs text-ink-3" },
|
|
305
|
+
el("p", { class: "m-0 flex items-center gap-2" }, "MIT licensed."),
|
|
265
306
|
el(
|
|
266
307
|
"p",
|
|
267
|
-
{ class: "
|
|
308
|
+
{ class: "m-0 flex items-center gap-2" },
|
|
268
309
|
"Source: ",
|
|
269
|
-
el("a", { href: REPO_URL, target: "_blank", rel: "noopener" }, "github.com/mevebu/mevebu"),
|
|
270
|
-
el("span", { class: "
|
|
310
|
+
el("a", { class: FOOT_LINK, href: REPO_URL, target: "_blank", rel: "noopener" }, "github.com/mevebu/mevebu"),
|
|
311
|
+
el("span", { class: "inline-flex items-center gap-1 text-2xs text-ink-3", id: "starcount" })
|
|
271
312
|
),
|
|
272
313
|
el(
|
|
273
314
|
"p",
|
|
274
|
-
{ class: "
|
|
315
|
+
{ class: "m-0 flex items-center gap-2" },
|
|
275
316
|
el("button", {
|
|
276
|
-
class:
|
|
277
|
-
|
|
317
|
+
class:
|
|
318
|
+
"inline-flex min-h-7 cursor-pointer items-center gap-1.5 rounded-full border border-orbit bg-transparent px-2.5 py-[3px] font-sans text-2xs tabular-nums text-ink-3 transition-colors duration-150 hover:border-orbit-hard hover:text-ink data-[new]:border-jade data-[new]:bg-jade-soft data-[new]:font-semibold data-[new]:text-jade data-[new]:hover:bg-jade data-[new]:hover:text-on-jade",
|
|
319
|
+
id: "version",
|
|
278
320
|
type: "button",
|
|
279
321
|
title: "Check for updates",
|
|
280
322
|
}, "-")
|
|
@@ -291,7 +333,8 @@ function starButton(tool, onToggle) {
|
|
|
291
333
|
const btn = el(
|
|
292
334
|
"button",
|
|
293
335
|
{
|
|
294
|
-
class:
|
|
336
|
+
class:
|
|
337
|
+
"me-2 inline-flex size-11 flex-none cursor-pointer items-center justify-center rounded-r1 border-0 bg-transparent text-orbit-hard transition-[color,transform] duration-150 hover:text-ink-2 active:scale-[0.88] data-[on]:text-star data-[on]:[&_svg]:fill-star",
|
|
295
338
|
type: "button",
|
|
296
339
|
"aria-pressed": String(on),
|
|
297
340
|
"aria-label": `${tool.name}: ${on ? "remove from favorites" : "add to favorites"}`,
|
|
@@ -299,13 +342,15 @@ function starButton(tool, onToggle) {
|
|
|
299
342
|
event.preventDefault();
|
|
300
343
|
event.stopPropagation();
|
|
301
344
|
const now = toggleFavorite(tool.id);
|
|
302
|
-
btn.
|
|
345
|
+
if (now) btn.dataset.on = "";
|
|
346
|
+
else delete btn.dataset.on;
|
|
303
347
|
btn.setAttribute("aria-pressed", String(now));
|
|
304
348
|
onToggle?.(now);
|
|
305
349
|
},
|
|
306
350
|
},
|
|
307
351
|
icon("star")
|
|
308
352
|
);
|
|
353
|
+
if (on) btn.dataset.on = "";
|
|
309
354
|
return btn;
|
|
310
355
|
}
|
|
311
356
|
|
|
@@ -314,18 +359,19 @@ function toolCard(tool) {
|
|
|
314
359
|
return el(
|
|
315
360
|
"a",
|
|
316
361
|
{
|
|
317
|
-
class:
|
|
362
|
+
class:
|
|
363
|
+
"relative grid gap-1.5 rounded-r2 border border-orbit bg-deck p-5 no-underline transition-colors duration-150 hover:border-jade",
|
|
318
364
|
href: toolHref(tool),
|
|
319
|
-
dataset: { haystack: `${tool.name} ${tool.note} ${tool.keywords ?? ""}`.toLowerCase() },
|
|
365
|
+
dataset: { tool: "", haystack: `${tool.name} ${tool.note} ${tool.keywords ?? ""}`.toLowerCase() },
|
|
320
366
|
},
|
|
321
367
|
el(
|
|
322
368
|
"div",
|
|
323
|
-
{ class: "
|
|
324
|
-
el("span", { class: "
|
|
325
|
-
el("span", { class: "
|
|
369
|
+
{ class: "flex items-center gap-3" },
|
|
370
|
+
el("span", { class: "grid size-[42px] flex-none place-items-center rounded-[13px] bg-jade-soft text-jade [&_svg]:size-[21px]" }, icon(tool.icon)),
|
|
371
|
+
el("span", { class: "text-md font-semibold tracking-[-0.01em]" }, tool.name)
|
|
326
372
|
),
|
|
327
|
-
el("span", { class: "
|
|
328
|
-
el("span", { class: "
|
|
373
|
+
el("span", { class: "min-h-11 text-pretty text-sm text-ink-2" }, tool.note),
|
|
374
|
+
el("span", { class: "mt-1.5 flex items-center gap-2 text-2xs font-semibold text-ink-3" }, category?.name ?? "")
|
|
329
375
|
);
|
|
330
376
|
}
|
|
331
377
|
|
|
@@ -333,14 +379,14 @@ function toolRow(tool, onToggle) {
|
|
|
333
379
|
return el(
|
|
334
380
|
"li",
|
|
335
381
|
{
|
|
336
|
-
class: "
|
|
337
|
-
dataset: { href: toolHref(tool), haystack: `${tool.name} ${tool.note} ${tool.keywords ?? ""}`.toLowerCase() },
|
|
382
|
+
class: "flex items-center border-b border-orbit last:border-b-0",
|
|
383
|
+
dataset: { tool: "", href: toolHref(tool), haystack: `${tool.name} ${tool.note} ${tool.keywords ?? ""}`.toLowerCase() },
|
|
338
384
|
},
|
|
339
385
|
el(
|
|
340
386
|
"a",
|
|
341
|
-
{ class: "
|
|
342
|
-
el("span", { class: "
|
|
343
|
-
el("span", { class: "
|
|
387
|
+
{ class: "grid min-w-0 flex-1 gap-px py-[13px] pe-1.5 ps-[18px] no-underline", href: toolHref(tool) },
|
|
388
|
+
el("span", { class: "text-md font-semibold tracking-[-0.01em]" }, tool.name),
|
|
389
|
+
el("span", { class: "text-sm text-ink-2" }, tool.note)
|
|
344
390
|
),
|
|
345
391
|
starButton(tool, onToggle)
|
|
346
392
|
);
|
|
@@ -348,7 +394,8 @@ function toolRow(tool, onToggle) {
|
|
|
348
394
|
|
|
349
395
|
function filterBar(placeholder = "Search tools") {
|
|
350
396
|
const input = el("input", {
|
|
351
|
-
class: "
|
|
397
|
+
class: "h-[calc(var(--ctl)+12px)] min-w-0 flex-1 border-0 bg-transparent text-md placeholder:text-ink-3 focus:outline-none",
|
|
398
|
+
dataset: { filter: "" },
|
|
352
399
|
type: "search",
|
|
353
400
|
placeholder,
|
|
354
401
|
"aria-label": placeholder,
|
|
@@ -357,28 +404,32 @@ function filterBar(placeholder = "Search tools") {
|
|
|
357
404
|
});
|
|
358
405
|
return el(
|
|
359
406
|
"form",
|
|
360
|
-
{
|
|
361
|
-
|
|
407
|
+
{
|
|
408
|
+
class: "mt-[clamp(22px,4vw,36px)] flex items-center gap-2.5 rounded-r2 border border-orbit bg-deck px-3.5 transition-colors duration-150 focus-within:border-jade",
|
|
409
|
+
role: "search",
|
|
410
|
+
onsubmit: (event) => event.preventDefault(),
|
|
411
|
+
},
|
|
412
|
+
el("span", { class: "flex text-ink-3" }, icon("search")),
|
|
362
413
|
input,
|
|
363
|
-
el("kbd", { class:
|
|
414
|
+
el("kbd", { class: KBD }, "/")
|
|
364
415
|
);
|
|
365
416
|
}
|
|
366
417
|
|
|
367
|
-
/** Live filters
|
|
418
|
+
/** Live filters the `[data-tool]` cards and rows already in the tree. */
|
|
368
419
|
function wireFilter(root, meta) {
|
|
369
|
-
const input = root.querySelector("
|
|
420
|
+
const input = root.querySelector("[data-filter]");
|
|
370
421
|
if (!input) return;
|
|
371
422
|
const run = () => {
|
|
372
423
|
const query = input.value.trim().toLowerCase();
|
|
373
424
|
let shown = 0;
|
|
374
|
-
for (const item of root.querySelectorAll("
|
|
425
|
+
for (const item of root.querySelectorAll("[data-tool]")) {
|
|
375
426
|
const hay = item.dataset.haystack ?? item.textContent.toLowerCase();
|
|
376
427
|
const hit = !query || hay.includes(query);
|
|
377
428
|
item.hidden = !hit;
|
|
378
429
|
if (hit) shown++;
|
|
379
430
|
}
|
|
380
|
-
for (const section of root.querySelectorAll("[data-
|
|
381
|
-
const any = [...section.querySelectorAll("
|
|
431
|
+
for (const section of root.querySelectorAll("[data-group]")) {
|
|
432
|
+
const any = [...section.querySelectorAll("[data-tool]")].some((item) => !item.hidden);
|
|
382
433
|
section.hidden = !any;
|
|
383
434
|
}
|
|
384
435
|
meta.textContent = query ? `${shown} of ${TOOLS.length} tools` : `${TOOLS.length} tools`;
|
|
@@ -386,7 +437,7 @@ function wireFilter(root, meta) {
|
|
|
386
437
|
input.addEventListener("input", run);
|
|
387
438
|
input.addEventListener("keydown", (event) => {
|
|
388
439
|
if (event.key !== "Enter") return;
|
|
389
|
-
const first = [...root.querySelectorAll("
|
|
440
|
+
const first = [...root.querySelectorAll("[data-tool]")].find((item) => !item.hidden);
|
|
390
441
|
if (first) {
|
|
391
442
|
const href = first.tagName === "A" ? first.getAttribute("href") : first.dataset.href;
|
|
392
443
|
go(href.slice(1));
|
|
@@ -400,23 +451,23 @@ function wireFilter(root, meta) {
|
|
|
400
451
|
function homePage() {
|
|
401
452
|
const hero = el(
|
|
402
453
|
"section",
|
|
403
|
-
{ class: "
|
|
454
|
+
{ class: "relative overflow-hidden border-b border-orbit" },
|
|
404
455
|
el(
|
|
405
456
|
"div",
|
|
406
|
-
{ class: "
|
|
457
|
+
{ class: "mx-[var(--margin)] grid items-center gap-[clamp(24px,5vw,48px)] py-[clamp(36px,7vw,68px)] min-[60rem]:grid-cols-[minmax(0,1fr)_auto]" },
|
|
407
458
|
el(
|
|
408
459
|
"div",
|
|
409
460
|
{},
|
|
410
|
-
el("h1", { class: "
|
|
461
|
+
el("h1", { class: "max-w-[20ch] text-balance text-hero font-bold tracking-[-0.03em]" }, "Small tools, one page each."),
|
|
411
462
|
el(
|
|
412
463
|
"div",
|
|
413
|
-
{ class: "
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
el("a", { class: "mvb-btn mvb-btn--plain", href: "#/tools" }, icon("shuffle"), el("span", {}, "Browse all"))
|
|
464
|
+
{ class: "mt-[clamp(22px,3vw,30px)] flex flex-wrap gap-2.5" },
|
|
465
|
+
button("Search tools", { icon: "search", kind: "fill", onclick: () => openPalette() }),
|
|
466
|
+
el("a", { class: LINK_PLAIN, href: "#/tools" }, icon("shuffle"), el("span", {}, "Browse all"))
|
|
417
467
|
)
|
|
418
468
|
),
|
|
419
|
-
el("div", { class: "
|
|
469
|
+
el("div", { class: "grid place-items-center rounded-[28px] border border-orbit bg-deck-2 p-6 max-[60rem]:hidden" },
|
|
470
|
+
el("img", { class: "block size-[220px]", src: "./icon.png", alt: "", width: "220", height: "220" }))
|
|
420
471
|
)
|
|
421
472
|
);
|
|
422
473
|
|
|
@@ -425,29 +476,28 @@ function homePage() {
|
|
|
425
476
|
"uuid-generator", "base64", "name-picker",
|
|
426
477
|
].map((id) => TOOLS.find((tool) => tool.id === id)).filter(Boolean);
|
|
427
478
|
|
|
428
|
-
const
|
|
479
|
+
const section = (title, iconName, more, tools, group) =>
|
|
429
480
|
el(
|
|
430
481
|
"section",
|
|
431
|
-
{ class: "
|
|
482
|
+
{ class: "pt-[clamp(32px,5vw,60px)]", dataset: { group } },
|
|
432
483
|
el(
|
|
433
484
|
"div",
|
|
434
|
-
{ class: "
|
|
435
|
-
el("h2", { class: "
|
|
436
|
-
|
|
485
|
+
{ class: "mb-4 flex items-baseline gap-3" },
|
|
486
|
+
el("h2", { class: "flex items-center gap-2.5 text-lg font-bold tracking-[-0.02em]" }, icon(iconName, "size-[22px] flex-none text-jade"), title),
|
|
487
|
+
more
|
|
437
488
|
),
|
|
438
|
-
el("div", { class: "
|
|
439
|
-
)
|
|
489
|
+
el("div", { class: "grid grid-cols-1 gap-[var(--gutter)] sm:grid-cols-2 lg:grid-cols-3" }, ...tools.map(toolCard))
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
const grid = el("div", { class: "px-[var(--margin)]" },
|
|
493
|
+
section("Popular", "star", el("a", { class: "ms-auto text-sm font-semibold text-jade no-underline hover:underline", href: "#/tools" }, "All tools"), popular, "popular"),
|
|
440
494
|
...CATEGORIES.slice(0, 4).map((category) =>
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
el(
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
el("h2", { class: "mvb-section__title" }, icon(category.icon), category.name),
|
|
448
|
-
el("a", { class: "mvb-section__more", href: `#/${category.id}` }, `Open ${category.name.toLowerCase()}`)
|
|
449
|
-
),
|
|
450
|
-
el("div", { class: "mvb-grid" }, ...toolsInCategory(category.id).map(toolCard))
|
|
495
|
+
section(
|
|
496
|
+
category.name,
|
|
497
|
+
category.icon,
|
|
498
|
+
el("a", { class: "ms-auto text-sm font-semibold text-jade no-underline hover:underline", href: `#/${category.id}` }, `Open ${category.name.toLowerCase()}`),
|
|
499
|
+
toolsInCategory(category.id).slice(0, 3),
|
|
500
|
+
category.id
|
|
451
501
|
)
|
|
452
502
|
)
|
|
453
503
|
);
|
|
@@ -457,28 +507,29 @@ function homePage() {
|
|
|
457
507
|
}
|
|
458
508
|
|
|
459
509
|
function toolsPage() {
|
|
460
|
-
const meta = el("p", {}, `${TOOLS.length} tools`);
|
|
510
|
+
const meta = el("p", { class: "m-0" }, `${TOOLS.length} tools`);
|
|
461
511
|
|
|
462
512
|
const groups = CATEGORIES.map((category) =>
|
|
463
513
|
el(
|
|
464
514
|
"section",
|
|
465
|
-
{ class:
|
|
515
|
+
{ class: SECTION, dataset: { group: category.id } },
|
|
466
516
|
el(
|
|
467
517
|
"div",
|
|
468
|
-
{ class:
|
|
469
|
-
el("h2", { class:
|
|
470
|
-
category.name, el("span", { class:
|
|
471
|
-
el("a", { class:
|
|
518
|
+
{ class: SECTION_HEAD },
|
|
519
|
+
el("h2", { class: SECTION_TITLE }, icon(category.icon, "size-[22px] flex-none text-jade"),
|
|
520
|
+
category.name, el("span", { class: SECTION_COUNT }, toolsInCategory(category.id).length)),
|
|
521
|
+
el("a", { class: SECTION_MORE, href: `#/${category.id}` }, "Open")
|
|
472
522
|
),
|
|
473
|
-
el("div", { class:
|
|
523
|
+
el("div", { class: GRID }, ...toolsInCategory(category.id).map(toolCard))
|
|
474
524
|
)
|
|
475
525
|
);
|
|
476
526
|
|
|
477
527
|
const page = el(
|
|
478
528
|
"div",
|
|
479
|
-
{ class:
|
|
529
|
+
{ class: PAGE },
|
|
530
|
+
el("h1", { class: LEAD }, "All tools."),
|
|
480
531
|
filterBar("Search here"),
|
|
481
|
-
el("div", { class: "
|
|
532
|
+
el("div", { class: "mt-2.5 flex min-h-[22px] items-baseline gap-3.5 text-sm text-ink-3" }, meta),
|
|
482
533
|
...groups
|
|
483
534
|
);
|
|
484
535
|
wireFilter(page, meta);
|
|
@@ -489,19 +540,22 @@ function toolsPage() {
|
|
|
489
540
|
function categoriesPage() {
|
|
490
541
|
const grid = el(
|
|
491
542
|
"div",
|
|
492
|
-
{ class:
|
|
543
|
+
{ class: GRID },
|
|
493
544
|
...CATEGORIES.map((category) =>
|
|
494
545
|
el(
|
|
495
546
|
"a",
|
|
496
|
-
{
|
|
547
|
+
{
|
|
548
|
+
class: "relative grid gap-1.5 rounded-r2 border border-orbit bg-deck p-5 no-underline transition-colors duration-150 hover:border-jade",
|
|
549
|
+
href: `#/${category.id}`,
|
|
550
|
+
},
|
|
497
551
|
el(
|
|
498
552
|
"div",
|
|
499
|
-
{ class: "
|
|
500
|
-
el("span", { class: "
|
|
501
|
-
el("span", { class: "
|
|
553
|
+
{ class: "flex items-center gap-3" },
|
|
554
|
+
el("span", { class: "grid size-[42px] flex-none place-items-center rounded-[13px] bg-jade-soft text-jade [&_svg]:size-[21px]" }, icon(category.icon)),
|
|
555
|
+
el("span", { class: "text-md font-semibold tracking-[-0.01em]" }, category.name)
|
|
502
556
|
),
|
|
503
|
-
el("span", { class: "
|
|
504
|
-
el("span", { class: "
|
|
557
|
+
el("span", { class: "min-h-11 text-pretty text-sm text-ink-2" }, `${toolsInCategory(category.id).length} tools inside.`),
|
|
558
|
+
el("span", { class: "mt-1.5 flex items-center gap-2 text-2xs font-semibold text-ink-3" }, "Open category")
|
|
505
559
|
)
|
|
506
560
|
)
|
|
507
561
|
);
|
|
@@ -509,9 +563,9 @@ function categoriesPage() {
|
|
|
509
563
|
document.title = "Categories · mevebu";
|
|
510
564
|
return el(
|
|
511
565
|
"div",
|
|
512
|
-
{ class:
|
|
513
|
-
el("h1", { class:
|
|
514
|
-
el("section", { class:
|
|
566
|
+
{ class: PAGE_NARROW },
|
|
567
|
+
el("h1", { class: LEAD }, "Browse by category."),
|
|
568
|
+
el("section", { class: SECTION }, grid)
|
|
515
569
|
);
|
|
516
570
|
}
|
|
517
571
|
|
|
@@ -521,15 +575,15 @@ function favoritesPage() {
|
|
|
521
575
|
document.title = "Favorites · mevebu";
|
|
522
576
|
return el(
|
|
523
577
|
"div",
|
|
524
|
-
{ class:
|
|
525
|
-
el("h1", { class:
|
|
578
|
+
{ class: PAGE_NARROW },
|
|
579
|
+
el("h1", { class: LEAD }, saved.length ? "Your favorites." : "No favorites yet."),
|
|
526
580
|
saved.length
|
|
527
|
-
? el("p", { class:
|
|
528
|
-
: el("p", { class:
|
|
581
|
+
? el("p", { class: LEDE }, "Tap the star on any tool to add or remove it. Saved in this browser only.")
|
|
582
|
+
: el("p", { class: LEDE },
|
|
529
583
|
"Tap the star on any tool to keep it here. ",
|
|
530
|
-
el("a", { class:
|
|
531
|
-
el("section", { class:
|
|
532
|
-
el("ul", { class: "
|
|
584
|
+
el("a", { class: TEXT_LINK, href: "#/tools" }, "Browse the tools")),
|
|
585
|
+
el("section", { class: SECTION },
|
|
586
|
+
el("ul", { class: "m-0 grid list-none overflow-clip rounded-r2 border border-orbit bg-deck p-0" }, ...saved.map((tool) => toolRow(tool, rerenderFavorites))))
|
|
533
587
|
);
|
|
534
588
|
}
|
|
535
589
|
|
|
@@ -538,11 +592,11 @@ function categoryPage(category) {
|
|
|
538
592
|
document.title = `${category.name} · mevebu`;
|
|
539
593
|
return el(
|
|
540
594
|
"div",
|
|
541
|
-
{ class:
|
|
542
|
-
el("h1", { class:
|
|
543
|
-
el("p", { class:
|
|
544
|
-
el("section", { class:
|
|
545
|
-
el("div", { class:
|
|
595
|
+
{ class: PAGE_NARROW },
|
|
596
|
+
el("h1", { class: LEAD }, category.name),
|
|
597
|
+
el("p", { class: LEDE }, `${tools.length} tools. Pick a card to open it.`),
|
|
598
|
+
el("section", { class: SECTION },
|
|
599
|
+
el("div", { class: GRID }, ...tools.map(toolCard)))
|
|
546
600
|
);
|
|
547
601
|
}
|
|
548
602
|
|
|
@@ -559,7 +613,8 @@ const rerenderFavorites = () => {
|
|
|
559
613
|
function benchHead(tool, category) {
|
|
560
614
|
const parent = category ? { href: `#/${category.id}`, label: category.name } : { href: "#/tools", label: "Tools" };
|
|
561
615
|
const helpButton = el("button", {
|
|
562
|
-
class:
|
|
616
|
+
class:
|
|
617
|
+
"inline-flex size-[var(--ctl)] flex-none cursor-pointer items-center justify-center rounded-full border border-orbit bg-deck-2 font-sans text-base font-bold text-ink-2 transition-colors duration-150 hover:border-orbit-hard hover:bg-deck hover:text-ink",
|
|
563
618
|
type: "button",
|
|
564
619
|
"aria-label": `How to use ${tool.name}`,
|
|
565
620
|
"aria-haspopup": "dialog",
|
|
@@ -568,19 +623,15 @@ function benchHead(tool, category) {
|
|
|
568
623
|
}, "?");
|
|
569
624
|
return el(
|
|
570
625
|
"div",
|
|
571
|
-
{ class: "
|
|
572
|
-
el("a", { class: "
|
|
573
|
-
el("div", { class: "
|
|
574
|
-
el("h1", { class: "
|
|
575
|
-
el("div", { class: "
|
|
576
|
-
el("p", { class: "
|
|
626
|
+
{ class: "mt-[clamp(24px,4vw,44px)] mb-[clamp(22px,3vw,32px)]" },
|
|
627
|
+
el("a", { class: "mb-4 inline-flex items-center gap-[7px] text-sm font-medium text-ink-2 no-underline hover:text-ink", href: parent.href }, icon("arrow-left"), parent.label),
|
|
628
|
+
el("div", { class: "flex items-start gap-2" },
|
|
629
|
+
el("h1", { class: "flex-1 text-balance text-title font-bold tracking-[-0.025em]" }, tool.name),
|
|
630
|
+
el("div", { class: "flex items-center gap-0.5 pt-1" }, helpButton, starButton(tool))),
|
|
631
|
+
el("p", { class: "mt-2 mb-0 max-w-[58ch] text-pretty text-md text-ink-2" }, tool.about)
|
|
577
632
|
);
|
|
578
633
|
}
|
|
579
634
|
|
|
580
|
-
function toolFailed(tool) {
|
|
581
|
-
return el("div", { class: "mvb-blank" }, icon("circle-alert", "mvb-ico mvb-ico--lg"), el("p", {}, `${tool.name} could not be loaded.`));
|
|
582
|
-
}
|
|
583
|
-
|
|
584
635
|
function moreFromMev(tool) {
|
|
585
636
|
const sameCat = toolsInCategory(tool.category).filter((t) => t.id !== tool.id);
|
|
586
637
|
const otherCat = TOOLS.filter((t) => t.id !== tool.id && t.category !== tool.category);
|
|
@@ -592,9 +643,9 @@ function moreFromMev(tool) {
|
|
|
592
643
|
|
|
593
644
|
return el(
|
|
594
645
|
"section",
|
|
595
|
-
{ class: "
|
|
596
|
-
el("h2", { class: "
|
|
597
|
-
el("div", { class:
|
|
646
|
+
{ class: "mt-[clamp(40px,6vw,72px)]" },
|
|
647
|
+
el("h2", { class: "mb-2.5 text-sm font-bold text-ink-3" }, "More from mevebu"),
|
|
648
|
+
el("div", { class: GRID }, ...picks.map((t) => toolCard(t)))
|
|
598
649
|
);
|
|
599
650
|
}
|
|
600
651
|
|
|
@@ -605,7 +656,7 @@ async function toolPage(tool, category) {
|
|
|
605
656
|
|
|
606
657
|
const container = el(
|
|
607
658
|
"div",
|
|
608
|
-
{ class:
|
|
659
|
+
{ class: PAGE_NARROW },
|
|
609
660
|
benchHead(tool, category),
|
|
610
661
|
slot,
|
|
611
662
|
more
|
|
@@ -622,7 +673,7 @@ async function toolPage(tool, category) {
|
|
|
622
673
|
} catch (error) {
|
|
623
674
|
console.error(`[mevebu] ${tool.id} failed`, error);
|
|
624
675
|
if (token !== navToken) return null;
|
|
625
|
-
const failed = toolcard({ body:
|
|
676
|
+
const failed = toolcard({ body: blank("circle-alert", `${tool.name} could not be loaded.`) });
|
|
626
677
|
slot.append(failed.node);
|
|
627
678
|
}
|
|
628
679
|
|
|
@@ -631,28 +682,28 @@ async function toolPage(tool, category) {
|
|
|
631
682
|
|
|
632
683
|
function aboutPage() {
|
|
633
684
|
const block = (title, ...nodes) =>
|
|
634
|
-
el("section", { class:
|
|
685
|
+
el("section", { class: ABOUT_BLOCK }, el("h2", { class: "text-lg font-bold tracking-[-0.01em]" }, title), ...nodes);
|
|
635
686
|
|
|
636
687
|
document.title = "About · mevebu";
|
|
637
688
|
return el(
|
|
638
689
|
"div",
|
|
639
|
-
{ class:
|
|
640
|
-
el("h1", { class:
|
|
641
|
-
el("p", { class:
|
|
690
|
+
{ class: PAGE_NARROW },
|
|
691
|
+
el("h1", { class: LEAD }, "About mevebu"),
|
|
692
|
+
el("p", { class: LEDE },
|
|
642
693
|
`mevebu is ${TOOLS.length} small tools that run in your browser. Each one has its own page and does a single job.`),
|
|
643
694
|
|
|
644
695
|
block("Running it",
|
|
645
696
|
el("p", {}, "Install it once from npm. The command starts a local server and opens your browser at the address it prints."),
|
|
646
|
-
el("pre", { class: "
|
|
647
|
-
el("p", {}, "Serve the ", el("code", {}, "mevebu/"), " folder from any static host. Routes are hash routes, so no rewrite rules are needed.")),
|
|
697
|
+
el("pre", { class: "m-0 overflow-x-auto rounded-r1 border border-orbit bg-deck-2 px-3.5 py-3 font-mono text-sm" }, "npm install -g mevebu\nmevebu"),
|
|
698
|
+
el("p", {}, "Serve the ", el("code", { class: CODE }, "mevebu/"), " folder from any static host. Routes are hash routes, so no rewrite rules are needed.")),
|
|
648
699
|
|
|
649
700
|
block("Adding a tool",
|
|
650
|
-
el("p", {}, "Add one object to ", el("code", {}, "mevebu/libs/js/registry.js"), " and one file in ", el("code", {}, "mevebu/libs/js/tools/"), ". The home page, the index, the categories, search and the route all come from that entry.")),
|
|
701
|
+
el("p", {}, "Add one object to ", el("code", { class: CODE }, "mevebu/libs/js/registry.js"), " and one file in ", el("code", { class: CODE }, "mevebu/libs/js/tools/"), ". The home page, the index, the categories, search and the route all come from that entry.")),
|
|
651
702
|
|
|
652
703
|
block("Built with",
|
|
653
704
|
el("p", {}, "Hand written HTML, CSS and JavaScript, with Tailwind for the stylesheet. Icons are Lucide and the QR decoder is jsQR."),
|
|
654
705
|
el("p", {}, "Source and issues: ",
|
|
655
|
-
el("a", { class:
|
|
706
|
+
el("a", { class: TEXT_LINK, href: REPO_URL, target: "_blank", rel: "noopener" }, "github.com/mevebu/mevebu")),
|
|
656
707
|
el("p", {}, copyButton(() => location.href, "Copy this page link")))
|
|
657
708
|
);
|
|
658
709
|
}
|
|
@@ -661,10 +712,10 @@ function notFound() {
|
|
|
661
712
|
document.title = "Page not found · mevebu";
|
|
662
713
|
return el(
|
|
663
714
|
"div",
|
|
664
|
-
{ class:
|
|
665
|
-
el("h1", { class:
|
|
666
|
-
el("p", { class:
|
|
667
|
-
el("p", {}, el("a", { class:
|
|
715
|
+
{ class: PAGE_NARROW },
|
|
716
|
+
el("h1", { class: LEAD }, "That page is not here."),
|
|
717
|
+
el("p", { class: LEDE }, "Check the address, or pick a tool from the index."),
|
|
718
|
+
el("p", {}, el("a", { class: TEXT_LINK, href: "#/" }, "Go home"))
|
|
668
719
|
);
|
|
669
720
|
}
|
|
670
721
|
|
|
@@ -724,9 +775,10 @@ route("/:category/:id", ({ category, id }) => {
|
|
|
724
775
|
// ---------- boot ----------
|
|
725
776
|
|
|
726
777
|
function markActiveNav(path) {
|
|
727
|
-
for (const link of $$("
|
|
778
|
+
for (const link of $$("a[data-path]")) {
|
|
728
779
|
const active = link.dataset.path === path;
|
|
729
|
-
link.
|
|
780
|
+
if (active) link.dataset.on = "";
|
|
781
|
+
else delete link.dataset.on;
|
|
730
782
|
if (active) link.setAttribute("aria-current", "page");
|
|
731
783
|
else link.removeAttribute("aria-current");
|
|
732
784
|
}
|
|
@@ -750,11 +802,11 @@ async function render(path) {
|
|
|
750
802
|
const found = match(path);
|
|
751
803
|
const node = found ? await found.handler(found.params, path) : notFound();
|
|
752
804
|
if (!node) return;
|
|
753
|
-
$("#
|
|
805
|
+
$("#view").replaceChildren(node);
|
|
754
806
|
markActiveNav(["/", "/tools", "/categories", "/about"].includes(path) ? path : "");
|
|
755
807
|
activePath = path;
|
|
756
808
|
window.scrollTo({ top: restoring ? scrollMemory.get(path) ?? 0 : 0 });
|
|
757
|
-
if (!history.state?.
|
|
809
|
+
if (!history.state?.mevebu) history.replaceState({ mevebu: true }, "", location.href);
|
|
758
810
|
}
|
|
759
811
|
|
|
760
812
|
window.addEventListener("hashchange", () => {
|
|
@@ -762,17 +814,17 @@ window.addEventListener("hashchange", () => {
|
|
|
762
814
|
});
|
|
763
815
|
|
|
764
816
|
// A history move replays the state stored for that entry, which this app tags
|
|
765
|
-
// with
|
|
817
|
+
// with mevebu. A plain hash change carries no such state.
|
|
766
818
|
window.addEventListener("popstate", (event) => {
|
|
767
|
-
historyMove = Boolean(event.state?.
|
|
819
|
+
historyMove = Boolean(event.state?.mevebu);
|
|
768
820
|
});
|
|
769
821
|
|
|
770
822
|
function mountShell() {
|
|
771
|
-
$("#app").append(masthead(), el("main", { class: "
|
|
823
|
+
$("#app").append(masthead(), el("main", { class: "flex-1 outline-none", id: "view", tabindex: "-1" }), footer());
|
|
772
824
|
}
|
|
773
825
|
|
|
774
826
|
function startStarCount() {
|
|
775
|
-
const node = $("#
|
|
827
|
+
const node = $("#starcount");
|
|
776
828
|
if (!node) return;
|
|
777
829
|
const controller = new AbortController();
|
|
778
830
|
const timer = setTimeout(() => controller.abort(), 4000);
|
|
@@ -792,7 +844,7 @@ function globalKeys() {
|
|
|
792
844
|
document.addEventListener("keydown", (event) => {
|
|
793
845
|
const typing = /^(INPUT|TEXTAREA|SELECT)$/.test(event.target.tagName);
|
|
794
846
|
if (event.key === "/" && !typing && !event.metaKey && !event.ctrlKey) {
|
|
795
|
-
const field = $("
|
|
847
|
+
const field = $("[data-filter]");
|
|
796
848
|
if (!field) return;
|
|
797
849
|
event.preventDefault();
|
|
798
850
|
field.focus();
|