mevebu 1.5.1 → 1.7.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.
@@ -133,7 +133,7 @@ function masthead() {
133
133
  "div",
134
134
  { class: "mx-[var(--margin)] flex h-[var(--bar)] items-center gap-[clamp(14px,3vw,32px)]" },
135
135
  el("a", { class: "inline-flex min-h-11 flex-none items-center gap-2.5 text-lg font-bold tracking-[-0.01em] text-ink no-underline", href: "#/" },
136
- el("img", { class: "block size-[30px] rounded-[9px] shadow-[0_0_0_1px_var(--orbit)]", src: "./icon.png", alt: "", width: "30", height: "30" }), "mevebu"),
136
+ el("img", { class: "block size-[30px]", src: "./assets/logo.png", alt: "", width: "30", height: "30" }), "mevebu"),
137
137
  nav,
138
138
  el("div", { class: "flex items-center gap-0.5 max-[45rem]:ms-auto" }, searchButton(), themeBtn, menuBtn)
139
139
  ),
@@ -326,7 +326,7 @@ function footer() {
326
326
  "div",
327
327
  { class: "grid gap-1.5" },
328
328
  el("a", { class: "inline-flex min-h-11 items-center gap-[9px] text-md font-bold tracking-[-0.01em] text-ink no-underline", href: "#/" },
329
- el("img", { class: "block size-[34px] rounded-[11px] shadow-[0_0_0_1px_var(--orbit)]", src: "./icon.png", alt: "", width: "34", height: "34" }), "mevebu"),
329
+ el("img", { class: "block size-[34px]", src: "./assets/logo.png", alt: "", width: "34", height: "34" }), "mevebu"),
330
330
  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.")
331
331
  ),
332
332
  el(
@@ -573,7 +573,7 @@ function homePage() {
573
573
  )
574
574
  ),
575
575
  el("div", { class: "grid place-items-center rounded-[28px] border border-orbit bg-deck p-7 max-[60rem]:hidden" },
576
- el("img", { class: "block size-[248px]", src: "./icon.png", alt: "", width: "248", height: "248" }))
576
+ el("img", { class: "block size-[248px]", src: "./assets/logo.png", alt: "", width: "248", height: "248" }))
577
577
  )
578
578
  );
579
579
 
@@ -631,6 +631,9 @@
631
631
  .grid-cols-1 {
632
632
  grid-template-columns: repeat(1, minmax(0, 1fr));
633
633
  }
634
+ .grid-cols-2 {
635
+ grid-template-columns: repeat(2, minmax(0, 1fr));
636
+ }
634
637
  .grid-cols-\[minmax\(0\,1fr\)_auto\] {
635
638
  grid-template-columns: minmax(0,1fr) auto;
636
639
  }
@@ -765,12 +768,6 @@
765
768
  .rounded-\[5px\] {
766
769
  border-radius: 5px;
767
770
  }
768
- .rounded-\[9px\] {
769
- border-radius: 9px;
770
- }
771
- .rounded-\[11px\] {
772
- border-radius: 11px;
773
- }
774
771
  .rounded-\[12px\] {
775
772
  border-radius: 12px;
776
773
  }
@@ -830,6 +827,9 @@
830
827
  --tw-border-style: dashed;
831
828
  border-style: dashed;
832
829
  }
830
+ .border-danger {
831
+ border-color: var(--danger);
832
+ }
833
833
  .border-jade {
834
834
  border-color: var(--jade);
835
835
  }
@@ -857,6 +857,9 @@
857
857
  .bg-black {
858
858
  background-color: var(--color-black);
859
859
  }
860
+ .bg-danger {
861
+ background-color: var(--danger);
862
+ }
860
863
  .bg-deck {
861
864
  background-color: var(--deck);
862
865
  }
@@ -1235,6 +1238,9 @@
1235
1238
  .text-jade {
1236
1239
  color: var(--jade);
1237
1240
  }
1241
+ .text-on-danger {
1242
+ color: var(--on-danger);
1243
+ }
1238
1244
  .text-on-jade {
1239
1245
  color: var(--on-jade);
1240
1246
  }
@@ -1274,10 +1280,6 @@
1274
1280
  --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
1275
1281
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
1276
1282
  }
1277
- .shadow-\[0_0_0_1px_var\(--orbit\)\] {
1278
- --tw-shadow: 0 0 0 1px var(--tw-shadow-color, var(--orbit));
1279
- box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
1280
- }
1281
1283
  .shadow-\[0_1px_2px_rgb\(0_0_0\/0\.18\)\] {
1282
1284
  --tw-shadow: 0 1px 2px var(--tw-shadow-color, rgb(0 0 0/0.18));
1283
1285
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -1429,6 +1431,9 @@
1429
1431
  border-color: var(--jade);
1430
1432
  }
1431
1433
  @media (hover: hover) {
1434
+ .hover\:border-danger-deep:hover {
1435
+ border-color: var(--danger-deep);
1436
+ }
1432
1437
  .hover\:border-jade:hover {
1433
1438
  border-color: var(--jade);
1434
1439
  }
@@ -1438,6 +1443,9 @@
1438
1443
  .hover\:border-orbit-hard:hover {
1439
1444
  border-color: var(--orbit-hard);
1440
1445
  }
1446
+ .hover\:bg-danger-deep:hover {
1447
+ background-color: var(--danger-deep);
1448
+ }
1441
1449
  .hover\:bg-deck:hover {
1442
1450
  background-color: var(--deck);
1443
1451
  }
@@ -1661,6 +1669,11 @@
1661
1669
  grid-template-columns: repeat(3, minmax(0, 1fr));
1662
1670
  }
1663
1671
  }
1672
+ @media (width >= 80rem) {
1673
+ .min-\[80rem\]\:grid-cols-2 {
1674
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1675
+ }
1676
+ }
1664
1677
  .\[\&_a\]\:text-jade a {
1665
1678
  color: var(--jade);
1666
1679
  }
@@ -2004,6 +2017,8 @@
2004
2017
  --on-jade: #ffffff;
2005
2018
  --jade-soft: #e0f0e7;
2006
2019
  --danger: #b0231d;
2020
+ --danger-deep: #8e1b16;
2021
+ --on-danger: #ffffff;
2007
2022
  --star: #b37700;
2008
2023
  --track: #bfcac1;
2009
2024
  --scrim: rgb(8 14 11 / 0.5);
@@ -2049,8 +2064,10 @@
2049
2064
  --on-jade: #04120b;
2050
2065
  --jade-soft: #10291f;
2051
2066
  --danger: #ff8a80;
2067
+ --danger-deep: #ffb3ab;
2068
+ --on-danger: #2a0b08;
2052
2069
  --star: #ffcf4d;
2053
- --track: #2b4033;
2070
+ --track: #3f5c49;
2054
2071
  --scrim: rgb(2 6 4 / 0.68);
2055
2072
  --shadow: 0 22px 50px -20px rgb(0 0 0 / 0.7);
2056
2073
  --caret: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2385978a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
@@ -2085,7 +2102,10 @@
2085
2102
  border-radius: 6px;
2086
2103
  }
2087
2104
  ::selection {
2088
- background: var(--jade-soft);
2105
+ background: var(--jade);
2106
+ @supports (color: color-mix(in lab, red, red)) {
2107
+ background: color-mix(in srgb, var(--jade) 34%, transparent);
2108
+ }
2089
2109
  }
2090
2110
  @media (prefers-reduced-motion: reduce) {
2091
2111
  *, *::before, *::after {
@@ -50,6 +50,8 @@
50
50
  --on-jade: #ffffff;
51
51
  --jade-soft: #e0f0e7;
52
52
  --danger: #b0231d;
53
+ --danger-deep: #8e1b16;
54
+ --on-danger: #ffffff;
53
55
  --star: #b37700;
54
56
  --track: #bfcac1;
55
57
  --scrim: rgb(8 14 11 / 0.5);
@@ -106,8 +108,10 @@
106
108
  --on-jade: #04120b;
107
109
  --jade-soft: #10291f;
108
110
  --danger: #ff8a80;
111
+ --danger-deep: #ffb3ab;
112
+ --on-danger: #2a0b08;
109
113
  --star: #ffcf4d;
110
- --track: #2b4033;
114
+ --track: #3f5c49;
111
115
  --scrim: rgb(2 6 4 / 0.68);
112
116
  --shadow: 0 22px 50px -20px rgb(0 0 0 / 0.7);
113
117
  --caret: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2385978a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
@@ -136,6 +140,8 @@
136
140
  --color-on-jade: var(--on-jade);
137
141
  --color-jade-soft: var(--jade-soft);
138
142
  --color-danger: var(--danger);
143
+ --color-danger-deep: var(--danger-deep);
144
+ --color-on-danger: var(--on-danger);
139
145
  --color-star: var(--star);
140
146
  --color-track: var(--track);
141
147
 
@@ -201,7 +207,13 @@
201
207
  border-radius: 6px;
202
208
  }
203
209
 
204
- ::selection { background: var(--jade-soft); }
210
+ /* A selection has to read on both grounds. The browser default nearly vanishes
211
+ on the dark deck, and `--jade-soft` is darker still there, so the highlight
212
+ is the accent mixed down instead, which stays visible over a field, a
213
+ readout and the page alike. */
214
+ ::selection {
215
+ background: color-mix(in srgb, var(--jade) 34%, transparent);
216
+ }
205
217
 
206
218
  @media (prefers-reduced-motion: reduce) {
207
219
  *, *::before, *::after {
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "mevebu",
3
+ "short_name": "mevebu",
4
+ "description": "Small tools that run in your browser.",
5
+ "start_url": "../",
6
+ "scope": "../",
7
+ "display": "standalone",
8
+ "background_color": "#eef1ed",
9
+ "theme_color": "#eef1ed",
10
+ "icons": [
11
+ { "src": "android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
12
+ { "src": "android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
13
+ ]
14
+ }
package/mevebu/index.html CHANGED
@@ -5,7 +5,11 @@
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
7
  <meta name="description" content="Small tools that run in your browser. Passwords, QR codes, random draws, text, dates and encoding. No accounts, no uploads, no tracking." />
8
- <link rel="icon" type="image/png" href="./icon.png" />
8
+ <link rel="icon" href="./assets/favicon.ico" sizes="any" />
9
+ <link rel="icon" type="image/png" sizes="32x32" href="./assets/favicon-32x32.png" />
10
+ <link rel="icon" type="image/png" sizes="16x16" href="./assets/favicon-16x16.png" />
11
+ <link rel="apple-touch-icon" sizes="180x180" href="./assets/apple-touch-icon.png" />
12
+ <link rel="manifest" href="./assets/site.webmanifest" />
9
13
  <link rel="stylesheet" href="./app/tailwind.css" />
10
14
  <script type="module">
11
15
  import { getTheme } from "./libs/js/core.js";
@@ -616,6 +616,7 @@ const BTN_KIND = {
616
616
  fill: "border-jade bg-jade px-4 text-on-jade hover:border-jade-deep hover:bg-jade-deep",
617
617
  plain: "border-orbit bg-deck px-4 text-ink hover:border-orbit-hard hover:bg-deck-2",
618
618
  quiet: "border-transparent bg-transparent px-2.5 text-ink-2 hover:bg-deck-2 hover:text-ink",
619
+ danger: "border-danger bg-danger px-4 text-on-danger hover:border-danger-deep hover:bg-danger-deep",
619
620
  };
620
621
 
621
622
  export function button(labelText, { icon: iconName, kind = "plain", class: extra, tag = "button", ...rest } = {}) {
@@ -623,7 +624,9 @@ export function button(labelText, { icon: iconName, kind = "plain", class: extra
623
624
  tag,
624
625
  { class: withExtra(`${BTN} ${BTN_KIND[kind] ?? BTN_KIND.plain}`, extra), ...(tag === "button" ? { type: "button" } : null), ...rest },
625
626
  iconName ? icon(iconName) : null,
626
- el("span", {}, labelText)
627
+ // An icon-only button skips the label span: an empty one still spends the
628
+ // row's gap, which pushes the icon off centre.
629
+ labelText ? el("span", {}, labelText) : null
627
630
  );
628
631
  }
629
632
 
@@ -681,10 +684,14 @@ export function chipRow(items, { value, onChange, label } = {}) {
681
684
  return wrap;
682
685
  }
683
686
 
684
- /** Independent toggles. Returns the wrapper with `.values()` reading the boxes. */
685
- export function toggleRow(items) {
687
+ /**
688
+ * Independent toggles. Returns the wrapper with `.values()` reading the boxes.
689
+ * `columns: 2` lays them out on an even grid, so a set of related boxes lines up
690
+ * instead of ragging across the row.
691
+ */
692
+ export function toggleRow(items, { columns } = {}) {
686
693
  const boxes = new Map();
687
- const wrap = el("div", { class: "flex flex-wrap gap-1.5" });
694
+ const wrap = el("div", { class: columns === 2 ? "grid grid-cols-2 gap-1.5" : "flex flex-wrap gap-1.5" });
688
695
  for (const item of items) {
689
696
  const node = checkbox(item.label, { checked: item.checked !== false });
690
697
  boxes.set(item.value, node.input);
@@ -743,7 +750,7 @@ export function blank(iconName, text) {
743
750
  // ---------- version ----------
744
751
 
745
752
  // Kept in one place. The CLI reads the same value from package.json.
746
- export const VERSION = "1.5.1";
753
+ export const VERSION = "1.7.0";
747
754
  export const PACKAGE_NAME = "mevebu";
748
755
  export const REPO_SLUG = "mevebu/mevebu";
749
756
 
@@ -34,7 +34,7 @@ export function mount(slot) {
34
34
  JSON.stringify(cards.map(({ question: q, answer: a }) => ({ question: q, answer: a })), null, 2);
35
35
 
36
36
  function removeButton(card) {
37
- const node = button("", { icon: "trash", "aria-label": `Remove ${card.question}` });
37
+ const node = button("", { icon: "trash", kind: "danger", "aria-label": `Remove ${card.question}` });
38
38
  node.addEventListener("click", () => {
39
39
  cards = cards.filter((entry) => entry.id !== card.id);
40
40
  if (index >= cards.length) index = Math.max(0, cards.length - 1);
@@ -52,10 +52,22 @@ export function mount(slot) {
52
52
  const card = cards[index];
53
53
  faceLabel.textContent = flipped ? "Answer" : "Question";
54
54
  face.textContent = flipped ? card.answer : card.question;
55
- listWrap.replaceChildren(el("table", { class: "w-full border-collapse font-mono text-sm [&_th]:wrap-anywhere [&_td]:wrap-anywhere [&_th]:border-b [&_td]:border-b [&_th]:border-orbit [&_td]:border-orbit [&_th]:px-2.5 [&_td]:px-2.5 [&_th]:py-2 [&_td]:py-2 [&_th]:text-start [&_td]:text-start [&_th]:font-sans [&_th]:text-2xs [&_th]:font-semibold [&_th]:text-ink-2 [&_tbody>tr:last-child>th]:border-b-0 [&_tbody>tr:last-child>td]:border-b-0" },
56
- el("tbody", {}, ...cards.map((entry, i) => el("tr", {},
57
- el("th", {}, `${i + 1}. ${entry.question}`),
58
- el("td", {}, removeButton(entry)))))));
55
+ // A row per card, not a table: the question takes the width it needs and
56
+ // the remove button stays on the centre line of every row.
57
+ listWrap.replaceChildren(
58
+ el(
59
+ "ol",
60
+ { class: "m-0 grid list-none gap-1.5 p-0" },
61
+ ...cards.map((entry, i) =>
62
+ el(
63
+ "li",
64
+ { class: "flex items-center gap-3 rounded-r1 border border-orbit bg-deck-2 px-3.5 py-2.5" },
65
+ el("span", { class: "min-w-0 flex-1 wrap-anywhere font-mono text-sm" }, `${i + 1}. ${entry.question}`),
66
+ removeButton(entry)
67
+ )
68
+ )
69
+ )
70
+ );
59
71
  }
60
72
  stats.replaceChildren(tile("Cards", cards.length), tile("Card", cards.length ? `${index + 1} / ${cards.length}` : "-"));
61
73
  flipButton.disabled = !cards.length;
@@ -1,9 +1,14 @@
1
- // Signs a JSON Web Token with HS256. Checks that the JSON parses and shows a
1
+ // Signs a JSON Web Token with HMAC. Checks that the JSON parses and shows a
2
2
  // readable decode of the payload, so a typo surfaces before signing.
3
- import { el, icon, bench, stack, panel, textArea, textInput, notice, copyButton, output, debounce, actions, toolcard } from "../core.js";
3
+ import { el, icon, bench, stack, panel, textArea, textInput, selectInput, notice, copyButton, output, button, debounce, actions, toolcard } from "../core.js";
4
4
 
5
5
  const encoder = new TextEncoder();
6
6
 
7
+ // What can actually be signed here. The select is built from this, so the list
8
+ // and the signing call can never drift apart.
9
+ const ALGS = { HS256: "SHA-256", HS384: "SHA-384", HS512: "SHA-512" };
10
+ const TYPES = ["JWT", "at+jwt"];
11
+
7
12
  // Only claims that carry a value the JSON does not show by itself: a Unix
8
13
  // timestamp means nothing to read, the date behind it does.
9
14
  const TIME_CLAIMS = [["iat", "Issued at"], ["nbf", "Not before"], ["exp", "Expires"]];
@@ -18,16 +23,14 @@ const toUrlString = (text) => toUrl(encoder.encode(text));
18
23
  export function mount(slot) {
19
24
  const now = Math.floor(Date.now() / 1000);
20
25
 
21
- const headerField = textArea({ rows: 3, value: '{\n "alg": "HS256",\n "typ": "JWT"\n}' });
26
+ const headerField = textArea({ rows: 6, value: '{\n "alg": "HS256",\n "typ": "JWT"\n}' });
22
27
  const payloadField = textArea({
23
- rows: 7,
28
+ rows: 6,
24
29
  value: `{\n "sub": "1234567890",\n "name": "Ada",\n "admin": true,\n "iat": ${now}\n}`});
25
30
  const secretField = textInput({ type: "password", value: "your-256-bit-secret", autocomplete: "new-password" });
26
- const showButton = el(
27
- "button",
28
- { class: "inline-flex 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", type: "button", "aria-label": "Show secret", "aria-pressed": "false" },
29
- icon("eye")
30
- );
31
+ const algField = selectInput(Object.keys(ALGS), { value: "HS256", "aria-label": "Algorithm" });
32
+ const typField = selectInput(TYPES, { value: "JWT", "aria-label": "Token type" });
33
+ const showButton = button("", { icon: "eye", kind: "plain", "aria-label": "Show secret", "aria-pressed": "false" });
31
34
  const error = notice("error");
32
35
  const tokenOut = output({ tag: "textarea", rows: 4 });
33
36
  const decoded = output({ rows: 4 });
@@ -43,6 +46,33 @@ export function mount(slot) {
43
46
  showButton.setAttribute("aria-pressed", String(!visible));
44
47
  });
45
48
 
49
+ /** The header as data, or null while it is mid edit. */
50
+ const readHeader = () => {
51
+ try {
52
+ const parsed = JSON.parse(headerField.value);
53
+ return parsed && typeof parsed === "object" ? parsed : null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ };
58
+
59
+ /** A header typed by hand still moves the two selects when it names a known value. */
60
+ function followHeader() {
61
+ const header = readHeader();
62
+ if (!header) return;
63
+ if (ALGS[header.alg]) algField.value = header.alg;
64
+ if (TYPES.includes(header.typ)) typField.value = header.typ;
65
+ }
66
+
67
+ /** A select writes back into the header, which stays the one source of truth. */
68
+ function writeHeader() {
69
+ const header = readHeader() ?? {};
70
+ header.alg = algField.value;
71
+ header.typ = typField.value;
72
+ headerField.value = JSON.stringify(header, null, 2);
73
+ sign();
74
+ }
75
+
46
76
  async function sign() {
47
77
  error.write("");
48
78
  let header;
@@ -54,8 +84,9 @@ export function mount(slot) {
54
84
  error.write(`That JSON does not parse. ${caught.message}`);
55
85
  return;
56
86
  }
57
- if (header.alg && header.alg !== "HS256") {
58
- error.write("HS256 is the only algorithm here. Change alg to HS256.");
87
+ const hash = ALGS[header.alg];
88
+ if (!hash) {
89
+ error.write(`Only ${Object.keys(ALGS).join(", ")} can be signed here. Set alg to one of those.`);
59
90
  return;
60
91
  }
61
92
  let token = "";
@@ -63,7 +94,7 @@ export function mount(slot) {
63
94
  const first = toUrlString(JSON.stringify(header));
64
95
  const second = toUrlString(JSON.stringify(payload));
65
96
  const message = `${first}.${second}`;
66
- const key = await crypto.subtle.importKey("raw", encoder.encode(secretField.value), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
97
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secretField.value), { name: "HMAC", hash }, false, ["sign"]);
67
98
  const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(message));
68
99
  token = `${message}.${toUrl(new Uint8Array(signature))}`;
69
100
  } catch (caught) {
@@ -84,19 +115,30 @@ export function mount(slot) {
84
115
  // Live signing. The textareas are the only slow field: invalid JSON while a
85
116
  // half-typed brace is on screen is normal, so the wait is longer than usual.
86
117
  const live = debounce(sign, 250);
87
- for (const field of [headerField, payloadField, secretField]) {
88
- field.addEventListener("input", live);
89
- }
118
+ headerField.addEventListener("input", () => { followHeader(); live(); });
119
+ for (const field of [payloadField, secretField]) field.addEventListener("input", live);
120
+ for (const field of [algField, typField]) field.addEventListener("change", writeHeader);
90
121
 
91
122
  const card = toolcard({
92
123
  body: bench(
93
124
  stack(
94
- panel("Header", headerField),
95
- panel("Payload", payloadField),
125
+ // The secret leads the column: it is the one field a visitor has to fill
126
+ // before the token means anything, and the two header choices are the
127
+ // adjustment after it.
96
128
  panel("Secret", el("div", { class: "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-2" }, secretField, showButton)),
129
+ panel("Algorithm", algField),
130
+ panel("Type", typField),
97
131
  error
98
132
  ),
99
- stack(panel("Token", tokenOut), timePanel)
133
+ stack(
134
+ // Header and payload sit side by side once there is room for two columns,
135
+ // which is the pair a reader compares anyway.
136
+ el("div", { class: "grid gap-4 min-[80rem]:grid-cols-2" },
137
+ panel("Header", headerField),
138
+ panel("Payload", payloadField)),
139
+ panel("Token", tokenOut),
140
+ timePanel
141
+ )
100
142
  ),
101
143
  foot: actions(copyBtn)});
102
144
  slot.append(card.node);
@@ -105,4 +147,4 @@ export function mount(slot) {
105
147
  return {};
106
148
  }
107
149
 
108
- export default { mount };
150
+ export default { mount };
@@ -15,11 +15,12 @@ export function mount(slot) {
15
15
  lengthLabel.textContent = `Length: ${lengthField.value}`;
16
16
  }
17
17
 
18
+ // Two columns, so the four groups read as a block instead of ragging.
18
19
  const groups = toggleRow([
19
20
  { value: "lower", label: "a to z" },
20
21
  { value: "upper", label: "A to Z" },
21
22
  { value: "digits", label: "0 to 9" },
22
- { value: "symbols", label: "Symbols" }]);
23
+ { value: "symbols", label: "Symbols" }], { columns: 2 });
23
24
 
24
25
  const secret = textInput({ mono: true, readonly: true, "aria-label": "Password", placeholder: "Press Generate" });
25
26
  const meter = el("div", { class: "h-2 overflow-hidden rounded-full bg-deck-2", role: "img", "aria-label": "Strength" }, el("div", { class: "h-full w-0 rounded-full bg-orbit-hard transition-[width,background-color] duration-150 data-[level=weak]:bg-danger data-[level=ok]:bg-star data-[level=strong]:bg-jade" }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mevebu",
3
- "version": "1.5.1",
3
+ "version": "1.7.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",
Binary file
package/mevebu/icon.png DELETED
Binary file