mevebu 1.7.0 → 1.7.2
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 +122 -20
- package/mevebu/app/mevebu.js +41 -1
- package/mevebu/app/tailwind.css +23 -12
- package/mevebu/libs/js/core.js +3 -3
- package/mevebu/libs/js/icons.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
# mevebu
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**186 small tools, each on its own page, all running in your browser.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
A box of browser tools for the jobs that do not deserve an app: convert a value,
|
|
6
|
+
hash a text, sign a token, format JSON, pick a color, run a timer, count words,
|
|
7
|
+
build a cron line, check a JWT, read a QR. Every tool opens on its own page and
|
|
8
|
+
most of them update as you type, so there is no submit button and no waiting.
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
Nothing leaves your tab. There is no account to make, nothing to upload, and
|
|
11
|
+
nothing saved to a server.
|
|
9
12
|
|
|
10
|
-
```
|
|
11
|
-
|
|
13
|
+
```
|
|
14
|
+
tools 186
|
|
15
|
+
categories 20
|
|
16
|
+
followers 1 (one tab, no account)
|
|
17
|
+
uploads 0
|
|
12
18
|
```
|
|
13
19
|
|
|
14
|
-
##
|
|
20
|
+
## Install and run
|
|
15
21
|
|
|
16
22
|
```sh
|
|
23
|
+
npm install -g mevebu
|
|
17
24
|
mevebu
|
|
18
25
|
```
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
`mevebu` starts a local server (it tries port 5555, then 5678, then 5432, then
|
|
28
|
+
any free port), opens your browser to the home page, and serves the static
|
|
29
|
+
folder bundled with the package. Press `Ctrl+C` in the terminal to stop it.
|
|
21
30
|
|
|
22
|
-
###
|
|
31
|
+
### CLI
|
|
23
32
|
|
|
24
33
|
```
|
|
25
34
|
-p, --port <number> Try this port first (5555, 5678, 5432 otherwise).
|
|
@@ -29,15 +38,16 @@ The tools open in your default browser. Press `Ctrl+C` in the terminal to stop.
|
|
|
29
38
|
-h, --help Print this help and exit.
|
|
30
39
|
```
|
|
31
40
|
|
|
32
|
-
The
|
|
33
|
-
|
|
41
|
+
The first three ports are stable across restarts, so the bookmark to your tools
|
|
42
|
+
page keeps working.
|
|
34
43
|
|
|
35
|
-
##
|
|
44
|
+
## What's inside
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
A tool is a small page that does one job. The registry lists them all; here is
|
|
47
|
+
the shape of it.
|
|
38
48
|
|
|
39
|
-
| Category |
|
|
40
|
-
| :--- |
|
|
49
|
+
| Category | Count |
|
|
50
|
+
| :--- | ---: |
|
|
41
51
|
| Random | 3 |
|
|
42
52
|
| Passwords and keys | 10 |
|
|
43
53
|
| Developer | 21 |
|
|
@@ -58,16 +68,108 @@ and falls back to any free port only when those are taken.
|
|
|
58
68
|
| GitHub | 8 |
|
|
59
69
|
| Units | 8 |
|
|
60
70
|
| QR codes | 2 |
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
| **Total** | **186** |
|
|
72
|
+
|
|
73
|
+
The counts are kept honest by the registry, so the page and the README never
|
|
74
|
+
drift apart. Press `Ctrl+K` in the app to search the full list by name, by
|
|
75
|
+
keyword, or by what the tool does.
|
|
76
|
+
|
|
77
|
+
A short tour of the most reached-for tools, one from each side of the catalog:
|
|
78
|
+
|
|
79
|
+
- **Password generator**: make a password, copy it, and the page never talks
|
|
80
|
+
to anything but your clipboard.
|
|
81
|
+
- **JSON formatter**: paste, and the formatted version fills in as you type.
|
|
82
|
+
- **JWT generator and decoder**: sign a token with HS256, RS256 or ES256, or
|
|
83
|
+
paste a token and read its header and payload.
|
|
84
|
+
- **Color palette**: pick one color, get a matching set in analogous,
|
|
85
|
+
complementary or monochrome schemes.
|
|
86
|
+
- **Countdown timer**: a quiet, fullscreen timer that keeps running when the
|
|
87
|
+
tab is hidden.
|
|
88
|
+
- **QR code generator**: turn a URL, a Wi-Fi config or any text into a scannable
|
|
89
|
+
image right in the tab.
|
|
90
|
+
- **Slug converter**: keep a sentence readable while you turn it into a URL.
|
|
91
|
+
|
|
92
|
+
The full list is at the home page and under each category.
|
|
93
|
+
|
|
94
|
+
## How it stays private
|
|
95
|
+
|
|
96
|
+
Every tool is a static HTML file, a tiny JavaScript module, and a Lucide icon.
|
|
97
|
+
The HTML and the JavaScript are served from your machine when you run `mevebu`,
|
|
98
|
+
or from the file you host when you copy the `mevebu/` folder somewhere. There is
|
|
99
|
+
no server-side code, no analytics, and no API key.
|
|
100
|
+
|
|
101
|
+
When you paste a password, a token, or a CSV into a tool:
|
|
102
|
+
|
|
103
|
+
- the text is processed in the browser tab, by the file that the page loaded
|
|
104
|
+
from, using the standard Web Crypto API or the standard browser APIs the
|
|
105
|
+
tool needs
|
|
106
|
+
- nothing is sent to a remote server, because there is no remote server in
|
|
107
|
+
the loop
|
|
108
|
+
- nothing is saved to disk by the tool, because the tool does not have a way
|
|
109
|
+
to write to your file system
|
|
110
|
+
- the only network request the page makes is to load itself, and to read the
|
|
111
|
+
GitHub star count for the footer chip (and that request fails silently when
|
|
112
|
+
you are offline)
|
|
113
|
+
|
|
114
|
+
You can verify this by opening the network tab in your browser's dev tools and
|
|
115
|
+
watching a tool do its work. There is no upload. The same tool, loaded the same
|
|
116
|
+
way, behaves the same in a hotel Wi-Fi as it does on a server room.
|
|
117
|
+
|
|
118
|
+
## Hosting the static folder
|
|
119
|
+
|
|
120
|
+
If you would rather not run the CLI, the whole app is a plain static site. Copy
|
|
121
|
+
the `mevebu/` folder from this repo, or from the npm package, to any host that
|
|
122
|
+
serves files: a static site host, a CDN, an object store, a USB drive. Routes
|
|
123
|
+
are hash routes (`#/security/password-generator`), so no server rewrites are
|
|
124
|
+
needed.
|
|
125
|
+
|
|
126
|
+
The shipped folder is plain files, the CSS is one committed stylesheet that
|
|
127
|
+
Tailwind built from one source file, and there is no separate build step. The
|
|
128
|
+
hosting layer never runs code on your behalf.
|
|
63
129
|
|
|
64
130
|
## Development
|
|
65
131
|
|
|
66
132
|
```sh
|
|
67
|
-
|
|
68
|
-
|
|
133
|
+
git clone https://github.com/mevebu/mevebu
|
|
134
|
+
cd mevebu
|
|
135
|
+
npm install
|
|
136
|
+
npm run css # build the stylesheet from tailwind.src.css
|
|
137
|
+
npm run icons # regenerate the vendored Lucide icon set
|
|
138
|
+
npm start # serve the static folder on http://localhost:5555
|
|
69
139
|
```
|
|
70
140
|
|
|
141
|
+
The app is a few hundred lines of vanilla JavaScript (ESM) in
|
|
142
|
+
`mevebu/app/mevebu.js` and a small shared vocabulary in `mevebu/libs/js/core.js`.
|
|
143
|
+
Every tool is a single file in `mevebu/libs/js/tools/`, exports a `mount(slot)`
|
|
144
|
+
function and (optionally) an `unmount()` to stop timers, cameras and observers.
|
|
145
|
+
The registry in `mevebu/libs/js/registry.js` is the single source of truth: add
|
|
146
|
+
an entry to `TOOLS` and a file, and the catalog, search, breadcrumb, and
|
|
147
|
+
category page pick it up.
|
|
148
|
+
|
|
149
|
+
The CSS is Tailwind v4, built from one source file, with the stylesheet output
|
|
150
|
+
committed so the shipped folder is still plain static files. There is no
|
|
151
|
+
component layer and no `mvb-` class: every utility is written at the call
|
|
152
|
+
site, and the design tokens live in `mevebu/app/tailwind.src.css`.
|
|
153
|
+
|
|
154
|
+
Layout and color decisions, the type scale, the orbit lines, the "energy 1,
|
|
155
|
+
rhythm 2, motion 1" dials, and the rest of the visual system, are recorded in
|
|
156
|
+
`AGENTS.md` and `DESIGN.md`, the two docs an agent reads first.
|
|
157
|
+
|
|
158
|
+
## Report a problem
|
|
159
|
+
|
|
160
|
+
Every tool page has a small "Report a problem" link at the bottom, alongside
|
|
161
|
+
the tool's actions. It opens a GitHub issue with the tool name, the page URL
|
|
162
|
+
and the mevebu version already filled in, so all you have to do is describe
|
|
163
|
+
what happened and what you expected.
|
|
164
|
+
|
|
71
165
|
## License
|
|
72
166
|
|
|
73
|
-
MIT
|
|
167
|
+
MIT. The whole list, the whole source, the whole thing is in the repository.
|
|
168
|
+
If a tool here saved you time, a coffee keeps the next one coming:
|
|
169
|
+
<https://buymeacoffee.com/fastro>.
|
|
170
|
+
|
|
171
|
+
## Links
|
|
172
|
+
|
|
173
|
+
- Repository: <https://github.com/mevebu/mevebu>
|
|
174
|
+
- npm: <https://www.npmjs.com/package/mevebu>
|
|
175
|
+
- Donate: <https://buymeacoffee.com/fastro>
|
package/mevebu/app/mevebu.js
CHANGED
|
@@ -128,7 +128,7 @@ function masthead() {
|
|
|
128
128
|
|
|
129
129
|
return el(
|
|
130
130
|
"header",
|
|
131
|
-
{
|
|
131
|
+
{ id: "masthead", class: "z-30 border-b border-orbit bg-deck data-[pin]:sticky data-[pin]:top-0 data-[pin]:transition-shadow" },
|
|
132
132
|
el(
|
|
133
133
|
"div",
|
|
134
134
|
{ class: "mx-[var(--margin)] flex h-[var(--bar)] items-center gap-[clamp(14px,3vw,32px)]" },
|
|
@@ -754,6 +754,30 @@ function moreFromMev(tool) {
|
|
|
754
754
|
);
|
|
755
755
|
}
|
|
756
756
|
|
|
757
|
+
function reportLink(tool) {
|
|
758
|
+
const title = `Bug in ${tool.name}`;
|
|
759
|
+
const body = [
|
|
760
|
+
"### What I was doing",
|
|
761
|
+
"<!-- one line is enough -->",
|
|
762
|
+
"",
|
|
763
|
+
"### What happened",
|
|
764
|
+
"",
|
|
765
|
+
"### What I expected",
|
|
766
|
+
"",
|
|
767
|
+
`### Tool`,
|
|
768
|
+
`mevebu ${VERSION}`,
|
|
769
|
+
`Page: #/${tool.category}/${tool.id}`,
|
|
770
|
+
"",
|
|
771
|
+
"### Browser",
|
|
772
|
+
].join("\n");
|
|
773
|
+
return el("a", {
|
|
774
|
+
class: "mt-3 flex items-center gap-1.5 self-start text-2xs font-semibold text-ink-3 no-underline hover:text-ink-2",
|
|
775
|
+
href: `https://github.com/mevebu/mevebu/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`,
|
|
776
|
+
target: "_blank",
|
|
777
|
+
rel: "noopener",
|
|
778
|
+
}, icon("bug"), el("span", {}, "Report a problem"));
|
|
779
|
+
}
|
|
780
|
+
|
|
757
781
|
async function toolPage(tool, category) {
|
|
758
782
|
const token = ++navToken;
|
|
759
783
|
const slot = el("div", {});
|
|
@@ -764,6 +788,7 @@ async function toolPage(tool, category) {
|
|
|
764
788
|
{ class: PAGE_NARROW },
|
|
765
789
|
benchHead(tool, category),
|
|
766
790
|
slot,
|
|
791
|
+
el("div", { class: "flex flex-col items-stretch" }, reportLink(tool)),
|
|
767
792
|
more
|
|
768
793
|
);
|
|
769
794
|
|
|
@@ -900,6 +925,20 @@ const scrollMemory = new Map();
|
|
|
900
925
|
let activePath = null;
|
|
901
926
|
let historyMove = false;
|
|
902
927
|
|
|
928
|
+
|
|
929
|
+
// Sticky everywhere except the tools index and a single tool page, where
|
|
930
|
+
// the page owns the whole frame and the navbar would steal vertical space.
|
|
931
|
+
function pinMasthead(path) {
|
|
932
|
+
const head = document.getElementById("masthead");
|
|
933
|
+
if (!head) return;
|
|
934
|
+
const pinned = !(
|
|
935
|
+
path === "/tools" ||
|
|
936
|
+
/^\/[^/]+\/[^/]+$/.test(path)
|
|
937
|
+
);
|
|
938
|
+
if (pinned) head.dataset.pin = "";
|
|
939
|
+
else delete head.dataset.pin;
|
|
940
|
+
}
|
|
941
|
+
|
|
903
942
|
async function render(path) {
|
|
904
943
|
const restoring = historyMove;
|
|
905
944
|
historyMove = false;
|
|
@@ -909,6 +948,7 @@ async function render(path) {
|
|
|
909
948
|
if (!node) return;
|
|
910
949
|
$("#view").replaceChildren(node);
|
|
911
950
|
markActiveNav(["/", "/tools", "/categories", "/about"].includes(path) ? path : "");
|
|
951
|
+
pinMasthead(path);
|
|
912
952
|
activePath = path;
|
|
913
953
|
window.scrollTo({ top: restoring ? scrollMemory.get(path) ?? 0 : 0 });
|
|
914
954
|
if (!history.state?.mevebu) history.replaceState({ mevebu: true }, "", location.href);
|
package/mevebu/app/tailwind.css
CHANGED
|
@@ -208,18 +208,12 @@
|
|
|
208
208
|
.static {
|
|
209
209
|
position: static;
|
|
210
210
|
}
|
|
211
|
-
.sticky {
|
|
212
|
-
position: sticky;
|
|
213
|
-
}
|
|
214
211
|
.inset-0 {
|
|
215
212
|
inset: 0px;
|
|
216
213
|
}
|
|
217
214
|
.end-3 {
|
|
218
215
|
inset-inline-end: calc(var(--spacing) * 3);
|
|
219
216
|
}
|
|
220
|
-
.top-0 {
|
|
221
|
-
top: 0px;
|
|
222
|
-
}
|
|
223
217
|
.top-0\.5 {
|
|
224
218
|
top: calc(var(--spacing) * 0.5);
|
|
225
219
|
}
|
|
@@ -265,6 +259,9 @@
|
|
|
265
259
|
.m-0 {
|
|
266
260
|
margin: 0px;
|
|
267
261
|
}
|
|
262
|
+
.m-auto {
|
|
263
|
+
margin: auto;
|
|
264
|
+
}
|
|
268
265
|
.mx-\[var\(--margin\)\] {
|
|
269
266
|
margin-inline: var(--margin);
|
|
270
267
|
}
|
|
@@ -661,6 +658,9 @@
|
|
|
661
658
|
.items-start {
|
|
662
659
|
align-items: flex-start;
|
|
663
660
|
}
|
|
661
|
+
.items-stretch {
|
|
662
|
+
align-items: stretch;
|
|
663
|
+
}
|
|
664
664
|
.justify-between {
|
|
665
665
|
justify-content: space-between;
|
|
666
666
|
}
|
|
@@ -736,6 +736,9 @@
|
|
|
736
736
|
.gap-y-3 {
|
|
737
737
|
row-gap: calc(var(--spacing) * 3);
|
|
738
738
|
}
|
|
739
|
+
.self-start {
|
|
740
|
+
align-self: flex-start;
|
|
741
|
+
}
|
|
739
742
|
.truncate {
|
|
740
743
|
overflow: hidden;
|
|
741
744
|
text-overflow: ellipsis;
|
|
@@ -1016,6 +1019,9 @@
|
|
|
1016
1019
|
.py-\[13px\] {
|
|
1017
1020
|
padding-block: 13px;
|
|
1018
1021
|
}
|
|
1022
|
+
.py-\[clamp\(20px\,5vh\,40px\)\] {
|
|
1023
|
+
padding-block: clamp(20px, 5vh, 40px);
|
|
1024
|
+
}
|
|
1019
1025
|
.py-\[clamp\(28px\,6vw\,56px\)\] {
|
|
1020
1026
|
padding-block: clamp(28px, 6vw, 56px);
|
|
1021
1027
|
}
|
|
@@ -1082,9 +1088,6 @@
|
|
|
1082
1088
|
.pt-\[clamp\(32px\,5vw\,60px\)\] {
|
|
1083
1089
|
padding-top: clamp(32px, 5vw, 60px);
|
|
1084
1090
|
}
|
|
1085
|
-
.pt-\[clamp\(60px\,9vh\,110px\)\] {
|
|
1086
|
-
padding-top: clamp(60px, 9vh, 110px);
|
|
1087
|
-
}
|
|
1088
1091
|
.pb-2 {
|
|
1089
1092
|
padding-bottom: calc(var(--spacing) * 2);
|
|
1090
1093
|
}
|
|
@@ -1094,9 +1097,6 @@
|
|
|
1094
1097
|
.pb-7 {
|
|
1095
1098
|
padding-bottom: calc(var(--spacing) * 7);
|
|
1096
1099
|
}
|
|
1097
|
-
.pb-10 {
|
|
1098
|
-
padding-bottom: calc(var(--spacing) * 10);
|
|
1099
|
-
}
|
|
1100
1100
|
.pb-\[var\(--margin\)\] {
|
|
1101
1101
|
padding-bottom: var(--margin);
|
|
1102
1102
|
}
|
|
@@ -1582,6 +1582,17 @@
|
|
|
1582
1582
|
--tw-shadow: 0 0 0 1px var(--tw-shadow-color, var(--orbit));
|
|
1583
1583
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
1584
1584
|
}
|
|
1585
|
+
.data-\[pin\]\:sticky[data-pin] {
|
|
1586
|
+
position: sticky;
|
|
1587
|
+
}
|
|
1588
|
+
.data-\[pin\]\:top-0[data-pin] {
|
|
1589
|
+
top: 0px;
|
|
1590
|
+
}
|
|
1591
|
+
.data-\[pin\]\:transition-shadow[data-pin] {
|
|
1592
|
+
transition-property: box-shadow;
|
|
1593
|
+
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
|
1594
|
+
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
|
1595
|
+
}
|
|
1585
1596
|
.data-\[state\=open\]\:opacity-100[data-state="open"] {
|
|
1586
1597
|
opacity: 100%;
|
|
1587
1598
|
}
|
package/mevebu/libs/js/core.js
CHANGED
|
@@ -197,14 +197,14 @@ export function modalHead(iconName, title) {
|
|
|
197
197
|
export function modal({ label, body, actions = [], onOpen, onClose, panelClass } = {}) {
|
|
198
198
|
lastFocus = document.activeElement;
|
|
199
199
|
const veil = el("div", {
|
|
200
|
-
class: "group fixed inset-0 z-80 grid
|
|
200
|
+
class: "group fixed inset-0 z-80 grid place-items-center overflow-y-auto bg-[var(--scrim)] px-5 py-[clamp(20px,5vh,40px)] opacity-0 transition-opacity duration-200 data-[state=open]:opacity-100",
|
|
201
201
|
});
|
|
202
202
|
const panel = el(
|
|
203
203
|
"div",
|
|
204
204
|
{
|
|
205
205
|
class:
|
|
206
206
|
"relative grid gap-4 rounded-r3 border border-orbit bg-deck shadow-[var(--shadow)] transition-transform duration-200 ease-[var(--ease)] translate-y-3.5 scale-[0.97] group-data-[state=open]:translate-y-0 group-data-[state=open]:scale-100 " +
|
|
207
|
-
(panelClass ?? "w-[min(480px,100%)] p-[clamp(22px,3vw,28px)]"),
|
|
207
|
+
(panelClass ?? "m-auto w-[min(480px,100%)] p-[clamp(22px,3vw,28px)]"),
|
|
208
208
|
role: "dialog",
|
|
209
209
|
"aria-modal": "true",
|
|
210
210
|
"aria-label": label,
|
|
@@ -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.7.
|
|
753
|
+
export const VERSION = "1.7.2";
|
|
754
754
|
export const PACKAGE_NAME = "mevebu";
|
|
755
755
|
export const REPO_SLUG = "mevebu/mevebu";
|
|
756
756
|
|
package/mevebu/libs/js/icons.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// GENERATED FILE, DO NOT EDIT.
|
|
2
|
-
// Source: lucide v1.
|
|
2
|
+
// Source: lucide v1.59.0 (ISC), icons/169 names from scripts/vendor-icons.mjs
|
|
3
3
|
// Regenerate with: npm run icons
|
|
4
4
|
|
|
5
5
|
const D = {
|
|
@@ -16,6 +16,7 @@ const D = {
|
|
|
16
16
|
"info": [ ["circle", { cx: "12", cy: "12", r: "10" }], ["path", { d: "M12 16v-4" }], ["path", { d: "M12 8h.01" }] ],
|
|
17
17
|
"circle-question-mark": [ ["circle", { cx: "12", cy: "12", r: "10" }], ["path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }], ["path", { d: "M12 17h.01" }] ],
|
|
18
18
|
"circle-arrow-up": [ ["circle", { cx: "12", cy: "12", r: "10" }], ["path", { d: "m16 12-4-4-4 4" }], ["path", { d: "M12 16V8" }] ],
|
|
19
|
+
"bug": [ ["path", { d: "M12 20v-9" }], ["path", { d: "M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z" }], ["path", { d: "M14.12 3.88 16 2" }], ["path", { d: "M21 21a4 4 0 0 0-3.81-4" }], ["path", { d: "M21 5a4 4 0 0 1-3.55 3.97" }], ["path", { d: "M22 13h-4" }], ["path", { d: "M3 21a4 4 0 0 1 3.81-4" }], ["path", { d: "M3 5a4 4 0 0 0 3.55 3.97" }], ["path", { d: "M6 13H2" }], ["path", { d: "m8 2 1.88 1.88" }], ["path", { d: "M9 7.13V6a3 3 0 1 1 6 0v1.13" }] ],
|
|
19
20
|
"shuffle": [ ["path", { d: "m18 14 4 4-4 4" }], ["path", { d: "m18 2 4 4-4 4" }], ["path", { d: "M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22" }], ["path", { d: "M2 6h1.972a4 4 0 0 1 3.6 2.2" }], ["path", { d: "M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45" }] ],
|
|
20
21
|
"shield-check": [ [ "path", { d: "M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" } ], ["path", { d: "m9 12 2 2 4-4" }] ],
|
|
21
22
|
"terminal": [ ["path", { d: "M12 19h8" }], ["path", { d: "m4 17 6-6-6-6" }] ],
|