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.
@@ -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
  }
@@ -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, size = "sm" } = {}) {
518
- const wrap = el("div", { class: `mvb-chips mvb-chips--${size}`, role: "group", "aria-label": label || "Options" });
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.2.0";
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 on the left.",
135
- "Pick an algorithm with the chips (1 to 512).",
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: "Version 4 UUIDs from the browser random source.",
275
+ about: "Versions 1, 4, 6 and 7, plus the Nil and Max sentinels.",
276
276
  tutorial: {
277
- intro: "Generate version 4 UUIDs. Copy as a list, JSON, or save as a file.",
277
+ intro: "Generate UUIDs and take them as a list, as JSON, or as a file.",
278
278
  steps: [
279
- "Set how many UUIDs to generate, between 1 and 1000.",
280
- "Toggle Uppercase if you want them in caps. The list updates as you change a setting.",
281
- "Use Copy all, Copy JSON, or Save .txt on the right.",
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 on the left, or type a hex, rgb, or hsl value.",
298
- "The other formats, the swatch, and a contrast reading appear on the right.",
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 on the right of each panel.",
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
- const encode = (text) => btoa(String.fromCharCode(...new TextEncoder().encode(text)));
6
- const decode = (text) => new TextDecoder().decode(
7
- Uint8Array.from(atob(text.replace(/\s+/g, "")), (char) => char.charCodeAt(0))
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("Select a case to copy it", list)) });
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(`Expires in ${days} days, within the ${threshold} day warning window.`);
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
- function show(color, keepPicker) {
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 (!keepPicker) {
99
- anyField.value = hex.toUpperCase();
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, keepPicker) {
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, keepPicker);
130
+ show(color, from);
124
131
  }
125
132
 
126
- anyField.addEventListener("input", () => read(anyField.value, false));
127
- picker.addEventListener("input", () => read(picker.value, true));
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", false);
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("Mask", kinds),
59
- el("p", { class: "mvb-note" }, "Masking runs in this tab. Check the result before sharing.")
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
- const text = format === "table" ? JSON.stringify(rows) : text.textContent;
87
- toast((await copyText(text)) ? "Copied" : "Copy failed");
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-scroll" }, text, table)
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, textArea, readPin, writePin, actions, toolcard } from "../core.js";
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
- const result = textArea({ rows: 12, readonly: true, class: "mvb-in mvb-in--area", "aria-label": "Generated text" });
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
- const result = textArea({ rows: 5, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Result" });
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
 
@@ -82,8 +82,7 @@ export function mount(slot) {
82
82
  body: bench(
83
83
  stack(
84
84
  panel("URL", source),
85
- error,
86
- el("p", { class: "mvb-note" }, "The link is read in this tab. Nothing is opened or requested.")
85
+ error
87
86
  ),
88
87
  stack(kv, panel("Checks", flags))
89
88
  ),
@@ -1,34 +1,129 @@
1
- // Version 4 UUIDs. Generates a list, copies the whole list, or copies JSON.
2
- import { el, toast, copyText, download, numberInput, checkbox, button, bench, stack, panel, output, readPin, writePin, actions, toolcard } from "../core.js";
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
- function uuid() {
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
- const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0"));
10
- return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
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 }, uuid);
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, { count: String(total), caps: capsBox.input.checked });
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(result.textContent)) ? `${current.length} copied` : "Copy failed");
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", result.textContent);
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
- el("div", { class: "mvb-scroll" }, result)
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mevebu",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Small tools that run in your browser. No accounts, no uploads, no tracking.",
5
5
  "license": "MIT",
6
6
  "type": "module",