mevebu 1.4.4 → 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.
@@ -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
- const SECTION_MORE = "ms-auto text-sm font-semibold text-jade no-underline hover:underline";
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";
@@ -486,62 +488,122 @@ function wireFilter(root, meta) {
486
488
 
487
489
  // ---------- pages ----------
488
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
+
489
550
  function homePage() {
490
551
  const hero = el(
491
552
  "section",
492
553
  { class: "relative overflow-hidden border-b border-orbit" },
493
554
  el(
494
555
  "div",
495
- { 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]" },
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]" },
496
557
  el(
497
558
  "div",
498
559
  {},
499
- el("h1", { class: "max-w-[20ch] text-balance text-hero font-bold tracking-[-0.03em]" }, "Small tools, one page each."),
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.`),
500
563
  el(
501
564
  "div",
502
- { class: "mt-[clamp(22px,3vw,30px)] flex flex-wrap gap-2.5" },
565
+ { class: "mt-[clamp(26px,3.5vw,36px)] flex flex-wrap items-center gap-2.5" },
503
566
  button("Search tools", { icon: "search", kind: "fill", onclick: () => openPalette() }),
504
567
  el("a", { class: LINK_PLAIN, href: "#/tools" }, icon("shuffle"), el("span", {}, "Browse all"))
505
568
  )
506
569
  ),
507
- el("div", { class: "grid place-items-center rounded-[28px] border border-orbit bg-deck-2 p-6 max-[60rem]:hidden" },
508
- el("img", { class: "block size-[220px]", src: "./icon.png", alt: "", width: "220", height: "220" }))
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" }))
509
572
  )
510
573
  );
511
574
 
512
- const popular = [
513
- "password-generator", "qr-generator", "spin-wheel",
514
- "uuid-generator", "base64", "name-picker",
515
- ].map((id) => TOOLS.find((tool) => tool.id === id)).filter(Boolean);
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;
516
579
 
517
- const section = (title, iconName, more, tools, group) =>
518
- el(
519
- "section",
520
- { class: "pt-[clamp(32px,5vw,60px)]", dataset: { group } },
521
- el(
522
- "div",
523
- { class: "mb-4 flex items-baseline gap-3" },
524
- 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),
525
- more
526
- ),
527
- el("div", { class: "grid grid-cols-1 gap-[var(--gutter)] sm:grid-cols-2 lg:grid-cols-3" }, ...tools.map(toolCard))
528
- );
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();
529
585
 
530
- const grid = el("div", { class: "px-[var(--margin)]" },
531
- section("Popular", "star", el("a", { class: "ms-auto text-sm font-semibold text-jade no-underline hover:underline", href: "#/tools" }, "All tools"), popular, "popular"),
532
- ...CATEGORIES.slice(0, 4).map((category) =>
533
- section(
534
- category.name,
535
- category.icon,
536
- el("a", { class: "ms-auto text-sm font-semibold text-jade no-underline hover:underline", href: `#/${category.id}` }, `Open ${category.name.toLowerCase()}`),
537
- toolsInCategory(category.id).slice(0, 3),
538
- category.id
539
- )
586
+ document.title = "mevebu · small tools that run in your browser";
587
+ return el(
588
+ "div",
589
+ {},
590
+ hero,
591
+ el(
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() }))
540
605
  )
541
606
  );
542
-
543
- document.title = "mevebu · small tools that run in your browser";
544
- return el("div", {}, hero, grid);
545
607
  }
546
608
 
547
609
  function toolsPage() {
@@ -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-\[220px\] {
413
- width: 220px;
414
- height: 220px;
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
  }
@@ -979,8 +1016,8 @@
979
1016
  .py-\[clamp\(28px\,6vw\,56px\)\] {
980
1017
  padding-block: clamp(28px, 6vw, 56px);
981
1018
  }
982
- .py-\[clamp\(36px\,7vw\,68px\)\] {
983
- padding-block: clamp(36px, 7vw, 68px);
1019
+ .py-\[clamp\(44px\,8vw\,88px\)\] {
1020
+ padding-block: clamp(44px, 8vw, 88px);
984
1021
  }
985
1022
  .py-\[var\(--band-pad\)\] {
986
1023
  padding-block: var(--band-pad);
@@ -1057,6 +1094,9 @@
1057
1094
  .pb-10 {
1058
1095
  padding-bottom: calc(var(--spacing) * 10);
1059
1096
  }
1097
+ .pb-\[var\(--margin\)\] {
1098
+ padding-bottom: var(--margin);
1099
+ }
1060
1100
  .text-center {
1061
1101
  text-align: center;
1062
1102
  }
@@ -1147,14 +1187,14 @@
1147
1187
  --tw-tracking: -0.02em;
1148
1188
  letter-spacing: -0.02em;
1149
1189
  }
1150
- .tracking-\[-0\.03em\] {
1151
- --tw-tracking: -0.03em;
1152
- letter-spacing: -0.03em;
1153
- }
1154
1190
  .tracking-\[-0\.025em\] {
1155
1191
  --tw-tracking: -0.025em;
1156
1192
  letter-spacing: -0.025em;
1157
1193
  }
1194
+ .tracking-\[-0\.035em\] {
1195
+ --tw-tracking: -0.035em;
1196
+ letter-spacing: -0.035em;
1197
+ }
1158
1198
  .tracking-\[0\.08em\] {
1159
1199
  --tw-tracking: 0.08em;
1160
1200
  letter-spacing: 0.08em;
@@ -1710,6 +1750,10 @@
1710
1750
  width: calc(var(--spacing) * 4);
1711
1751
  height: calc(var(--spacing) * 4);
1712
1752
  }
1753
+ .\[\&_svg\]\:size-\[19px\] svg {
1754
+ width: 19px;
1755
+ height: 19px;
1756
+ }
1713
1757
  .\[\&_svg\]\:size-\[21px\] svg {
1714
1758
  width: 21px;
1715
1759
  height: 21px;
@@ -1992,14 +2036,14 @@
1992
2036
  color-scheme: light;
1993
2037
  }
1994
2038
  :root[data-theme="dark"] {
1995
- --void: #060d0a;
1996
- --deck: #0c1610;
1997
- --deck-2: #122019;
2039
+ --void: #050b08;
2040
+ --deck: #12211a;
2041
+ --deck-2: #1b2f24;
1998
2042
  --ink: #e7f0ea;
1999
2043
  --ink-2: #a7bbae;
2000
2044
  --ink-3: #85978a;
2001
- --orbit: #1d2c23;
2002
- --orbit-hard: #2b4033;
2045
+ --orbit: #2e4437;
2046
+ --orbit-hard: #3f5c49;
2003
2047
  --jade: #4ad69a;
2004
2048
  --jade-deep: #6ee6b0;
2005
2049
  --on-jade: #04120b;
@@ -93,14 +93,14 @@
93
93
  }
94
94
 
95
95
  :root[data-theme="dark"] {
96
- --void: #060d0a;
97
- --deck: #0c1610;
98
- --deck-2: #122019;
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: #1d2c23;
103
- --orbit-hard: #2b4033;
102
+ --orbit: #2e4437;
103
+ --orbit-hard: #3f5c49;
104
104
  --jade: #4ad69a;
105
105
  --jade-deep: #6ee6b0;
106
106
  --on-jade: #04120b;
@@ -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.4";
746
+ export const VERSION = "1.5.0";
747
747
  export const PACKAGE_NAME = "mevebu";
748
748
  export const REPO_SLUG = "mevebu/mevebu";
749
749
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mevebu",
3
- "version": "1.4.4",
3
+ "version": "1.5.0",
4
4
  "description": "Small tools that run in your browser. No accounts, no uploads, no tracking.",
5
5
  "license": "MIT",
6
6
  "type": "module",