mevebu 1.10.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 +17 -21
- package/mevebu/app/mevebu.js +1 -1
- package/mevebu/app/tailwind.css +110 -17
- package/mevebu/libs/js/core.js +1 -1
- package/mevebu/libs/js/csv.js +87 -0
- package/mevebu/libs/js/files.js +125 -0
- package/mevebu/libs/js/icons.js +2 -1
- package/mevebu/libs/js/pdf-engine.js +67 -0
- package/mevebu/libs/js/registry.js +253 -37
- package/mevebu/libs/js/tools/audio-converter.js +228 -0
- 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/image-to-pdf.js +251 -0
- package/mevebu/libs/js/tools/pdf-merge.js +158 -0
- package/mevebu/libs/js/tools/pdf-organizer.js +215 -0
- package/mevebu/libs/js/tools/pdf-split.js +198 -0
- package/mevebu/libs/js/tools/pdf-to-image.js +190 -0
- package/mevebu/libs/js/tools/pdf-to-text.js +172 -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/svg-to-png.js +207 -0
- package/mevebu/libs/js/tools/team-generator.js +203 -0
- package/mevebu/libs/js/tools/text-to-pdf.js +182 -0
- package/mevebu/libs/js/tools/video-converter.js +316 -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,14 +64,14 @@ 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
|
-
| Convert |
|
|
74
|
+
| Convert | 15 |
|
|
76
75
|
| Text and data | 14 |
|
|
77
76
|
| Daily use | 8 |
|
|
78
77
|
| Math | 17 |
|
|
@@ -89,7 +88,7 @@ mevebu has **188 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
|
@@ -218,6 +218,9 @@
|
|
|
218
218
|
.inset-0 {
|
|
219
219
|
inset: 0px;
|
|
220
220
|
}
|
|
221
|
+
.start-1 {
|
|
222
|
+
inset-inline-start: calc(var(--spacing) * 1);
|
|
223
|
+
}
|
|
221
224
|
.end-1 {
|
|
222
225
|
inset-inline-end: calc(var(--spacing) * 1);
|
|
223
226
|
}
|
|
@@ -290,6 +293,9 @@
|
|
|
290
293
|
.ms-1\.5 {
|
|
291
294
|
margin-inline-start: calc(var(--spacing) * 1.5);
|
|
292
295
|
}
|
|
296
|
+
.ms-2 {
|
|
297
|
+
margin-inline-start: calc(var(--spacing) * 2);
|
|
298
|
+
}
|
|
293
299
|
.ms-auto {
|
|
294
300
|
margin-inline-start: auto;
|
|
295
301
|
}
|
|
@@ -401,6 +407,10 @@
|
|
|
401
407
|
.aspect-\[4\/3\] {
|
|
402
408
|
aspect-ratio: 4/3;
|
|
403
409
|
}
|
|
410
|
+
.size-1\.5 {
|
|
411
|
+
width: calc(var(--spacing) * 1.5);
|
|
412
|
+
height: calc(var(--spacing) * 1.5);
|
|
413
|
+
}
|
|
404
414
|
.size-3 {
|
|
405
415
|
width: calc(var(--spacing) * 3);
|
|
406
416
|
height: calc(var(--spacing) * 3);
|
|
@@ -421,6 +431,10 @@
|
|
|
421
431
|
width: calc(var(--spacing) * 6);
|
|
422
432
|
height: calc(var(--spacing) * 6);
|
|
423
433
|
}
|
|
434
|
+
.size-7 {
|
|
435
|
+
width: calc(var(--spacing) * 7);
|
|
436
|
+
height: calc(var(--spacing) * 7);
|
|
437
|
+
}
|
|
424
438
|
.size-8 {
|
|
425
439
|
width: calc(var(--spacing) * 8);
|
|
426
440
|
height: calc(var(--spacing) * 8);
|
|
@@ -433,6 +447,10 @@
|
|
|
433
447
|
width: calc(var(--spacing) * 11);
|
|
434
448
|
height: calc(var(--spacing) * 11);
|
|
435
449
|
}
|
|
450
|
+
.size-12 {
|
|
451
|
+
width: calc(var(--spacing) * 12);
|
|
452
|
+
height: calc(var(--spacing) * 12);
|
|
453
|
+
}
|
|
436
454
|
.size-14 {
|
|
437
455
|
width: calc(var(--spacing) * 14);
|
|
438
456
|
height: calc(var(--spacing) * 14);
|
|
@@ -441,10 +459,6 @@
|
|
|
441
459
|
width: calc(var(--spacing) * 16);
|
|
442
460
|
height: calc(var(--spacing) * 16);
|
|
443
461
|
}
|
|
444
|
-
.size-32 {
|
|
445
|
-
width: calc(var(--spacing) * 32);
|
|
446
|
-
height: calc(var(--spacing) * 32);
|
|
447
|
-
}
|
|
448
462
|
.size-\[15px\] {
|
|
449
463
|
width: 15px;
|
|
450
464
|
height: 15px;
|
|
@@ -495,6 +509,9 @@
|
|
|
495
509
|
.h-14 {
|
|
496
510
|
height: calc(var(--spacing) * 14);
|
|
497
511
|
}
|
|
512
|
+
.h-24 {
|
|
513
|
+
height: calc(var(--spacing) * 24);
|
|
514
|
+
}
|
|
498
515
|
.h-\[22px\] {
|
|
499
516
|
height: 22px;
|
|
500
517
|
}
|
|
@@ -525,6 +542,9 @@
|
|
|
525
542
|
.max-h-80 {
|
|
526
543
|
max-height: calc(var(--spacing) * 80);
|
|
527
544
|
}
|
|
545
|
+
.max-h-\[320px\] {
|
|
546
|
+
max-height: 320px;
|
|
547
|
+
}
|
|
528
548
|
.max-h-\[340px\] {
|
|
529
549
|
max-height: 340px;
|
|
530
550
|
}
|
|
@@ -588,9 +608,6 @@
|
|
|
588
608
|
.w-\[min\(300px\,100\%\)\] {
|
|
589
609
|
width: min(300px, 100%);
|
|
590
610
|
}
|
|
591
|
-
.w-\[min\(320px\,100\%\)\] {
|
|
592
|
-
width: min(320px, 100%);
|
|
593
|
-
}
|
|
594
611
|
.w-\[min\(420px\,100\%\)\] {
|
|
595
612
|
width: min(420px, 100%);
|
|
596
613
|
}
|
|
@@ -603,6 +620,9 @@
|
|
|
603
620
|
.w-\[min\(640px\,100\%\)\] {
|
|
604
621
|
width: min(640px, 100%);
|
|
605
622
|
}
|
|
623
|
+
.w-auto {
|
|
624
|
+
width: auto;
|
|
625
|
+
}
|
|
606
626
|
.w-full {
|
|
607
627
|
width: 100%;
|
|
608
628
|
}
|
|
@@ -633,15 +653,15 @@
|
|
|
633
653
|
.max-w-\[64ch\] {
|
|
634
654
|
max-width: 64ch;
|
|
635
655
|
}
|
|
636
|
-
.max-w-\[340px\] {
|
|
637
|
-
max-width: 340px;
|
|
638
|
-
}
|
|
639
656
|
.max-w-\[560px\] {
|
|
640
657
|
max-width: 560px;
|
|
641
658
|
}
|
|
642
659
|
.max-w-\[var\(--bench\)\] {
|
|
643
660
|
max-width: var(--bench);
|
|
644
661
|
}
|
|
662
|
+
.max-w-full {
|
|
663
|
+
max-width: 100%;
|
|
664
|
+
}
|
|
645
665
|
.min-w-0 {
|
|
646
666
|
min-width: 0px;
|
|
647
667
|
}
|
|
@@ -657,6 +677,9 @@
|
|
|
657
677
|
.shrink {
|
|
658
678
|
flex-shrink: 1;
|
|
659
679
|
}
|
|
680
|
+
.grow {
|
|
681
|
+
flex-grow: 1;
|
|
682
|
+
}
|
|
660
683
|
.border-collapse {
|
|
661
684
|
border-collapse: collapse;
|
|
662
685
|
}
|
|
@@ -675,6 +698,15 @@
|
|
|
675
698
|
.scale-\[0\.97\] {
|
|
676
699
|
scale: 0.97;
|
|
677
700
|
}
|
|
701
|
+
.-rotate-90 {
|
|
702
|
+
rotate: calc(90deg * -1);
|
|
703
|
+
}
|
|
704
|
+
.rotate-90 {
|
|
705
|
+
rotate: 90deg;
|
|
706
|
+
}
|
|
707
|
+
.rotate-180 {
|
|
708
|
+
rotate: 180deg;
|
|
709
|
+
}
|
|
678
710
|
.transform {
|
|
679
711
|
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
|
680
712
|
}
|
|
@@ -702,15 +734,33 @@
|
|
|
702
734
|
.\[grid-template-columns\:minmax\(0\,1fr\)_auto\] {
|
|
703
735
|
grid-template-columns: minmax(0,1fr) auto;
|
|
704
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
|
+
}
|
|
705
740
|
.\[grid-template-columns\:minmax\(90px\,34\%\)_minmax\(0\,1fr\)\] {
|
|
706
741
|
grid-template-columns: minmax(90px,34%) minmax(0,1fr);
|
|
707
742
|
}
|
|
743
|
+
.\[grid-template-columns\:minmax\(110px\,150px\)_minmax\(0\,1fr\)\] {
|
|
744
|
+
grid-template-columns: minmax(110px,150px) minmax(0,1fr);
|
|
745
|
+
}
|
|
746
|
+
.\[grid-template-columns\:repeat\(96\,minmax\(0\,1fr\)\)\] {
|
|
747
|
+
grid-template-columns: repeat(96,minmax(0,1fr));
|
|
748
|
+
}
|
|
708
749
|
.\[grid-template-columns\:repeat\(auto-fill\,minmax\(92px\,1fr\)\)\] {
|
|
709
750
|
grid-template-columns: repeat(auto-fill,minmax(92px,1fr));
|
|
710
751
|
}
|
|
752
|
+
.\[grid-template-columns\:repeat\(auto-fill\,minmax\(150px\,1fr\)\)\] {
|
|
753
|
+
grid-template-columns: repeat(auto-fill,minmax(150px,1fr));
|
|
754
|
+
}
|
|
711
755
|
.\[grid-template-columns\:repeat\(auto-fill\,minmax\(160px\,1fr\)\)\] {
|
|
712
756
|
grid-template-columns: repeat(auto-fill,minmax(160px,1fr));
|
|
713
757
|
}
|
|
758
|
+
.\[grid-template-columns\:repeat\(auto-fill\,minmax\(170px\,1fr\)\)\] {
|
|
759
|
+
grid-template-columns: repeat(auto-fill,minmax(170px,1fr));
|
|
760
|
+
}
|
|
761
|
+
.\[grid-template-columns\:repeat\(auto-fill\,minmax\(220px\,1fr\)\)\] {
|
|
762
|
+
grid-template-columns: repeat(auto-fill,minmax(220px,1fr));
|
|
763
|
+
}
|
|
714
764
|
.\[grid-template-columns\:repeat\(auto-fit\,minmax\(96px\,1fr\)\)\] {
|
|
715
765
|
grid-template-columns: repeat(auto-fit,minmax(96px,1fr));
|
|
716
766
|
}
|
|
@@ -750,6 +800,9 @@
|
|
|
750
800
|
.items-center {
|
|
751
801
|
align-items: center;
|
|
752
802
|
}
|
|
803
|
+
.items-end {
|
|
804
|
+
align-items: flex-end;
|
|
805
|
+
}
|
|
753
806
|
.items-start {
|
|
754
807
|
align-items: flex-start;
|
|
755
808
|
}
|
|
@@ -828,6 +881,9 @@
|
|
|
828
881
|
.gap-y-3 {
|
|
829
882
|
row-gap: calc(var(--spacing) * 3);
|
|
830
883
|
}
|
|
884
|
+
.self-start {
|
|
885
|
+
align-self: flex-start;
|
|
886
|
+
}
|
|
831
887
|
.truncate {
|
|
832
888
|
overflow: hidden;
|
|
833
889
|
text-overflow: ellipsis;
|
|
@@ -851,6 +907,9 @@
|
|
|
851
907
|
.rounded {
|
|
852
908
|
border-radius: 0.25rem;
|
|
853
909
|
}
|
|
910
|
+
.rounded-\[1px\] {
|
|
911
|
+
border-radius: 1px;
|
|
912
|
+
}
|
|
854
913
|
.rounded-\[3px\] {
|
|
855
914
|
border-radius: 3px;
|
|
856
915
|
}
|
|
@@ -973,6 +1032,12 @@
|
|
|
973
1032
|
.bg-jade-soft {
|
|
974
1033
|
background-color: var(--jade-soft);
|
|
975
1034
|
}
|
|
1035
|
+
.bg-jade\/70 {
|
|
1036
|
+
background-color: var(--jade);
|
|
1037
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1038
|
+
background-color: color-mix(in oklab, var(--jade) 70%, transparent);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
976
1041
|
.bg-orbit {
|
|
977
1042
|
background-color: var(--orbit);
|
|
978
1043
|
}
|
|
@@ -997,9 +1062,15 @@
|
|
|
997
1062
|
.bg-\[image\:var\(--caret\)\] {
|
|
998
1063
|
background-image: var(--caret);
|
|
999
1064
|
}
|
|
1065
|
+
.bg-\[repeating-conic-gradient\(\#0000_0\%_25\%\,var\(--deck-2\)_0\%_50\%\)\] {
|
|
1066
|
+
background-image: repeating-conic-gradient(#0000 0% 25%,var(--deck-2) 0% 50%);
|
|
1067
|
+
}
|
|
1000
1068
|
.bg-\[length\:14px\] {
|
|
1001
1069
|
background-size: 14px;
|
|
1002
1070
|
}
|
|
1071
|
+
.bg-\[length\:16px_16px\] {
|
|
1072
|
+
background-size: 16px 16px;
|
|
1073
|
+
}
|
|
1003
1074
|
.bg-\[position\:right_12px_center\] {
|
|
1004
1075
|
background-position: right 12px center;
|
|
1005
1076
|
}
|
|
@@ -1009,6 +1080,9 @@
|
|
|
1009
1080
|
.object-contain {
|
|
1010
1081
|
object-fit: contain;
|
|
1011
1082
|
}
|
|
1083
|
+
.object-cover {
|
|
1084
|
+
object-fit: cover;
|
|
1085
|
+
}
|
|
1012
1086
|
.p-0 {
|
|
1013
1087
|
padding: 0px;
|
|
1014
1088
|
}
|
|
@@ -1027,6 +1101,9 @@
|
|
|
1027
1101
|
.p-2\.5 {
|
|
1028
1102
|
padding: calc(var(--spacing) * 2.5);
|
|
1029
1103
|
}
|
|
1104
|
+
.p-3 {
|
|
1105
|
+
padding: calc(var(--spacing) * 3);
|
|
1106
|
+
}
|
|
1030
1107
|
.p-3\.5 {
|
|
1031
1108
|
padding: calc(var(--spacing) * 3.5);
|
|
1032
1109
|
}
|
|
@@ -1060,6 +1137,9 @@
|
|
|
1060
1137
|
.px-1 {
|
|
1061
1138
|
padding-inline: var(--spacing);
|
|
1062
1139
|
}
|
|
1140
|
+
.px-1\.5 {
|
|
1141
|
+
padding-inline: calc(var(--spacing) * 1.5);
|
|
1142
|
+
}
|
|
1063
1143
|
.px-2 {
|
|
1064
1144
|
padding-inline: calc(var(--spacing) * 2);
|
|
1065
1145
|
}
|
|
@@ -1096,9 +1176,15 @@
|
|
|
1096
1176
|
.px-\[var\(--margin\)\] {
|
|
1097
1177
|
padding-inline: var(--margin);
|
|
1098
1178
|
}
|
|
1179
|
+
.py-0\.5 {
|
|
1180
|
+
padding-block: calc(var(--spacing) * 0.5);
|
|
1181
|
+
}
|
|
1099
1182
|
.py-1 {
|
|
1100
1183
|
padding-block: var(--spacing);
|
|
1101
1184
|
}
|
|
1185
|
+
.py-1\.5 {
|
|
1186
|
+
padding-block: calc(var(--spacing) * 1.5);
|
|
1187
|
+
}
|
|
1102
1188
|
.py-2 {
|
|
1103
1189
|
padding-block: calc(var(--spacing) * 2);
|
|
1104
1190
|
}
|
|
@@ -1244,10 +1330,6 @@
|
|
|
1244
1330
|
font-size: var(--text-base);
|
|
1245
1331
|
line-height: var(--tw-leading, var(--text-base--line-height));
|
|
1246
1332
|
}
|
|
1247
|
-
.text-display {
|
|
1248
|
-
font-size: var(--text-display);
|
|
1249
|
-
line-height: var(--tw-leading, var(--text-display--line-height));
|
|
1250
|
-
}
|
|
1251
1333
|
.text-hero {
|
|
1252
1334
|
font-size: var(--text-hero);
|
|
1253
1335
|
line-height: var(--tw-leading, var(--text-hero--line-height));
|
|
@@ -1516,6 +1598,11 @@
|
|
|
1516
1598
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
|
1517
1599
|
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
|
1518
1600
|
}
|
|
1601
|
+
.transition-\[width\] {
|
|
1602
|
+
transition-property: width;
|
|
1603
|
+
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
|
1604
|
+
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
|
1605
|
+
}
|
|
1519
1606
|
.transition-colors {
|
|
1520
1607
|
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
|
|
1521
1608
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
|
@@ -1704,9 +1791,15 @@
|
|
|
1704
1791
|
.disabled\:cursor-not-allowed:disabled {
|
|
1705
1792
|
cursor: not-allowed;
|
|
1706
1793
|
}
|
|
1794
|
+
.disabled\:opacity-40:disabled {
|
|
1795
|
+
opacity: 40%;
|
|
1796
|
+
}
|
|
1707
1797
|
.disabled\:opacity-45:disabled {
|
|
1708
1798
|
opacity: 45%;
|
|
1709
1799
|
}
|
|
1800
|
+
.disabled\:opacity-50:disabled {
|
|
1801
|
+
opacity: 50%;
|
|
1802
|
+
}
|
|
1710
1803
|
.has-checked\:border-jade:has(:checked) {
|
|
1711
1804
|
border-color: var(--jade);
|
|
1712
1805
|
}
|
|
@@ -1795,9 +1888,6 @@
|
|
|
1795
1888
|
.data-\[state\=open\]\:opacity-100[data-state="open"] {
|
|
1796
1889
|
opacity: 100%;
|
|
1797
1890
|
}
|
|
1798
|
-
.data-\[toss\]\:animate-toss[data-toss] {
|
|
1799
|
-
animation: var(--animate-toss);
|
|
1800
|
-
}
|
|
1801
1891
|
@media (width < 60rem) {
|
|
1802
1892
|
.max-\[60rem\]\:hidden {
|
|
1803
1893
|
display: none;
|
|
@@ -1833,6 +1923,9 @@
|
|
|
1833
1923
|
}
|
|
1834
1924
|
}
|
|
1835
1925
|
@media (width >= 40rem) {
|
|
1926
|
+
.sm\:col-span-2 {
|
|
1927
|
+
grid-column: span 2 / span 2;
|
|
1928
|
+
}
|
|
1836
1929
|
.sm\:grid-cols-2 {
|
|
1837
1930
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1838
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
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// File plumbing shared by the converter tools: a drop target, a size label, a
|
|
2
|
+
// name without its extension, and an image decoder. Kept here so ten tools do
|
|
3
|
+
// not each grow their own copy.
|
|
4
|
+
import { el, icon } from "./core.js";
|
|
5
|
+
|
|
6
|
+
export function formatBytes(bytes) {
|
|
7
|
+
if (!bytes) return "0 B";
|
|
8
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
9
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(bytes < 10240 ? 1 : 0)} KB`;
|
|
10
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function baseName(name) {
|
|
14
|
+
return (name || "file").replace(/\.[^.]+$/, "") || "file";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function extension(name) {
|
|
18
|
+
const match = /\.([^.]+)$/.exec(name || "");
|
|
19
|
+
return match ? match[1].toLowerCase() : "";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A dashed drop zone. Calls `onFiles` with a FileList or an array of files. */
|
|
23
|
+
export function dropzone({ label, hint = "or choose a file", accept = "", multiple = false, iconName = "image-up" }) {
|
|
24
|
+
const picker = el("input", { type: "file", accept, multiple, class: "hidden" });
|
|
25
|
+
const node = el(
|
|
26
|
+
"div",
|
|
27
|
+
{
|
|
28
|
+
class:
|
|
29
|
+
"grid cursor-pointer justify-items-center gap-1.5 rounded-r2 border border-dashed border-orbit-hard bg-deck-2 px-4 py-7 text-center transition-[border-color,background-color] duration-150 hover:border-jade focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-jade data-[drag]:border-jade data-[drag]:bg-jade-soft",
|
|
30
|
+
role: "button",
|
|
31
|
+
tabindex: "0",
|
|
32
|
+
"aria-label": label,
|
|
33
|
+
},
|
|
34
|
+
icon(iconName, "size-6 text-ink-3"),
|
|
35
|
+
el("p", { class: "text-sm font-semibold" }, label),
|
|
36
|
+
el("p", { class: "text-2xs text-ink-3" }, hint),
|
|
37
|
+
picker
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
let handler = () => {};
|
|
41
|
+
let depth = 0;
|
|
42
|
+
|
|
43
|
+
node.addEventListener("click", () => picker.click());
|
|
44
|
+
node.addEventListener("keydown", (event) => {
|
|
45
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
picker.click();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
picker.addEventListener("change", () => {
|
|
51
|
+
if (picker.files?.length) handler(picker.files);
|
|
52
|
+
picker.value = "";
|
|
53
|
+
});
|
|
54
|
+
node.addEventListener("dragenter", (event) => {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
depth++;
|
|
57
|
+
node.dataset.drag = "";
|
|
58
|
+
});
|
|
59
|
+
node.addEventListener("dragover", (event) => event.preventDefault());
|
|
60
|
+
node.addEventListener("dragleave", () => {
|
|
61
|
+
depth = Math.max(0, depth - 1);
|
|
62
|
+
if (!depth) delete node.dataset.drag;
|
|
63
|
+
});
|
|
64
|
+
node.addEventListener("drop", (event) => {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
depth = 0;
|
|
67
|
+
delete node.dataset.drag;
|
|
68
|
+
const files = Array.from(event.dataTransfer?.files ?? []);
|
|
69
|
+
if (files.length) handler(files);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
node.onFiles = (fn) => {
|
|
73
|
+
handler = fn;
|
|
74
|
+
};
|
|
75
|
+
node.picker = picker;
|
|
76
|
+
return node;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Decodes an image file, preferring createImageBitmap and falling back to <img>. */
|
|
80
|
+
export async function decodeImage(file) {
|
|
81
|
+
if (typeof createImageBitmap === "function") {
|
|
82
|
+
try {
|
|
83
|
+
return await createImageBitmap(file, { imageOrientation: "from-image" });
|
|
84
|
+
} catch {
|
|
85
|
+
// SVG and a few odd files only decode through an <img>.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const url = URL.createObjectURL(file);
|
|
89
|
+
try {
|
|
90
|
+
const image = new Image();
|
|
91
|
+
image.decoding = "sync";
|
|
92
|
+
await new Promise((resolve, reject) => {
|
|
93
|
+
image.onload = resolve;
|
|
94
|
+
image.onerror = () => reject(new Error("This file is not an image the browser can read."));
|
|
95
|
+
image.src = url;
|
|
96
|
+
});
|
|
97
|
+
return image;
|
|
98
|
+
} finally {
|
|
99
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function imageSize(source) {
|
|
104
|
+
return {
|
|
105
|
+
width: source.width || source.naturalWidth || 0,
|
|
106
|
+
height: source.height || source.naturalHeight || 0,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function canvasToBlob(canvas, type = "image/png", quality) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
canvas.toBlob(
|
|
113
|
+
(blob) => (blob ? resolve(blob) : reject(new Error("The browser could not write this file."))),
|
|
114
|
+
type,
|
|
115
|
+
quality
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Sorts files by name with numbers read as numbers, so page-2 comes before page-10. */
|
|
121
|
+
export function naturalSort(files) {
|
|
122
|
+
return [...files].sort((a, b) =>
|
|
123
|
+
a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" })
|
|
124
|
+
);
|
|
125
|
+
}
|