mevebu 1.2.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/tailwind.css +6 -0
- package/mevebu/app/tailwind.src.css +7 -0
- package/mevebu/libs/js/core.js +3 -3
- package/mevebu/libs/js/registry.js +11 -11
- 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/color-converter.js +17 -10
- 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/lorem-ipsum.js +4 -2
- 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/tailwind.css
CHANGED
|
@@ -1417,6 +1417,12 @@
|
|
|
1417
1417
|
border-inline-start-color: var(--mvb-danger);
|
|
1418
1418
|
color: var(--mvb-danger);
|
|
1419
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
|
+
}
|
|
1420
1426
|
.mvb-note--good {
|
|
1421
1427
|
border-inline-start-color: var(--mvb-aurora);
|
|
1422
1428
|
color: var(--mvb-aurora);
|
|
@@ -667,6 +667,13 @@
|
|
|
667
667
|
@apply border-s-danger text-danger;
|
|
668
668
|
}
|
|
669
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
|
+
|
|
670
677
|
.mvb-note--good {
|
|
671
678
|
@apply border-s-aurora text-aurora;
|
|
672
679
|
}
|
package/mevebu/libs/js/core.js
CHANGED
|
@@ -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
|
|
|
@@ -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)
|
|
@@ -93,12 +93,14 @@ export function mount(slot) {
|
|
|
93
93
|
const error = notice("error");
|
|
94
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
|
});
|
|
@@ -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));
|
|
@@ -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 };
|