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.
- package/mevebu/app/mevebu.js +3 -3
- package/mevebu/app/tailwind.css +32 -12
- package/mevebu/app/tailwind.src.css +14 -2
- package/mevebu/assets/android-chrome-192x192.png +0 -0
- package/mevebu/assets/android-chrome-512x512.png +0 -0
- package/mevebu/assets/apple-touch-icon.png +0 -0
- package/mevebu/assets/favicon-16x16.png +0 -0
- package/mevebu/assets/favicon-32x32.png +0 -0
- package/mevebu/assets/favicon.ico +0 -0
- package/mevebu/assets/logo-on-white.png +0 -0
- package/mevebu/assets/logo.png +0 -0
- package/mevebu/assets/site.webmanifest +14 -0
- package/mevebu/index.html +5 -1
- package/mevebu/libs/js/core.js +12 -5
- package/mevebu/libs/js/tools/flashcard-creator.js +17 -5
- package/mevebu/libs/js/tools/jwt-generator.js +61 -19
- package/mevebu/libs/js/tools/password-generator.js +2 -1
- package/package.json +1 -1
- package/mevebu/favicon-64.png +0 -0
- package/mevebu/icon.png +0 -0
package/mevebu/app/mevebu.js
CHANGED
|
@@ -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]
|
|
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]
|
|
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: "./
|
|
576
|
+
el("img", { class: "block size-[248px]", src: "./assets/logo.png", alt: "", width: "248", height: "248" }))
|
|
577
577
|
)
|
|
578
578
|
);
|
|
579
579
|
|
package/mevebu/app/tailwind.css
CHANGED
|
@@ -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: #
|
|
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
|
|
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: #
|
|
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
|
-
|
|
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
|
|
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"
|
|
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";
|
package/mevebu/libs/js/core.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
685
|
-
|
|
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.
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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:
|
|
26
|
+
const headerField = textArea({ rows: 6, value: '{\n "alg": "HS256",\n "typ": "JWT"\n}' });
|
|
22
27
|
const payloadField = textArea({
|
|
23
|
-
rows:
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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(
|
|
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
package/mevebu/favicon-64.png
DELETED
|
Binary file
|
package/mevebu/icon.png
DELETED
|
Binary file
|