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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # mevebu
4
4
 
5
- **198 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,10 +10,10 @@ 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-198-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
- [Quick start](#quick-start) · [What's inside](#whats-inside) · [Privacy](#privacy-by-design) · [Host it yourself](#host-it-yourself) · [Development](#development) · [FAQ](#faq)
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 **198 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
 
30
- You can run it on your own computer with one command, or copy the folder to any static host and use it from anywhere.
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 **198 tools in 20 categories**. Here is the full breakdown:
67
+ mevebu has **205 tools in 20 categories**. Here is the full breakdown:
69
68
 
70
69
  | Category | Tools |
71
70
  | ------------------ | ----: |
72
- | Random | 3 |
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** | **198** |
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
- ## Host it yourself
129
+ ## Credit
132
130
 
133
- Prefer not to use the CLI? mevebu is a plain static site, so you can host it anywhere files can be served: a static site host, a CDN, an object store, or even a USB drive.
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
- 1. Copy the `mevebu/` folder from this repo (or from the npm package).
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
- Routes are hash routes, like `#/security/password-generator`, so you do not need any server rewrites. The CSS is one committed stylesheet built by Tailwind, and there is no extra build step. Your host never has to run code.
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
- Want to hack on mevebu or add your own tool? Here is how to get started:
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 198 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.
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 host mevebu on my own site?**
195
- Yes. Copy the `mevebu/` folder to any static host. No server side setup is needed.
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.
@@ -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", {}, "Serve the ", el("code", { class: CODE }, "mevebu/"), " folder from any static host. Routes are hash routes, so no rewrite rules are needed.")),
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.")),
@@ -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
  }
@@ -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.11.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,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: "random", name: "Random", icon: "shuffle" },
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: "spin-wheel",
35
- category: "random",
36
- name: "Spin the Wheel",
37
- icon: "circle-dashed",
38
- note: "Let the wheel pick from your options.",
39
- about: "Type your options, press Spin, and the wheel chooses one.",
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: "Add one option per line. The wheel picks one at random when you spin.",
41
+ intro: "Turn a list of names into balanced groups.",
42
42
  steps: [
43
- "Type your options, one per line.",
44
- "Press Spin. The wheel eases to a stop on one entry.",
45
- "Each pick is added to the history strip below. Press Clear past picks to reset.",
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 picker chooser decide raffle",
49
- load: () => import("./tools/spin-wheel.js"),
48
+ keywords: "team generator group splitter random groups classroom workshop",
49
+ load: () => import("./tools/team-generator.js"),
50
50
  },
51
51
  {
52
- id: "name-picker",
53
- category: "random",
54
- name: "Name Picker",
55
- icon: "users",
56
- note: "Draw one name or several from a list.",
57
- about: "Paste a list, set how many you need, and draw.",
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: "Paste a list of names and draw one or many at a time.",
59
+ intro: "Build a duty roster that rotates fairly.",
60
60
  steps: [
61
- "Paste your list, one entry per line.",
62
- "Set how many to draw. Toggle No repeats if you want unique picks.",
63
- "The draw updates as you edit. Press Draw for a fresh result, then Copy.",
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: "random draw winner lottery team",
67
- load: () => import("./tools/name-picker.js"),
84
+ keywords: "sample csv fake table fixture test data generator seed",
85
+ load: () => import("./tools/sample-csv.js"),
68
86
  },
69
87
  {
70
- id: "coin-flip",
71
- category: "random",
72
- name: "Coin Flip",
73
- icon: "coins",
74
- note: "Heads or tails, with a running count.",
75
- about: "Flip as many times as you like. The tally keeps the score.",
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: "Flip a fair coin. The tally remembers your count across visits.",
203
+ intro: "Test a results dashboard with plausible responses.",
78
204
  steps: [
79
- "Press Flip. The coin spins and lands on heads or tails.",
80
- "The result lands in the field below the coin.",
81
- "The tally updates and persists. Press Reset the count to clear it.",
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: "random toss decide heads tails",
85
- load: () => import("./tools/coin-flip.js"),
210
+ keywords: "survey responses fake data dashboard test weights csv",
211
+ load: () => import("./tools/survey-responses.js"),
86
212
  },
87
213
 
88
214
  {