mevebu 1.1.0 → 1.3.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 +16 -63
- package/mevebu/app/tailwind.src.css +16 -25
- package/mevebu/libs/js/core.js +5 -5
- package/mevebu/libs/js/registry.js +12 -12
- package/mevebu/libs/js/tools/base64.js +29 -8
- package/mevebu/libs/js/tools/case-converter.js +1 -1
- package/mevebu/libs/js/tools/cert-expiry.js +1 -3
- package/mevebu/libs/js/tools/coin-flip.js +34 -11
- package/mevebu/libs/js/tools/color-converter.js +18 -11
- package/mevebu/libs/js/tools/data-masker.js +2 -2
- package/mevebu/libs/js/tools/fake-data.js +15 -3
- 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/lorem-ipsum.js +4 -2
- 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/mevebu/libs/js/tools/url-encoder.js +4 -2
- package/mevebu/libs/js/tools/url-inspector.js +1 -2
- package/mevebu/libs/js/tools/uuid-generator.js +109 -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;
|
|
@@ -1444,6 +1417,12 @@
|
|
|
1444
1417
|
border-inline-start-color: var(--mvb-danger);
|
|
1445
1418
|
color: var(--mvb-danger);
|
|
1446
1419
|
}
|
|
1420
|
+
.mvb-note--warn {
|
|
1421
|
+
border-inline-start-color: var(--mvb-star);
|
|
1422
|
+
--tw-font-weight: var(--font-weight-medium);
|
|
1423
|
+
font-weight: var(--font-weight-medium);
|
|
1424
|
+
color: var(--mvb-ink);
|
|
1425
|
+
}
|
|
1447
1426
|
.mvb-note--good {
|
|
1448
1427
|
border-inline-start-color: var(--mvb-aurora);
|
|
1449
1428
|
color: var(--mvb-aurora);
|
|
@@ -1679,7 +1658,7 @@
|
|
|
1679
1658
|
margin: 0px;
|
|
1680
1659
|
padding-inline: 18px;
|
|
1681
1660
|
padding-top: calc(var(--spacing) * 2);
|
|
1682
|
-
font-size: 0.
|
|
1661
|
+
font-size: 0.8125rem;
|
|
1683
1662
|
color: var(--mvb-ink-3);
|
|
1684
1663
|
}
|
|
1685
1664
|
.mvb-pal__list {
|
|
@@ -1751,7 +1730,7 @@
|
|
|
1751
1730
|
color: var(--mvb-ink-2);
|
|
1752
1731
|
}
|
|
1753
1732
|
.mvb-pal__cat {
|
|
1754
|
-
font-size: 0.
|
|
1733
|
+
font-size: 0.8125rem;
|
|
1755
1734
|
white-space: nowrap;
|
|
1756
1735
|
color: var(--mvb-ink-3);
|
|
1757
1736
|
}
|
|
@@ -1772,21 +1751,9 @@
|
|
|
1772
1751
|
border-color: var(--mvb-orbit);
|
|
1773
1752
|
padding-inline: 18px;
|
|
1774
1753
|
padding-block: calc(var(--spacing) * 2.5);
|
|
1775
|
-
font-size: 0.
|
|
1754
|
+
font-size: 0.8125rem;
|
|
1776
1755
|
color: var(--mvb-ink-3);
|
|
1777
1756
|
}
|
|
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
1757
|
.mvb-veil--help {
|
|
1791
1758
|
padding-top: clamp(60px, 10vh, 120px);
|
|
1792
1759
|
}
|
|
@@ -1858,20 +1825,13 @@
|
|
|
1858
1825
|
place-items: center;
|
|
1859
1826
|
border-radius: calc(infinity * 1px);
|
|
1860
1827
|
background-color: var(--mvb-nebula);
|
|
1861
|
-
font-size: 0.
|
|
1828
|
+
font-size: 0.8125rem;
|
|
1862
1829
|
--tw-font-weight: var(--font-weight-bold);
|
|
1863
1830
|
font-weight: var(--font-weight-bold);
|
|
1864
1831
|
color: var(--mvb-comet);
|
|
1865
1832
|
--tw-numeric-spacing: tabular-nums;
|
|
1866
1833
|
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
|
1867
1834
|
}
|
|
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
1835
|
.mvb-foot {
|
|
1876
1836
|
margin-top: clamp(56px, 8vw, 96px);
|
|
1877
1837
|
border-top-style: var(--tw-border-style);
|
|
@@ -1932,7 +1892,7 @@
|
|
|
1932
1892
|
.mvb-foot__nav h4 {
|
|
1933
1893
|
margin: 0px;
|
|
1934
1894
|
margin-bottom: calc(var(--spacing) * 2);
|
|
1935
|
-
font-size: 0.
|
|
1895
|
+
font-size: 0.8125rem;
|
|
1936
1896
|
--tw-font-weight: var(--font-weight-bold);
|
|
1937
1897
|
font-weight: var(--font-weight-bold);
|
|
1938
1898
|
--tw-tracking: var(--tracking-normal);
|
|
@@ -2117,13 +2077,6 @@
|
|
|
2117
2077
|
--tw-tracking: -0.01em;
|
|
2118
2078
|
letter-spacing: -0.01em;
|
|
2119
2079
|
}
|
|
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
2080
|
.mvb-meter {
|
|
2128
2081
|
height: calc(var(--spacing) * 2);
|
|
2129
2082
|
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 {
|
|
@@ -674,6 +667,13 @@
|
|
|
674
667
|
@apply border-s-danger text-danger;
|
|
675
668
|
}
|
|
676
669
|
|
|
670
|
+
/* A warning is a caution, not a failure: amber rule, text that stays readable
|
|
671
|
+
on both surfaces. Without this rule a warn notice fell back to the neutral
|
|
672
|
+
note, so a real warning looked quieter than the all clear. */
|
|
673
|
+
.mvb-note--warn {
|
|
674
|
+
@apply border-s-star font-medium text-ink;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
677
|
.mvb-note--good {
|
|
678
678
|
@apply border-s-aurora text-aurora;
|
|
679
679
|
}
|
|
@@ -766,7 +766,7 @@
|
|
|
766
766
|
.mvb-pal__in:focus { @apply outline-none; }
|
|
767
767
|
.mvb-pal__in::placeholder { @apply text-ink-3; }
|
|
768
768
|
|
|
769
|
-
.mvb-pal__count { @apply m-0 px-[18px] pt-2 text-[0.
|
|
769
|
+
.mvb-pal__count { @apply m-0 px-[18px] pt-2 text-[0.8125rem] text-ink-3; }
|
|
770
770
|
|
|
771
771
|
.mvb-pal__list {
|
|
772
772
|
@apply m-0 mt-1.5 max-h-[min(60vh,360px)] overflow-y-auto px-2 pb-2;
|
|
@@ -787,16 +787,12 @@
|
|
|
787
787
|
.mvb-pal__text { @apply grid min-w-0 flex-1 gap-px; }
|
|
788
788
|
.mvb-pal__name { @apply text-[0.9375rem] font-semibold tracking-[-0.01em]; }
|
|
789
789
|
.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.
|
|
790
|
+
.mvb-pal__cat { @apply whitespace-nowrap text-[0.8125rem] text-ink-3; }
|
|
791
791
|
|
|
792
792
|
.mvb-pal__empty { @apply p-[18px] text-sm text-ink-3; }
|
|
793
793
|
|
|
794
794
|
.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;
|
|
795
|
+
@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
796
|
}
|
|
801
797
|
|
|
802
798
|
/* ---------- help modal ---------- */
|
|
@@ -830,11 +826,7 @@
|
|
|
830
826
|
.mvb-help__steps li::before {
|
|
831
827
|
content: counter(mvb-step);
|
|
832
828
|
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;
|
|
829
|
+
@apply grid size-[22px] place-items-center rounded-full bg-nebula text-[0.8125rem] font-bold tabular-nums text-comet;
|
|
838
830
|
}
|
|
839
831
|
|
|
840
832
|
/* ---------- footer ---------- */
|
|
@@ -869,7 +861,7 @@
|
|
|
869
861
|
@apply grid grid-cols-2 gap-x-[clamp(20px,4vw,40px)] gap-y-1.5;
|
|
870
862
|
}
|
|
871
863
|
|
|
872
|
-
.mvb-foot__nav h4 { @apply m-0 mb-2 text-[0.
|
|
864
|
+
.mvb-foot__nav h4 { @apply m-0 mb-2 text-[0.8125rem] font-bold tracking-normal text-ink-3 normal-case; }
|
|
873
865
|
.mvb-foot__nav ul { @apply grid gap-1.5; }
|
|
874
866
|
.mvb-foot__nav a { @apply text-[0.90625rem] text-ink-2 no-underline; }
|
|
875
867
|
.mvb-foot__nav a:hover { @apply text-comet; }
|
|
@@ -944,7 +936,6 @@
|
|
|
944
936
|
}
|
|
945
937
|
|
|
946
938
|
.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
939
|
|
|
949
940
|
/* ---------- meter ---------- */
|
|
950
941
|
|
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
|
|
|
@@ -514,8 +514,8 @@ export function downloadButton(getData, filename, labelText = "Download", mime =
|
|
|
514
514
|
* Single choice chips. `items` is [{ value, label }]. Returns the wrapper with
|
|
515
515
|
* a `.select(value)` method so the tool can read the current value.
|
|
516
516
|
*/
|
|
517
|
-
export function chipRow(items, { value, onChange, label
|
|
518
|
-
const wrap = el("div", { class:
|
|
517
|
+
export function chipRow(items, { value, onChange, label } = {}) {
|
|
518
|
+
const wrap = el("div", { class: "mvb-chips", role: "group", "aria-label": label || "Options" });
|
|
519
519
|
let current = value;
|
|
520
520
|
const paint = () => {
|
|
521
521
|
for (const chip of wrap.children) {
|
|
@@ -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.3.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
|
],
|
|
@@ -131,8 +131,8 @@ export const TOOLS = [
|
|
|
131
131
|
tutorial: {
|
|
132
132
|
intro: "Hash text with the SHA family. Hashing happens in this tab.",
|
|
133
133
|
steps: [
|
|
134
|
-
"Type or paste text
|
|
135
|
-
"Pick an algorithm with the chips
|
|
134
|
+
"Type or paste text.",
|
|
135
|
+
"Pick an algorithm with the chips.",
|
|
136
136
|
"The hash updates as you type. Press Copy hash to copy the result.",
|
|
137
137
|
],
|
|
138
138
|
},
|
|
@@ -272,16 +272,16 @@ export const TOOLS = [
|
|
|
272
272
|
name: "UUID Generator",
|
|
273
273
|
icon: "fingerprint-pattern",
|
|
274
274
|
note: "Generate UUIDs, one or a thousand.",
|
|
275
|
-
about: "
|
|
275
|
+
about: "Versions 1, 4, 6 and 7, plus the Nil and Max sentinels.",
|
|
276
276
|
tutorial: {
|
|
277
|
-
intro: "Generate
|
|
277
|
+
intro: "Generate UUIDs and take them as a list, as JSON, or as a file.",
|
|
278
278
|
steps: [
|
|
279
|
-
"
|
|
280
|
-
"
|
|
281
|
-
"Use Copy all, Copy JSON, or Save .txt
|
|
279
|
+
"Pick the version. v4 and v7 are random and v7 sorts by time. v1 and v6 carry a timestamp. Nil and Max are the two sentinels tests look for.",
|
|
280
|
+
"Set how many to generate, between 1 and 1000. Uppercase and No hyphens change the output only.",
|
|
281
|
+
"Use Copy all, Copy JSON, or Save .txt to take the list.",
|
|
282
282
|
],
|
|
283
283
|
},
|
|
284
|
-
keywords: "guid unique id v4 identifier",
|
|
284
|
+
keywords: "guid unique id v1 v4 v6 v7 nil max identifier",
|
|
285
285
|
load: () => import("./tools/uuid-generator.js"),
|
|
286
286
|
},
|
|
287
287
|
{
|
|
@@ -294,8 +294,8 @@ export const TOOLS = [
|
|
|
294
294
|
tutorial: {
|
|
295
295
|
intro: "Convert a color between hex, rgb, and hsl. The swatch and contrast reading update live.",
|
|
296
296
|
steps: [
|
|
297
|
-
"Pick a color with the picker
|
|
298
|
-
"The other formats, the swatch, and a contrast reading
|
|
297
|
+
"Pick a color with the picker, or type a hex, rgb, or hsl value.",
|
|
298
|
+
"The other formats, the swatch, and a contrast reading update as you type.",
|
|
299
299
|
"Click any of the four readouts to copy that value.",
|
|
300
300
|
],
|
|
301
301
|
},
|
|
@@ -315,7 +315,7 @@ export const TOOLS = [
|
|
|
315
315
|
steps: [
|
|
316
316
|
"Type or paste on either side. The other side updates as you type.",
|
|
317
317
|
"Edit the Base64 side directly to decode. Invalid Base64 shows an error.",
|
|
318
|
-
"Use Copy Base64 or Copy text
|
|
318
|
+
"Use Copy Base64 or Copy text to take either side.",
|
|
319
319
|
],
|
|
320
320
|
},
|
|
321
321
|
keywords: "encode decode binary data uri atob btoa",
|
|
@@ -1,21 +1,40 @@
|
|
|
1
1
|
// Text to Base64 and back, live in both directions. Emoji and other non-Latin
|
|
2
2
|
// text survive the round trip because the bytes are UTF-8.
|
|
3
|
-
import { bench, stack, panel, textArea, notice, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
import { checkbox, bench, stack, panel, textArea, notice, copyButton, actions, toolcard } from "../core.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
// btoa and atob take a binary string. A long input is converted in chunks,
|
|
6
|
+
// because spreading a whole document into String.fromCharCode overflows the
|
|
7
|
+
// call stack.
|
|
8
|
+
const CHUNK = 0x8000;
|
|
9
|
+
|
|
10
|
+
function encode(text, urlSafe) {
|
|
11
|
+
const bytes = new TextEncoder().encode(text);
|
|
12
|
+
let binary = "";
|
|
13
|
+
for (let at = 0; at < bytes.length; at += CHUNK) {
|
|
14
|
+
binary += String.fromCharCode(...bytes.subarray(at, at + CHUNK));
|
|
15
|
+
}
|
|
16
|
+
const base = btoa(binary);
|
|
17
|
+
return urlSafe ? base.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") : base;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function decode(text) {
|
|
21
|
+
// Both alphabets are accepted and the padding goes back on: the URL-safe form
|
|
22
|
+
// drops its padding, and a JWT segment turns up that way.
|
|
23
|
+
const base = text.replace(/\s+/g, "").replace(/-/g, "+").replace(/_/g, "/");
|
|
24
|
+
const padded = base.padEnd(base.length + ((4 - (base.length % 4)) % 4), "=");
|
|
25
|
+
return new TextDecoder().decode(Uint8Array.from(atob(padded), (char) => char.charCodeAt(0)));
|
|
26
|
+
}
|
|
9
27
|
|
|
10
28
|
export function mount(slot) {
|
|
11
29
|
const plain = textArea({ rows: 7, value: "hello mevebu", "aria-label": "Plain text" });
|
|
12
30
|
const coded = textArea({ rows: 7, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Base64" });
|
|
31
|
+
const urlSafeBox = checkbox("URL-safe, no padding", { checked: false });
|
|
13
32
|
const error = notice("error");
|
|
14
33
|
let guard = false;
|
|
15
34
|
|
|
16
35
|
function refresh() {
|
|
17
36
|
try {
|
|
18
|
-
coded.value = plain.value ? encode(plain.value) : "";
|
|
37
|
+
coded.value = plain.value ? encode(plain.value, urlSafeBox.input.checked) : "";
|
|
19
38
|
error.write("");
|
|
20
39
|
} catch {
|
|
21
40
|
error.write("That text could not be encoded.");
|
|
@@ -42,10 +61,12 @@ export function mount(slot) {
|
|
|
42
61
|
}
|
|
43
62
|
});
|
|
44
63
|
|
|
64
|
+
urlSafeBox.input.addEventListener("change", refresh);
|
|
65
|
+
|
|
45
66
|
const card = toolcard({
|
|
46
67
|
body: bench(
|
|
47
68
|
stack(panel("Text", plain)),
|
|
48
|
-
stack(panel("Base64", coded), error)
|
|
69
|
+
stack(panel("Base64", coded), urlSafeBox, error)
|
|
49
70
|
),
|
|
50
71
|
foot: actions(
|
|
51
72
|
copyButton(() => plain.value, "Copy text"),
|
|
@@ -58,4 +79,4 @@ export function mount(slot) {
|
|
|
58
79
|
return {};
|
|
59
80
|
}
|
|
60
81
|
|
|
61
|
-
export default { mount };
|
|
82
|
+
export default { mount };
|
|
@@ -69,7 +69,7 @@ export function mount(slot) {
|
|
|
69
69
|
|
|
70
70
|
source.addEventListener("input", convert);
|
|
71
71
|
|
|
72
|
-
const card = toolcard({ body: stack(panel("Text", source), panel("
|
|
72
|
+
const card = toolcard({ body: stack(panel("Text", source), panel("Cases", list)) });
|
|
73
73
|
slot.append(card.node);
|
|
74
74
|
convert();
|
|
75
75
|
|
|
@@ -53,8 +53,7 @@ export function mount(slot) {
|
|
|
53
53
|
|
|
54
54
|
if (days < 0) status.write(`Expired ${Math.abs(days)} days ago.`);
|
|
55
55
|
else if (days === 0) status.write("Expires today.");
|
|
56
|
-
else if (days <= threshold) status.write(`
|
|
57
|
-
else status.write(`Expires in ${days} days.`);
|
|
56
|
+
else if (days <= threshold) status.write(`Inside the ${threshold} day warning window.`);
|
|
58
57
|
|
|
59
58
|
summary = `Expires ${expiry.toLocaleDateString()}, ${days} days left, ${state}.`;
|
|
60
59
|
}
|
|
@@ -67,7 +66,6 @@ export function mount(slot) {
|
|
|
67
66
|
stack(
|
|
68
67
|
panel("Expiry date", dateField),
|
|
69
68
|
panel("Warn within days", warnField),
|
|
70
|
-
el("p", { class: "mvb-note" }, "The date stays in this tab."),
|
|
71
69
|
error
|
|
72
70
|
),
|
|
73
71
|
stack(stats, status)
|
|
@@ -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,14 +91,16 @@ 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
|
+
// `from` names the control the value came from. The text field is never
|
|
97
|
+
// rewritten while it is being typed into: "#abc" is valid shorthand one
|
|
98
|
+
// keystroke before it is valid six digit hex, so echoing the expanded form
|
|
99
|
+
// back would wipe out the rest of what was being typed.
|
|
100
|
+
function show(color, from) {
|
|
97
101
|
const hex = hexOf(color);
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
picker.value = hex;
|
|
101
|
-
}
|
|
102
|
+
if (from !== "text") anyField.value = hex.toUpperCase();
|
|
103
|
+
picker.value = hex;
|
|
102
104
|
hexField.querySelector("input").value = hex.toUpperCase();
|
|
103
105
|
const { h, s, l } = hslOf(color);
|
|
104
106
|
rgbField.querySelector("input").value = color.a < 1
|
|
@@ -113,18 +115,23 @@ export function mount(slot) {
|
|
|
113
115
|
reading.textContent = `Contrast ${onWhite}:1 on white, ${onBlack}:1 on black.${Number(onWhite) >= 4.5 ? "" : " Low on white."}${Number(onBlack) >= 4.5 ? "" : " Low on black."}`;
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
function read(text,
|
|
118
|
+
function read(text, from) {
|
|
119
|
+
// An empty field is not a mistake, it is someone clearing it to start over.
|
|
120
|
+
if (!text.trim()) {
|
|
121
|
+
error.write("");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
117
124
|
const color = parse(text);
|
|
118
125
|
if (!color) {
|
|
119
126
|
error.write("That color was not recognized.");
|
|
120
127
|
return;
|
|
121
128
|
}
|
|
122
129
|
error.write("");
|
|
123
|
-
show(color,
|
|
130
|
+
show(color, from);
|
|
124
131
|
}
|
|
125
132
|
|
|
126
|
-
anyField.addEventListener("input", () => read(anyField.value,
|
|
127
|
-
picker.addEventListener("input", () => read(picker.value,
|
|
133
|
+
anyField.addEventListener("input", () => read(anyField.value, "text"));
|
|
134
|
+
picker.addEventListener("input", () => read(picker.value, "picker"));
|
|
128
135
|
|
|
129
136
|
const card = toolcard({
|
|
130
137
|
body: bench(
|
|
@@ -138,7 +145,7 @@ export function mount(slot) {
|
|
|
138
145
|
),
|
|
139
146
|
});
|
|
140
147
|
slot.append(card.node);
|
|
141
|
-
read("#F5B942",
|
|
148
|
+
read("#F5B942", "init");
|
|
142
149
|
|
|
143
150
|
return {};
|
|
144
151
|
}
|
|
@@ -55,8 +55,8 @@ export function mount(slot) {
|
|
|
55
55
|
body: bench(
|
|
56
56
|
stack(
|
|
57
57
|
panel("Values", source),
|
|
58
|
-
panel("
|
|
59
|
-
el("p", { class: "mvb-note" }, "
|
|
58
|
+
panel("What to mask", kinds),
|
|
59
|
+
el("p", { class: "mvb-note" }, "Pattern matching can miss a value, so check the result before sharing.")
|
|
60
60
|
),
|
|
61
61
|
stack(panel("Masked", result))
|
|
62
62
|
),
|
|
@@ -46,6 +46,15 @@ export function mount(slot) {
|
|
|
46
46
|
return Math.max(1, Math.min(200, parseInt(countField.value || "1", 10) || 1));
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** The same rows as the table, tab separated. */
|
|
50
|
+
function tsv() {
|
|
51
|
+
const columns = Object.keys(rows[0] ?? {});
|
|
52
|
+
return [
|
|
53
|
+
columns.join("\t"),
|
|
54
|
+
...rows.map((row) => columns.map((key) => String(row[key])).join("\t")),
|
|
55
|
+
].join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
function render() {
|
|
50
59
|
const total = totalCount();
|
|
51
60
|
rows = Array.from({ length: total }, (_, i) => person(i + 1));
|
|
@@ -83,8 +92,11 @@ export function mount(slot) {
|
|
|
83
92
|
countField.value = String(totalCount());
|
|
84
93
|
});
|
|
85
94
|
copyButton.addEventListener("click", async () => {
|
|
86
|
-
|
|
87
|
-
|
|
95
|
+
// In table mode the visible table is what gets copied, as tab separated text
|
|
96
|
+
// so it lands in a spreadsheet as a table. The local name here used to shadow
|
|
97
|
+
// the readout above, which threw before the copy ever ran.
|
|
98
|
+
const payload = format === "table" ? tsv() : text.textContent;
|
|
99
|
+
toast((await copyText(payload)) ? "Copied" : "Copy failed");
|
|
88
100
|
});
|
|
89
101
|
saveButton.addEventListener("click", () => {
|
|
90
102
|
if (format === "csv") download("test-data.csv", text.textContent, "text/csv");
|
|
@@ -94,7 +106,7 @@ export function mount(slot) {
|
|
|
94
106
|
const card = toolcard({
|
|
95
107
|
body: bench(
|
|
96
108
|
stack(panel("Format", chips), panel("Records", countField)),
|
|
97
|
-
el("div", { class: "mvb-
|
|
109
|
+
el("div", { class: "mvb-stack" }, text, table)
|
|
98
110
|
),
|
|
99
111
|
foot: actions(copyButton, saveButton, againButton),
|
|
100
112
|
});
|
|
@@ -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
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Placeholder text by the word, the sentence or the paragraph. The classic
|
|
2
2
|
// opening is a checkbox, not a lock-in, and regeneration never clears the copy.
|
|
3
|
-
import { toast, copyText, chipRow, numberInput, checkbox, button, bench, stack, panel,
|
|
3
|
+
import { toast, copyText, chipRow, numberInput, checkbox, output, button, bench, stack, panel, readPin, writePin, actions, toolcard } from "../core.js";
|
|
4
4
|
|
|
5
5
|
const WORDS = "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua enim ad minim veniam quis nostrud exercitation ullamco laboris nisi aliquip ex ea commodo consequat duis aute irure in reprehenderit voluptate velit esse cillum eu fugiat nulla pariatur excepteur sint occaecat cupidatat non proident sunt culpa qui officia deserunt mollit anim id est laborum".split(" ");
|
|
6
6
|
|
|
@@ -42,7 +42,9 @@ export function mount(slot) {
|
|
|
42
42
|
);
|
|
43
43
|
const amount = numberInput({ value: saved.amount ?? "3", min: "1", max: "200", "aria-label": "How many" });
|
|
44
44
|
const classic = checkbox("Start with the classic opening", { checked: saved.classic !== false });
|
|
45
|
-
|
|
45
|
+
// Generated text is read, not typed, so it sits on the recessed surface.
|
|
46
|
+
const result = output({ tag: "textarea", rows: 12 });
|
|
47
|
+
result.setAttribute("aria-label", "Generated text");
|
|
46
48
|
|
|
47
49
|
function totalCount() {
|
|
48
50
|
return Math.max(1, Math.min(200, parseInt(amount.value || "1", 10) || 1));
|
|
@@ -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();
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Percent-encoding for a single value, following encodeURIComponent. The
|
|
2
2
|
// swap button moves the result back into the input for a second round.
|
|
3
|
-
import { bench, stack, panel, textArea, notice, chipRow, button, copyButton, actions, toolcard } from "../core.js";
|
|
3
|
+
import { bench, stack, panel, textArea, output, notice, chipRow, button, copyButton, actions, toolcard } from "../core.js";
|
|
4
4
|
|
|
5
5
|
export function mount(slot) {
|
|
6
6
|
const source = textArea({ rows: 5, value: "q=hello world&lang=id", "aria-label": "Input" });
|
|
7
|
-
|
|
7
|
+
// The encoded text is read, not typed, so it sits on the recessed surface.
|
|
8
|
+
const result = output({ tag: "textarea", rows: 5 });
|
|
9
|
+
result.setAttribute("aria-label", "Result");
|
|
8
10
|
const error = notice("error");
|
|
9
11
|
let mode = "encode";
|
|
10
12
|
|
|
@@ -1,34 +1,129 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
1
|
+
// UUIDs in the versions that reach a schema: v4 and v7 are random, v7 sorts by
|
|
2
|
+
// time, v1 and v6 are the legacy timestamp pair, and Nil and Max are the two
|
|
3
|
+
// sentinels a test suite looks for.
|
|
4
|
+
import { el, toast, copyText, download, numberInput, checkbox, chipRow, button, bench, stack, panel, output, readPin, writePin, actions, toolcard } from "../core.js";
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
// 100 ns ticks between 1582-10-15 and 1970-01-01, the epoch a v1 stamp counts from.
|
|
7
|
+
const GREGORIAN_OFFSET = 122192928000000000n;
|
|
8
|
+
|
|
9
|
+
const VERSIONS = [
|
|
10
|
+
{ value: "v4", label: "v4" },
|
|
11
|
+
{ value: "v7", label: "v7" },
|
|
12
|
+
{ value: "v1", label: "v1" },
|
|
13
|
+
{ value: "v6", label: "v6" },
|
|
14
|
+
{ value: "nil", label: "Nil" },
|
|
15
|
+
{ value: "max", label: "Max" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const toHex = (bytes) => [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
19
|
+
const group = (hex) => `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
20
|
+
const randomHex = (bytes) => toHex(crypto.getRandomValues(new Uint8Array(bytes)));
|
|
21
|
+
|
|
22
|
+
function v4() {
|
|
5
23
|
if (crypto.randomUUID) return crypto.randomUUID();
|
|
6
24
|
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
7
25
|
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
8
26
|
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
9
|
-
|
|
10
|
-
|
|
27
|
+
return group(toHex(bytes));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let lastStamp = 0;
|
|
31
|
+
let counter = 0;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* v7 puts a 48 bit millisecond stamp in front of random bits, so string order
|
|
35
|
+
* matches time order. A generated batch lands several UUIDs in the same
|
|
36
|
+
* millisecond, where the random tail alone would shuffle them, so the 12 bit
|
|
37
|
+
* field behind the stamp steps like a counter for the rest of that millisecond.
|
|
38
|
+
* It starts from a random value each millisecond to keep the tail unguessable.
|
|
39
|
+
*/
|
|
40
|
+
function v7() {
|
|
41
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
42
|
+
// A clock that steps backwards would break the order as well, so never step back.
|
|
43
|
+
let ms = Math.max(Date.now(), lastStamp);
|
|
44
|
+
counter = counter + 1;
|
|
45
|
+
if (ms !== lastStamp || counter > 0x0fff) {
|
|
46
|
+
// A new millisecond, or more than 4095 in one: start over, and move the stamp
|
|
47
|
+
// on rather than hand out two UUIDs with the same timestamp and tail.
|
|
48
|
+
if (ms === lastStamp) ms += 1;
|
|
49
|
+
counter = (bytes[6] << 4) | bytes[7];
|
|
50
|
+
}
|
|
51
|
+
lastStamp = ms;
|
|
52
|
+
for (let at = 5; at >= 0; at--) {
|
|
53
|
+
bytes[at] = ms % 256;
|
|
54
|
+
ms = Math.floor(ms / 256);
|
|
55
|
+
}
|
|
56
|
+
bytes[6] = 0x70 | (counter >> 8);
|
|
57
|
+
bytes[7] = counter & 0xff;
|
|
58
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
59
|
+
return group(toHex(bytes));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The 60 bit timestamp v1 and v6 store, as 15 hex characters, most significant first. */
|
|
63
|
+
const stamp = () => (BigInt(Date.now()) * 10000n + GREGORIAN_OFFSET).toString(16).padStart(15, "0");
|
|
64
|
+
|
|
65
|
+
/** A node id nobody owns, so a generated v1 cannot point at real hardware. */
|
|
66
|
+
const nodeHex = () => (parseInt(randomHex(1), 16) | 0x01).toString(16).padStart(2, "0") + randomHex(5);
|
|
67
|
+
|
|
68
|
+
const sequenceHex = () => ((parseInt(randomHex(2), 16) & 0x3fff) | 0x8000).toString(16).padStart(4, "0");
|
|
69
|
+
|
|
70
|
+
/** v1 and v6 share every field; only the order of the timestamp differs. */
|
|
71
|
+
function timed(version) {
|
|
72
|
+
const ticks = stamp();
|
|
73
|
+
const sequence = sequenceHex();
|
|
74
|
+
const node = nodeHex();
|
|
75
|
+
// v1 stores the stamp low, mid, high, which is why it never sorts. v6 puts the
|
|
76
|
+
// high half first so the string order matches the time order.
|
|
77
|
+
return version === 1
|
|
78
|
+
? `${ticks.slice(7)}-${ticks.slice(3, 7)}-1${ticks.slice(0, 3)}-${sequence}-${node}`
|
|
79
|
+
: `${ticks.slice(0, 8)}-${ticks.slice(8, 12)}-6${ticks.slice(12)}-${sequence}-${node}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function make(version) {
|
|
83
|
+
if (version === "nil") return "00000000-0000-0000-0000-000000000000";
|
|
84
|
+
if (version === "max") return "ffffffff-ffff-ffff-ffff-ffffffffffff";
|
|
85
|
+
if (version === "v7") return v7();
|
|
86
|
+
if (version === "v1") return timed(1);
|
|
87
|
+
if (version === "v6") return timed(6);
|
|
88
|
+
return v4();
|
|
11
89
|
}
|
|
12
90
|
|
|
13
91
|
export function mount(slot) {
|
|
14
92
|
const PIN = "uuid-generator";
|
|
15
|
-
const saved = readPin(PIN, { count: "5", caps: false });
|
|
93
|
+
const saved = readPin(PIN, { count: "5", caps: false, noHyphens: false, version: "v4" });
|
|
16
94
|
|
|
17
95
|
const countField = numberInput({ value: saved.count ?? "5", min: "1", max: "1000", "aria-label": "How many" });
|
|
18
96
|
const capsBox = checkbox("Uppercase", { checked: Boolean(saved.caps) });
|
|
97
|
+
const hyphensBox = checkbox("No hyphens", { checked: saved.noHyphens === true });
|
|
19
98
|
const result = output({ rows: 8 });
|
|
99
|
+
let version = VERSIONS.some((item) => item.value === saved.version) ? saved.version : "v4";
|
|
20
100
|
let current = [];
|
|
21
101
|
|
|
102
|
+
const versions = chipRow(VERSIONS, {
|
|
103
|
+
value: version,
|
|
104
|
+
label: "Version",
|
|
105
|
+
onChange: (value) => {
|
|
106
|
+
version = value;
|
|
107
|
+
generate();
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
22
111
|
function totalCount() {
|
|
23
112
|
return Math.max(1, Math.min(1000, parseInt(countField.value || "1", 10) || 1));
|
|
24
113
|
}
|
|
25
114
|
|
|
26
115
|
function generate() {
|
|
27
116
|
const total = totalCount();
|
|
28
|
-
current = Array.from({ length: total },
|
|
117
|
+
current = Array.from({ length: total }, () => make(version));
|
|
29
118
|
if (capsBox.input.checked) current = current.map((value) => value.toUpperCase());
|
|
119
|
+
if (hyphensBox.input.checked) current = current.map((value) => value.replace(/-/g, ""));
|
|
30
120
|
result.write(current.join("\n"));
|
|
31
|
-
writePin(PIN, {
|
|
121
|
+
writePin(PIN, {
|
|
122
|
+
count: String(total),
|
|
123
|
+
caps: capsBox.input.checked,
|
|
124
|
+
noHyphens: hyphensBox.input.checked,
|
|
125
|
+
version,
|
|
126
|
+
});
|
|
32
127
|
}
|
|
33
128
|
|
|
34
129
|
const generateButton = button("Generate", { icon: "refresh-cw", kind: "fill" });
|
|
@@ -44,9 +139,10 @@ export function mount(slot) {
|
|
|
44
139
|
countField.value = String(totalCount());
|
|
45
140
|
});
|
|
46
141
|
capsBox.input.addEventListener("change", generate);
|
|
142
|
+
hyphensBox.input.addEventListener("change", generate);
|
|
47
143
|
copyButton.addEventListener("click", async () => {
|
|
48
144
|
if (!current.length) return toast("Generate first, then copy");
|
|
49
|
-
toast((await copyText(
|
|
145
|
+
toast((await copyText(current.join("\n"))) ? `${current.length} copied` : "Copy failed");
|
|
50
146
|
});
|
|
51
147
|
jsonButton.addEventListener("click", async () => {
|
|
52
148
|
if (!current.length) return toast("Generate first, then copy");
|
|
@@ -55,13 +151,13 @@ export function mount(slot) {
|
|
|
55
151
|
const saveButton = button("Save .txt", { icon: "download" });
|
|
56
152
|
saveButton.addEventListener("click", () => {
|
|
57
153
|
if (!current.length) return toast("Generate first");
|
|
58
|
-
download("uuids.txt",
|
|
154
|
+
download("uuids.txt", current.join("\n"));
|
|
59
155
|
});
|
|
60
156
|
|
|
61
157
|
const card = toolcard({
|
|
62
158
|
body: bench(
|
|
63
|
-
stack(panel("How many", countField), capsBox),
|
|
64
|
-
|
|
159
|
+
stack(panel("Version", versions), panel("How many", countField), el("div", { class: "mvb-chips" }, capsBox, hyphensBox)),
|
|
160
|
+
result
|
|
65
161
|
),
|
|
66
162
|
foot: actions(copyButton, jsonButton, saveButton, generateButton),
|
|
67
163
|
});
|
|
@@ -71,4 +167,4 @@ export function mount(slot) {
|
|
|
71
167
|
return {};
|
|
72
168
|
}
|
|
73
169
|
|
|
74
|
-
export default { mount };
|
|
170
|
+
export default { mount };
|