mevebu 1.1.0 → 1.2.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 +2 -5
- package/mevebu/app/tailwind.css +10 -63
- package/mevebu/app/tailwind.src.css +9 -25
- package/mevebu/libs/js/core.js +3 -3
- package/mevebu/libs/js/registry.js +1 -1
- package/mevebu/libs/js/tools/coin-flip.js +34 -11
- package/mevebu/libs/js/tools/color-converter.js +1 -1
- package/mevebu/libs/js/tools/hmac-generator.js +1 -2
- package/mevebu/libs/js/tools/jwt-generator.js +15 -16
- package/mevebu/libs/js/tools/password-generator.js +12 -7
- package/mevebu/libs/js/tools/password-strength.js +12 -12
- package/mevebu/libs/js/tools/qr-generator.js +1 -1
- package/mevebu/libs/js/tools/secure-token.js +1 -2
- package/mevebu/libs/js/tools/spin-wheel.js +25 -13
- package/package.json +1 -1
package/mevebu/app/mevebu.js
CHANGED
|
@@ -32,7 +32,7 @@ function searchButton() {
|
|
|
32
32
|
},
|
|
33
33
|
icon("search"),
|
|
34
34
|
el("span", { class: "mvb-searchbtn__txt" }, "Search"),
|
|
35
|
-
el("kbd", { class: "mvb-
|
|
35
|
+
el("kbd", { class: "mvb-kbd" }, "⌘K")
|
|
36
36
|
);
|
|
37
37
|
return btn;
|
|
38
38
|
}
|
|
@@ -106,9 +106,6 @@ function openHelp(tool) {
|
|
|
106
106
|
el("p", { class: "mvb-help__note" }, tool.tutorial?.intro ?? tool.about),
|
|
107
107
|
el("ol", { class: "mvb-help__steps" },
|
|
108
108
|
...(tool.tutorial?.steps ?? []).map((step) => el("li", {}, step))),
|
|
109
|
-
el("p", { class: "mvb-help__note" },
|
|
110
|
-
"Open the tool: ",
|
|
111
|
-
el("a", { class: "mvb-link", href: toolHref(tool) }, toolHref(tool).slice(1))),
|
|
112
109
|
];
|
|
113
110
|
modal({
|
|
114
111
|
label: `${tool.name} how to use it`,
|
|
@@ -363,7 +360,7 @@ function filterBar(placeholder = "Search tools") {
|
|
|
363
360
|
{ class: "mvb-filter", role: "search", onsubmit: (event) => event.preventDefault() },
|
|
364
361
|
el("span", { class: "mvb-filter__icon" }, icon("search")),
|
|
365
362
|
input,
|
|
366
|
-
el("kbd", { class: "mvb-
|
|
363
|
+
el("kbd", { class: "mvb-kbd" }, "/")
|
|
367
364
|
);
|
|
368
365
|
}
|
|
369
366
|
|
package/mevebu/app/tailwind.css
CHANGED
|
@@ -457,20 +457,6 @@
|
|
|
457
457
|
font-size: var(--text-sm);
|
|
458
458
|
line-height: var(--tw-leading, var(--text-sm--line-height));
|
|
459
459
|
}
|
|
460
|
-
.mvb-searchbtn__key {
|
|
461
|
-
border-radius: 4px;
|
|
462
|
-
border-style: var(--tw-border-style);
|
|
463
|
-
border-width: 1px;
|
|
464
|
-
border-color: var(--mvb-orbit);
|
|
465
|
-
background-color: var(--mvb-deck);
|
|
466
|
-
padding-inline: calc(var(--spacing) * 1.5);
|
|
467
|
-
padding-block: 1px;
|
|
468
|
-
font-family: var(--mvb-sans);
|
|
469
|
-
font-size: 0.71875rem;
|
|
470
|
-
--tw-font-weight: var(--font-weight-semibold);
|
|
471
|
-
font-weight: var(--font-weight-semibold);
|
|
472
|
-
color: var(--mvb-ink-3);
|
|
473
|
-
}
|
|
474
460
|
.mvb-sheet {
|
|
475
461
|
display: grid;
|
|
476
462
|
border-top-style: var(--tw-border-style);
|
|
@@ -530,13 +516,14 @@
|
|
|
530
516
|
border-radius: 5px;
|
|
531
517
|
border-style: var(--tw-border-style);
|
|
532
518
|
border-width: 1px;
|
|
533
|
-
border-color: var(--mvb-orbit);
|
|
534
|
-
background-color: var(--mvb-deck-2);
|
|
519
|
+
border-color: var(--mvb-orbit-hard);
|
|
535
520
|
padding-inline: 7px;
|
|
536
521
|
padding-block: 1px;
|
|
537
522
|
font-family: var(--mvb-sans);
|
|
538
|
-
font-size: 0.
|
|
539
|
-
|
|
523
|
+
font-size: 0.8125rem;
|
|
524
|
+
--tw-leading: 1.45;
|
|
525
|
+
line-height: 1.45;
|
|
526
|
+
color: var(--mvb-ink-3);
|
|
540
527
|
}
|
|
541
528
|
.mvb-hero {
|
|
542
529
|
position: relative;
|
|
@@ -875,17 +862,6 @@
|
|
|
875
862
|
.mvb-filter__in::placeholder {
|
|
876
863
|
color: var(--mvb-ink-3);
|
|
877
864
|
}
|
|
878
|
-
.mvb-filter__key {
|
|
879
|
-
border-radius: 5px;
|
|
880
|
-
border-style: var(--tw-border-style);
|
|
881
|
-
border-width: 1px;
|
|
882
|
-
border-color: var(--mvb-orbit);
|
|
883
|
-
padding-inline: 7px;
|
|
884
|
-
padding-block: calc(var(--spacing) * 0.5);
|
|
885
|
-
font-family: var(--mvb-sans);
|
|
886
|
-
font-size: 0.75rem;
|
|
887
|
-
color: var(--mvb-ink-3);
|
|
888
|
-
}
|
|
889
865
|
.mvb-filter__meta {
|
|
890
866
|
margin-top: calc(var(--spacing) * 2.5);
|
|
891
867
|
display: flex;
|
|
@@ -896,9 +872,6 @@
|
|
|
896
872
|
line-height: var(--tw-leading, var(--text-sm--line-height));
|
|
897
873
|
color: var(--mvb-ink-3);
|
|
898
874
|
}
|
|
899
|
-
.mvb-tally {
|
|
900
|
-
color: var(--mvb-danger);
|
|
901
|
-
}
|
|
902
875
|
.mvb-lead {
|
|
903
876
|
margin-top: clamp(28px, 5vw, 56px);
|
|
904
877
|
max-width: 20ch;
|
|
@@ -1679,7 +1652,7 @@
|
|
|
1679
1652
|
margin: 0px;
|
|
1680
1653
|
padding-inline: 18px;
|
|
1681
1654
|
padding-top: calc(var(--spacing) * 2);
|
|
1682
|
-
font-size: 0.
|
|
1655
|
+
font-size: 0.8125rem;
|
|
1683
1656
|
color: var(--mvb-ink-3);
|
|
1684
1657
|
}
|
|
1685
1658
|
.mvb-pal__list {
|
|
@@ -1751,7 +1724,7 @@
|
|
|
1751
1724
|
color: var(--mvb-ink-2);
|
|
1752
1725
|
}
|
|
1753
1726
|
.mvb-pal__cat {
|
|
1754
|
-
font-size: 0.
|
|
1727
|
+
font-size: 0.8125rem;
|
|
1755
1728
|
white-space: nowrap;
|
|
1756
1729
|
color: var(--mvb-ink-3);
|
|
1757
1730
|
}
|
|
@@ -1772,21 +1745,9 @@
|
|
|
1772
1745
|
border-color: var(--mvb-orbit);
|
|
1773
1746
|
padding-inline: 18px;
|
|
1774
1747
|
padding-block: calc(var(--spacing) * 2.5);
|
|
1775
|
-
font-size: 0.
|
|
1748
|
+
font-size: 0.8125rem;
|
|
1776
1749
|
color: var(--mvb-ink-3);
|
|
1777
1750
|
}
|
|
1778
|
-
.mvb-pal__key {
|
|
1779
|
-
border-radius: 4px;
|
|
1780
|
-
border-style: var(--tw-border-style);
|
|
1781
|
-
border-width: 1px;
|
|
1782
|
-
border-color: var(--mvb-orbit);
|
|
1783
|
-
background-color: var(--mvb-deck-2);
|
|
1784
|
-
padding-inline: calc(var(--spacing) * 1.5);
|
|
1785
|
-
padding-block: 1px;
|
|
1786
|
-
font-family: var(--mvb-sans);
|
|
1787
|
-
font-size: 0.6875rem;
|
|
1788
|
-
color: var(--mvb-ink-2);
|
|
1789
|
-
}
|
|
1790
1751
|
.mvb-veil--help {
|
|
1791
1752
|
padding-top: clamp(60px, 10vh, 120px);
|
|
1792
1753
|
}
|
|
@@ -1858,20 +1819,13 @@
|
|
|
1858
1819
|
place-items: center;
|
|
1859
1820
|
border-radius: calc(infinity * 1px);
|
|
1860
1821
|
background-color: var(--mvb-nebula);
|
|
1861
|
-
font-size: 0.
|
|
1822
|
+
font-size: 0.8125rem;
|
|
1862
1823
|
--tw-font-weight: var(--font-weight-bold);
|
|
1863
1824
|
font-weight: var(--font-weight-bold);
|
|
1864
1825
|
color: var(--mvb-comet);
|
|
1865
1826
|
--tw-numeric-spacing: tabular-nums;
|
|
1866
1827
|
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
|
1867
1828
|
}
|
|
1868
|
-
.mvb-help__actions {
|
|
1869
|
-
margin-top: calc(var(--spacing) * 5);
|
|
1870
|
-
display: flex;
|
|
1871
|
-
flex-wrap: wrap;
|
|
1872
|
-
justify-content: flex-end;
|
|
1873
|
-
gap: calc(var(--spacing) * 2);
|
|
1874
|
-
}
|
|
1875
1829
|
.mvb-foot {
|
|
1876
1830
|
margin-top: clamp(56px, 8vw, 96px);
|
|
1877
1831
|
border-top-style: var(--tw-border-style);
|
|
@@ -1932,7 +1886,7 @@
|
|
|
1932
1886
|
.mvb-foot__nav h4 {
|
|
1933
1887
|
margin: 0px;
|
|
1934
1888
|
margin-bottom: calc(var(--spacing) * 2);
|
|
1935
|
-
font-size: 0.
|
|
1889
|
+
font-size: 0.8125rem;
|
|
1936
1890
|
--tw-font-weight: var(--font-weight-bold);
|
|
1937
1891
|
font-weight: var(--font-weight-bold);
|
|
1938
1892
|
--tw-tracking: var(--tracking-normal);
|
|
@@ -2117,13 +2071,6 @@
|
|
|
2117
2071
|
--tw-tracking: -0.01em;
|
|
2118
2072
|
letter-spacing: -0.01em;
|
|
2119
2073
|
}
|
|
2120
|
-
.mvb-flip__tally {
|
|
2121
|
-
font-size: var(--text-sm);
|
|
2122
|
-
line-height: var(--tw-leading, var(--text-sm--line-height));
|
|
2123
|
-
color: var(--mvb-ink-3);
|
|
2124
|
-
--tw-numeric-spacing: tabular-nums;
|
|
2125
|
-
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
|
2126
|
-
}
|
|
2127
2074
|
.mvb-meter {
|
|
2128
2075
|
height: calc(var(--spacing) * 2);
|
|
2129
2076
|
overflow: hidden;
|
|
@@ -238,10 +238,6 @@
|
|
|
238
238
|
|
|
239
239
|
.mvb-searchbtn__txt { @apply flex-1 text-start text-sm; }
|
|
240
240
|
|
|
241
|
-
.mvb-searchbtn__key {
|
|
242
|
-
@apply rounded-[4px] border border-orbit bg-deck px-1.5 py-px font-sans text-[0.71875rem] font-semibold text-ink-3;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
241
|
/* nav sheet on a phone */
|
|
246
242
|
|
|
247
243
|
.mvb-sheet {
|
|
@@ -272,8 +268,11 @@
|
|
|
272
268
|
|
|
273
269
|
.mvb-link { @apply text-comet underline-offset-[3px]; }
|
|
274
270
|
|
|
271
|
+
/* One key chip for the whole app: the search button, the filter, the palette
|
|
272
|
+
footer and the help modal all point at the same key. It sits on a raised and a
|
|
273
|
+
recessed surface in turn, so it carries its own border instead of a fill. */
|
|
275
274
|
.mvb-kbd {
|
|
276
|
-
@apply rounded-[5px] border border-orbit
|
|
275
|
+
@apply rounded-[5px] border border-orbit-hard px-[7px] py-px font-sans text-[0.8125rem] leading-[1.45] text-ink-3;
|
|
277
276
|
}
|
|
278
277
|
|
|
279
278
|
/* hero, only on the home page */
|
|
@@ -411,16 +410,10 @@
|
|
|
411
410
|
.mvb-filter__in:focus { @apply outline-none; }
|
|
412
411
|
.mvb-filter__in::placeholder { @apply text-ink-3; }
|
|
413
412
|
|
|
414
|
-
.mvb-filter__key {
|
|
415
|
-
@apply rounded-[5px] border border-orbit px-[7px] py-0.5 font-sans text-[0.75rem] text-ink-3;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
413
|
.mvb-filter__meta {
|
|
419
414
|
@apply mt-2.5 flex min-h-[22px] items-baseline gap-3.5 text-sm text-ink-3;
|
|
420
415
|
}
|
|
421
416
|
|
|
422
|
-
.mvb-tally { @apply text-danger; }
|
|
423
|
-
|
|
424
417
|
/* ---------- page headings ---------- */
|
|
425
418
|
|
|
426
419
|
.mvb-lead {
|
|
@@ -766,7 +759,7 @@
|
|
|
766
759
|
.mvb-pal__in:focus { @apply outline-none; }
|
|
767
760
|
.mvb-pal__in::placeholder { @apply text-ink-3; }
|
|
768
761
|
|
|
769
|
-
.mvb-pal__count { @apply m-0 px-[18px] pt-2 text-[0.
|
|
762
|
+
.mvb-pal__count { @apply m-0 px-[18px] pt-2 text-[0.8125rem] text-ink-3; }
|
|
770
763
|
|
|
771
764
|
.mvb-pal__list {
|
|
772
765
|
@apply m-0 mt-1.5 max-h-[min(60vh,360px)] overflow-y-auto px-2 pb-2;
|
|
@@ -787,16 +780,12 @@
|
|
|
787
780
|
.mvb-pal__text { @apply grid min-w-0 flex-1 gap-px; }
|
|
788
781
|
.mvb-pal__name { @apply text-[0.9375rem] font-semibold tracking-[-0.01em]; }
|
|
789
782
|
.mvb-pal__note { @apply overflow-hidden text-[0.84375rem] text-ellipsis whitespace-nowrap text-ink-2; }
|
|
790
|
-
.mvb-pal__cat { @apply whitespace-nowrap text-[0.
|
|
783
|
+
.mvb-pal__cat { @apply whitespace-nowrap text-[0.8125rem] text-ink-3; }
|
|
791
784
|
|
|
792
785
|
.mvb-pal__empty { @apply p-[18px] text-sm text-ink-3; }
|
|
793
786
|
|
|
794
787
|
.mvb-pal__foot {
|
|
795
|
-
@apply m-0 flex flex-wrap items-center gap-2.5 border-t border-orbit px-[18px] py-2.5 text-[0.
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
.mvb-pal__key {
|
|
799
|
-
@apply rounded-[4px] border border-orbit bg-deck-2 px-1.5 py-px font-sans text-[0.6875rem] text-ink-2;
|
|
788
|
+
@apply m-0 flex flex-wrap items-center gap-2.5 border-t border-orbit px-[18px] py-2.5 text-[0.8125rem] text-ink-3;
|
|
800
789
|
}
|
|
801
790
|
|
|
802
791
|
/* ---------- help modal ---------- */
|
|
@@ -830,11 +819,7 @@
|
|
|
830
819
|
.mvb-help__steps li::before {
|
|
831
820
|
content: counter(mvb-step);
|
|
832
821
|
counter-increment: mvb-step;
|
|
833
|
-
@apply grid size-[22px] place-items-center rounded-full bg-nebula text-[0.
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
.mvb-help__actions {
|
|
837
|
-
@apply mt-5 flex flex-wrap justify-end gap-2;
|
|
822
|
+
@apply grid size-[22px] place-items-center rounded-full bg-nebula text-[0.8125rem] font-bold tabular-nums text-comet;
|
|
838
823
|
}
|
|
839
824
|
|
|
840
825
|
/* ---------- footer ---------- */
|
|
@@ -869,7 +854,7 @@
|
|
|
869
854
|
@apply grid grid-cols-2 gap-x-[clamp(20px,4vw,40px)] gap-y-1.5;
|
|
870
855
|
}
|
|
871
856
|
|
|
872
|
-
.mvb-foot__nav h4 { @apply m-0 mb-2 text-[0.
|
|
857
|
+
.mvb-foot__nav h4 { @apply m-0 mb-2 text-[0.8125rem] font-bold tracking-normal text-ink-3 normal-case; }
|
|
873
858
|
.mvb-foot__nav ul { @apply grid gap-1.5; }
|
|
874
859
|
.mvb-foot__nav a { @apply text-[0.90625rem] text-ink-2 no-underline; }
|
|
875
860
|
.mvb-foot__nav a:hover { @apply text-comet; }
|
|
@@ -944,7 +929,6 @@
|
|
|
944
929
|
}
|
|
945
930
|
|
|
946
931
|
.mvb-flip__result { @apply text-[1.1875rem] font-bold tracking-[-0.01em]; }
|
|
947
|
-
.mvb-flip__tally { @apply text-sm tabular-nums text-ink-3; }
|
|
948
932
|
|
|
949
933
|
/* ---------- meter ---------- */
|
|
950
934
|
|
package/mevebu/libs/js/core.js
CHANGED
|
@@ -241,9 +241,9 @@ export function openPalette({ onPick } = {}) {
|
|
|
241
241
|
"p",
|
|
242
242
|
{ class: "mvb-pal__foot" },
|
|
243
243
|
"↑↓ navigate ",
|
|
244
|
-
el("kbd", { class: "mvb-
|
|
244
|
+
el("kbd", { class: "mvb-kbd" }, "↵"),
|
|
245
245
|
" open ",
|
|
246
|
-
el("kbd", { class: "mvb-
|
|
246
|
+
el("kbd", { class: "mvb-kbd" }, "esc"),
|
|
247
247
|
" close"
|
|
248
248
|
);
|
|
249
249
|
|
|
@@ -593,7 +593,7 @@ export function blank(iconName, text) {
|
|
|
593
593
|
// ---------- version ----------
|
|
594
594
|
|
|
595
595
|
// Kept in one place. The CLI reads the same value from package.json.
|
|
596
|
-
export const VERSION = "1.
|
|
596
|
+
export const VERSION = "1.2.0";
|
|
597
597
|
export const PACKAGE_NAME = "mevebu";
|
|
598
598
|
export const REPO_SLUG = "mevebu/mevebu";
|
|
599
599
|
|
|
@@ -40,7 +40,7 @@ export const TOOLS = [
|
|
|
40
40
|
tutorial: {
|
|
41
41
|
intro: "Add one option per line. The wheel picks one at random when you spin.",
|
|
42
42
|
steps: [
|
|
43
|
-
"Type your options
|
|
43
|
+
"Type your options, one per line.",
|
|
44
44
|
"Press Spin. The wheel eases to a stop on one entry.",
|
|
45
45
|
"Each pick is added to the history strip below. Press Clear past picks to reset.",
|
|
46
46
|
],
|
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
// Heads or tails with a running count that survives a page change.
|
|
2
|
-
import { el, button, bench, stack, panel,
|
|
2
|
+
import { el, button, bench, stack, panel, actions, readPin, writePin, randomInt, toolcard } from "../core.js";
|
|
3
|
+
|
|
4
|
+
// A toss is one flip: a click while the coin is still turning replaces the turn
|
|
5
|
+
// in progress instead of queueing another one behind it.
|
|
6
|
+
const TOSS_MS = 400;
|
|
7
|
+
|
|
8
|
+
/** A tally tile. The value is the node the tool writes to. */
|
|
9
|
+
function tile(key) {
|
|
10
|
+
const value = el("span", { class: "mvb-stat__v" }, "0");
|
|
11
|
+
const row = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, key));
|
|
12
|
+
return { row, value };
|
|
13
|
+
}
|
|
3
14
|
|
|
4
15
|
export function mount(slot) {
|
|
5
16
|
const PIN = "coin-flip";
|
|
6
17
|
const saved = readPin(PIN, { heads: 0, tails: 0 });
|
|
7
18
|
let heads = Number(saved.heads) || 0;
|
|
8
19
|
let tails = Number(saved.tails) || 0;
|
|
20
|
+
let timer = 0;
|
|
9
21
|
|
|
10
|
-
const coin = el("div", { class: "mvb-coin", role: "img", "aria-label": "Coin" }, "?");
|
|
11
|
-
const
|
|
12
|
-
const
|
|
22
|
+
const coin = el("div", { class: "mvb-coin", role: "img", "aria-label": "Coin, not flipped yet" }, "?");
|
|
23
|
+
const verdict = el("p", { class: "mvb-flip__result min-h-7", "aria-live": "polite" });
|
|
24
|
+
const headsTile = tile("Heads");
|
|
25
|
+
const tailsTile = tile("Tails");
|
|
26
|
+
const totalTile = tile("Total");
|
|
27
|
+
const stats = el("div", { class: "mvb-stats" }, headsTile.row, tailsTile.row, totalTile.row);
|
|
13
28
|
|
|
14
29
|
function render(decision) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
verdict.textContent = decision;
|
|
31
|
+
coin.setAttribute("aria-label", decision ? `Coin: ${decision.toLowerCase()}` : "Coin, not flipped yet");
|
|
32
|
+
headsTile.value.textContent = String(heads);
|
|
33
|
+
tailsTile.value.textContent = String(tails);
|
|
34
|
+
totalTile.value.textContent = String(heads + tails);
|
|
18
35
|
writePin(PIN, { heads, tails });
|
|
19
36
|
}
|
|
20
37
|
|
|
@@ -22,19 +39,21 @@ export function mount(slot) {
|
|
|
22
39
|
const resetButton = button("Reset the count", { kind: "quiet" });
|
|
23
40
|
|
|
24
41
|
flipButton.addEventListener("click", () => {
|
|
42
|
+
clearTimeout(timer);
|
|
25
43
|
coin.classList.remove("mvb-toss");
|
|
26
44
|
void coin.offsetWidth;
|
|
27
45
|
coin.classList.add("mvb-toss");
|
|
28
|
-
setTimeout(() => {
|
|
46
|
+
timer = setTimeout(() => {
|
|
29
47
|
const isHeads = randomInt(2) === 0;
|
|
30
48
|
if (isHeads) heads++;
|
|
31
49
|
else tails++;
|
|
32
50
|
coin.textContent = isHeads ? "H" : "T";
|
|
33
51
|
render(isHeads ? "Heads" : "Tails");
|
|
34
|
-
},
|
|
52
|
+
}, TOSS_MS);
|
|
35
53
|
});
|
|
36
54
|
|
|
37
55
|
resetButton.addEventListener("click", () => {
|
|
56
|
+
clearTimeout(timer);
|
|
38
57
|
heads = 0;
|
|
39
58
|
tails = 0;
|
|
40
59
|
coin.textContent = "?";
|
|
@@ -44,14 +63,18 @@ export function mount(slot) {
|
|
|
44
63
|
const card = toolcard({
|
|
45
64
|
body: bench(
|
|
46
65
|
stack(panel("Coin", coin)),
|
|
47
|
-
stack(panel("Result",
|
|
66
|
+
stack(panel("Result", verdict), stats)
|
|
48
67
|
),
|
|
49
68
|
foot: actions(resetButton, flipButton),
|
|
50
69
|
});
|
|
51
70
|
slot.append(card.node);
|
|
52
71
|
render("");
|
|
53
72
|
|
|
54
|
-
return {
|
|
73
|
+
return {
|
|
74
|
+
unmount() {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
55
78
|
}
|
|
56
79
|
|
|
57
80
|
export default { mount };
|
|
@@ -91,7 +91,7 @@ export function mount(slot) {
|
|
|
91
91
|
const cssField = readonly("CSS variable");
|
|
92
92
|
|
|
93
93
|
const error = notice("error");
|
|
94
|
-
const reading = el("p", { class: "note" });
|
|
94
|
+
const reading = el("p", { class: "mvb-note" });
|
|
95
95
|
|
|
96
96
|
function show(color, keepPicker) {
|
|
97
97
|
const hex = hexOf(color);
|
|
@@ -47,8 +47,7 @@ export function mount(slot) {
|
|
|
47
47
|
stack(
|
|
48
48
|
panel("Message", message),
|
|
49
49
|
panel("Secret key", el("div", { class: "mvb-secret" }, secret, showButton)),
|
|
50
|
-
panel("Hash", hashField)
|
|
51
|
-
el("p", { class: "mvb-note" }, "The message and key stay in this tab. Do not paste a production secret into a site you do not trust.")
|
|
50
|
+
panel("Hash", hashField)
|
|
52
51
|
),
|
|
53
52
|
stack(panel("Signature", result), error)
|
|
54
53
|
),
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
import { el, icon, bench, stack, panel, textArea, textInput, notice, copyButton, output, debounce, actions, toolcard } from "../core.js";
|
|
4
4
|
|
|
5
5
|
const encoder = new TextEncoder();
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
|
|
7
|
+
// Only claims that carry a value the JSON does not show by itself: a Unix
|
|
8
|
+
// timestamp means nothing to read, the date behind it does.
|
|
9
|
+
const TIME_CLAIMS = [["iat", "Issued at"], ["nbf", "Not before"], ["exp", "Expires"]];
|
|
9
10
|
|
|
10
11
|
const toUrl = (bytes) => {
|
|
11
12
|
let text = "";
|
|
@@ -31,8 +32,9 @@ export function mount(slot) {
|
|
|
31
32
|
const error = notice("error");
|
|
32
33
|
const tokenOut = output({ tag: "textarea", rows: 4 });
|
|
33
34
|
const decoded = output({ rows: 4 });
|
|
35
|
+
const timePanel = panel("Time claims", decoded);
|
|
34
36
|
|
|
35
|
-
const copyBtn = copyButton(() => tokenOut.
|
|
37
|
+
const copyBtn = copyButton(() => tokenOut.value, "Copy token", "fill");
|
|
36
38
|
|
|
37
39
|
showButton.addEventListener("click", () => {
|
|
38
40
|
const visible = secretField.type === "text";
|
|
@@ -70,17 +72,14 @@ export function mount(slot) {
|
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
72
74
|
tokenOut.write(token);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
} catch {
|
|
82
|
-
decoded.write("");
|
|
83
|
-
}
|
|
75
|
+
const times = TIME_CLAIMS
|
|
76
|
+
.filter(([key]) => payload[key] !== undefined)
|
|
77
|
+
.map(([key, label]) => {
|
|
78
|
+
const value = Number(payload[key]);
|
|
79
|
+
return Number.isFinite(value) ? `${label}: ${new Date(value * 1000).toLocaleString()}` : `${label}: ${payload[key]}`;
|
|
80
|
+
});
|
|
81
|
+
timePanel.hidden = !times.length;
|
|
82
|
+
decoded.write(times.join("\n"));
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
// Live signing. The textareas are the only slow field: invalid JSON while a
|
|
@@ -98,7 +97,7 @@ export function mount(slot) {
|
|
|
98
97
|
panel("Secret", el("div", { class: "mvb-secret" }, secretField, showButton)),
|
|
99
98
|
error
|
|
100
99
|
),
|
|
101
|
-
stack(panel("Token", tokenOut),
|
|
100
|
+
stack(panel("Token", tokenOut), timePanel)
|
|
102
101
|
),
|
|
103
102
|
foot: actions(copyBtn),
|
|
104
103
|
});
|
|
@@ -11,11 +11,9 @@ const SETS = {
|
|
|
11
11
|
export function mount(slot) {
|
|
12
12
|
const lengthField = rangeInput({ min: "6", max: "64", value: "16", "aria-label": "Length" });
|
|
13
13
|
const lengthLabel = el("span", { class: "mvb-field__label" });
|
|
14
|
-
const lengthRead = el("p", { class: "note" });
|
|
15
14
|
|
|
16
15
|
function showLength() {
|
|
17
16
|
lengthLabel.textContent = `Length: ${lengthField.value}`;
|
|
18
|
-
lengthRead.textContent = `${lengthField.value} characters.`;
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
const groups = toggleRow([
|
|
@@ -27,7 +25,7 @@ export function mount(slot) {
|
|
|
27
25
|
|
|
28
26
|
const secret = textInput({ class: "mvb-in mvb-in--mono", readonly: true, "aria-label": "Password", placeholder: "Press Generate" });
|
|
29
27
|
const meter = el("div", { class: "mvb-meter", role: "img", "aria-label": "Strength" }, el("div", { class: "mvb-meter__fill" }));
|
|
30
|
-
const strength =
|
|
28
|
+
const strength = notice("note");
|
|
31
29
|
const error = notice("error");
|
|
32
30
|
|
|
33
31
|
const generateButton = button("Generate", { kind: "fill" });
|
|
@@ -37,10 +35,15 @@ export function mount(slot) {
|
|
|
37
35
|
return groups.values().map((key) => SETS[key]).join("");
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
function draw(
|
|
38
|
+
function draw() {
|
|
41
39
|
const source = pool();
|
|
40
|
+
const fill = meter.querySelector(".mvb-meter__fill");
|
|
42
41
|
if (!source) {
|
|
43
42
|
secret.value = "";
|
|
43
|
+
fill.style.width = "0%";
|
|
44
|
+
fill.className = "mvb-meter__fill";
|
|
45
|
+
meter.setAttribute("aria-label", "Strength");
|
|
46
|
+
strength.write("");
|
|
44
47
|
error.write("Pick at least one group above.");
|
|
45
48
|
return;
|
|
46
49
|
}
|
|
@@ -60,10 +63,12 @@ export function mount(slot) {
|
|
|
60
63
|
secret.value = password;
|
|
61
64
|
showLength();
|
|
62
65
|
const bits = Math.round(length * Math.log2(source.length));
|
|
63
|
-
const
|
|
66
|
+
const word = bits < 50 ? "weak" : bits < 90 ? "good" : "strong";
|
|
64
67
|
fill.style.width = `${Math.min(100, (bits / 128) * 100)}%`;
|
|
65
68
|
fill.className = `mvb-meter__fill ${bits < 50 ? "mvb-weak" : bits < 90 ? "mvb-ok" : "mvb-strong"}`;
|
|
66
|
-
|
|
69
|
+
// The bar is colour only, so the reading it stands for also goes on the bar itself.
|
|
70
|
+
meter.setAttribute("aria-label", `Strength: ${bits} bits, ${word}`);
|
|
71
|
+
strength.write(`${bits} bits, ${word}.`);
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
lengthField.addEventListener("input", () => { showLength(); draw(); });
|
|
@@ -76,7 +81,7 @@ export function mount(slot) {
|
|
|
76
81
|
|
|
77
82
|
const card = toolcard({
|
|
78
83
|
body: bench(
|
|
79
|
-
stack(panel(lengthLabel, lengthField),
|
|
84
|
+
stack(panel(lengthLabel, lengthField), groups, error),
|
|
80
85
|
stack(panel("Password", secret), meter, strength)
|
|
81
86
|
),
|
|
82
87
|
foot: actions(copyButton, generateButton),
|
|
@@ -14,12 +14,12 @@ const GROUPS = [
|
|
|
14
14
|
];
|
|
15
15
|
|
|
16
16
|
const LEVELS = [
|
|
17
|
-
{ name: "
|
|
18
|
-
{ name: "
|
|
19
|
-
{ name: "
|
|
20
|
-
{ name: "
|
|
21
|
-
{ name: "
|
|
22
|
-
{ name: "
|
|
17
|
+
{ name: "Very weak", cls: "mvb-weak" },
|
|
18
|
+
{ name: "Weak", cls: "mvb-weak" },
|
|
19
|
+
{ name: "Fair", cls: "mvb-ok" },
|
|
20
|
+
{ name: "Good", cls: "mvb-ok" },
|
|
21
|
+
{ name: "Strong", cls: "mvb-strong" },
|
|
22
|
+
{ name: "Very strong", cls: "mvb-strong" },
|
|
23
23
|
];
|
|
24
24
|
|
|
25
25
|
function analyze(value) {
|
|
@@ -53,7 +53,7 @@ export function mount(slot) {
|
|
|
53
53
|
const field = passwordInput({ autocomplete: "off", "aria-label": "Password", placeholder: "Type a password" });
|
|
54
54
|
const showButton = button("Show", { icon: "eye" });
|
|
55
55
|
const meter = el("div", { class: "mvb-meter", role: "img", "aria-label": "Strength" }, el("div", { class: "mvb-meter__fill" }));
|
|
56
|
-
const label = el("p", { class: "note" });
|
|
56
|
+
const label = el("p", { class: "mvb-note text-[1.0625rem] font-bold text-ink" });
|
|
57
57
|
const lengthValue = el("span", { class: "mvb-kv__v" }, "-");
|
|
58
58
|
const groupsValue = el("span", { class: "mvb-kv__v" }, "-");
|
|
59
59
|
const kv = el("div", { class: "mvb-kv" }, kvRow("Length", lengthValue), kvRow("Character groups", groupsValue));
|
|
@@ -75,6 +75,7 @@ export function mount(slot) {
|
|
|
75
75
|
fill.style.width = "0%";
|
|
76
76
|
fill.className = "mvb-meter__fill";
|
|
77
77
|
label.textContent = "";
|
|
78
|
+
meter.setAttribute("aria-label", "Strength");
|
|
78
79
|
lengthValue.textContent = "-";
|
|
79
80
|
groupsValue.textContent = "-";
|
|
80
81
|
status.write("");
|
|
@@ -85,7 +86,9 @@ export function mount(slot) {
|
|
|
85
86
|
const level = LEVELS[score];
|
|
86
87
|
fill.style.width = `${(score / 5) * 100}%`;
|
|
87
88
|
fill.className = `mvb-meter__fill ${level.cls}`;
|
|
88
|
-
label.textContent =
|
|
89
|
+
label.textContent = level.name;
|
|
90
|
+
// The bar is colour only, so the reading it stands for also goes on the bar itself.
|
|
91
|
+
meter.setAttribute("aria-label", `Strength: ${level.name}`);
|
|
89
92
|
lengthValue.textContent = `${length} characters`;
|
|
90
93
|
groupsValue.textContent = `${groups} of 4`;
|
|
91
94
|
for (const text of why) {
|
|
@@ -101,10 +104,7 @@ export function mount(slot) {
|
|
|
101
104
|
|
|
102
105
|
const card = toolcard({
|
|
103
106
|
body: bench(
|
|
104
|
-
stack(
|
|
105
|
-
panel("Password", el("div", { class: "mvb-secret" }, field, showButton)),
|
|
106
|
-
el("p", { class: "mvb-note" }, "The check runs in this tab. Do not type a real password into a site you do not trust.")
|
|
107
|
-
),
|
|
107
|
+
stack(panel("Password", el("div", { class: "mvb-secret" }, field, showButton))),
|
|
108
108
|
stack(meter, label, kv, hints, status)
|
|
109
109
|
),
|
|
110
110
|
foot: actions(copyButton(() => report, "Copy report", "fill")),
|
|
@@ -7,7 +7,7 @@ export function mount(slot) {
|
|
|
7
7
|
const content = textArea({ rows: 4, value: "https://mevebu.app", "aria-label": "Text or link", placeholder: "Text or link" });
|
|
8
8
|
const sizeField = numberInput({ value: "256", min: "120", max: "1024", step: "8", "aria-label": "Size in pixels" });
|
|
9
9
|
const canvas = el("canvas", { class: "mvb-qrcode", role: "img", "aria-label": "QR code" });
|
|
10
|
-
const reading = el("p", { class: "note" });
|
|
10
|
+
const reading = el("p", { class: "mvb-note" });
|
|
11
11
|
const error = notice("error");
|
|
12
12
|
let svg = "";
|
|
13
13
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Random tokens from the browser random source, in hex or URL-safe Base64.
|
|
2
|
-
import {
|
|
2
|
+
import { numberInput, chipRow, output, notice, button, copyButton, bench, stack, panel, actions, toolcard } from "../core.js";
|
|
3
3
|
|
|
4
4
|
function toHex(bytes) {
|
|
5
5
|
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
@@ -44,7 +44,6 @@ export function mount(slot) {
|
|
|
44
44
|
stack(
|
|
45
45
|
panel("Bytes", bytesField),
|
|
46
46
|
panel("Format", formats),
|
|
47
|
-
el("p", { class: "mvb-note" }, "The token is drawn from the browser random source in this tab. Store real tokens safely."),
|
|
48
47
|
error
|
|
49
48
|
),
|
|
50
49
|
stack(panel("Token", result))
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
// Spins through your options and lands on one. Canvas wheel with an eased
|
|
2
2
|
// spin, driven by the browser random source, so every option has equal odds.
|
|
3
|
-
import { el, toast, randomInt, bench, stage, stack, panel, textArea, button,
|
|
3
|
+
import { el, toast, copyButton, randomInt, bench, stage, stack, panel, textArea, button, readPin, writePin, toolcard, actions } from "../core.js";
|
|
4
4
|
|
|
5
5
|
const SEGMENTS = ["#2456D6", "#151D2E", "#5F8BFF", "#4B5871", "#1A43AD", "#69748C", "#1D2A4D", "#C2CBDF"];
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The slice label, in whichever ink reads better on that fill. Every fill in the
|
|
9
|
+
* palette sits far enough from mid grey that one of the two clears 4.5:1, so a
|
|
10
|
+
* pale slice never gets white text.
|
|
11
|
+
*/
|
|
12
|
+
function textOn(hex) {
|
|
13
|
+
const linear = (value) => (value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4);
|
|
14
|
+
const [r, g, b] = [1, 3, 5].map((at) => linear(parseInt(hex.slice(at, at + 2), 16) / 255));
|
|
15
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.2056 ? "#151D2E" : "#FFFFFF";
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
function entriesFrom(text) {
|
|
8
19
|
return text.split(/\n+/).map((line) => line.trim()).filter(Boolean);
|
|
9
20
|
}
|
|
@@ -15,18 +26,18 @@ export function mount(slot) {
|
|
|
15
26
|
const listField = textArea({ value: saved.entries ?? "", "aria-label": "Options, one per line", placeholder: "One option per line" });
|
|
16
27
|
const canvas = el("canvas", { class: "mvb-wheel", width: 360, height: 360, role: "img", "aria-label": "The wheel" });
|
|
17
28
|
const context = canvas.getContext("2d");
|
|
18
|
-
const result = output({ rows: 2 });
|
|
19
29
|
const historyBox = el("div", { class: "mvb-wheel__history", role: "list" });
|
|
20
30
|
|
|
21
31
|
const spinButton = button("Spin", { icon: "rotate-cw", kind: "fill" });
|
|
22
32
|
const clearButton = button("Clear past picks", { icon: "trash", kind: "quiet" });
|
|
33
|
+
const copyWinner = copyButton(() => winner, "Copy winner");
|
|
23
34
|
|
|
24
35
|
const status = el("div", { class: "mvb-wheel__status", "aria-live": "polite" }, "Add options, then spin.");
|
|
25
|
-
result.write("");
|
|
26
36
|
|
|
27
37
|
let rotation = 0;
|
|
28
38
|
let busy = false;
|
|
29
39
|
let frame = 0;
|
|
40
|
+
let winner = "";
|
|
30
41
|
|
|
31
42
|
function draw() {
|
|
32
43
|
const entries = entriesFrom(listField.value);
|
|
@@ -44,7 +55,7 @@ export function mount(slot) {
|
|
|
44
55
|
context.fillStyle = faint;
|
|
45
56
|
context.font = "15px system-ui";
|
|
46
57
|
context.textAlign = "center";
|
|
47
|
-
context.fillText(entries.length ? "Add one more option to spin" : "Type options
|
|
58
|
+
context.fillText(entries.length ? "Add one more option to spin" : "Type options to spin", center, center);
|
|
48
59
|
status.textContent = entries.length ? "One more option to go." : "Add options, then spin.";
|
|
49
60
|
return;
|
|
50
61
|
}
|
|
@@ -61,7 +72,7 @@ export function mount(slot) {
|
|
|
61
72
|
context.translate(center, center);
|
|
62
73
|
context.rotate(a0 + slice / 2);
|
|
63
74
|
context.textAlign = "right";
|
|
64
|
-
context.fillStyle =
|
|
75
|
+
context.fillStyle = textOn(SEGMENTS[i % SEGMENTS.length]);
|
|
65
76
|
context.font = "600 13px system-ui, sans-serif";
|
|
66
77
|
const label = entry.length > 13 ? `${entry.slice(0, 12)}…` : entry;
|
|
67
78
|
context.fillText(label, radius - 12, 4);
|
|
@@ -92,8 +103,8 @@ export function mount(slot) {
|
|
|
92
103
|
spinButton.disabled = true;
|
|
93
104
|
status.dataset.winner = "";
|
|
94
105
|
const slice = (Math.PI * 2) / entries.length;
|
|
95
|
-
const
|
|
96
|
-
const finish = (-Math.PI / 2) - (
|
|
106
|
+
const winnerIndex = randomInt(entries.length);
|
|
107
|
+
const finish = (-Math.PI / 2) - (winnerIndex * slice + slice / 2);
|
|
97
108
|
const fullTurns = Math.PI * 2 * (5 + randomInt(3));
|
|
98
109
|
const delta = ((finish - rotation) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2) + fullTurns;
|
|
99
110
|
const start = rotation;
|
|
@@ -110,9 +121,9 @@ export function mount(slot) {
|
|
|
110
121
|
busy = false;
|
|
111
122
|
spinButton.disabled = false;
|
|
112
123
|
status.dataset.winner = "1";
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const chip = el("span", { class: "mvb-wheel__chip", role: "listitem" },
|
|
124
|
+
winner = entries[winnerIndex];
|
|
125
|
+
status.textContent = winner;
|
|
126
|
+
const chip = el("span", { class: "mvb-wheel__chip", role: "listitem" }, winner);
|
|
116
127
|
historyBox.prepend(chip);
|
|
117
128
|
while (historyBox.children.length > 12) historyBox.lastChild.remove();
|
|
118
129
|
}
|
|
@@ -122,13 +133,14 @@ export function mount(slot) {
|
|
|
122
133
|
|
|
123
134
|
listField.addEventListener("input", () => {
|
|
124
135
|
status.dataset.winner = "";
|
|
136
|
+
winner = "";
|
|
125
137
|
writePin(PIN, { entries: listField.value });
|
|
126
138
|
draw();
|
|
127
139
|
});
|
|
128
140
|
spinButton.addEventListener("click", spin);
|
|
129
141
|
clearButton.addEventListener("click", () => {
|
|
130
142
|
historyBox.replaceChildren();
|
|
131
|
-
|
|
143
|
+
winner = "";
|
|
132
144
|
status.dataset.winner = "";
|
|
133
145
|
status.textContent = "Past picks cleared.";
|
|
134
146
|
});
|
|
@@ -136,9 +148,9 @@ export function mount(slot) {
|
|
|
136
148
|
const card = toolcard({
|
|
137
149
|
body: bench(
|
|
138
150
|
stack(panel("Options, one per line", listField)),
|
|
139
|
-
stage(canvas, status,
|
|
151
|
+
stage(canvas, status, historyBox)
|
|
140
152
|
),
|
|
141
|
-
foot: actions(clearButton, spinButton),
|
|
153
|
+
foot: actions(copyWinner, clearButton, spinButton),
|
|
142
154
|
});
|
|
143
155
|
slot.append(card.node);
|
|
144
156
|
draw();
|