mevebu 2.0.0 → 2.1.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # mevebu
4
4
 
5
- **200 free browser tools, one page each. No sign up. No uploads. Everything runs in your tab.**
5
+ **205 free browser tools, one page each. No sign up. No uploads. Everything runs in your tab.**
6
6
 
7
7
  A private toolbox for the small jobs that do not deserve a whole app: format JSON, decode a JWT, make a password, build a cron line, pick a color, scan a QR code, and a lot more.
8
8
 
@@ -10,7 +10,7 @@ A private toolbox for the small jobs that do not deserve a whole app: format JSO
10
10
  [![npm downloads](https://img.shields.io/npm/dm/mevebu?color=2ea44f)](https://www.npmjs.com/package/mevebu)
11
11
  [![License: MIT](https://img.shields.io/github/license/mevebu/mevebu?color=blue)](./LICENSE)
12
12
  [![GitHub stars](https://img.shields.io/github/stars/mevebu/mevebu?style=flat&color=yellow)](https://github.com/mevebu/mevebu/stargazers)
13
- ![Tools](https://img.shields.io/badge/tools-200-brightgreen)
13
+ ![Tools](https://img.shields.io/badge/tools-205-brightgreen)
14
14
  ![Privacy](https://img.shields.io/badge/privacy-100%25%20in--browser-blue)
15
15
 
16
16
  [Quick start](#quick-start) · [What's inside](#whats-inside) · [Privacy](#privacy-by-design) · [Credit](#credit) · [Development](#development) · [FAQ](#faq)
@@ -23,7 +23,7 @@ A private toolbox for the small jobs that do not deserve a whole app: format JSO
23
23
 
24
24
  ## What is mevebu?
25
25
 
26
- mevebu is a collection of **200 small online tools** that all run **inside your browser**. Each tool lives on its own page, does one job, and does it fast. Most of them update as you type, so there is no submit button and no waiting.
26
+ mevebu is a collection of **205 small online tools** that all run **inside your browser**. Each tool lives on its own page, does one job, and does it fast. Most of them update as you type, so there is no submit button and no waiting.
27
27
 
28
28
  Think of it as a clean, private alternative to the usual "free online tools" websites. There are no accounts, no ads in your way, no uploads, and no tracking. Your text, passwords, tokens, and files stay on your device.
29
29
 
@@ -64,11 +64,11 @@ mevebu tries port `5555` first, then `5678`, then `5432`, and finally any free p
64
64
 
65
65
  ## What's inside
66
66
 
67
- mevebu has **200 tools in 20 categories**. Here is the full breakdown:
67
+ mevebu has **205 tools in 20 categories**. Here is the full breakdown:
68
68
 
69
69
  | Category | Tools |
70
70
  | ------------------ | ----: |
71
- | Data Generator | 5 |
71
+ | Data Generator | 10 |
72
72
  | Passwords and keys | 10 |
73
73
  | Developer | 21 |
74
74
  | Convert | 15 |
@@ -88,7 +88,7 @@ mevebu has **200 tools in 20 categories**. Here is the full breakdown:
88
88
  | GitHub | 8 |
89
89
  | Units | 8 |
90
90
  | QR codes | 2 |
91
- | **Total** | **200** |
91
+ | **Total** | **205** |
92
92
 
93
93
  The counts come straight from the tool registry, so this table and the app never drift apart.
94
94
 
@@ -176,7 +176,7 @@ That is all. The catalog, search, breadcrumbs, and category pages pick it up aut
176
176
  ## FAQ
177
177
 
178
178
  **What is mevebu?**
179
- mevebu is a free, open source collection of 200 browser based tools for developers, students, creators, and everyday use. Every tool runs locally in your browser.
179
+ mevebu is a free, open source collection of 205 browser based tools for developers, students, creators, and everyday use. Every tool runs locally in your browser.
180
180
 
181
181
  **Is mevebu really free?**
182
182
  Yes. It is MIT licensed, with no paid tier, no account, and no usage limits.
@@ -37,9 +37,6 @@
37
37
  --text-lead--line-height: 1.06;
38
38
  --text-hero: clamp(34px, 6vw, 58px);
39
39
  --text-hero--line-height: 1.03;
40
- --text-display: 44px;
41
- --text-display--line-height: 1;
42
- --animate-toss: toss 450ms ease;
43
40
  }
44
41
  }
45
42
  @layer base {
@@ -512,6 +509,9 @@
512
509
  .h-24 {
513
510
  height: calc(var(--spacing) * 24);
514
511
  }
512
+ .h-40 {
513
+ height: calc(var(--spacing) * 40);
514
+ }
515
515
  .h-\[22px\] {
516
516
  height: 22px;
517
517
  }
@@ -539,6 +539,9 @@
539
539
  .h-px {
540
540
  height: 1px;
541
541
  }
542
+ .max-h-56 {
543
+ max-height: calc(var(--spacing) * 56);
544
+ }
542
545
  .max-h-80 {
543
546
  max-height: calc(var(--spacing) * 80);
544
547
  }
@@ -773,6 +776,9 @@
773
776
  .grid-cols-2 {
774
777
  grid-template-columns: repeat(2, minmax(0, 1fr));
775
778
  }
779
+ .grid-cols-3 {
780
+ grid-template-columns: repeat(3, minmax(0, 1fr));
781
+ }
776
782
  .grid-cols-4 {
777
783
  grid-template-columns: repeat(4, minmax(0, 1fr));
778
784
  }
@@ -750,7 +750,7 @@ export function blank(iconName, text) {
750
750
  // ---------- version ----------
751
751
 
752
752
  // Kept in one place. The CLI reads the same value from package.json.
753
- export const VERSION = "2.0.0";
753
+ export const VERSION = "2.1.0";
754
754
  export const PACKAGE_NAME = "mevebu";
755
755
  export const REPO_SLUG = "mevebu/mevebu";
756
756
 
@@ -0,0 +1,54 @@
1
+ // A small seeded generator shared by the data tools. A seed is worth writing
2
+ // down: the same one always produces the same file, so a fixture can be rebuilt
3
+ // after it drifts.
4
+
5
+ /** Mulberry32. Fast, deterministic, and good enough for fixtures. */
6
+ export function seeded(seed) {
7
+ let state = hashSeed(String(seed)) >>> 0;
8
+ return () => {
9
+ state = (state + 0x6d2b79f5) >>> 0;
10
+ let t = state;
11
+ t = Math.imul(t ^ (t >>> 15), t | 1);
12
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
13
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
14
+ };
15
+ }
16
+
17
+ export function hashSeed(text) {
18
+ let hash = 2166136261;
19
+ for (let i = 0; i < text.length; i++) {
20
+ hash ^= text.charCodeAt(i);
21
+ hash = Math.imul(hash, 16777619);
22
+ }
23
+ return hash >>> 0;
24
+ }
25
+
26
+ export function pick(random, list) {
27
+ return list[Math.floor(random() * list.length)];
28
+ }
29
+
30
+ export function shuffle(list, random) {
31
+ const out = [...list];
32
+ for (let i = out.length - 1; i > 0; i--) {
33
+ const j = Math.floor(random() * (i + 1));
34
+ [out[i], out[j]] = [out[j], out[i]];
35
+ }
36
+ return out;
37
+ }
38
+
39
+ /** A number from a normal distribution, by Box-Muller. */
40
+ export function gaussian(random, mean = 0, deviation = 1) {
41
+ const u = Math.max(1e-9, random());
42
+ const v = random();
43
+ return mean + deviation * Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
44
+ }
45
+
46
+ export function uuid(random) {
47
+ const hex = "0123456789abcdef";
48
+ let out = "";
49
+ for (let i = 0; i < 32; i++) {
50
+ if (i === 8 || i === 12 || i === 16 || i === 20) out += "-";
51
+ out += i === 12 ? "4" : hex[Math.floor(random() * 16)];
52
+ }
53
+ return out;
54
+ }
@@ -120,6 +120,96 @@ export const TOOLS = [
120
120
  keywords: "data dictionary csv schema profile columns nulls distinct metadata",
121
121
  load: () => import("./tools/data-dictionary.js"),
122
122
  },
123
+ {
124
+ id: "json-schema-sample",
125
+ category: "data-gen",
126
+ name: "JSON Schema Sample",
127
+ icon: "braces",
128
+ note: "Generate documents from a JSON Schema.",
129
+ about: "Required fields, types, enums, bounds and patterns, with a seed.",
130
+ tutorial: {
131
+ intro: "Make sample documents that pass a schema.",
132
+ steps: [
133
+ "Paste a JSON Schema.",
134
+ "Set how many documents and a seed.",
135
+ "Press Generate, then copy the JSON or take a ZIP with one file per document.",
136
+ ],
137
+ },
138
+ keywords: "json schema sample mock fixture generate validator draft 2020-12",
139
+ load: () => import("./tools/json-schema-sample.js"),
140
+ },
141
+ {
142
+ id: "mock-api-payload",
143
+ category: "data-gen",
144
+ name: "Mock API Payload",
145
+ icon: "braces",
146
+ note: "Write the JSON an endpoint returns.",
147
+ about: "A list with pagination, a single item, or an error, from one field list.",
148
+ tutorial: {
149
+ intro: "Build a fixture before the endpoint exists.",
150
+ steps: [
151
+ "Define the fields and their types.",
152
+ "Pick the shape: list, single item, or error.",
153
+ "Copy the JSON or download it.",
154
+ ],
155
+ },
156
+ keywords: "mock api payload fixture json response pagination error frontend",
157
+ load: () => import("./tools/mock-api-payload.js"),
158
+ },
159
+ {
160
+ id: "dataset-splitter",
161
+ category: "data-gen",
162
+ name: "Dataset Splitter",
163
+ icon: "table",
164
+ note: "Split a CSV into train, validation and test.",
165
+ about: "Seeded and stratified, and every row lands in exactly one set.",
166
+ tutorial: {
167
+ intro: "Prepare a dataset for training.",
168
+ steps: [
169
+ "Drop a CSV.",
170
+ "Set the train and validation percentages, a seed, and an optional stratify column.",
171
+ "Press Split and download for a ZIP with three files.",
172
+ ],
173
+ },
174
+ keywords: "dataset split train validation test machine learning csv stratified seed",
175
+ load: () => import("./tools/dataset-splitter.js"),
176
+ },
177
+ {
178
+ id: "time-series",
179
+ category: "data-gen",
180
+ name: "Synthetic Time Series",
181
+ icon: "chart-line",
182
+ note: "Generate a dated series with trend and season.",
183
+ about: "Preview the chart, add a spike, then download the CSV.",
184
+ tutorial: {
185
+ intro: "Make a series that looks like real data.",
186
+ steps: [
187
+ "Set the start date, the frequency and the length.",
188
+ "Shape the trend, the season and the noise, then add a spike if you need one.",
189
+ "Press Download CSV.",
190
+ ],
191
+ },
192
+ keywords: "time series synthetic data trend season noise chart demo seed",
193
+ load: () => import("./tools/time-series.js"),
194
+ },
195
+ {
196
+ id: "survey-responses",
197
+ category: "data-gen",
198
+ name: "Survey Responses",
199
+ icon: "clipboard-list",
200
+ note: "Generate fake survey answers.",
201
+ about: "Weights per option, so answers lean the way real answers lean.",
202
+ tutorial: {
203
+ intro: "Test a results dashboard with plausible responses.",
204
+ steps: [
205
+ "Add questions and their options.",
206
+ "Set a weight per option and the number of respondents.",
207
+ "Press Download CSV. One row is one respondent.",
208
+ ],
209
+ },
210
+ keywords: "survey responses fake data dashboard test weights csv",
211
+ load: () => import("./tools/survey-responses.js"),
212
+ },
123
213
 
124
214
  {
125
215
  id: "password-generator",
@@ -0,0 +1,181 @@
1
+ // Split a CSV into train, validation and test sets. Seeded, so the same seed
2
+ // and the same file always give the same split, and a row never lands in two
3
+ // sets.
4
+ import {
5
+ el, toast, download, bench, stack, panel, numberInput, textInput, selectInput,
6
+ button, actions, toolcard, blank, notice,
7
+ } from "../core.js";
8
+ import { dropzone, baseName, formatBytes } from "../files.js";
9
+ import { parseDelimited, detectDelimiter, headerKeys, toCsv } from "../csv.js";
10
+ import { seeded, shuffle } from "../random.js";
11
+ import { zip } from "../zip.js";
12
+
13
+ export function mount(slot) {
14
+ let file = null;
15
+ let header = [];
16
+ let rows = [];
17
+ let split = null;
18
+
19
+ const train = numberInput({ min: "0", max: "100", step: "5", value: "70", "aria-label": "Train percent" });
20
+ const validation = numberInput({ min: "0", max: "100", step: "5", value: "15", "aria-label": "Validation percent" });
21
+ const seed = textInput({ value: "mevebu", "aria-label": "Seed", class: "font-mono" });
22
+ const stratify = selectInput([{ value: "", label: "Do not stratify" }], { value: "", "aria-label": "Stratify by" });
23
+
24
+ const error = notice("warn");
25
+ const summary = el("p", { class: "text-sm text-ink-2" });
26
+ const tiles = el("div", { class: "grid grid-cols-3 gap-2" });
27
+ const empty = blank("table", "Drop a CSV to split it.");
28
+
29
+ const drop = dropzone({ label: "Drop a CSV here", accept: ".csv,text/csv,text/plain", iconName: "table" });
30
+ const clearButton = button("Clear", { icon: "trash", kind: "quiet" });
31
+ const splitButton = button("Split and download", { icon: "download", kind: "fill" });
32
+
33
+ function tile(label) {
34
+ const value = el("span", { class: "text-xl font-bold tracking-[-0.02em] tabular-nums" }, "0");
35
+ const rows = el("span", { class: "text-2xs text-ink-3" }, "");
36
+ const node = el("div", { class: "grid gap-0.5 rounded-r1 border border-orbit bg-deck-2 px-3 py-2.5" },
37
+ el("span", { class: "text-2xs font-semibold tracking-[0.06em] text-ink-3 uppercase" }, label),
38
+ value, rows);
39
+ node.set = (count, percent) => {
40
+ value.textContent = String(count);
41
+ rows.textContent = `${percent}% of rows`;
42
+ };
43
+ return node;
44
+ }
45
+
46
+ const trainTile = tile("Train");
47
+ const validationTile = tile("Validation");
48
+ const testTile = tile("Test");
49
+ tiles.append(trainTile, validationTile, testTile);
50
+
51
+ function weights() {
52
+ const a = Math.max(0, Number(train.value) || 0);
53
+ const b = Math.max(0, Number(validation.value) || 0);
54
+ const c = Math.max(0, 100 - a - b);
55
+ return { train: a, validation: b, test: c };
56
+ }
57
+
58
+ function compute() {
59
+ const w = weights();
60
+ const random = seeded(seed.value || "mevebu");
61
+ const key = stratify.value;
62
+ const keyIndex = key ? header.indexOf(key) : -1;
63
+
64
+ const buckets = new Map();
65
+ for (const row of rows) {
66
+ const group = keyIndex >= 0 ? row[keyIndex] ?? "" : "__all__";
67
+ if (!buckets.has(group)) buckets.set(group, []);
68
+ buckets.get(group).push(row);
69
+ }
70
+
71
+ const out = { train: [], validation: [], test: [] };
72
+ for (const group of buckets.values()) {
73
+ const list = shuffle(group, random);
74
+ const n = list.length;
75
+ const trainEnd = Math.round((w.train / 100) * n);
76
+ const validationEnd = trainEnd + Math.round((w.validation / 100) * n);
77
+ out.train.push(...list.slice(0, trainEnd));
78
+ out.validation.push(...list.slice(trainEnd, validationEnd));
79
+ out.test.push(...list.slice(validationEnd));
80
+ }
81
+
82
+ // Shuffle the final sets so a stratified split is not grouped by class.
83
+ for (const name of Object.keys(out)) out[name] = shuffle(out[name], random);
84
+ return out;
85
+ }
86
+
87
+ function render() {
88
+ empty.hidden = rows.length > 0;
89
+ tiles.hidden = rows.length === 0;
90
+ splitButton.disabled = rows.length === 0;
91
+ clearButton.disabled = rows.length === 0;
92
+ if (!rows.length) {
93
+ summary.textContent = "";
94
+ return;
95
+ }
96
+ const w = weights();
97
+ split = compute();
98
+ const total = rows.length;
99
+ trainTile.set(split.train.length, Math.round((split.train.length / total) * 100));
100
+ validationTile.set(split.validation.length, Math.round((split.validation.length / total) * 100));
101
+ testTile.set(split.test.length, Math.round((split.test.length / total) * 100));
102
+ summary.textContent = `${total} rows · ${header.length} columns · seed ${seed.value || "mevebu"}${w.test < 0 ? " · the percentages add to more than 100, so test is empty" : ""}`;
103
+ if (w.test < 0) error.write("Train and validation add to more than 100 percent. Test gets nothing.");
104
+ else error.write("");
105
+ }
106
+
107
+ async function open(files) {
108
+ const picked = Array.from(files).find((entry) => /\.(csv|tsv|txt)$/i.test(entry.name) || entry.type.startsWith("text/"));
109
+ if (!picked) {
110
+ error.write("That file is not a CSV.");
111
+ return;
112
+ }
113
+ try {
114
+ const text = await picked.text();
115
+ const parsed = parseDelimited(text, detectDelimiter(text));
116
+ if (parsed.length < 2) throw new Error("A CSV needs a header row and at least one data row.");
117
+ file = picked;
118
+ header = headerKeys(parsed[0]);
119
+ rows = parsed.slice(1).filter((row) => row.some((cell) => cell !== ""));
120
+ stratify.replaceChildren(
121
+ el("option", { value: "" }, "Do not stratify"),
122
+ ...header.map((name) => el("option", { value: name }, name))
123
+ );
124
+ stratify.value = "";
125
+ error.write("");
126
+ render();
127
+ } catch (caught) {
128
+ error.write(caught?.message || "This file could not be read.");
129
+ }
130
+ }
131
+
132
+ drop.onFiles(open);
133
+ train.addEventListener("input", render);
134
+ validation.addEventListener("input", render);
135
+ seed.addEventListener("input", render);
136
+ stratify.addEventListener("change", render);
137
+
138
+ splitButton.addEventListener("click", () => {
139
+ if (!split) return;
140
+ const base = baseName(file?.name || "data");
141
+ const files = ["train", "validation", "test"].map((name) => ({
142
+ name: `${base}-${name}.csv`,
143
+ bytes: new TextEncoder().encode(toCsv([header, ...split[name]])),
144
+ }));
145
+ download(`${base}-split.zip`, zip(files), "application/zip");
146
+ toast("Three files ready");
147
+ });
148
+
149
+ clearButton.addEventListener("click", () => {
150
+ file = null;
151
+ header = [];
152
+ rows = [];
153
+ split = null;
154
+ error.write("");
155
+ render();
156
+ });
157
+
158
+ render();
159
+
160
+ const shell = toolcard({
161
+ body: bench(
162
+ stack(
163
+ drop,
164
+ el("div", { class: "grid grid-cols-2 gap-3" }, panel("Train %", train), panel("Validation %", validation)),
165
+ panel("Seed", seed, "The same seed and file give the same split."),
166
+ panel("Stratify by", stratify, "Keeps each class proportional across the three sets."),
167
+ error
168
+ ),
169
+ stack(summary, empty, tiles,
170
+ el("p", { class: "m-0 text-2xs leading-[1.6] text-ink-3" },
171
+ "Every row goes to exactly one set. The test share is whatever is left after train and validation."))
172
+ ),
173
+ foot: actions(clearButton, splitButton),
174
+ });
175
+
176
+ slot.append(shell.node);
177
+
178
+ return {};
179
+ }
180
+
181
+ export default { mount };