mevebu 1.4.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mevebu/app/mevebu.js +157 -59
- package/mevebu/app/tailwind.css +67 -20
- package/mevebu/app/tailwind.src.css +5 -5
- package/mevebu/libs/js/core.js +35 -17
- package/mevebu/libs/js/registry.js +4 -4
- package/mevebu/libs/js/tools/case-inspector.js +3 -3
- package/mevebu/libs/js/tools/json-formatter.js +2 -3
- package/mevebu/libs/js/tools/json-minifier.js +2 -3
- package/mevebu/libs/js/tools/json-to-typescript.js +0 -1
- package/mevebu/libs/js/tools/json-validator.js +1 -1
- package/mevebu/libs/js/tools/jwt-decoder.js +1 -1
- package/mevebu/libs/js/tools/prefix-suffix.js +4 -4
- package/mevebu/libs/js/tools/regex-tester.js +1 -1
- package/mevebu/libs/js/tools/slug-converter.js +1 -1
- package/mevebu/libs/js/tools/text-diff.js +5 -4
- package/package.json +1 -1
package/mevebu/app/mevebu.js
CHANGED
|
@@ -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";
|
|
@@ -38,7 +38,9 @@ const GRID = "grid grid-cols-1 gap-[var(--gutter)] sm:grid-cols-2 lg:grid-cols-3
|
|
|
38
38
|
const SECTION = "pt-[clamp(32px,5vw,60px)]";
|
|
39
39
|
const SECTION_HEAD = "mb-4 flex items-baseline gap-3";
|
|
40
40
|
const SECTION_TITLE = "flex items-center gap-2.5 text-lg font-bold tracking-[-0.02em]";
|
|
41
|
-
|
|
41
|
+
/* The padding grows the target to a finger while the negative margin cancels
|
|
42
|
+
the height, so a section head keeps its baseline alignment on a phone. */
|
|
43
|
+
const SECTION_MORE = "ms-auto -my-3 inline-flex items-center py-3 text-sm font-semibold text-jade no-underline hover:underline";
|
|
42
44
|
const SECTION_COUNT = "ms-1.5 rounded-full bg-deck-2 px-2 py-px text-2xs font-semibold text-ink-3";
|
|
43
45
|
const LEAD = "mt-[clamp(28px,5vw,56px)] mb-0 max-w-[20ch] text-balance text-lead font-bold tracking-[-0.025em]";
|
|
44
46
|
const LEDE = "mt-3 mb-0 max-w-[60ch] text-pretty text-md text-ink-2";
|
|
@@ -174,17 +176,18 @@ function openDonate() {
|
|
|
174
176
|
});
|
|
175
177
|
}
|
|
176
178
|
|
|
177
|
-
// `latest` is the published version, or null when the
|
|
179
|
+
// The footer chip opens this. `latest` is the published version, or null when the
|
|
180
|
+
// check has not answered; opening the modal retries it, so a registry hiccup on
|
|
181
|
+
// the first load is not a dead end. The body and the actions repaint in place
|
|
182
|
+
// while that retry runs.
|
|
178
183
|
function openUpdate(latest) {
|
|
179
|
-
|
|
180
|
-
|
|
184
|
+
let resolved = latest;
|
|
185
|
+
let busy = false;
|
|
181
186
|
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
copyButton(() => command, "Copy command", "fill")
|
|
187
|
-
);
|
|
187
|
+
const bodyBox = el("div", { class: "grid gap-2.5" });
|
|
188
|
+
const actionBox = el("div", { class: "flex flex-wrap items-center justify-end gap-2" });
|
|
189
|
+
const command = updateCommand();
|
|
190
|
+
const updatable = () => Boolean(resolved) && compareVersions(resolved, VERSION) > 0;
|
|
188
191
|
|
|
189
192
|
const versionRow = (label, value, isNew) =>
|
|
190
193
|
el(
|
|
@@ -194,22 +197,57 @@ function openUpdate(latest) {
|
|
|
194
197
|
el("span", { class: `font-mono text-sm font-semibold ${isNew ? "text-jade" : ""}` }, value)
|
|
195
198
|
);
|
|
196
199
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
modalHead(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
200
|
+
function paint() {
|
|
201
|
+
const newer = updatable();
|
|
202
|
+
bodyBox.replaceChildren(...[
|
|
203
|
+
modalHead(
|
|
204
|
+
busy || !resolved ? "refresh-cw" : "circle-arrow-up",
|
|
205
|
+
busy
|
|
206
|
+
? "Checking for updates"
|
|
207
|
+
: newer
|
|
208
|
+
? `mevebu ${resolved} is out`
|
|
209
|
+
: resolved
|
|
210
|
+
? "mevebu is up to date"
|
|
211
|
+
: "Could not check for updates"
|
|
212
|
+
),
|
|
213
|
+
newer
|
|
214
|
+
? el("p", {}, "A newer version is published. Update your global install, then run ", el("code", { class: CODE }, PACKAGE_NAME), " again.")
|
|
215
|
+
: null,
|
|
204
216
|
el("div", { class: "mt-[18px] overflow-hidden rounded-r2 border border-orbit" },
|
|
205
217
|
versionRow("Installed", VERSION, false),
|
|
206
|
-
versionRow("Latest",
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
218
|
+
versionRow("Latest", busy ? "Checking" : resolved ?? "Unknown", newer)),
|
|
219
|
+
newer ? el("p", { class: "mt-3.5 text-sm" }, "Copy the command below and run it in your terminal.") : null,
|
|
220
|
+
newer
|
|
221
|
+
? 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" },
|
|
222
|
+
el("code", { class: "min-w-0 flex-1 font-mono text-sm [overflow-wrap:anywhere]" }, command),
|
|
223
|
+
copyButton(() => command, "Copy command", "fill"))
|
|
224
|
+
: null,
|
|
225
|
+
].filter(Boolean));
|
|
226
|
+
|
|
227
|
+
actionBox.replaceChildren(...[
|
|
228
|
+
!resolved && !busy ? button("Check again", { icon: "refresh-cw", onclick: () => check() }) : null,
|
|
229
|
+
el("a", { class: LINK_PLAIN, href: releaseUrl(resolved), target: "_blank", rel: "noopener" }, "What changed"),
|
|
230
|
+
].filter(Boolean));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function check() {
|
|
234
|
+
if (busy) return;
|
|
235
|
+
busy = true;
|
|
236
|
+
paint();
|
|
237
|
+
const result = await checkForUpdate();
|
|
238
|
+
busy = false;
|
|
239
|
+
if (result?.latest) resolved = result.latest;
|
|
240
|
+
paint();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
paint();
|
|
244
|
+
modal({
|
|
245
|
+
label: "Update mevebu",
|
|
246
|
+
body: [bodyBox],
|
|
247
|
+
actions: [actionBox],
|
|
248
|
+
onOpen: () => {
|
|
249
|
+
if (!resolved) check();
|
|
250
|
+
},
|
|
213
251
|
});
|
|
214
252
|
}
|
|
215
253
|
|
|
@@ -450,62 +488,122 @@ function wireFilter(root, meta) {
|
|
|
450
488
|
|
|
451
489
|
// ---------- pages ----------
|
|
452
490
|
|
|
491
|
+
/* Home furniture. The tiles are shorter than a tool card on purpose: a category
|
|
492
|
+
is a way in, not a destination, so it gets one line and its count. */
|
|
493
|
+
const FAVORITE_CARDS = 6;
|
|
494
|
+
const RANDOM_CARDS = 9;
|
|
495
|
+
const TILE =
|
|
496
|
+
"flex items-center gap-3 rounded-r2 border border-orbit bg-deck px-4 py-3.5 no-underline transition-colors duration-150 hover:border-jade";
|
|
497
|
+
const COUNT_PILL = "flex-none rounded-full bg-deck-2 px-2.5 py-1 text-2xs font-semibold text-ink-3";
|
|
498
|
+
const REDIRECT_CARD =
|
|
499
|
+
"relative grid content-center justify-items-center gap-1.5 rounded-r2 border border-orbit bg-deck p-5 text-center no-underline transition-colors duration-150 hover:border-jade";
|
|
500
|
+
|
|
501
|
+
function homeSection(title, iconName, aside, body) {
|
|
502
|
+
return el(
|
|
503
|
+
"section",
|
|
504
|
+
{ class: SECTION },
|
|
505
|
+
el(
|
|
506
|
+
"div",
|
|
507
|
+
{ class: SECTION_HEAD },
|
|
508
|
+
el("h2", { class: SECTION_TITLE }, icon(iconName, "size-[22px] flex-none text-jade"), title),
|
|
509
|
+
aside
|
|
510
|
+
),
|
|
511
|
+
body
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const homeMore = (href, label) => el("a", { class: SECTION_MORE, href }, label);
|
|
516
|
+
|
|
517
|
+
function categoryTile(category) {
|
|
518
|
+
return el(
|
|
519
|
+
"a",
|
|
520
|
+
{ class: TILE, href: `#/${category.id}` },
|
|
521
|
+
el("span", { class: "grid size-[38px] flex-none place-items-center rounded-[12px] bg-jade-soft text-jade [&_svg]:size-[19px]" }, icon(category.icon)),
|
|
522
|
+
el("span", { class: "min-w-0 flex-1 truncate text-md font-semibold tracking-[-0.01em]" }, category.name),
|
|
523
|
+
el("span", { class: COUNT_PILL }, toolsInCategory(category.id).length)
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/* The favourites row is a shortcut, so it earns a shorter sentence than the
|
|
528
|
+
empty favourites page: the reader sees why it is empty and where to go. */
|
|
529
|
+
function favoritesEmpty() {
|
|
530
|
+
return el(
|
|
531
|
+
"div",
|
|
532
|
+
{ class: "flex flex-wrap items-center gap-x-4 gap-y-3 rounded-r2 border border-dashed border-orbit-hard bg-deck p-6" },
|
|
533
|
+
el("span", { class: "grid size-[42px] flex-none place-items-center rounded-[13px] bg-jade-soft text-jade [&_svg]:size-[21px]" }, icon("star")),
|
|
534
|
+
el("p", { class: "m-0 max-w-[54ch] text-pretty text-md text-ink-2" },
|
|
535
|
+
"Nothing saved yet. Tap the star on any tool and it lands on this row, so it stays one click away."),
|
|
536
|
+
el("a", { class: `${LINK_PLAIN} ms-auto`, href: "#/tools" }, "Browse tools")
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function favoritesRest(rest) {
|
|
541
|
+
return el(
|
|
542
|
+
"a",
|
|
543
|
+
{ class: REDIRECT_CARD, href: "#/favorites" },
|
|
544
|
+
el("span", { class: "grid size-[42px] place-items-center rounded-[13px] bg-jade-soft text-jade [&_svg]:size-[21px]" }, icon("star")),
|
|
545
|
+
el("span", { class: "text-md font-semibold tracking-[-0.01em]" }, rest > 0 ? `${rest} more` : "See all"),
|
|
546
|
+
el("span", { class: "text-sm text-ink-2" }, "Open your favorites")
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
453
550
|
function homePage() {
|
|
454
551
|
const hero = el(
|
|
455
552
|
"section",
|
|
456
553
|
{ class: "relative overflow-hidden border-b border-orbit" },
|
|
457
554
|
el(
|
|
458
555
|
"div",
|
|
459
|
-
{ class: "mx-[var(--margin)] grid items-center gap-[clamp(
|
|
556
|
+
{ class: "mx-[var(--margin)] grid items-center gap-[clamp(32px,5vw,64px)] py-[clamp(44px,8vw,88px)] min-[60rem]:grid-cols-[minmax(0,1fr)_auto]" },
|
|
460
557
|
el(
|
|
461
558
|
"div",
|
|
462
559
|
{},
|
|
463
|
-
el("h1", { class: "max-w-[
|
|
560
|
+
el("h1", { class: "max-w-[18ch] text-balance text-hero font-bold tracking-[-0.035em]" }, "Small tools, one page each."),
|
|
561
|
+
el("p", { class: "mt-5 mb-0 max-w-[48ch] text-pretty text-md text-ink-2" },
|
|
562
|
+
`${TOOLS.length} tools across ${CATEGORIES.length} categories, and every one of them runs in your browser.`),
|
|
464
563
|
el(
|
|
465
564
|
"div",
|
|
466
|
-
{ class: "mt-[clamp(
|
|
565
|
+
{ class: "mt-[clamp(26px,3.5vw,36px)] flex flex-wrap items-center gap-2.5" },
|
|
467
566
|
button("Search tools", { icon: "search", kind: "fill", onclick: () => openPalette() }),
|
|
468
567
|
el("a", { class: LINK_PLAIN, href: "#/tools" }, icon("shuffle"), el("span", {}, "Browse all"))
|
|
469
568
|
)
|
|
470
569
|
),
|
|
471
|
-
el("div", { class: "grid place-items-center rounded-[28px] border border-orbit bg-deck
|
|
472
|
-
el("img", { class: "block size-[
|
|
570
|
+
el("div", { class: "grid place-items-center rounded-[28px] border border-orbit bg-deck p-7 max-[60rem]:hidden" },
|
|
571
|
+
el("img", { class: "block size-[248px]", src: "./icon.png", alt: "", width: "248", height: "248" }))
|
|
473
572
|
)
|
|
474
573
|
);
|
|
475
574
|
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
575
|
+
const favorites = getFavorites();
|
|
576
|
+
const saved = TOOLS.filter((tool) => favorites.has(tool.id));
|
|
577
|
+
const shown = saved.slice(0, FAVORITE_CARDS - 1);
|
|
578
|
+
const rest = saved.length - shown.length;
|
|
579
|
+
|
|
580
|
+
// Nine picks, reshuffled on demand: the point is to meet a tool you did not
|
|
581
|
+
// know was there, so the order is drawn fresh every visit.
|
|
582
|
+
const randomGrid = el("div", { class: GRID });
|
|
583
|
+
const fillRandom = () => randomGrid.replaceChildren(...shuffle(TOOLS).slice(0, RANDOM_CARDS).map(toolCard));
|
|
584
|
+
fillRandom();
|
|
480
585
|
|
|
481
|
-
|
|
586
|
+
document.title = "mevebu · small tools that run in your browser";
|
|
587
|
+
return el(
|
|
588
|
+
"div",
|
|
589
|
+
{},
|
|
590
|
+
hero,
|
|
482
591
|
el(
|
|
483
|
-
"
|
|
484
|
-
{ class:
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
...CATEGORIES.slice(0, 4).map((category) =>
|
|
497
|
-
section(
|
|
498
|
-
category.name,
|
|
499
|
-
category.icon,
|
|
500
|
-
el("a", { class: "ms-auto text-sm font-semibold text-jade no-underline hover:underline", href: `#/${category.id}` }, `Open ${category.name.toLowerCase()}`),
|
|
501
|
-
toolsInCategory(category.id).slice(0, 3),
|
|
502
|
-
category.id
|
|
503
|
-
)
|
|
592
|
+
"div",
|
|
593
|
+
{ class: `pb-[var(--margin)] ${PAGE}` },
|
|
594
|
+
homeSection("Your favorites", "star", saved.length ? homeMore("#/favorites", "See all") : null,
|
|
595
|
+
saved.length
|
|
596
|
+
? el("div", { class: GRID }, ...shown.map(toolCard), favoritesRest(rest))
|
|
597
|
+
: favoritesEmpty()),
|
|
598
|
+
homeSection("Categories", "layout-grid", homeMore("#/categories", "All categories"),
|
|
599
|
+
el("div", { class: GRID }, ...CATEGORIES.map(categoryTile))),
|
|
600
|
+
homeSection("Random picks", "shuffle",
|
|
601
|
+
el("button", { class: `${LINK_PLAIN} ms-auto`, type: "button", onclick: fillRandom }, icon("shuffle"), el("span", {}, "Shuffle")),
|
|
602
|
+
randomGrid),
|
|
603
|
+
el("div", { class: "mt-[clamp(30px,5vw,54px)] flex justify-center" },
|
|
604
|
+
button("Search tools", { icon: "search", kind: "fill", onclick: () => openPalette() }))
|
|
504
605
|
)
|
|
505
606
|
);
|
|
506
|
-
|
|
507
|
-
document.title = "mevebu · small tools that run in your browser";
|
|
508
|
-
return el("div", {}, hero, grid);
|
|
509
607
|
}
|
|
510
608
|
|
|
511
609
|
function toolsPage() {
|
package/mevebu/app/tailwind.css
CHANGED
|
@@ -271,6 +271,9 @@
|
|
|
271
271
|
.mx-auto {
|
|
272
272
|
margin-inline: auto;
|
|
273
273
|
}
|
|
274
|
+
.-my-3 {
|
|
275
|
+
margin-block: calc(var(--spacing) * -3);
|
|
276
|
+
}
|
|
274
277
|
.ms-1\.5 {
|
|
275
278
|
margin-inline-start: calc(var(--spacing) * 1.5);
|
|
276
279
|
}
|
|
@@ -301,27 +304,33 @@
|
|
|
301
304
|
.mt-3\.5 {
|
|
302
305
|
margin-top: calc(var(--spacing) * 3.5);
|
|
303
306
|
}
|
|
307
|
+
.mt-5 {
|
|
308
|
+
margin-top: calc(var(--spacing) * 5);
|
|
309
|
+
}
|
|
304
310
|
.mt-8 {
|
|
305
311
|
margin-top: calc(var(--spacing) * 8);
|
|
306
312
|
}
|
|
307
313
|
.mt-\[18px\] {
|
|
308
314
|
margin-top: 18px;
|
|
309
315
|
}
|
|
310
|
-
.mt-\[clamp\(22px\,3vw\,30px\)\] {
|
|
311
|
-
margin-top: clamp(22px, 3vw, 30px);
|
|
312
|
-
}
|
|
313
316
|
.mt-\[clamp\(22px\,4vw\,36px\)\] {
|
|
314
317
|
margin-top: clamp(22px, 4vw, 36px);
|
|
315
318
|
}
|
|
316
319
|
.mt-\[clamp\(24px\,4vw\,44px\)\] {
|
|
317
320
|
margin-top: clamp(24px, 4vw, 44px);
|
|
318
321
|
}
|
|
322
|
+
.mt-\[clamp\(26px\,3\.5vw\,36px\)\] {
|
|
323
|
+
margin-top: clamp(26px, 3.5vw, 36px);
|
|
324
|
+
}
|
|
319
325
|
.mt-\[clamp\(26px\,4vw\,44px\)\] {
|
|
320
326
|
margin-top: clamp(26px, 4vw, 44px);
|
|
321
327
|
}
|
|
322
328
|
.mt-\[clamp\(28px\,5vw\,56px\)\] {
|
|
323
329
|
margin-top: clamp(28px, 5vw, 56px);
|
|
324
330
|
}
|
|
331
|
+
.mt-\[clamp\(30px\,5vw\,54px\)\] {
|
|
332
|
+
margin-top: clamp(30px, 5vw, 54px);
|
|
333
|
+
}
|
|
325
334
|
.mt-\[clamp\(40px\,6vw\,72px\)\] {
|
|
326
335
|
margin-top: clamp(40px, 6vw, 72px);
|
|
327
336
|
}
|
|
@@ -405,13 +414,17 @@
|
|
|
405
414
|
width: 34px;
|
|
406
415
|
height: 34px;
|
|
407
416
|
}
|
|
417
|
+
.size-\[38px\] {
|
|
418
|
+
width: 38px;
|
|
419
|
+
height: 38px;
|
|
420
|
+
}
|
|
408
421
|
.size-\[42px\] {
|
|
409
422
|
width: 42px;
|
|
410
423
|
height: 42px;
|
|
411
424
|
}
|
|
412
|
-
.size-\[
|
|
413
|
-
width:
|
|
414
|
-
height:
|
|
425
|
+
.size-\[248px\] {
|
|
426
|
+
width: 248px;
|
|
427
|
+
height: 248px;
|
|
415
428
|
}
|
|
416
429
|
.size-\[var\(--ctl\)\] {
|
|
417
430
|
width: var(--ctl);
|
|
@@ -516,12 +529,21 @@
|
|
|
516
529
|
.w-full {
|
|
517
530
|
width: 100%;
|
|
518
531
|
}
|
|
532
|
+
.max-w-\[18ch\] {
|
|
533
|
+
max-width: 18ch;
|
|
534
|
+
}
|
|
519
535
|
.max-w-\[20ch\] {
|
|
520
536
|
max-width: 20ch;
|
|
521
537
|
}
|
|
522
538
|
.max-w-\[30ch\] {
|
|
523
539
|
max-width: 30ch;
|
|
524
540
|
}
|
|
541
|
+
.max-w-\[48ch\] {
|
|
542
|
+
max-width: 48ch;
|
|
543
|
+
}
|
|
544
|
+
.max-w-\[54ch\] {
|
|
545
|
+
max-width: 54ch;
|
|
546
|
+
}
|
|
525
547
|
.max-w-\[58ch\] {
|
|
526
548
|
max-width: 58ch;
|
|
527
549
|
}
|
|
@@ -621,6 +643,9 @@
|
|
|
621
643
|
.place-items-center {
|
|
622
644
|
place-items: center;
|
|
623
645
|
}
|
|
646
|
+
.content-center {
|
|
647
|
+
align-content: center;
|
|
648
|
+
}
|
|
624
649
|
.content-start {
|
|
625
650
|
align-content: flex-start;
|
|
626
651
|
}
|
|
@@ -681,27 +706,33 @@
|
|
|
681
706
|
.gap-\[clamp\(14px\,3vw\,32px\)\] {
|
|
682
707
|
gap: clamp(14px, 3vw, 32px);
|
|
683
708
|
}
|
|
684
|
-
.gap-\[clamp\(24px\,5vw\,48px\)\] {
|
|
685
|
-
gap: clamp(24px, 5vw, 48px);
|
|
686
|
-
}
|
|
687
709
|
.gap-\[clamp\(24px\,5vw\,64px\)\] {
|
|
688
710
|
gap: clamp(24px, 5vw, 64px);
|
|
689
711
|
}
|
|
690
712
|
.gap-\[clamp\(28px\,6vw\,72px\)\] {
|
|
691
713
|
gap: clamp(28px, 6vw, 72px);
|
|
692
714
|
}
|
|
715
|
+
.gap-\[clamp\(32px\,5vw\,64px\)\] {
|
|
716
|
+
gap: clamp(32px, 5vw, 64px);
|
|
717
|
+
}
|
|
693
718
|
.gap-\[var\(--gutter\)\] {
|
|
694
719
|
gap: var(--gutter);
|
|
695
720
|
}
|
|
696
721
|
.gap-px {
|
|
697
722
|
gap: 1px;
|
|
698
723
|
}
|
|
724
|
+
.gap-x-4 {
|
|
725
|
+
column-gap: calc(var(--spacing) * 4);
|
|
726
|
+
}
|
|
699
727
|
.gap-x-5 {
|
|
700
728
|
column-gap: calc(var(--spacing) * 5);
|
|
701
729
|
}
|
|
702
730
|
.gap-y-2 {
|
|
703
731
|
row-gap: calc(var(--spacing) * 2);
|
|
704
732
|
}
|
|
733
|
+
.gap-y-3 {
|
|
734
|
+
row-gap: calc(var(--spacing) * 3);
|
|
735
|
+
}
|
|
705
736
|
.truncate {
|
|
706
737
|
overflow: hidden;
|
|
707
738
|
text-overflow: ellipsis;
|
|
@@ -740,6 +771,9 @@
|
|
|
740
771
|
.rounded-\[11px\] {
|
|
741
772
|
border-radius: 11px;
|
|
742
773
|
}
|
|
774
|
+
.rounded-\[12px\] {
|
|
775
|
+
border-radius: 12px;
|
|
776
|
+
}
|
|
743
777
|
.rounded-\[13px\] {
|
|
744
778
|
border-radius: 13px;
|
|
745
779
|
}
|
|
@@ -892,6 +926,9 @@
|
|
|
892
926
|
.p-6 {
|
|
893
927
|
padding: calc(var(--spacing) * 6);
|
|
894
928
|
}
|
|
929
|
+
.p-7 {
|
|
930
|
+
padding: calc(var(--spacing) * 7);
|
|
931
|
+
}
|
|
895
932
|
.p-\[18px\] {
|
|
896
933
|
padding: 18px;
|
|
897
934
|
}
|
|
@@ -961,6 +998,9 @@
|
|
|
961
998
|
.py-7 {
|
|
962
999
|
padding-block: calc(var(--spacing) * 7);
|
|
963
1000
|
}
|
|
1001
|
+
.py-8 {
|
|
1002
|
+
padding-block: calc(var(--spacing) * 8);
|
|
1003
|
+
}
|
|
964
1004
|
.py-\[3px\] {
|
|
965
1005
|
padding-block: 3px;
|
|
966
1006
|
}
|
|
@@ -976,8 +1016,8 @@
|
|
|
976
1016
|
.py-\[clamp\(28px\,6vw\,56px\)\] {
|
|
977
1017
|
padding-block: clamp(28px, 6vw, 56px);
|
|
978
1018
|
}
|
|
979
|
-
.py-\[clamp\(
|
|
980
|
-
padding-block: clamp(
|
|
1019
|
+
.py-\[clamp\(44px\,8vw\,88px\)\] {
|
|
1020
|
+
padding-block: clamp(44px, 8vw, 88px);
|
|
981
1021
|
}
|
|
982
1022
|
.py-\[var\(--band-pad\)\] {
|
|
983
1023
|
padding-block: var(--band-pad);
|
|
@@ -1054,6 +1094,9 @@
|
|
|
1054
1094
|
.pb-10 {
|
|
1055
1095
|
padding-bottom: calc(var(--spacing) * 10);
|
|
1056
1096
|
}
|
|
1097
|
+
.pb-\[var\(--margin\)\] {
|
|
1098
|
+
padding-bottom: var(--margin);
|
|
1099
|
+
}
|
|
1057
1100
|
.text-center {
|
|
1058
1101
|
text-align: center;
|
|
1059
1102
|
}
|
|
@@ -1144,14 +1187,14 @@
|
|
|
1144
1187
|
--tw-tracking: -0.02em;
|
|
1145
1188
|
letter-spacing: -0.02em;
|
|
1146
1189
|
}
|
|
1147
|
-
.tracking-\[-0\.03em\] {
|
|
1148
|
-
--tw-tracking: -0.03em;
|
|
1149
|
-
letter-spacing: -0.03em;
|
|
1150
|
-
}
|
|
1151
1190
|
.tracking-\[-0\.025em\] {
|
|
1152
1191
|
--tw-tracking: -0.025em;
|
|
1153
1192
|
letter-spacing: -0.025em;
|
|
1154
1193
|
}
|
|
1194
|
+
.tracking-\[-0\.035em\] {
|
|
1195
|
+
--tw-tracking: -0.035em;
|
|
1196
|
+
letter-spacing: -0.035em;
|
|
1197
|
+
}
|
|
1155
1198
|
.tracking-\[0\.08em\] {
|
|
1156
1199
|
--tw-tracking: 0.08em;
|
|
1157
1200
|
letter-spacing: 0.08em;
|
|
@@ -1707,6 +1750,10 @@
|
|
|
1707
1750
|
width: calc(var(--spacing) * 4);
|
|
1708
1751
|
height: calc(var(--spacing) * 4);
|
|
1709
1752
|
}
|
|
1753
|
+
.\[\&_svg\]\:size-\[19px\] svg {
|
|
1754
|
+
width: 19px;
|
|
1755
|
+
height: 19px;
|
|
1756
|
+
}
|
|
1710
1757
|
.\[\&_svg\]\:size-\[21px\] svg {
|
|
1711
1758
|
width: 21px;
|
|
1712
1759
|
height: 21px;
|
|
@@ -1989,14 +2036,14 @@
|
|
|
1989
2036
|
color-scheme: light;
|
|
1990
2037
|
}
|
|
1991
2038
|
:root[data-theme="dark"] {
|
|
1992
|
-
--void: #
|
|
1993
|
-
--deck: #
|
|
1994
|
-
--deck-2: #
|
|
2039
|
+
--void: #050b08;
|
|
2040
|
+
--deck: #12211a;
|
|
2041
|
+
--deck-2: #1b2f24;
|
|
1995
2042
|
--ink: #e7f0ea;
|
|
1996
2043
|
--ink-2: #a7bbae;
|
|
1997
2044
|
--ink-3: #85978a;
|
|
1998
|
-
--orbit: #
|
|
1999
|
-
--orbit-hard: #
|
|
2045
|
+
--orbit: #2e4437;
|
|
2046
|
+
--orbit-hard: #3f5c49;
|
|
2000
2047
|
--jade: #4ad69a;
|
|
2001
2048
|
--jade-deep: #6ee6b0;
|
|
2002
2049
|
--on-jade: #04120b;
|
|
@@ -93,14 +93,14 @@
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
:root[data-theme="dark"] {
|
|
96
|
-
--void: #
|
|
97
|
-
--deck: #
|
|
98
|
-
--deck-2: #
|
|
96
|
+
--void: #050b08;
|
|
97
|
+
--deck: #12211a;
|
|
98
|
+
--deck-2: #1b2f24;
|
|
99
99
|
--ink: #e7f0ea;
|
|
100
100
|
--ink-2: #a7bbae;
|
|
101
101
|
--ink-3: #85978a;
|
|
102
|
-
--orbit: #
|
|
103
|
-
--orbit-hard: #
|
|
102
|
+
--orbit: #2e4437;
|
|
103
|
+
--orbit-hard: #3f5c49;
|
|
104
104
|
--jade: #4ad69a;
|
|
105
105
|
--jade-deep: #6ee6b0;
|
|
106
106
|
--on-jade: #04120b;
|
package/mevebu/libs/js/core.js
CHANGED
|
@@ -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.
|
|
746
|
+
export const VERSION = "1.5.0";
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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"
|
|
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-
|
|
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-
|
|
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 =
|
|
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 =
|
|
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"
|
|
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"
|
|
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({
|
|
7
|
-
const suffixField = textInput({
|
|
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 =
|
|
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
|
|
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",
|
|
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"
|
|
8
|
-
const right = textArea({ rows: 9, "aria-label": "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
|
-
|
|
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("
|
|
58
|
+
note.write("");
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
60
61
|
const truncated = aRaw.length > MAX_LINES || bRaw.length > MAX_LINES;
|