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.
Files changed (164) hide show
  1. package/mevebu/app/mevebu.js +260 -208
  2. package/mevebu/app/tailwind.css +1635 -2079
  3. package/mevebu/app/tailwind.src.css +155 -1127
  4. package/mevebu/index.html +2 -2
  5. package/mevebu/libs/js/core.js +235 -95
  6. package/mevebu/libs/js/tools/abstract-checklist.js +5 -7
  7. package/mevebu/libs/js/tools/add-subtract-days.js +9 -11
  8. package/mevebu/libs/js/tools/age-calculator.js +7 -8
  9. package/mevebu/libs/js/tools/api-request-builder.js +3 -4
  10. package/mevebu/libs/js/tools/area-converter.js +7 -9
  11. package/mevebu/libs/js/tools/aspect-ratio.js +8 -9
  12. package/mevebu/libs/js/tools/average-calculator.js +4 -5
  13. package/mevebu/libs/js/tools/base64.js +2 -3
  14. package/mevebu/libs/js/tools/binary-calculator.js +6 -7
  15. package/mevebu/libs/js/tools/border-radius.js +7 -9
  16. package/mevebu/libs/js/tools/box-shadow.js +6 -7
  17. package/mevebu/libs/js/tools/break-even.js +4 -5
  18. package/mevebu/libs/js/tools/business-days.js +7 -8
  19. package/mevebu/libs/js/tools/canonical-tag.js +5 -6
  20. package/mevebu/libs/js/tools/caption-counter.js +5 -7
  21. package/mevebu/libs/js/tools/case-converter.js +10 -11
  22. package/mevebu/libs/js/tools/case-inspector.js +14 -16
  23. package/mevebu/libs/js/tools/cash-denomination.js +7 -8
  24. package/mevebu/libs/js/tools/cash-flow.js +7 -8
  25. package/mevebu/libs/js/tools/cert-expiry.js +5 -7
  26. package/mevebu/libs/js/tools/changelog-generator.js +4 -6
  27. package/mevebu/libs/js/tools/character-counter.js +10 -13
  28. package/mevebu/libs/js/tools/checklist.js +6 -7
  29. package/mevebu/libs/js/tools/citation-number-check.js +10 -13
  30. package/mevebu/libs/js/tools/coin-flip.js +9 -11
  31. package/mevebu/libs/js/tools/color-converter.js +6 -7
  32. package/mevebu/libs/js/tools/color-palette.js +6 -8
  33. package/mevebu/libs/js/tools/color-picker.js +4 -5
  34. package/mevebu/libs/js/tools/content-calendar.js +3 -4
  35. package/mevebu/libs/js/tools/contrast-checker.js +5 -6
  36. package/mevebu/libs/js/tools/cron-builder.js +4 -6
  37. package/mevebu/libs/js/tools/css-clamp.js +5 -6
  38. package/mevebu/libs/js/tools/css-formatter.js +4 -7
  39. package/mevebu/libs/js/tools/css-grid.js +6 -7
  40. package/mevebu/libs/js/tools/css-minifier.js +5 -8
  41. package/mevebu/libs/js/tools/csv-to-json.js +2 -3
  42. package/mevebu/libs/js/tools/csv-viewer.js +9 -11
  43. package/mevebu/libs/js/tools/currency-format.js +5 -7
  44. package/mevebu/libs/js/tools/daily-planner.js +9 -10
  45. package/mevebu/libs/js/tools/data-masker.js +6 -8
  46. package/mevebu/libs/js/tools/data-storage.js +7 -9
  47. package/mevebu/libs/js/tools/data-validator.js +9 -11
  48. package/mevebu/libs/js/tools/date-difference.js +6 -7
  49. package/mevebu/libs/js/tools/date-format.js +11 -13
  50. package/mevebu/libs/js/tools/delimiter-converter.js +3 -5
  51. package/mevebu/libs/js/tools/discount-calculator.js +4 -5
  52. package/mevebu/libs/js/tools/doi-resolver.js +4 -5
  53. package/mevebu/libs/js/tools/env-formatter.js +3 -6
  54. package/mevebu/libs/js/tools/equation-formatter.js +5 -9
  55. package/mevebu/libs/js/tools/expense-split.js +6 -7
  56. package/mevebu/libs/js/tools/fake-data.js +8 -13
  57. package/mevebu/libs/js/tools/find-replace.js +3 -5
  58. package/mevebu/libs/js/tools/flashcard-creator.js +9 -10
  59. package/mevebu/libs/js/tools/font-pairing.js +6 -8
  60. package/mevebu/libs/js/tools/fraction-calculator.js +10 -13
  61. package/mevebu/libs/js/tools/git-command-builder.js +11 -22
  62. package/mevebu/libs/js/tools/gpa-calculator.js +8 -10
  63. package/mevebu/libs/js/tools/grade-calculator.js +7 -8
  64. package/mevebu/libs/js/tools/gradient-generator.js +10 -12
  65. package/mevebu/libs/js/tools/habit-tracker.js +8 -9
  66. package/mevebu/libs/js/tools/hash-generator.js +3 -5
  67. package/mevebu/libs/js/tools/hashtag-formatter.js +6 -8
  68. package/mevebu/libs/js/tools/hmac-generator.js +3 -5
  69. package/mevebu/libs/js/tools/hpp-calculator.js +4 -5
  70. package/mevebu/libs/js/tools/html-entity.js +2 -3
  71. package/mevebu/libs/js/tools/html-formatter.js +3 -6
  72. package/mevebu/libs/js/tools/http-status.js +5 -7
  73. package/mevebu/libs/js/tools/inventory-quantity.js +7 -8
  74. package/mevebu/libs/js/tools/json-formatter.js +3 -6
  75. package/mevebu/libs/js/tools/json-minifier.js +5 -8
  76. package/mevebu/libs/js/tools/json-to-csv.js +3 -5
  77. package/mevebu/libs/js/tools/json-to-typescript.js +2 -5
  78. package/mevebu/libs/js/tools/json-validator.js +2 -3
  79. package/mevebu/libs/js/tools/jwt-decoder.js +6 -7
  80. package/mevebu/libs/js/tools/jwt-expiry.js +5 -6
  81. package/mevebu/libs/js/tools/jwt-generator.js +4 -6
  82. package/mevebu/libs/js/tools/length-converter.js +7 -9
  83. package/mevebu/libs/js/tools/license-reference.js +10 -13
  84. package/mevebu/libs/js/tools/line-counter.js +5 -7
  85. package/mevebu/libs/js/tools/list-difference.js +5 -7
  86. package/mevebu/libs/js/tools/list-merger.js +4 -5
  87. package/mevebu/libs/js/tools/locale-number.js +5 -8
  88. package/mevebu/libs/js/tools/markdown-preview.js +5 -8
  89. package/mevebu/libs/js/tools/markdown-table.js +5 -7
  90. package/mevebu/libs/js/tools/markup-calculator.js +4 -5
  91. package/mevebu/libs/js/tools/meeting-agenda.js +8 -9
  92. package/mevebu/libs/js/tools/meta-preview.js +12 -13
  93. package/mevebu/libs/js/tools/meta-tags.js +6 -7
  94. package/mevebu/libs/js/tools/mime-types.js +5 -7
  95. package/mevebu/libs/js/tools/name-picker.js +2 -3
  96. package/mevebu/libs/js/tools/number-base-converter.js +9 -12
  97. package/mevebu/libs/js/tools/password-generator.js +9 -12
  98. package/mevebu/libs/js/tools/password-strength.js +21 -25
  99. package/mevebu/libs/js/tools/percent-to-fraction.js +6 -7
  100. package/mevebu/libs/js/tools/percentage-calculator.js +6 -9
  101. package/mevebu/libs/js/tools/percentage-change.js +4 -5
  102. package/mevebu/libs/js/tools/permutation-combination.js +9 -12
  103. package/mevebu/libs/js/tools/prefix-suffix.js +2 -3
  104. package/mevebu/libs/js/tools/priority-matrix.js +6 -10
  105. package/mevebu/libs/js/tools/profit-margin.js +4 -5
  106. package/mevebu/libs/js/tools/qr-generator.js +3 -4
  107. package/mevebu/libs/js/tools/qr-scanner.js +11 -12
  108. package/mevebu/libs/js/tools/quadratic-solver.js +7 -8
  109. package/mevebu/libs/js/tools/query-builder.js +2 -3
  110. package/mevebu/libs/js/tools/quiz-formatter.js +4 -6
  111. package/mevebu/libs/js/tools/ratio-calculator.js +7 -8
  112. package/mevebu/libs/js/tools/readability.js +12 -15
  113. package/mevebu/libs/js/tools/reading-time.js +5 -7
  114. package/mevebu/libs/js/tools/reference-completeness.js +6 -8
  115. package/mevebu/libs/js/tools/regex-tester.js +7 -10
  116. package/mevebu/libs/js/tools/remove-duplicate-lines.js +2 -3
  117. package/mevebu/libs/js/tools/remove-extra-spaces.js +3 -5
  118. package/mevebu/libs/js/tools/robots-txt.js +3 -4
  119. package/mevebu/libs/js/tools/roman-numeral-converter.js +6 -8
  120. package/mevebu/libs/js/tools/sales-commission.js +8 -9
  121. package/mevebu/libs/js/tools/scientific-calculator.js +10 -14
  122. package/mevebu/libs/js/tools/scientific-notation.js +8 -9
  123. package/mevebu/libs/js/tools/script-planner.js +8 -10
  124. package/mevebu/libs/js/tools/selling-price.js +4 -5
  125. package/mevebu/libs/js/tools/semver-helper.js +3 -5
  126. package/mevebu/libs/js/tools/sentence-length.js +9 -10
  127. package/mevebu/libs/js/tools/sequence-analyzer.js +7 -8
  128. package/mevebu/libs/js/tools/sitemap-xml.js +4 -7
  129. package/mevebu/libs/js/tools/slug-converter.js +3 -5
  130. package/mevebu/libs/js/tools/slug-preview.js +6 -7
  131. package/mevebu/libs/js/tools/sort-lines.js +3 -5
  132. package/mevebu/libs/js/tools/speed-converter.js +7 -9
  133. package/mevebu/libs/js/tools/spin-wheel.js +19 -17
  134. package/mevebu/libs/js/tools/sql-formatter.js +5 -9
  135. package/mevebu/libs/js/tools/sql-minifier.js +5 -8
  136. package/mevebu/libs/js/tools/standard-deviation.js +6 -9
  137. package/mevebu/libs/js/tools/stopwatch.js +4 -6
  138. package/mevebu/libs/js/tools/study-schedule.js +8 -9
  139. package/mevebu/libs/js/tools/subtitle-formatter.js +10 -11
  140. package/mevebu/libs/js/tools/table-to-markdown.js +8 -12
  141. package/mevebu/libs/js/tools/tax-calculator.js +4 -5
  142. package/mevebu/libs/js/tools/temperature-converter.js +8 -11
  143. package/mevebu/libs/js/tools/text-diff.js +11 -7
  144. package/mevebu/libs/js/tools/text-direction.js +3 -5
  145. package/mevebu/libs/js/tools/text-reverser.js +3 -5
  146. package/mevebu/libs/js/tools/time-duration.js +8 -9
  147. package/mevebu/libs/js/tools/timestamp-converter.js +4 -6
  148. package/mevebu/libs/js/tools/timezone-converter.js +9 -13
  149. package/mevebu/libs/js/tools/title-length.js +6 -7
  150. package/mevebu/libs/js/tools/unicode-inspector.js +4 -5
  151. package/mevebu/libs/js/tools/url-inspector.js +8 -9
  152. package/mevebu/libs/js/tools/url-parser.js +12 -13
  153. package/mevebu/libs/js/tools/utf8-bytes.js +6 -9
  154. package/mevebu/libs/js/tools/uuid-generator.js +5 -9
  155. package/mevebu/libs/js/tools/version-compare.js +3 -4
  156. package/mevebu/libs/js/tools/video-duration.js +5 -7
  157. package/mevebu/libs/js/tools/volume-converter.js +7 -9
  158. package/mevebu/libs/js/tools/weekly-planner.js +8 -10
  159. package/mevebu/libs/js/tools/weight-converter.js +7 -9
  160. package/mevebu/libs/js/tools/weighted-average.js +4 -5
  161. package/mevebu/libs/js/tools/word-counter.js +10 -13
  162. package/mevebu/libs/js/tools/word-limit.js +4 -5
  163. package/mevebu/libs/js/tools/yaml-formatter.js +6 -7
  164. package/package.json +1 -1
@@ -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
- const btn = el(
50
+ return el(
26
51
  "button",
27
52
  {
28
- class: "mvb-searchbtn",
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: "mvb-searchbtn__txt" }, "Search"),
35
- el("kbd", { class: "mvb-kbd" }, "⌘K")
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
- { class: "mvb-icobtn mvb-bar__menu", type: "button", "aria-expanded": "false", "aria-controls": "mvb-navsheet", "aria-label": "Menu" },
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: "mvb-sheet", id: "mvb-navsheet", hidden: true },
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: "mvb-sheet__nav", "aria-label": "Site" },
53
- ...NAV.map((item) => el("a", { class: "mvb-sheet__link", href: `#${item.path}` }, item.label)),
54
- el("a", { class: "mvb-sheet__link", href: REPO_URL, target: "_blank", rel: "noopener" }, "GitHub"),
55
- el("button", { class: "mvb-sheet__link mvb-sheet__action", type: "button", onclick: () => openPalette() },
56
- icon("search"), el("span", {}, "Search tools"))
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: "mvb-icobtn", type: "button", "aria-label": "Switch theme" }, icon("moon"));
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: "mvb-nav", "aria-label": "Site" },
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("a", { class: "mvb-nav__link", href: `#${item.path}`, dataset: { path: item.path } }, item.label)
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: "mvb-bar" },
129
+ { class: "sticky top-0 z-30 border-b border-orbit bg-deck" },
88
130
  el(
89
131
  "div",
90
- { class: "mvb-bar__in" },
91
- el("a", { class: "mvb-mark", href: "#/" },
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: "mvb-bar__end" }, searchButton(), themeBtn, menuBtn)
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: "mvb-btn mvb-btn--fill", href: toolHref(tool) },
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
- el("div", { class: "mvb-modal__art" }, el("img", { src: "./icon.png", alt: "", width: "88", height: "88" })),
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: "mvb-cmd" },
146
- el("code", { class: "mvb-cmd__text" }, command),
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
- el("div", { class: "mvb-modal__art" }, el("img", { src: "./icon.png", alt: "", width: "72", height: "72" })),
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: "mvb-vers" },
159
- el("div", { class: "mvb-vers__row" },
160
- el("span", { class: "mvb-vers__label" }, "Installed"),
161
- el("span", { class: "mvb-vers__val" }, VERSION)),
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: "mvb-btn", href: `${REPO_URL}/releases/tag/v${latest}`, target: "_blank", rel: "noopener" }, "What changed")]
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 = $("#mvb-version");
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.classList.add("mvb-version--new");
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: "mvb-foot" },
273
+ { class: "mt-[clamp(40px,8vw,88px)] border-t border-orbit bg-deck" },
223
274
  el(
224
275
  "div",
225
- { class: "mvb-foot__in" },
276
+ { class: "mx-[var(--margin)] pt-[clamp(30px,5vw,48px)] pb-7" },
226
277
  el(
227
278
  "div",
228
- { class: "mvb-foot__top" },
279
+ { class: "flex flex-wrap items-start justify-between gap-[clamp(24px,5vw,64px)]" },
229
280
  el(
230
281
  "div",
231
- { class: "mvb-foot__brand" },
232
- el("a", { class: "mvb-foot__mark", href: "#/" },
233
- el("img", { src: "./icon.png", alt: "", width: "34", height: "34" }), "mevebu"),
234
- el("p", { class: "mvb-foot__tag" }, "Small tools that run in your browser. No accounts, no uploads, no tracking.")
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: "mvb-foot__nav", "aria-label": "Site" },
239
- el(
240
- "div",
241
- {},
242
- el("h4", {}, "Browse"),
243
- el("ul", {},
244
- el("li", {}, el("a", { href: "#/tools" }, "All tools")),
245
- el("li", {}, el("a", { href: "#/categories" }, "Categories")),
246
- el("li", {}, el("a", { href: "#/favorites" }, "Favorites"))
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: "mvb-foot__bottom" },
264
- el("p", { class: "mvb-foot__legal" }, "MIT licensed."),
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: "mvb-foot__legal" },
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: "mvb-foot__count", id: "mvb-starcount" })
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: "mvb-foot__legal" },
315
+ { class: "m-0 flex items-center gap-2" },
275
316
  el("button", {
276
- class: "mvb-version",
277
- id: "mvb-version",
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: "mvb-star" + (on ? " mvb-on" : ""),
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.classList.toggle("mvb-on", now);
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: "mvb-card",
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: "mvb-card__top" },
324
- el("span", { class: "mvb-card__sigil" }, icon(tool.icon)),
325
- el("span", { class: "mvb-card__name" }, tool.name)
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: "mvb-card__note" }, tool.note),
328
- el("span", { class: "mvb-card__foot" }, category?.name ?? "")
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: "mvb-listrow",
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: "mvb-listrow__link", href: toolHref(tool) },
342
- el("span", { class: "mvb-listrow__name" }, tool.name),
343
- el("span", { class: "mvb-listrow__note" }, tool.note)
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: "mvb-filter__in",
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
- { class: "mvb-filter", role: "search", onsubmit: (event) => event.preventDefault() },
361
- el("span", { class: "mvb-filter__icon" }, icon("search")),
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: "mvb-kbd" }, "/")
414
+ el("kbd", { class: KBD }, "/")
364
415
  );
365
416
  }
366
417
 
367
- /** Live filters `.mvb-card` grids and `.mvb-listrow` lists already in the tree. */
418
+ /** Live filters the `[data-tool]` cards and rows already in the tree. */
368
419
  function wireFilter(root, meta) {
369
- const input = root.querySelector(".mvb-filter__in");
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(".mvb-card, .mvb-listrow")) {
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-mvb-group]")) {
381
- const any = [...section.querySelectorAll(".mvb-card, .mvb-listrow")].some((item) => !item.hidden);
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(".mvb-card, .mvb-listrow")].find((item) => !item.hidden);
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: "mvb-hero" },
454
+ { class: "relative overflow-hidden border-b border-orbit" },
404
455
  el(
405
456
  "div",
406
- { class: "mvb-hero__in" },
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: "mvb-hero__title" }, "Small tools, one page each."),
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: "mvb-hero__cta" },
414
- el("button", { class: "mvb-btn mvb-btn--fill", type: "button", onclick: () => openPalette() },
415
- icon("search"), el("span", {}, "Search tools")),
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: "mvb-hero__orbit" }, el("img", { src: "./icon.png", alt: "mevebu planet mark", width: "300", height: "300" }))
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 grid = el("div", { class: "mvb-page" },
479
+ const section = (title, iconName, more, tools, group) =>
429
480
  el(
430
481
  "section",
431
- { class: "mvb-section", "data-mvb-group": "popular" },
482
+ { class: "pt-[clamp(32px,5vw,60px)]", dataset: { group } },
432
483
  el(
433
484
  "div",
434
- { class: "mvb-section__head" },
435
- el("h2", { class: "mvb-section__title" }, icon("star"), "Popular"),
436
- el("a", { class: "mvb-section__more", href: "#/tools" }, "All tools")
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: "mvb-grid" }, ...popular.map(toolCard))
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
- el(
442
- "section",
443
- { class: "mvb-section", "data-mvb-group": category.id },
444
- el(
445
- "div",
446
- { class: "mvb-section__head" },
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: "mvb-section", "data-mvb-group": category.id },
515
+ { class: SECTION, dataset: { group: category.id } },
466
516
  el(
467
517
  "div",
468
- { class: "mvb-section__head" },
469
- el("h2", { class: "mvb-section__title" }, icon(category.icon),
470
- category.name, el("span", { class: "mvb-section__count" }, toolsInCategory(category.id).length)),
471
- el("a", { class: "mvb-section__more", href: `#/${category.id}` }, "Open")
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: "mvb-grid" }, ...toolsInCategory(category.id).map(toolCard))
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: "mvb-page mvb-page--narrow" },
529
+ { class: PAGE },
530
+ el("h1", { class: LEAD }, "All tools."),
480
531
  filterBar("Search here"),
481
- el("div", { class: "mvb-filter__meta" }, meta),
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: "mvb-grid" },
543
+ { class: GRID },
493
544
  ...CATEGORIES.map((category) =>
494
545
  el(
495
546
  "a",
496
- { class: "mvb-card", href: `#/${category.id}` },
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: "mvb-card__top" },
500
- el("span", { class: "mvb-card__sigil" }, icon(category.icon)),
501
- el("span", { class: "mvb-card__name" }, category.name)
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: "mvb-card__note" }, `${toolsInCategory(category.id).length} tools inside.`),
504
- el("span", { class: "mvb-card__foot" }, "Open category")
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: "mvb-page mvb-page--narrow" },
513
- el("h1", { class: "mvb-lead" }, "Browse by category."),
514
- el("section", { class: "mvb-section" }, grid)
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: "mvb-page mvb-page--narrow" },
525
- el("h1", { class: "mvb-lead" }, saved.length ? "Your favorites." : "No favorites yet."),
578
+ { class: PAGE_NARROW },
579
+ el("h1", { class: LEAD }, saved.length ? "Your favorites." : "No favorites yet."),
526
580
  saved.length
527
- ? el("p", { class: "mvb-lede" }, "Tap the star on any tool to add or remove it. Saved in this browser only.")
528
- : el("p", { class: "mvb-lede" },
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: "mvb-link", href: "#/tools" }, "Browse the tools")),
531
- el("section", { class: "mvb-section" },
532
- el("ul", { class: "mvb-list" }, ...saved.map((tool) => toolRow(tool, rerenderFavorites))))
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: "mvb-page mvb-page--narrow" },
542
- el("h1", { class: "mvb-lead" }, category.name),
543
- el("p", { class: "mvb-lede" }, `${tools.length} tools. Pick a card to open it.`),
544
- el("section", { class: "mvb-section" },
545
- el("div", { class: "mvb-grid" }, ...tools.map(toolCard)))
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: "mvb-helpbtn",
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: "mvb-head" },
572
- el("a", { class: "mvb-head__back", href: parent.href }, icon("arrow-left"), parent.label),
573
- el("div", { class: "mvb-head__row" },
574
- el("h1", { class: "mvb-head__title" }, tool.name),
575
- el("div", { class: "mvb-head__actions" }, helpButton, starButton(tool))),
576
- el("p", { class: "mvb-head__about" }, tool.about)
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: "mvb-more" },
596
- el("h2", { class: "mvb-more__title" }, "More from mevebu"),
597
- el("div", { class: "mvb-grid" }, ...picks.map((t) => toolCard(t)))
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: "mvb-page mvb-page--tool" },
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: toolFailed(tool) });
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: "mvb-about" }, el("h2", { class: "mvb-about__title" }, title), ...nodes);
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: "mvb-page mvb-page--narrow" },
640
- el("h1", { class: "mvb-lead" }, "About mevebu"),
641
- el("p", { class: "mvb-lede" },
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: "mvb-code" }, "npm install -g mevebu\nmevebu"),
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: "mvb-link", href: REPO_URL, target: "_blank", rel: "noopener" }, "github.com/mevebu/mevebu")),
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: "mvb-page mvb-page--narrow" },
665
- el("h1", { class: "mvb-lead" }, "That page is not here."),
666
- el("p", { class: "mvb-lede" }, "Check the address, or pick a tool from the index."),
667
- el("p", {}, el("a", { class: "mvb-link", href: "#/" }, "Go home"))
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 $$(".mvb-nav__link")) {
778
+ for (const link of $$("a[data-path]")) {
728
779
  const active = link.dataset.path === path;
729
- link.classList.toggle("mvb-on", active);
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
- $("#mvb-view").replaceChildren(node);
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?.mvb) history.replaceState({ mvb: true }, "", location.href);
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 mvb. A plain hash change carries no such state.
817
+ // with mevebu. A plain hash change carries no such state.
766
818
  window.addEventListener("popstate", (event) => {
767
- historyMove = Boolean(event.state?.mvb);
819
+ historyMove = Boolean(event.state?.mevebu);
768
820
  });
769
821
 
770
822
  function mountShell() {
771
- $("#app").append(masthead(), el("main", { class: "mvb-main", id: "mvb-view", tabindex: "-1" }), footer());
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 = $("#mvb-starcount");
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 = $(".mvb-filter__in");
847
+ const field = $("[data-filter]");
796
848
  if (!field) return;
797
849
  event.preventDefault();
798
850
  field.focus();