mevebu 1.11.0 → 2.0.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 +25 -17
- package/mevebu/libs/js/core.js +1 -1
- package/mevebu/libs/js/csv.js +87 -0
- package/mevebu/libs/js/registry.js +73 -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/roster-generator.js +195 -0
- package/mevebu/libs/js/tools/sample-csv.js +252 -0
- package/mevebu/libs/js/tools/sql-insert.js +184 -0
- package/mevebu/libs/js/tools/team-generator.js +203 -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
|
+
**200 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 **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.
|
|
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 **200 tools in 20 categories**. Here is the full breakdown:
|
|
69
68
|
|
|
70
69
|
| Category | Tools |
|
|
71
70
|
| ------------------ | ----: |
|
|
72
|
-
|
|
|
71
|
+
| Data Generator | 5 |
|
|
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** | **200** |
|
|
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 200 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
|
@@ -293,6 +293,9 @@
|
|
|
293
293
|
.ms-1\.5 {
|
|
294
294
|
margin-inline-start: calc(var(--spacing) * 1.5);
|
|
295
295
|
}
|
|
296
|
+
.ms-2 {
|
|
297
|
+
margin-inline-start: calc(var(--spacing) * 2);
|
|
298
|
+
}
|
|
296
299
|
.ms-auto {
|
|
297
300
|
margin-inline-start: auto;
|
|
298
301
|
}
|
|
@@ -404,6 +407,10 @@
|
|
|
404
407
|
.aspect-\[4\/3\] {
|
|
405
408
|
aspect-ratio: 4/3;
|
|
406
409
|
}
|
|
410
|
+
.size-1\.5 {
|
|
411
|
+
width: calc(var(--spacing) * 1.5);
|
|
412
|
+
height: calc(var(--spacing) * 1.5);
|
|
413
|
+
}
|
|
407
414
|
.size-3 {
|
|
408
415
|
width: calc(var(--spacing) * 3);
|
|
409
416
|
height: calc(var(--spacing) * 3);
|
|
@@ -452,10 +459,6 @@
|
|
|
452
459
|
width: calc(var(--spacing) * 16);
|
|
453
460
|
height: calc(var(--spacing) * 16);
|
|
454
461
|
}
|
|
455
|
-
.size-32 {
|
|
456
|
-
width: calc(var(--spacing) * 32);
|
|
457
|
-
height: calc(var(--spacing) * 32);
|
|
458
|
-
}
|
|
459
462
|
.size-\[15px\] {
|
|
460
463
|
width: 15px;
|
|
461
464
|
height: 15px;
|
|
@@ -605,9 +608,6 @@
|
|
|
605
608
|
.w-\[min\(300px\,100\%\)\] {
|
|
606
609
|
width: min(300px, 100%);
|
|
607
610
|
}
|
|
608
|
-
.w-\[min\(320px\,100\%\)\] {
|
|
609
|
-
width: min(320px, 100%);
|
|
610
|
-
}
|
|
611
611
|
.w-\[min\(420px\,100\%\)\] {
|
|
612
612
|
width: min(420px, 100%);
|
|
613
613
|
}
|
|
@@ -653,9 +653,6 @@
|
|
|
653
653
|
.max-w-\[64ch\] {
|
|
654
654
|
max-width: 64ch;
|
|
655
655
|
}
|
|
656
|
-
.max-w-\[340px\] {
|
|
657
|
-
max-width: 340px;
|
|
658
|
-
}
|
|
659
656
|
.max-w-\[560px\] {
|
|
660
657
|
max-width: 560px;
|
|
661
658
|
}
|
|
@@ -737,9 +734,15 @@
|
|
|
737
734
|
.\[grid-template-columns\:minmax\(0\,1fr\)_auto\] {
|
|
738
735
|
grid-template-columns: minmax(0,1fr) auto;
|
|
739
736
|
}
|
|
737
|
+
.\[grid-template-columns\:minmax\(90px\,1fr\)_70px_52px_64px_minmax\(70px\,\.9fr\)_minmax\(70px\,\.9fr\)_minmax\(90px\,1\.1fr\)\] {
|
|
738
|
+
grid-template-columns: minmax(90px,1fr) 70px 52px 64px minmax(70px,.9fr) minmax(70px,.9fr) minmax(90px,1.1fr);
|
|
739
|
+
}
|
|
740
740
|
.\[grid-template-columns\:minmax\(90px\,34\%\)_minmax\(0\,1fr\)\] {
|
|
741
741
|
grid-template-columns: minmax(90px,34%) minmax(0,1fr);
|
|
742
742
|
}
|
|
743
|
+
.\[grid-template-columns\:minmax\(110px\,150px\)_minmax\(0\,1fr\)\] {
|
|
744
|
+
grid-template-columns: minmax(110px,150px) minmax(0,1fr);
|
|
745
|
+
}
|
|
743
746
|
.\[grid-template-columns\:repeat\(96\,minmax\(0\,1fr\)\)\] {
|
|
744
747
|
grid-template-columns: repeat(96,minmax(0,1fr));
|
|
745
748
|
}
|
|
@@ -755,6 +758,9 @@
|
|
|
755
758
|
.\[grid-template-columns\:repeat\(auto-fill\,minmax\(170px\,1fr\)\)\] {
|
|
756
759
|
grid-template-columns: repeat(auto-fill,minmax(170px,1fr));
|
|
757
760
|
}
|
|
761
|
+
.\[grid-template-columns\:repeat\(auto-fill\,minmax\(220px\,1fr\)\)\] {
|
|
762
|
+
grid-template-columns: repeat(auto-fill,minmax(220px,1fr));
|
|
763
|
+
}
|
|
758
764
|
.\[grid-template-columns\:repeat\(auto-fit\,minmax\(96px\,1fr\)\)\] {
|
|
759
765
|
grid-template-columns: repeat(auto-fit,minmax(96px,1fr));
|
|
760
766
|
}
|
|
@@ -875,6 +881,9 @@
|
|
|
875
881
|
.gap-y-3 {
|
|
876
882
|
row-gap: calc(var(--spacing) * 3);
|
|
877
883
|
}
|
|
884
|
+
.self-start {
|
|
885
|
+
align-self: flex-start;
|
|
886
|
+
}
|
|
878
887
|
.truncate {
|
|
879
888
|
overflow: hidden;
|
|
880
889
|
text-overflow: ellipsis;
|
|
@@ -1173,6 +1182,9 @@
|
|
|
1173
1182
|
.py-1 {
|
|
1174
1183
|
padding-block: var(--spacing);
|
|
1175
1184
|
}
|
|
1185
|
+
.py-1\.5 {
|
|
1186
|
+
padding-block: calc(var(--spacing) * 1.5);
|
|
1187
|
+
}
|
|
1176
1188
|
.py-2 {
|
|
1177
1189
|
padding-block: calc(var(--spacing) * 2);
|
|
1178
1190
|
}
|
|
@@ -1318,10 +1330,6 @@
|
|
|
1318
1330
|
font-size: var(--text-base);
|
|
1319
1331
|
line-height: var(--tw-leading, var(--text-base--line-height));
|
|
1320
1332
|
}
|
|
1321
|
-
.text-display {
|
|
1322
|
-
font-size: var(--text-display);
|
|
1323
|
-
line-height: var(--tw-leading, var(--text-display--line-height));
|
|
1324
|
-
}
|
|
1325
1333
|
.text-hero {
|
|
1326
1334
|
font-size: var(--text-hero);
|
|
1327
1335
|
line-height: var(--tw-leading, var(--text-hero--line-height));
|
|
@@ -1880,9 +1888,6 @@
|
|
|
1880
1888
|
.data-\[state\=open\]\:opacity-100[data-state="open"] {
|
|
1881
1889
|
opacity: 100%;
|
|
1882
1890
|
}
|
|
1883
|
-
.data-\[toss\]\:animate-toss[data-toss] {
|
|
1884
|
-
animation: var(--animate-toss);
|
|
1885
|
-
}
|
|
1886
1891
|
@media (width < 60rem) {
|
|
1887
1892
|
.max-\[60rem\]\:hidden {
|
|
1888
1893
|
display: none;
|
|
@@ -1918,6 +1923,9 @@
|
|
|
1918
1923
|
}
|
|
1919
1924
|
}
|
|
1920
1925
|
@media (width >= 40rem) {
|
|
1926
|
+
.sm\:col-span-2 {
|
|
1927
|
+
grid-column: span 2 / span 2;
|
|
1928
|
+
}
|
|
1921
1929
|
.sm\:grid-cols-2 {
|
|
1922
1930
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1923
1931
|
}
|
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 = "
|
|
753
|
+
export const VERSION = "2.0.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
|
+
}
|
|
@@ -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,94 @@ 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.",
|
|
76
112
|
tutorial: {
|
|
77
|
-
intro: "
|
|
113
|
+
intro: "Write the document a dataset needs.",
|
|
78
114
|
steps: [
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
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.",
|
|
82
118
|
],
|
|
83
119
|
},
|
|
84
|
-
keywords: "
|
|
85
|
-
load: () => import("./tools/
|
|
120
|
+
keywords: "data dictionary csv schema profile columns nulls distinct metadata",
|
|
121
|
+
load: () => import("./tools/data-dictionary.js"),
|
|
86
122
|
},
|
|
87
123
|
|
|
88
124
|
{
|
|
@@ -1,56 +1,6 @@
|
|
|
1
1
|
// Reads CSV or delimited text as a table, with row and column counts.
|
|
2
2
|
import { el, bench, stack, panel, textArea, selectInput, fileInput, notice, copyButton, button, actions, toolcard } from "../core.js";
|
|
3
|
-
|
|
4
|
-
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] === '"') { field += '"'; i++; }
|
|
14
|
-
else quoted = false;
|
|
15
|
-
} else field += char;
|
|
16
|
-
} else if (char === '"') {
|
|
17
|
-
quoted = true;
|
|
18
|
-
} else if (char === delimiter) {
|
|
19
|
-
row.push(field);
|
|
20
|
-
field = "";
|
|
21
|
-
} else if (char === "\n") {
|
|
22
|
-
row.push(field);
|
|
23
|
-
rows.push(row);
|
|
24
|
-
row = [];
|
|
25
|
-
field = "";
|
|
26
|
-
} else if (char !== "\r") {
|
|
27
|
-
field += char;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
if (field !== "" || row.length) {
|
|
31
|
-
row.push(field);
|
|
32
|
-
rows.push(row);
|
|
33
|
-
}
|
|
34
|
-
return rows;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function detectDelimiter(text) {
|
|
38
|
-
const candidates = [",", ";", "\t", "|"];
|
|
39
|
-
const first = text.split(/\r?\n/)[0] ?? "";
|
|
40
|
-
const counts = { ",": 0, ";": 0, "\t": 0, "|": 0 };
|
|
41
|
-
let quoted = false;
|
|
42
|
-
for (let i = 0; i < first.length; i++) {
|
|
43
|
-
const char = first[i];
|
|
44
|
-
if (char === '"') quoted = !quoted;
|
|
45
|
-
else if (!quoted && char in counts) counts[char]++;
|
|
46
|
-
}
|
|
47
|
-
let best = ",";
|
|
48
|
-
let bestCount = 0;
|
|
49
|
-
for (const candidate of candidates) {
|
|
50
|
-
if (counts[candidate] > bestCount) { bestCount = counts[candidate]; best = candidate; }
|
|
51
|
-
}
|
|
52
|
-
return best;
|
|
53
|
-
}
|
|
3
|
+
import { parseDelimited, detectDelimiter } from "../csv.js";
|
|
54
4
|
|
|
55
5
|
function tile(label) {
|
|
56
6
|
const value = el("span", { class: "wrap-anywhere text-xl font-bold tracking-[-0.02em] tabular-nums" }, "0");
|