mevebu 1.11.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 +16 -20
- package/mevebu/app/mevebu.js +1 -1
- package/mevebu/app/tailwind.css +34 -20
- package/mevebu/libs/js/core.js +1 -1
- package/mevebu/libs/js/csv.js +87 -0
- package/mevebu/libs/js/random.js +54 -0
- package/mevebu/libs/js/registry.js +163 -37
- package/mevebu/libs/js/tools/csv-viewer.js +1 -51
- package/mevebu/libs/js/tools/data-dictionary.js +204 -0
- package/mevebu/libs/js/tools/dataset-splitter.js +181 -0
- package/mevebu/libs/js/tools/json-schema-sample.js +254 -0
- package/mevebu/libs/js/tools/mock-api-payload.js +224 -0
- package/mevebu/libs/js/tools/roster-generator.js +195 -0
- package/mevebu/libs/js/tools/sample-csv.js +218 -0
- package/mevebu/libs/js/tools/sql-insert.js +184 -0
- package/mevebu/libs/js/tools/survey-responses.js +225 -0
- package/mevebu/libs/js/tools/team-generator.js +203 -0
- package/mevebu/libs/js/tools/time-series.js +191 -0
- package/package.json +1 -1
- package/mevebu/libs/js/tools/coin-flip.js +0 -78
- package/mevebu/libs/js/tools/name-picker.js +0 -68
- package/mevebu/libs/js/tools/spin-wheel.js +0 -168
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# mevebu
|
|
4
4
|
|
|
5
|
-
**
|
|
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,10 +10,10 @@ A private toolbox for the small jobs that do not deserve a whole app: format JSO
|
|
|
10
10
|
[](https://www.npmjs.com/package/mevebu)
|
|
11
11
|
[](./LICENSE)
|
|
12
12
|
[](https://github.com/mevebu/mevebu/stargazers)
|
|
13
|
-

|
|
14
14
|

|
|
15
15
|
|
|
16
|
-
[Quick start](#quick-start) · [What's inside](#whats-inside) · [Privacy](#privacy-by-design) · [
|
|
16
|
+
[Quick start](#quick-start) · [What's inside](#whats-inside) · [Privacy](#privacy-by-design) · [Credit](#credit) · [Development](#development) · [FAQ](#faq)
|
|
17
17
|
|
|
18
18
|
</div>
|
|
19
19
|
|
|
@@ -23,11 +23,11 @@ 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 **
|
|
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
|
|
|
30
|
-
|
|
30
|
+
Install it once with one command and it runs on your machine, with no server behind it and no account to create.
|
|
31
31
|
|
|
32
32
|
## Why you will like it
|
|
33
33
|
|
|
@@ -37,7 +37,6 @@ You can run it on your own computer with one command, or copy the folder to any
|
|
|
37
37
|
- **One tool, one page.** Every tool has its own clean page you can bookmark.
|
|
38
38
|
- **Instant search.** Press `Ctrl+K` and find any tool by name, keyword, or what it does.
|
|
39
39
|
- **Works offline.** Once installed, it runs fully on your machine.
|
|
40
|
-
- **Easy to host.** It is a plain static site. Drop it on any file host.
|
|
41
40
|
- **Free and open source.** MIT licensed.
|
|
42
41
|
|
|
43
42
|
## Quick start
|
|
@@ -65,11 +64,11 @@ mevebu tries port `5555` first, then `5678`, then `5432`, and finally any free p
|
|
|
65
64
|
|
|
66
65
|
## What's inside
|
|
67
66
|
|
|
68
|
-
mevebu has **
|
|
67
|
+
mevebu has **205 tools in 20 categories**. Here is the full breakdown:
|
|
69
68
|
|
|
70
69
|
| Category | Tools |
|
|
71
70
|
| ------------------ | ----: |
|
|
72
|
-
|
|
|
71
|
+
| Data Generator | 10 |
|
|
73
72
|
| Passwords and keys | 10 |
|
|
74
73
|
| Developer | 21 |
|
|
75
74
|
| Convert | 15 |
|
|
@@ -89,7 +88,7 @@ mevebu has **198 tools in 20 categories**. Here is the full breakdown:
|
|
|
89
88
|
| GitHub | 8 |
|
|
90
89
|
| Units | 8 |
|
|
91
90
|
| QR codes | 2 |
|
|
92
|
-
| **Total** | **
|
|
91
|
+
| **Total** | **205** |
|
|
93
92
|
|
|
94
93
|
The counts come straight from the tool registry, so this table and the app never drift apart.
|
|
95
94
|
|
|
@@ -126,21 +125,18 @@ Do not just take our word for it. Open the Network tab in your browser dev tools
|
|
|
126
125
|
| Your data uploaded | Never | Sometimes |
|
|
127
126
|
| Works offline | Yes | Rarely |
|
|
128
127
|
| Open source | Yes (MIT) | Rarely |
|
|
129
|
-
| Self-hostable | Yes | Rarely |
|
|
130
128
|
|
|
131
|
-
##
|
|
129
|
+
## Credit
|
|
132
130
|
|
|
133
|
-
|
|
131
|
+
mevebu is built and maintained by the [mevebu](https://github.com/mevebu) organization, created by [Ilyas Mukhlisin](https://www.instagram.com/ilyas.mukhlisin). Two repositories live there: **Mevebu**, this toolbox, and **Waitsec**, a set of guardrails for coding agents.
|
|
134
132
|
|
|
135
|
-
|
|
136
|
-
2. Upload it to your host.
|
|
137
|
-
3. Open `index.html`.
|
|
133
|
+
The code is MIT licensed. If you learn from it, borrow a helper, or ship something inspired by it, keep the copyright notice and say where it came from. A link back to [github.com/mevebu/mevebu](https://github.com/mevebu/mevebu) is enough.
|
|
138
134
|
|
|
139
|
-
|
|
135
|
+
Bug reports and tool ideas are welcome in the issues. The best way to support the project is to use it, share it, and tell us what is missing.
|
|
140
136
|
|
|
141
137
|
## Development
|
|
142
138
|
|
|
143
|
-
|
|
139
|
+
Contributions are welcome. Here is how to run the project locally while you work on it:
|
|
144
140
|
|
|
145
141
|
```bash
|
|
146
142
|
git clone https://github.com/mevebu/mevebu
|
|
@@ -180,7 +176,7 @@ That is all. The catalog, search, breadcrumbs, and category pages pick it up aut
|
|
|
180
176
|
## FAQ
|
|
181
177
|
|
|
182
178
|
**What is mevebu?**
|
|
183
|
-
mevebu is a free, open source collection of
|
|
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.
|
|
184
180
|
|
|
185
181
|
**Is mevebu really free?**
|
|
186
182
|
Yes. It is MIT licensed, with no paid tier, no account, and no usage limits.
|
|
@@ -191,8 +187,8 @@ No. Your input is processed in your browser tab and never sent to a server.
|
|
|
191
187
|
**Can I use mevebu offline?**
|
|
192
188
|
Yes. After you install it with npm, it runs on your own machine without needing the internet.
|
|
193
189
|
|
|
194
|
-
**Can I
|
|
195
|
-
Yes.
|
|
190
|
+
**Can I reuse the code?**
|
|
191
|
+
Yes. It is MIT licensed, so keep the copyright notice and the license text, and credit [mevebu](https://github.com/mevebu/mevebu) where you use it.
|
|
196
192
|
|
|
197
193
|
**How do I find a tool quickly?**
|
|
198
194
|
Press `Ctrl+K` and search by tool name, keyword, or what the tool does.
|
package/mevebu/app/mevebu.js
CHANGED
|
@@ -1013,7 +1013,7 @@ function aboutPage() {
|
|
|
1013
1013
|
block("Running it",
|
|
1014
1014
|
el("p", {}, "Install it once from npm. The command starts a local server and opens your browser at the address it prints."),
|
|
1015
1015
|
el("pre", { class: "m-0 overflow-x-auto rounded-r1 border border-orbit bg-deck-2 px-3.5 py-3 font-mono text-sm" }, "npm install -g mevebu\nmevebu"),
|
|
1016
|
-
el("p", {}, "
|
|
1016
|
+
el("p", {}, "The command serves the tools that ship with the package, so there is nothing to configure and no server to run.")),
|
|
1017
1017
|
|
|
1018
1018
|
block("Adding a tool",
|
|
1019
1019
|
el("p", {}, "Add one object to ", el("code", { class: CODE }, "mevebu/libs/js/registry.js"), " and one file in ", el("code", { class: CODE }, "mevebu/libs/js/tools/"), ". The home page, the index, the categories, search and the route all come from that entry.")),
|
package/mevebu/app/tailwind.css
CHANGED
|
@@ -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 {
|
|
@@ -293,6 +290,9 @@
|
|
|
293
290
|
.ms-1\.5 {
|
|
294
291
|
margin-inline-start: calc(var(--spacing) * 1.5);
|
|
295
292
|
}
|
|
293
|
+
.ms-2 {
|
|
294
|
+
margin-inline-start: calc(var(--spacing) * 2);
|
|
295
|
+
}
|
|
296
296
|
.ms-auto {
|
|
297
297
|
margin-inline-start: auto;
|
|
298
298
|
}
|
|
@@ -404,6 +404,10 @@
|
|
|
404
404
|
.aspect-\[4\/3\] {
|
|
405
405
|
aspect-ratio: 4/3;
|
|
406
406
|
}
|
|
407
|
+
.size-1\.5 {
|
|
408
|
+
width: calc(var(--spacing) * 1.5);
|
|
409
|
+
height: calc(var(--spacing) * 1.5);
|
|
410
|
+
}
|
|
407
411
|
.size-3 {
|
|
408
412
|
width: calc(var(--spacing) * 3);
|
|
409
413
|
height: calc(var(--spacing) * 3);
|
|
@@ -452,10 +456,6 @@
|
|
|
452
456
|
width: calc(var(--spacing) * 16);
|
|
453
457
|
height: calc(var(--spacing) * 16);
|
|
454
458
|
}
|
|
455
|
-
.size-32 {
|
|
456
|
-
width: calc(var(--spacing) * 32);
|
|
457
|
-
height: calc(var(--spacing) * 32);
|
|
458
|
-
}
|
|
459
459
|
.size-\[15px\] {
|
|
460
460
|
width: 15px;
|
|
461
461
|
height: 15px;
|
|
@@ -509,6 +509,9 @@
|
|
|
509
509
|
.h-24 {
|
|
510
510
|
height: calc(var(--spacing) * 24);
|
|
511
511
|
}
|
|
512
|
+
.h-40 {
|
|
513
|
+
height: calc(var(--spacing) * 40);
|
|
514
|
+
}
|
|
512
515
|
.h-\[22px\] {
|
|
513
516
|
height: 22px;
|
|
514
517
|
}
|
|
@@ -536,6 +539,9 @@
|
|
|
536
539
|
.h-px {
|
|
537
540
|
height: 1px;
|
|
538
541
|
}
|
|
542
|
+
.max-h-56 {
|
|
543
|
+
max-height: calc(var(--spacing) * 56);
|
|
544
|
+
}
|
|
539
545
|
.max-h-80 {
|
|
540
546
|
max-height: calc(var(--spacing) * 80);
|
|
541
547
|
}
|
|
@@ -605,9 +611,6 @@
|
|
|
605
611
|
.w-\[min\(300px\,100\%\)\] {
|
|
606
612
|
width: min(300px, 100%);
|
|
607
613
|
}
|
|
608
|
-
.w-\[min\(320px\,100\%\)\] {
|
|
609
|
-
width: min(320px, 100%);
|
|
610
|
-
}
|
|
611
614
|
.w-\[min\(420px\,100\%\)\] {
|
|
612
615
|
width: min(420px, 100%);
|
|
613
616
|
}
|
|
@@ -653,9 +656,6 @@
|
|
|
653
656
|
.max-w-\[64ch\] {
|
|
654
657
|
max-width: 64ch;
|
|
655
658
|
}
|
|
656
|
-
.max-w-\[340px\] {
|
|
657
|
-
max-width: 340px;
|
|
658
|
-
}
|
|
659
659
|
.max-w-\[560px\] {
|
|
660
660
|
max-width: 560px;
|
|
661
661
|
}
|
|
@@ -737,9 +737,15 @@
|
|
|
737
737
|
.\[grid-template-columns\:minmax\(0\,1fr\)_auto\] {
|
|
738
738
|
grid-template-columns: minmax(0,1fr) auto;
|
|
739
739
|
}
|
|
740
|
+
.\[grid-template-columns\:minmax\(90px\,1fr\)_70px_52px_64px_minmax\(70px\,\.9fr\)_minmax\(70px\,\.9fr\)_minmax\(90px\,1\.1fr\)\] {
|
|
741
|
+
grid-template-columns: minmax(90px,1fr) 70px 52px 64px minmax(70px,.9fr) minmax(70px,.9fr) minmax(90px,1.1fr);
|
|
742
|
+
}
|
|
740
743
|
.\[grid-template-columns\:minmax\(90px\,34\%\)_minmax\(0\,1fr\)\] {
|
|
741
744
|
grid-template-columns: minmax(90px,34%) minmax(0,1fr);
|
|
742
745
|
}
|
|
746
|
+
.\[grid-template-columns\:minmax\(110px\,150px\)_minmax\(0\,1fr\)\] {
|
|
747
|
+
grid-template-columns: minmax(110px,150px) minmax(0,1fr);
|
|
748
|
+
}
|
|
743
749
|
.\[grid-template-columns\:repeat\(96\,minmax\(0\,1fr\)\)\] {
|
|
744
750
|
grid-template-columns: repeat(96,minmax(0,1fr));
|
|
745
751
|
}
|
|
@@ -755,6 +761,9 @@
|
|
|
755
761
|
.\[grid-template-columns\:repeat\(auto-fill\,minmax\(170px\,1fr\)\)\] {
|
|
756
762
|
grid-template-columns: repeat(auto-fill,minmax(170px,1fr));
|
|
757
763
|
}
|
|
764
|
+
.\[grid-template-columns\:repeat\(auto-fill\,minmax\(220px\,1fr\)\)\] {
|
|
765
|
+
grid-template-columns: repeat(auto-fill,minmax(220px,1fr));
|
|
766
|
+
}
|
|
758
767
|
.\[grid-template-columns\:repeat\(auto-fit\,minmax\(96px\,1fr\)\)\] {
|
|
759
768
|
grid-template-columns: repeat(auto-fit,minmax(96px,1fr));
|
|
760
769
|
}
|
|
@@ -767,6 +776,9 @@
|
|
|
767
776
|
.grid-cols-2 {
|
|
768
777
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
769
778
|
}
|
|
779
|
+
.grid-cols-3 {
|
|
780
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
781
|
+
}
|
|
770
782
|
.grid-cols-4 {
|
|
771
783
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
772
784
|
}
|
|
@@ -875,6 +887,9 @@
|
|
|
875
887
|
.gap-y-3 {
|
|
876
888
|
row-gap: calc(var(--spacing) * 3);
|
|
877
889
|
}
|
|
890
|
+
.self-start {
|
|
891
|
+
align-self: flex-start;
|
|
892
|
+
}
|
|
878
893
|
.truncate {
|
|
879
894
|
overflow: hidden;
|
|
880
895
|
text-overflow: ellipsis;
|
|
@@ -1173,6 +1188,9 @@
|
|
|
1173
1188
|
.py-1 {
|
|
1174
1189
|
padding-block: var(--spacing);
|
|
1175
1190
|
}
|
|
1191
|
+
.py-1\.5 {
|
|
1192
|
+
padding-block: calc(var(--spacing) * 1.5);
|
|
1193
|
+
}
|
|
1176
1194
|
.py-2 {
|
|
1177
1195
|
padding-block: calc(var(--spacing) * 2);
|
|
1178
1196
|
}
|
|
@@ -1318,10 +1336,6 @@
|
|
|
1318
1336
|
font-size: var(--text-base);
|
|
1319
1337
|
line-height: var(--tw-leading, var(--text-base--line-height));
|
|
1320
1338
|
}
|
|
1321
|
-
.text-display {
|
|
1322
|
-
font-size: var(--text-display);
|
|
1323
|
-
line-height: var(--tw-leading, var(--text-display--line-height));
|
|
1324
|
-
}
|
|
1325
1339
|
.text-hero {
|
|
1326
1340
|
font-size: var(--text-hero);
|
|
1327
1341
|
line-height: var(--tw-leading, var(--text-hero--line-height));
|
|
@@ -1880,9 +1894,6 @@
|
|
|
1880
1894
|
.data-\[state\=open\]\:opacity-100[data-state="open"] {
|
|
1881
1895
|
opacity: 100%;
|
|
1882
1896
|
}
|
|
1883
|
-
.data-\[toss\]\:animate-toss[data-toss] {
|
|
1884
|
-
animation: var(--animate-toss);
|
|
1885
|
-
}
|
|
1886
1897
|
@media (width < 60rem) {
|
|
1887
1898
|
.max-\[60rem\]\:hidden {
|
|
1888
1899
|
display: none;
|
|
@@ -1918,6 +1929,9 @@
|
|
|
1918
1929
|
}
|
|
1919
1930
|
}
|
|
1920
1931
|
@media (width >= 40rem) {
|
|
1932
|
+
.sm\:col-span-2 {
|
|
1933
|
+
grid-column: span 2 / span 2;
|
|
1934
|
+
}
|
|
1921
1935
|
.sm\:grid-cols-2 {
|
|
1922
1936
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1923
1937
|
}
|
package/mevebu/libs/js/core.js
CHANGED
|
@@ -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 = "1.
|
|
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,87 @@
|
|
|
1
|
+
// CSV reading and writing shared by the data tools. Quoted fields, doubled
|
|
2
|
+
// quotes, CRLF, and a delimiter guess, so five tools do not each carry a copy.
|
|
3
|
+
|
|
4
|
+
export function parseDelimited(text, delimiter = ",") {
|
|
5
|
+
const rows = [];
|
|
6
|
+
let row = [];
|
|
7
|
+
let field = "";
|
|
8
|
+
let quoted = false;
|
|
9
|
+
for (let i = 0; i < text.length; i++) {
|
|
10
|
+
const char = text[i];
|
|
11
|
+
if (quoted) {
|
|
12
|
+
if (char === '"') {
|
|
13
|
+
if (text[i + 1] === '"') {
|
|
14
|
+
field += '"';
|
|
15
|
+
i++;
|
|
16
|
+
} else quoted = false;
|
|
17
|
+
} else field += char;
|
|
18
|
+
} else if (char === '"') {
|
|
19
|
+
quoted = true;
|
|
20
|
+
} else if (char === delimiter) {
|
|
21
|
+
row.push(field);
|
|
22
|
+
field = "";
|
|
23
|
+
} else if (char === "\n") {
|
|
24
|
+
row.push(field);
|
|
25
|
+
rows.push(row);
|
|
26
|
+
row = [];
|
|
27
|
+
field = "";
|
|
28
|
+
} else if (char !== "\r") {
|
|
29
|
+
field += char;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (field !== "" || row.length) {
|
|
33
|
+
row.push(field);
|
|
34
|
+
rows.push(row);
|
|
35
|
+
}
|
|
36
|
+
// A trailing blank line is an artefact of the file, not a row.
|
|
37
|
+
if (rows.length && rows[rows.length - 1].every((cell) => cell === "")) rows.pop();
|
|
38
|
+
return rows;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function detectDelimiter(text) {
|
|
42
|
+
const candidates = [",", ";", "\t", "|"];
|
|
43
|
+
const first = text.split(/\r?\n/)[0] ?? "";
|
|
44
|
+
const counts = { ",": 0, ";": 0, "\t": 0, "|": 0 };
|
|
45
|
+
let quoted = false;
|
|
46
|
+
for (let i = 0; i < first.length; i++) {
|
|
47
|
+
const char = first[i];
|
|
48
|
+
if (char === '"') quoted = !quoted;
|
|
49
|
+
else if (!quoted && char in counts) counts[char]++;
|
|
50
|
+
}
|
|
51
|
+
let best = ",";
|
|
52
|
+
let bestCount = 0;
|
|
53
|
+
for (const candidate of candidates) {
|
|
54
|
+
if (counts[candidate] > bestCount) {
|
|
55
|
+
bestCount = counts[candidate];
|
|
56
|
+
best = candidate;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return best;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const needsQuote = (value) => /[",\n\r]/.test(value) || /^\s|\s$/.test(value);
|
|
63
|
+
|
|
64
|
+
/** Rows of strings to CSV text. Every field that needs quoting gets it. */
|
|
65
|
+
export function toCsv(rows) {
|
|
66
|
+
return rows
|
|
67
|
+
.map((row) =>
|
|
68
|
+
row
|
|
69
|
+
.map((cell) => {
|
|
70
|
+
const text = cell === null || cell === undefined ? "" : String(cell);
|
|
71
|
+
return needsQuote(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
72
|
+
})
|
|
73
|
+
.join(",")
|
|
74
|
+
)
|
|
75
|
+
.join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The header row, with duplicates and blanks made usable as keys. */
|
|
79
|
+
export function headerKeys(header) {
|
|
80
|
+
const seen = new Map();
|
|
81
|
+
return header.map((cell, index) => {
|
|
82
|
+
const base = (cell || `column_${index + 1}`).trim() || `column_${index + 1}`;
|
|
83
|
+
const count = seen.get(base) ?? 0;
|
|
84
|
+
seen.set(base, count + 1);
|
|
85
|
+
return count ? `${base}_${count + 1}` : base;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// one object here plus one file in ./tools/.
|
|
5
5
|
|
|
6
6
|
export const CATEGORIES = [
|
|
7
|
-
{ id: "
|
|
7
|
+
{ id: "data-gen", name: "Data Generator", icon: "database" },
|
|
8
8
|
{ id: "security", name: "Passwords and keys", icon: "shield-check" },
|
|
9
9
|
{ id: "developer", name: "Developer", icon: "terminal" },
|
|
10
10
|
{ id: "convert", name: "Convert", icon: "repeat" },
|
|
@@ -31,58 +31,184 @@ export const CATEGORIES = [
|
|
|
31
31
|
// one-line intro plus a short list of steps. Keep steps concrete.
|
|
32
32
|
export const TOOLS = [
|
|
33
33
|
{
|
|
34
|
-
id: "
|
|
35
|
-
category: "
|
|
36
|
-
name: "
|
|
37
|
-
icon: "
|
|
38
|
-
note: "
|
|
39
|
-
about: "
|
|
34
|
+
id: "team-generator",
|
|
35
|
+
category: "data-gen",
|
|
36
|
+
name: "Team Generator",
|
|
37
|
+
icon: "users-round",
|
|
38
|
+
note: "Split a list into balanced groups.",
|
|
39
|
+
about: "Two modes, exclusion pairs, and one lead per group.",
|
|
40
40
|
tutorial: {
|
|
41
|
-
intro: "
|
|
41
|
+
intro: "Turn a list of names into balanced groups.",
|
|
42
42
|
steps: [
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
43
|
+
"Paste the names, one per line.",
|
|
44
|
+
"Pick the number of groups or the size per group.",
|
|
45
|
+
"Add exclusion pairs and leads if you need them, then press Generate.",
|
|
46
46
|
],
|
|
47
47
|
},
|
|
48
|
-
keywords: "random
|
|
49
|
-
load: () => import("./tools/
|
|
48
|
+
keywords: "team generator group splitter random groups classroom workshop",
|
|
49
|
+
load: () => import("./tools/team-generator.js"),
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
|
-
id: "
|
|
53
|
-
category: "
|
|
54
|
-
name: "
|
|
55
|
-
icon: "
|
|
56
|
-
note: "
|
|
57
|
-
about: "
|
|
52
|
+
id: "roster-generator",
|
|
53
|
+
category: "data-gen",
|
|
54
|
+
name: "Roster Rotation",
|
|
55
|
+
icon: "calendar-check",
|
|
56
|
+
note: "Build a rotating duty roster.",
|
|
57
|
+
about: "Names take turns across dates, with pins and weekend skips.",
|
|
58
58
|
tutorial: {
|
|
59
|
-
intro: "
|
|
59
|
+
intro: "Build a duty roster that rotates fairly.",
|
|
60
60
|
steps: [
|
|
61
|
-
"Paste
|
|
62
|
-
"Set how many
|
|
63
|
-
"
|
|
61
|
+
"Paste the names in the order they should take turns.",
|
|
62
|
+
"Set the date range, how many people per day, and whether to skip weekends.",
|
|
63
|
+
"Pin a date if someone must take it, then copy or download the roster.",
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
keywords: "roster rotation duty schedule shift piket jadwal",
|
|
67
|
+
load: () => import("./tools/roster-generator.js"),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "sample-csv",
|
|
71
|
+
category: "data-gen",
|
|
72
|
+
name: "Sample CSV",
|
|
73
|
+
icon: "table",
|
|
74
|
+
note: "Generate a CSV with the columns you define.",
|
|
75
|
+
about: "Pick a type per column, a row count and a seed that reproduces the file.",
|
|
76
|
+
tutorial: {
|
|
77
|
+
intro: "Make a CSV that fits a shape you describe.",
|
|
78
|
+
steps: [
|
|
79
|
+
"Add a column and pick its type.",
|
|
80
|
+
"Set the row count and a seed. The same seed always makes the same file.",
|
|
81
|
+
"Press Download CSV, or copy from the preview.",
|
|
64
82
|
],
|
|
65
83
|
},
|
|
66
|
-
keywords: "
|
|
67
|
-
load: () => import("./tools/
|
|
84
|
+
keywords: "sample csv fake table fixture test data generator seed",
|
|
85
|
+
load: () => import("./tools/sample-csv.js"),
|
|
68
86
|
},
|
|
69
87
|
{
|
|
70
|
-
id: "
|
|
71
|
-
category: "
|
|
72
|
-
name: "
|
|
73
|
-
icon: "
|
|
74
|
-
note: "
|
|
75
|
-
about: "
|
|
88
|
+
id: "sql-insert",
|
|
89
|
+
category: "data-gen",
|
|
90
|
+
name: "SQL Insert",
|
|
91
|
+
icon: "database",
|
|
92
|
+
note: "Turn a CSV into INSERT statements.",
|
|
93
|
+
about: "Postgres, MySQL and SQLite, with correct quoting and NULL handling.",
|
|
94
|
+
tutorial: {
|
|
95
|
+
intro: "Seed a database from a spreadsheet.",
|
|
96
|
+
steps: [
|
|
97
|
+
"Drop a CSV with a header row.",
|
|
98
|
+
"Set the table, the schema and the dialect.",
|
|
99
|
+
"Copy the SQL or download a .sql file.",
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
keywords: "sql insert generator csv to sql seed database postgres mysql sqlite",
|
|
103
|
+
load: () => import("./tools/sql-insert.js"),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "data-dictionary",
|
|
107
|
+
category: "data-gen",
|
|
108
|
+
name: "Data Dictionary",
|
|
109
|
+
icon: "table-2",
|
|
110
|
+
note: "Describe a CSV column by column.",
|
|
111
|
+
about: "Type, nulls, distinct values, range and a sample, as a table or Markdown.",
|
|
112
|
+
tutorial: {
|
|
113
|
+
intro: "Write the document a dataset needs.",
|
|
114
|
+
steps: [
|
|
115
|
+
"Drop a CSV.",
|
|
116
|
+
"Read the table: type, empty cells, distinct count, min, max, sample.",
|
|
117
|
+
"Copy the Markdown or download the CSV version.",
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
keywords: "data dictionary csv schema profile columns nulls distinct metadata",
|
|
121
|
+
load: () => import("./tools/data-dictionary.js"),
|
|
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.",
|
|
76
202
|
tutorial: {
|
|
77
|
-
intro: "
|
|
203
|
+
intro: "Test a results dashboard with plausible responses.",
|
|
78
204
|
steps: [
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
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.",
|
|
82
208
|
],
|
|
83
209
|
},
|
|
84
|
-
keywords: "
|
|
85
|
-
load: () => import("./tools/
|
|
210
|
+
keywords: "survey responses fake data dashboard test weights csv",
|
|
211
|
+
load: () => import("./tools/survey-responses.js"),
|
|
86
212
|
},
|
|
87
213
|
|
|
88
214
|
{
|