mevebu 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,11 +31,11 @@ The tools open in your default browser. Press `Ctrl+C` in the terminal to stop.
31
31
 
32
32
  ## Tools
33
33
 
34
- 16 tools, each with its own page. Most update as you type, with no submit button.
34
+ 15 tools, each with its own page. Most update as you type, with no submit button.
35
35
 
36
36
  | Category | Tools |
37
37
  | :--- | :--- |
38
- | Random | Spin the Wheel, Name Picker, Coin Flip, Dice Roller |
38
+ | Random | Spin the Wheel, Name Picker, Coin Flip |
39
39
  | Passwords and keys | Password Generator, JWT Generator, Hash Generator |
40
40
  | Developer | UUID Generator, Color Converter |
41
41
  | Convert | Base64, URL Encoder, Case Converter |
@@ -962,6 +962,28 @@ textarea.mvb-out { resize: vertical; }
962
962
  .mvb-modal__body p { margin-top: 8px; color: var(--mvb-ink-2); font-size: 15.5px; text-wrap: pretty; }
963
963
  .mvb-modal__actions { margin-top: 20px; display: grid; gap: 8px; }
964
964
 
965
+ /* version readout inside the update modal */
966
+ .mvb-vers { margin-top: 18px; border: 1px solid var(--mvb-orbit); border-radius: var(--mvb-r2); overflow: hidden; }
967
+ .mvb-vers__row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; }
968
+ .mvb-vers__row + .mvb-vers__row { border-top: 1px solid var(--mvb-orbit); }
969
+ .mvb-vers__label { font-size: 14px; color: var(--mvb-ink-3); }
970
+ .mvb-vers__val { font-family: var(--mvb-mono); font-size: 15px; font-weight: 600; }
971
+ .mvb-vers__row--new .mvb-vers__val { color: var(--mvb-comet); }
972
+ .mvb-vers__hint { margin-top: 14px !important; font-size: 14px !important; }
973
+
974
+ .mvb-cmd {
975
+ margin-top: 10px;
976
+ display: flex;
977
+ align-items: center;
978
+ gap: 8px;
979
+ flex-wrap: wrap;
980
+ background: var(--mvb-deck-2);
981
+ border: 1px solid var(--mvb-orbit);
982
+ border-radius: var(--mvb-r2);
983
+ padding: 10px 10px 10px 16px;
984
+ }
985
+ .mvb-cmd__text { flex: 1 1 auto; min-width: 0; font-family: var(--mvb-mono); font-size: 14px; overflow-wrap: anywhere; }
986
+
965
987
  /* ---------- command palette ---------- */
966
988
 
967
989
  .mvb-veil--pal { padding: clamp(60px, 12vh, 140px) 20px 20px; }
@@ -1105,6 +1127,29 @@ textarea.mvb-out { resize: vertical; }
1105
1127
  .mvb-foot__count { font-variant-numeric: tabular-nums; }
1106
1128
  .mvb-foot__count:empty { display: none; }
1107
1129
 
1130
+ /* version chip in the footer; turns into an update button when npm has a newer one */
1131
+ .mvb-version {
1132
+ display: inline-flex;
1133
+ align-items: center;
1134
+ gap: 6px;
1135
+ padding: 3px 10px;
1136
+ min-height: 28px;
1137
+ font: inherit;
1138
+ font-size: 13px;
1139
+ font-variant-numeric: tabular-nums;
1140
+ color: var(--mvb-ink-3);
1141
+ background: transparent;
1142
+ border: 1px solid var(--mvb-orbit);
1143
+ border-radius: 999px;
1144
+ cursor: pointer;
1145
+ transition: color var(--mvb-t) var(--mvb-ease), border-color var(--mvb-t) var(--mvb-ease), background var(--mvb-t) var(--mvb-ease);
1146
+ }
1147
+ .mvb-version:hover { color: var(--mvb-ink); border-color: var(--mvb-orbit-hard); }
1148
+ .mvb-version:focus-visible { outline: 2px solid var(--mvb-comet); outline-offset: 2px; }
1149
+ .mvb-version--new { color: var(--mvb-comet); border-color: var(--mvb-comet); background: var(--mvb-nebula); font-weight: 600; }
1150
+ .mvb-version--new:hover { color: var(--mvb-on-comet); background: var(--mvb-comet); }
1151
+ .mvb-version .mvb-ico { width: 15px; height: 15px; }
1152
+
1108
1153
  @media (max-width: 720px) {
1109
1154
  .mvb-foot__top { grid-template-columns: 1fr; }
1110
1155
  .mvb-foot__nav { grid-template-columns: repeat(2, 1fr); }
@@ -6,6 +6,7 @@ import { route, start, match, currentPath, go } from "../libs/js/router.js";
6
6
  import {
7
7
  $, $$, el, icon, getFavorites, isFavorite, toggleFavorite, setTheme,
8
8
  modal, button, copyButton, openPalette, shuffle, toolcard,
9
+ VERSION, PACKAGE_NAME, checkForUpdate, updateCommand, compareVersions,
9
10
  } from "../libs/js/core.js";
10
11
 
11
12
  const DONATE_URL = "https://buymeacoffee.com/fastro";
@@ -137,6 +138,79 @@ function openDonate() {
137
138
  });
138
139
  }
139
140
 
141
+ // `latest` is the published version, or null when the check could not run.
142
+ function openUpdate(latest) {
143
+ const updatable = latest && compareVersions(latest, VERSION) > 0;
144
+ const command = updateCommand();
145
+
146
+ const commandBox = el(
147
+ "div",
148
+ { class: "mvb-cmd" },
149
+ el("code", { class: "mvb-cmd__text" }, command),
150
+ copyButton(() => command, "Copy command", "fill")
151
+ );
152
+
153
+ modal({
154
+ label: "Update mevebu",
155
+ body: [
156
+ el("div", { class: "mvb-modal__art" }, el("img", { src: "./icon.png", alt: "", width: "72", height: "72" })),
157
+ el("h2", { class: "mvb-modal__title" }, updatable ? `mevebu ${latest} is out` : "mevebu is up to date"),
158
+ updatable
159
+ ? el("p", {}, "A newer version is published. Update your global install, then run ", el("code", {}, PACKAGE_NAME), " again.")
160
+ : el("p", {}, "You are on the latest version. Nothing to do."),
161
+ el("div", { class: "mvb-vers" },
162
+ el("div", { class: "mvb-vers__row" },
163
+ el("span", { class: "mvb-vers__label" }, "Installed"),
164
+ el("span", { class: "mvb-vers__val" }, VERSION)),
165
+ el("div", { class: "mvb-vers__row" + (updatable ? " mvb-vers__row--new" : "") },
166
+ el("span", { class: "mvb-vers__label" }, "Latest"),
167
+ el("span", { class: "mvb-vers__val" }, latest ?? "—"))),
168
+ updatable ? el("p", { class: "mvb-vers__hint" }, "Copy the command below and run it in your terminal.") : null,
169
+ updatable ? commandBox : null,
170
+ ],
171
+ actions: updatable
172
+ ? [el("a", { class: "mvb-btn", href: `https://www.npmjs.com/package/${PACKAGE_NAME}/v/${latest}`, target: "_blank", rel: "noopener" }, "What changed")]
173
+ : [],
174
+ veilClass: "mvb-veil--help",
175
+ });
176
+ }
177
+
178
+ // ---------- version ----------
179
+
180
+ // The footer chip shows the current version. When npm reports a newer one, the
181
+ // chip turns into an "update" button and the modal explains how to upgrade.
182
+ async function startVersionChip() {
183
+ const chip = $("#mvb-version");
184
+ if (!chip) return;
185
+ chip.textContent = `v${VERSION}`;
186
+
187
+ let latest = null;
188
+ try {
189
+ const result = await checkForUpdate();
190
+ latest = result?.latest ?? null;
191
+ } catch {
192
+ /* offline: keep the plain version label */
193
+ }
194
+
195
+ const outdated = latest && compareVersions(latest, VERSION) > 0;
196
+ if (outdated) {
197
+ chip.classList.add("mvb-version--new");
198
+ chip.setAttribute("aria-label", `Update available: version ${latest}`);
199
+ chip.replaceChildren(icon("circle-arrow-up"), el("span", {}, `Update to v${latest}`));
200
+ }
201
+
202
+ chip.addEventListener("click", (event) => {
203
+ event.preventDefault();
204
+ openUpdate(latest);
205
+ });
206
+ chip.addEventListener("keydown", (event) => {
207
+ if (event.key === "Enter" || event.key === " ") {
208
+ event.preventDefault();
209
+ openUpdate(latest);
210
+ }
211
+ });
212
+ }
213
+
140
214
  function footer() {
141
215
  const donate = button("Donate", {
142
216
  icon: "coffee",
@@ -197,6 +271,16 @@ function footer() {
197
271
  "Source: ",
198
272
  el("a", { href: REPO_URL, target: "_blank", rel: "noopener" }, "github.com/mevebu/mevebu"),
199
273
  el("span", { class: "mvb-foot__count", id: "mvb-starcount" })
274
+ ),
275
+ el(
276
+ "p",
277
+ { class: "mvb-foot__legal" },
278
+ el("button", {
279
+ class: "mvb-version",
280
+ id: "mvb-version",
281
+ type: "button",
282
+ title: "Check for updates",
283
+ }, "—")
200
284
  )
201
285
  )
202
286
  )
@@ -727,4 +811,5 @@ function globalKeys() {
727
811
  mountShell();
728
812
  globalKeys();
729
813
  startStarCount();
814
+ startVersionChip();
730
815
  start(render);
package/mevebu/index.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>mevebu, small tools that run in your browser</title>
7
- <meta name="description" content="16 small tools that run in your browser. Passwords, QR codes, random draws, text and encoding. No accounts, no tracking." />
7
+ <meta name="description" content="15 small tools that run in your browser. Passwords, QR codes, random draws, text and encoding. No accounts, no tracking." />
8
8
  <link rel="icon" type="image/png" href="./icon.png" />
9
9
  <link rel="stylesheet" href="./app/mevebu.css" />
10
10
  <link rel="stylesheet" href="./libs/css/tools.css" />
@@ -43,26 +43,6 @@
43
43
  .mvb-flip__result { font-size: 19px; font-weight: 700; letter-spacing: -0.01em; }
44
44
  .mvb-flip__tally { font-size: 14px; color: var(--mvb-ink-3); font-variant-numeric: tabular-nums; }
45
45
 
46
- /* ---------- dice ---------- */
47
-
48
- .mvb-faces { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
49
-
50
- .mvb-die {
51
- min-width: 56px;
52
- height: 56px;
53
- display: inline-flex;
54
- align-items: center;
55
- justify-content: center;
56
- padding: 0 10px;
57
- background: var(--mvb-deck);
58
- border: 1px solid var(--mvb-orbit-hard);
59
- border-radius: var(--mvb-r1);
60
- font-family: var(--mvb-mono);
61
- font-size: 20px;
62
- font-weight: 700;
63
- font-variant-numeric: tabular-nums;
64
- }
65
-
66
46
  /* ---------- meter ---------- */
67
47
 
68
48
  .mvb-meter { height: 8px; border-radius: 999px; background: var(--mvb-deck-2); overflow: hidden; }
@@ -590,4 +590,52 @@ export function blank(iconName, text) {
590
590
  );
591
591
  }
592
592
 
593
+ // ---------- version ----------
594
+
595
+ // Kept in one place. The CLI reads the same value from package.json.
596
+ export const VERSION = "0.2.1";
597
+ export const PACKAGE_NAME = "mevebu";
598
+ export const REPO_SLUG = "mevebu/mevebu";
599
+
600
+ /** Compare two X.Y.Z strings. Returns 1, 0, or -1 for a > b, a == b, a < b. */
601
+ export function compareVersions(a, b) {
602
+ const left = String(a).split(".").map((part) => parseInt(part, 10) || 0);
603
+ const right = String(b).split(".").map((part) => parseInt(part, 10) || 0);
604
+ for (let i = 0; i < 3; i++) {
605
+ if ((left[i] ?? 0) > (right[i] ?? 0)) return 1;
606
+ if ((left[i] ?? 0) < (right[i] ?? 0)) return -1;
607
+ }
608
+ return 0;
609
+ }
610
+
611
+ /**
612
+ * Asks the npm registry for the latest published version. Fails silently when
613
+ * offline, so the app never blocks on the network. Resolves to
614
+ * `{ latest, current, outdated }` or null when the check did not complete.
615
+ */
616
+ export async function checkForUpdate({ timeout = 5000 } = {}) {
617
+ const controller = new AbortController();
618
+ const timer = setTimeout(() => controller.abort(), timeout);
619
+ try {
620
+ const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
621
+ signal: controller.signal,
622
+ headers: { accept: "application/json" },
623
+ });
624
+ clearTimeout(timer);
625
+ if (!response.ok) return null;
626
+ const data = await response.json();
627
+ const latest = data?.version;
628
+ if (!latest) return null;
629
+ return { latest, current: VERSION, outdated: compareVersions(latest, VERSION) > 0 };
630
+ } catch {
631
+ clearTimeout(timer);
632
+ return null;
633
+ }
634
+ }
635
+
636
+ /** The command that updates an existing global install. */
637
+ export function updateCommand() {
638
+ return `npm install -g ${PACKAGE_NAME}`;
639
+ }
640
+
593
641
  export { hasIcon };
@@ -1,5 +1,5 @@
1
1
  // GENERATED FILE, DO NOT EDIT.
2
- // Source: lucide v1.57.0 (ISC), icons/55 names from scripts/vendor-icons.mjs
2
+ // Source: lucide v1.57.0 (ISC), icons/54 names from scripts/vendor-icons.mjs
3
3
  // Regenerate with: npm run icons
4
4
 
5
5
  const D = {
@@ -15,6 +15,7 @@ const D = {
15
15
  "corner-down-left": [ ["path", { d: "M20 4v7a4 4 0 0 1-4 4H4" }], ["path", { d: "m9 10-5 5 5 5" }] ],
16
16
  "info": [ ["circle", { cx: "12", cy: "12", r: "10" }], ["path", { d: "M12 16v-4" }], ["path", { d: "M12 8h.01" }] ],
17
17
  "circle-question-mark": [ ["circle", { cx: "12", cy: "12", r: "10" }], ["path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }], ["path", { d: "M12 17h.01" }] ],
18
+ "circle-arrow-up": [ ["circle", { cx: "12", cy: "12", r: "10" }], ["path", { d: "m16 12-4-4-4 4" }], ["path", { d: "M12 16V8" }] ],
18
19
  "shuffle": [ ["path", { d: "m18 14 4 4-4 4" }], ["path", { d: "m18 2 4 4-4 4" }], ["path", { d: "M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22" }], ["path", { d: "M2 6h1.972a4 4 0 0 1 3.6 2.2" }], ["path", { d: "M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45" }] ],
19
20
  "shield-check": [ [ "path", { d: "M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" } ], ["path", { d: "m9 12 2 2 4-4" }] ],
20
21
  "terminal": [ ["path", { d: "M12 19h8" }], ["path", { d: "m4 17 6-6-6-6" }] ],
@@ -24,7 +25,6 @@ const D = {
24
25
  "circle-dashed": [ ["path", { d: "M10.1 2.182a10 10 0 0 1 3.8 0" }], ["path", { d: "M13.9 21.818a10 10 0 0 1-3.8 0" }], ["path", { d: "M17.609 3.721a10 10 0 0 1 2.69 2.7" }], ["path", { d: "M2.182 13.9a10 10 0 0 1 0-3.8" }], ["path", { d: "M20.279 17.609a10 10 0 0 1-2.7 2.69" }], ["path", { d: "M21.818 10.1a10 10 0 0 1 0 3.8" }], ["path", { d: "M3.721 6.391a10 10 0 0 1 2.7-2.69" }], ["path", { d: "M6.391 20.279a10 10 0 0 1-2.69-2.7" }] ],
25
26
  "users": [ ["path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }], ["path", { d: "M16 3.128a4 4 0 0 1 0 7.744" }], ["path", { d: "M22 21v-2a4 4 0 0 0-3-3.87" }], ["circle", { cx: "9", cy: "7", r: "4" }] ],
26
27
  "coins": [ ["path", { d: "M13.744 17.736a6 6 0 1 1-7.48-7.48" }], ["path", { d: "M15 6h1v4" }], ["path", { d: "m6.134 14.768.866-.5 2 3.464" }], ["circle", { cx: "16", cy: "8", r: "6" }] ],
27
- "dices": [ ["rect", { width: "12", height: "12", x: "2", y: "10", rx: "2", ry: "2" }], ["path", { d: "m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6" }], ["path", { d: "M6 18h.01" }], ["path", { d: "M10 14h.01" }], ["path", { d: "M15 6h.01" }], ["path", { d: "M18 9h.01" }] ],
28
28
  "key-round": [ [ "path", { d: "M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" } ], ["circle", { cx: "16.5", cy: "7.5", r: ".5", fill: "currentColor" }] ],
29
29
  "signature": [ [ "path", { d: "m21 17-2.156-1.868A.5.5 0 0 0 18 15.5v.5a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1c0-2.545-3.991-3.97-8.5-4a1 1 0 0 0 0 5c4.153 0 4.745-11.295 5.708-13.5a2.5 2.5 0 1 1 3.31 3.284" } ], ["path", { d: "M3 21h18" }] ],
30
30
  "hash": [ ["line", { x1: "4", x2: "20", y1: "9", y2: "9" }], ["line", { x1: "4", x2: "20", y1: "15", y2: "15" }], ["line", { x1: "10", x2: "8", y1: "3", y2: "21" }], ["line", { x1: "16", x2: "14", y1: "3", y2: "21" }] ],
@@ -57,7 +57,6 @@ const D = {
57
57
  "loader": [ ["path", { d: "M12 2v4" }], ["path", { d: "m16.2 7.8 2.9-2.9" }], ["path", { d: "M18 12h4" }], ["path", { d: "m16.2 16.2 2.9 2.9" }], ["path", { d: "M12 18v4" }], ["path", { d: "m4.9 19.1 2.9-2.9" }], ["path", { d: "M2 12h4" }], ["path", { d: "m4.9 4.9 2.9 2.9" }] ],
58
58
  "play": [ [ "path", { d: "M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" } ] ],
59
59
  "lock": [ ["rect", { width: "18", height: "11", x: "3", y: "11", rx: "2", ry: "2" }], ["path", { d: "M7 11V7a5 5 0 0 1 10 0v4" }] ],
60
- "dice-5": [ ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", ry: "2" }], ["path", { d: "M16 8h.01" }], ["path", { d: "M8 8h.01" }], ["path", { d: "M8 16h.01" }], ["path", { d: "M16 16h.01" }], ["path", { d: "M12 12h.01" }] ],
61
60
  };
62
61
 
63
62
  const SVG_NS = "http://www.w3.org/2000/svg";
@@ -70,24 +70,6 @@ export const TOOLS = [
70
70
  keywords: "random toss decide heads tails",
71
71
  load: () => import("./tools/coin-flip.js"),
72
72
  },
73
- {
74
- id: "dice-roller",
75
- category: "random",
76
- name: "Dice Roller",
77
- icon: "dices",
78
- note: "Roll d4 to d20, one die or fifty.",
79
- about: "Pick a die, set how many to roll, and see each result.",
80
- tutorial: {
81
- intro: "Roll polyhedral dice from d4 to d20, one or many at a time.",
82
- steps: [
83
- "Pick a die with the chips (d4 to d20).",
84
- "Set how many dice to roll, between 1 and 50.",
85
- "The dice roll as you change a setting. Press Roll for a fresh throw.",
86
- ],
87
- },
88
- keywords: "random dice d6 d20 roll tabletop rpg",
89
- load: () => import("./tools/dice-roller.js"),
90
- },
91
73
 
92
74
  {
93
75
  id: "password-generator",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mevebu",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Small tools that run in your browser. No accounts, no uploads, no tracking.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,59 +0,0 @@
1
- // Polyhedral dice from d4 to d20, one die or many, with each result shown.
2
- import { el, randomInt, chipRow, numberInput, button, bench, stack, panel, output, actions, readPin, writePin, toolcard } from "../core.js";
3
-
4
- const SIDES = [4, 6, 8, 10, 12, 20];
5
-
6
- export function mount(slot) {
7
- const PIN = "dice-roller";
8
- const saved = readPin(PIN, { sides: 6, count: "3" });
9
- let sides = SIDES.includes(Number(saved.sides)) ? Number(saved.sides) : 6;
10
-
11
- const chips = chipRow(SIDES.map((value) => ({ value, label: `d${value}` })), {
12
- value: sides,
13
- label: "Die",
14
- onChange: (value) => {
15
- sides = value;
16
- roll();
17
- },
18
- });
19
- const countField = numberInput({ value: saved.count ?? "3", min: "1", max: "50", "aria-label": "How many dice" });
20
- const faces = el("div", { class: "mvb-faces", "aria-live": "polite" });
21
- const sum = output({ rows: 1 });
22
- const rollButton = button("Roll", { icon: "dices", kind: "fill" });
23
-
24
- function totalCount() {
25
- return Math.max(1, Math.min(50, parseInt(countField.value || "1", 10) || 1));
26
- }
27
-
28
- function roll() {
29
- const total = totalCount();
30
- const rolls = Array.from({ length: total }, () => 1 + randomInt(sides));
31
- faces.replaceChildren(...rolls.map((value) => el("span", { class: "mvb-die" }, String(value))));
32
- sum.write(String(rolls.reduce((a, b) => a + b, 0)));
33
- writePin(PIN, { sides, count: String(total) });
34
- }
35
-
36
- // A new die or a new count rolls right away. The button stays for a fresh throw.
37
- rollButton.addEventListener("click", () => {
38
- countField.value = String(totalCount());
39
- roll();
40
- });
41
- countField.addEventListener("input", roll);
42
- countField.addEventListener("change", () => {
43
- countField.value = String(totalCount());
44
- });
45
-
46
- const card = toolcard({
47
- body: bench(
48
- stack(panel("Die", chips), panel("Dice to roll", countField)),
49
- stack(panel("Results", faces), panel("Total", sum))
50
- ),
51
- foot: actions(rollButton),
52
- });
53
- slot.append(card.node);
54
- roll();
55
-
56
- return {};
57
- }
58
-
59
- export default { mount };