mevebu 1.4.3 → 1.4.4

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.
@@ -6,7 +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, modalHead, button, copyButton, openPalette, shuffle, toolcard, blank,
9
- VERSION, PACKAGE_NAME, checkForUpdate, updateCommand, compareVersions,
9
+ VERSION, PACKAGE_NAME, checkForUpdate, updateCommand, compareVersions, releaseUrl,
10
10
  } from "../libs/js/core.js";
11
11
 
12
12
  const DONATE_URL = "https://buymeacoffee.com/fastro";
@@ -174,17 +174,18 @@ function openDonate() {
174
174
  });
175
175
  }
176
176
 
177
- // `latest` is the published version, or null when the check could not run.
177
+ // The footer chip opens this. `latest` is the published version, or null when the
178
+ // check has not answered; opening the modal retries it, so a registry hiccup on
179
+ // the first load is not a dead end. The body and the actions repaint in place
180
+ // while that retry runs.
178
181
  function openUpdate(latest) {
179
- const updatable = latest && compareVersions(latest, VERSION) > 0;
180
- const command = updateCommand();
182
+ let resolved = latest;
183
+ let busy = false;
181
184
 
182
- const commandBox = el(
183
- "div",
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),
186
- copyButton(() => command, "Copy command", "fill")
187
- );
185
+ const bodyBox = el("div", { class: "grid gap-2.5" });
186
+ const actionBox = el("div", { class: "flex flex-wrap items-center justify-end gap-2" });
187
+ const command = updateCommand();
188
+ const updatable = () => Boolean(resolved) && compareVersions(resolved, VERSION) > 0;
188
189
 
189
190
  const versionRow = (label, value, isNew) =>
190
191
  el(
@@ -194,22 +195,57 @@ function openUpdate(latest) {
194
195
  el("span", { class: `font-mono text-sm font-semibold ${isNew ? "text-jade" : ""}` }, value)
195
196
  );
196
197
 
197
- modal({
198
- label: "Update mevebu",
199
- body: [
200
- modalHead("circle-arrow-up", updatable ? `mevebu ${latest} is out` : "mevebu is up to date"),
201
- updatable
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.")
203
- : el("p", {}, "You are on the latest version. Nothing to do."),
198
+ function paint() {
199
+ const newer = updatable();
200
+ bodyBox.replaceChildren(...[
201
+ modalHead(
202
+ busy || !resolved ? "refresh-cw" : "circle-arrow-up",
203
+ busy
204
+ ? "Checking for updates"
205
+ : newer
206
+ ? `mevebu ${resolved} is out`
207
+ : resolved
208
+ ? "mevebu is up to date"
209
+ : "Could not check for updates"
210
+ ),
211
+ newer
212
+ ? el("p", {}, "A newer version is published. Update your global install, then run ", el("code", { class: CODE }, PACKAGE_NAME), " again.")
213
+ : null,
204
214
  el("div", { class: "mt-[18px] overflow-hidden rounded-r2 border border-orbit" },
205
215
  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,
208
- updatable ? commandBox : null,
209
- ],
210
- actions: updatable
211
- ? [el("a", { class: LINK_PLAIN, href: `${REPO_URL}/releases/tag/v${latest}`, target: "_blank", rel: "noopener" }, "What changed")]
212
- : [],
216
+ versionRow("Latest", busy ? "Checking" : resolved ?? "Unknown", newer)),
217
+ newer ? el("p", { class: "mt-3.5 text-sm" }, "Copy the command below and run it in your terminal.") : null,
218
+ newer
219
+ ? el("div", { 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" },
220
+ el("code", { class: "min-w-0 flex-1 font-mono text-sm [overflow-wrap:anywhere]" }, command),
221
+ copyButton(() => command, "Copy command", "fill"))
222
+ : null,
223
+ ].filter(Boolean));
224
+
225
+ actionBox.replaceChildren(...[
226
+ !resolved && !busy ? button("Check again", { icon: "refresh-cw", onclick: () => check() }) : null,
227
+ el("a", { class: LINK_PLAIN, href: releaseUrl(resolved), target: "_blank", rel: "noopener" }, "What changed"),
228
+ ].filter(Boolean));
229
+ }
230
+
231
+ async function check() {
232
+ if (busy) return;
233
+ busy = true;
234
+ paint();
235
+ const result = await checkForUpdate();
236
+ busy = false;
237
+ if (result?.latest) resolved = result.latest;
238
+ paint();
239
+ }
240
+
241
+ paint();
242
+ modal({
243
+ label: "Update mevebu",
244
+ body: [bodyBox],
245
+ actions: [actionBox],
246
+ onOpen: () => {
247
+ if (!resolved) check();
248
+ },
213
249
  });
214
250
  }
215
251
 
@@ -961,6 +961,9 @@
961
961
  .py-7 {
962
962
  padding-block: calc(var(--spacing) * 7);
963
963
  }
964
+ .py-8 {
965
+ padding-block: calc(var(--spacing) * 8);
966
+ }
964
967
  .py-\[3px\] {
965
968
  padding-block: 3px;
966
969
  }
@@ -743,7 +743,7 @@ export function blank(iconName, text) {
743
743
  // ---------- version ----------
744
744
 
745
745
  // Kept in one place. The CLI reads the same value from package.json.
746
- export const VERSION = "1.4.3";
746
+ export const VERSION = "1.4.4";
747
747
  export const PACKAGE_NAME = "mevebu";
748
748
  export const REPO_SLUG = "mevebu/mevebu";
749
749
 
@@ -758,34 +758,52 @@ export function compareVersions(a, b) {
758
758
  return 0;
759
759
  }
760
760
 
761
- /**
762
- * Asks the npm registry for the latest published version. Fails silently when
763
- * offline, so the app never blocks on the network. Resolves to
764
- * `{ latest, current, outdated }` or null when the check did not complete.
765
- */
766
- export async function checkForUpdate({ timeout = 5000 } = {}) {
761
+ /** One JSON GET with its own timeout. Answers null on any failure. */
762
+ async function getJson(url, timeout) {
767
763
  const controller = new AbortController();
768
764
  const timer = setTimeout(() => controller.abort(), timeout);
769
765
  try {
770
- const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
771
- signal: controller.signal,
772
- headers: { accept: "application/json" },
773
- });
774
- clearTimeout(timer);
766
+ const response = await fetch(url, { signal: controller.signal, headers: { accept: "application/json" } });
775
767
  if (!response.ok) return null;
776
- const data = await response.json();
777
- const latest = data?.version;
778
- if (!latest) return null;
779
- return { latest, current: VERSION, outdated: compareVersions(latest, VERSION) > 0 };
768
+ return await response.json();
780
769
  } catch {
781
- clearTimeout(timer);
782
770
  return null;
771
+ } finally {
772
+ clearTimeout(timer);
783
773
  }
784
774
  }
785
775
 
776
+ async function npmVersion(timeout) {
777
+ const data = await getJson(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, timeout);
778
+ return data?.version ?? null;
779
+ }
780
+
781
+ async function githubVersion(timeout) {
782
+ const data = await getJson(`https://api.github.com/repos/${REPO_SLUG}/releases/latest`, timeout);
783
+ return (data?.tag_name ?? "").replace(/^v/, "") || null;
784
+ }
785
+
786
+ /**
787
+ * Reads the newest published version. npm answers first and the GitHub release
788
+ * is the fallback, since either one can be unreachable on its own. Resolves to
789
+ * `{ latest, current, outdated }`, or null when neither answered, so the app
790
+ * never blocks on the network and the modal can offer a retry.
791
+ */
792
+ export async function checkForUpdate({ timeout = 5000 } = {}) {
793
+ const latest = (await npmVersion(timeout)) ?? (await githubVersion(timeout));
794
+ if (!latest) return null;
795
+ return { latest, current: VERSION, outdated: compareVersions(latest, VERSION) > 0 };
796
+ }
797
+
786
798
  /** The command that updates an existing global install. */
787
799
  export function updateCommand() {
788
800
  return `npm install -g ${PACKAGE_NAME}`;
789
801
  }
790
802
 
803
+ /** The GitHub page listing what changed, for one version or for every release. */
804
+ export function releaseUrl(version) {
805
+ const base = `https://github.com/${REPO_SLUG}/releases`;
806
+ return version ? `${base}/tag/v${version}` : base;
807
+ }
808
+
791
809
  export { hasIcon };
@@ -622,7 +622,7 @@ export const TOOLS = [
622
622
  tutorial: {
623
623
  intro: "Compare two versions of a text.",
624
624
  steps: [
625
- "Paste the first text on the left and the second on the right.",
625
+ "Paste the first text and the second text.",
626
626
  "Added and removed lines are marked in the result.",
627
627
  "Toggle Ignore case or Ignore spaces to soften the comparison.",
628
628
  ],
@@ -656,7 +656,7 @@ export const TOOLS = [
656
656
  name: "JWT Decoder",
657
657
  icon: "signature",
658
658
  note: "Read a JWT header and payload.",
659
- about: "Decodes in this tab. Decoding is not signature validation.",
659
+ about: "Reads the header and payload. Decoding is not signature validation.",
660
660
  tutorial: {
661
661
  intro: "Read the header and payload of a JSON Web Token.",
662
662
  steps: [
@@ -678,7 +678,7 @@ export const TOOLS = [
678
678
  tutorial: {
679
679
  intro: "Format or compact a JSON document.",
680
680
  steps: [
681
- "Paste JSON on the left.",
681
+ "Paste JSON.",
682
682
  "Pick 2 or 4 spaces, or Compact.",
683
683
  "Copy the result. An invalid document shows where parsing stopped.",
684
684
  ],
@@ -714,7 +714,7 @@ export const TOOLS = [
714
714
  intro: "Compact JSON by removing needless whitespace.",
715
715
  steps: [
716
716
  "Paste JSON.",
717
- "The compacted document and the size saved appear on the right.",
717
+ "The compacted document and the size saved are shown.",
718
718
  "Copy the result.",
719
719
  ],
720
720
  },
@@ -51,7 +51,7 @@ function detectCase(text) {
51
51
  }
52
52
 
53
53
  export function mount(slot) {
54
- const source = textArea({ rows: 5, value: "helloWorld", "aria-label": "Text", placeholder: "Type or paste text" });
54
+ const source = textArea({ rows: 5, value: "helloWorld", "aria-label": "Text" });
55
55
  const detected = el("span", { class: "wrap-anywhere font-mono text-2xs" }, "No letters");
56
56
  const kv = el(
57
57
  "div",
@@ -76,7 +76,7 @@ export function mount(slot) {
76
76
  const on = row.dataset.case === active;
77
77
  if (on) row.dataset.on = "";
78
78
  else delete row.dataset.on;
79
- row.setAttribute("aria-pressed", String(on));
79
+ row.setAttribute("aria-current", on ? "true" : "false");
80
80
  }
81
81
  }
82
82
 
@@ -84,7 +84,7 @@ export function mount(slot) {
84
84
  const button = el(
85
85
  "button",
86
86
  {
87
- class: "group flex w-full cursor-pointer items-baseline justify-between gap-3 rounded-r1 border border-transparent bg-transparent px-3 py-[9px] text-start transition-colors duration-150 hover:bg-deck-2 data-[on]:border-jade data-[on]:bg-jade-soft", type: "button", dataset: { case: name }, "aria-pressed": "false",
87
+ class: "group flex w-full cursor-pointer items-baseline justify-between gap-3 rounded-r1 border border-transparent bg-transparent px-3 py-[9px] text-start transition-colors duration-150 hover:bg-deck-2 data-[on]:border-jade data-[on]:bg-jade-soft", type: "button", dataset: { case: name }, "aria-current": "false",
88
88
  onclick: async () => {
89
89
  const value = button.querySelector("[data-value]").textContent.trim();
90
90
  if (value) toast((await copyText(value)) ? "Copied" : "Copy failed");
@@ -1,13 +1,12 @@
1
1
  // Pretty-prints or compacts JSON and points at the error when it is invalid.
2
- import { bench, stack, panel, textArea, notice, chipRow, copyButton, actions, toolcard } from "../core.js";
2
+ import { bench, stack, panel, textArea, output, notice, chipRow, copyButton, actions, toolcard } from "../core.js";
3
3
 
4
4
  export function mount(slot) {
5
5
  const source = textArea({
6
6
  rows: 12,
7
7
  "aria-label": "JSON",
8
- placeholder: "Paste JSON",
9
8
  value: '{"hello":"world","count":3,"items":[1,2,3]}'});
10
- const result = textArea({ rows: 12, readonly: true, "aria-label": "Result" });
9
+ const result = output({ tag: "textarea", rows: 12 });
11
10
  const error = notice("error");
12
11
  let indent = "2";
13
12
 
@@ -1,5 +1,5 @@
1
1
  // Compacts valid JSON and reports the size saved.
2
- import { el, bench, stack, panel, textArea, notice, copyButton, actions, toolcard } from "../core.js";
2
+ import { el, bench, stack, panel, textArea, output, notice, copyButton, actions, toolcard } from "../core.js";
3
3
 
4
4
  const bytes = (text) => new TextEncoder().encode(text).length;
5
5
 
@@ -10,9 +10,8 @@ export function mount(slot) {
10
10
  const source = textArea({
11
11
  rows: 12,
12
12
  "aria-label": "JSON",
13
- placeholder: "Paste JSON",
14
13
  value: '{\n "hello": "world",\n "count": 3\n}'});
15
- const result = textArea({ rows: 12, readonly: true, "aria-label": "Minified JSON" });
14
+ const result = output({ tag: "textarea", rows: 12 });
16
15
  const stats = el("div", { class: "grid gap-2.5 [grid-template-columns:repeat(auto-fit,minmax(120px,1fr))]" });
17
16
  const error = notice("error");
18
17
 
@@ -56,7 +56,6 @@ export function mount(slot) {
56
56
  const source = textArea({
57
57
  rows: 12,
58
58
  "aria-label": "JSON",
59
- placeholder: "Paste a JSON object or array",
60
59
  value: '{\n "id": 1,\n "name": "Ada",\n "tags": ["dev"],\n "address": { "city": "London" }\n}'});
61
60
  const nameInput = textInput({ value: "Root", "aria-label": "Root type name" });
62
61
  const result = output({ rows: 16 });
@@ -12,7 +12,7 @@ function describe(data) {
12
12
  }
13
13
 
14
14
  export function mount(slot) {
15
- const source = textArea({ rows: 12, "aria-label": "JSON", placeholder: "Paste JSON" });
15
+ const source = textArea({ rows: 12, "aria-label": "JSON" });
16
16
  const report = output({ rows: 4 });
17
17
  const error = notice("error");
18
18
 
@@ -12,7 +12,7 @@ function decodeSegment(segment) {
12
12
  }
13
13
 
14
14
  export function mount(slot) {
15
- const source = textArea({ rows: 4, "aria-label": "Token", placeholder: "Paste a JWT" });
15
+ const source = textArea({ rows: 4, "aria-label": "Token" });
16
16
  const headerOut = output({ rows: 4 });
17
17
  const payloadOut = output({ rows: 8 });
18
18
  const claims = el("div", { class: "grid gap-px overflow-hidden rounded-r1 border border-orbit bg-orbit" });
@@ -1,12 +1,12 @@
1
1
  // Adds a prefix, a suffix, or automatic numbering to every line.
2
- import { bench, stack, panel, textArea, textInput, checkbox, copyButton, actions, toolcard } from "../core.js";
2
+ import { bench, stack, panel, textArea, textInput, output, checkbox, copyButton, actions, toolcard } from "../core.js";
3
3
 
4
4
  export function mount(slot) {
5
5
  const source = textArea({ rows: 11, "aria-label": "Text", placeholder: "Paste your list, one entry per line" });
6
- const prefixField = textInput({ placeholder: "Prefix", "aria-label": "Prefix" });
7
- const suffixField = textInput({ placeholder: "Suffix", "aria-label": "Suffix" });
6
+ const prefixField = textInput({ "aria-label": "Prefix" });
7
+ const suffixField = textInput({ "aria-label": "Suffix" });
8
8
  const numbering = checkbox("Numbering", { checked: false });
9
- const result = textArea({ rows: 11, readonly: true, "aria-label": "Result" });
9
+ const result = output({ tag: "textarea", rows: 11 });
10
10
 
11
11
  function run() {
12
12
  const text = source.value;
@@ -10,7 +10,7 @@ export function mount(slot) {
10
10
  rows: 8,
11
11
  "aria-label": "Text to test",
12
12
  value: "Contact ada@example.com or grace@navy.com for details."});
13
- const pane = el("div", { class: "min-h-[120px] max-h-[380px] overflow-auto wrap-anywhere rounded-r1 border border-orbit bg-deck-2 px-4 py-3.5 leading-normal [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_h1]:my-4 [&_h2]:my-4 [&_h3]:my-4 [&_h1]:tracking-[-0.01em] [&_h2]:tracking-[-0.01em] [&_h3]:tracking-[-0.01em] [&_p]:mb-2.5 [&_ul]:mb-2.5 [&_ol]:mb-2.5 [&_pre]:mb-2.5 [&_ul]:ps-[22px] [&_ol]:ps-[22px] [&_code]:rounded-[5px] [&_code]:bg-deck [&_code]:px-[5px] [&_code]:py-px [&_code]:font-mono [&_code]:text-2xs [&_pre]:overflow-auto [&_pre]:rounded-r1 [&_pre]:bg-deck [&_pre]:px-3 [&_pre]:py-2.5 [&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_a]:text-jade [&_table]:border-collapse [&_th]:border [&_td]:border [&_th]:border-orbit [&_td]:border-orbit [&_th]:px-[9px] [&_td]:px-[9px] [&_th]:py-1.5 [&_td]:py-1.5" });
13
+ const pane = el("div", { class: "min-h-[120px] max-h-[380px] overflow-auto wrap-anywhere rounded-r1 border border-orbit bg-deck-2 px-4 py-3.5 leading-normal" });
14
14
  const listWrap = el("div", { class: "max-h-80 overflow-auto" });
15
15
  const error = notice("error");
16
16
  let lines = [];
@@ -13,7 +13,7 @@ function slugify(text, sep) {
13
13
  }
14
14
 
15
15
  export function mount(slot) {
16
- const source = textInput({ value: "Hello mevebu world", placeholder: "Title", "aria-label": "Title" });
16
+ const source = textInput({ value: "Hello mevebu world", "aria-label": "Title" });
17
17
  const result = textInput({ readonly: true, mono: true, "aria-label": "Slug" });
18
18
 
19
19
  let sep = "-";
@@ -4,8 +4,8 @@ import { el, bench, stack, panel, textArea, toggleRow, notice, button, copyButto
4
4
  const MAX_LINES = 800;
5
5
 
6
6
  export function mount(slot) {
7
- const left = textArea({ rows: 9, "aria-label": "First text", placeholder: "First text" });
8
- const right = textArea({ rows: 9, "aria-label": "Second text", placeholder: "Second text" });
7
+ const left = textArea({ rows: 9, "aria-label": "First text" });
8
+ const right = textArea({ rows: 9, "aria-label": "Second text" });
9
9
  const options = toggleRow([
10
10
  { value: "ignore", label: "Ignore case", checked: false },
11
11
  { value: "spaces", label: "Ignore spaces", checked: false }]);
@@ -52,9 +52,10 @@ export function mount(slot) {
52
52
  const aRaw = left.value === "" ? [] : left.value.split("\n");
53
53
  const bRaw = right.value === "" ? [] : right.value.split("\n");
54
54
  if (!aRaw.length && !bRaw.length) {
55
- diffBox.replaceChildren();
55
+ // The result area explains itself while there is nothing to compare.
56
+ diffBox.replaceChildren(el("div", { class: "bg-deck-2 px-4 py-8 text-center text-sm text-ink-3" }, "Paste two texts to compare."));
56
57
  rows = [];
57
- note.write("Paste two texts to compare.");
58
+ note.write("");
58
59
  return;
59
60
  }
60
61
  const truncated = aRaw.length > MAX_LINES || bRaw.length > MAX_LINES;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mevebu",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
4
4
  "description": "Small tools that run in your browser. No accounts, no uploads, no tracking.",
5
5
  "license": "MIT",
6
6
  "type": "module",