mevebu 1.2.0 → 1.4.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/mevebu/app/mevebu.js +260 -208
- package/mevebu/app/tailwind.css +1639 -2077
- package/mevebu/app/tailwind.src.css +155 -1120
- package/mevebu/index.html +2 -2
- package/mevebu/libs/js/core.js +236 -96
- package/mevebu/libs/js/registry.js +11 -11
- package/mevebu/libs/js/tools/abstract-checklist.js +5 -7
- package/mevebu/libs/js/tools/add-subtract-days.js +9 -11
- package/mevebu/libs/js/tools/age-calculator.js +7 -8
- package/mevebu/libs/js/tools/api-request-builder.js +3 -4
- package/mevebu/libs/js/tools/area-converter.js +7 -9
- package/mevebu/libs/js/tools/aspect-ratio.js +8 -9
- package/mevebu/libs/js/tools/average-calculator.js +4 -5
- package/mevebu/libs/js/tools/base64.js +31 -11
- package/mevebu/libs/js/tools/binary-calculator.js +6 -7
- package/mevebu/libs/js/tools/border-radius.js +7 -9
- package/mevebu/libs/js/tools/box-shadow.js +6 -7
- package/mevebu/libs/js/tools/break-even.js +4 -5
- package/mevebu/libs/js/tools/business-days.js +7 -8
- package/mevebu/libs/js/tools/canonical-tag.js +5 -6
- package/mevebu/libs/js/tools/caption-counter.js +5 -7
- package/mevebu/libs/js/tools/case-converter.js +11 -12
- package/mevebu/libs/js/tools/case-inspector.js +14 -16
- package/mevebu/libs/js/tools/cash-denomination.js +7 -8
- package/mevebu/libs/js/tools/cash-flow.js +7 -8
- package/mevebu/libs/js/tools/cert-expiry.js +6 -10
- package/mevebu/libs/js/tools/changelog-generator.js +4 -6
- package/mevebu/libs/js/tools/character-counter.js +10 -13
- package/mevebu/libs/js/tools/checklist.js +6 -7
- package/mevebu/libs/js/tools/citation-number-check.js +10 -13
- package/mevebu/libs/js/tools/coin-flip.js +9 -11
- package/mevebu/libs/js/tools/color-converter.js +23 -17
- package/mevebu/libs/js/tools/color-palette.js +6 -8
- package/mevebu/libs/js/tools/color-picker.js +4 -5
- package/mevebu/libs/js/tools/content-calendar.js +3 -4
- package/mevebu/libs/js/tools/contrast-checker.js +5 -6
- package/mevebu/libs/js/tools/cron-builder.js +4 -6
- package/mevebu/libs/js/tools/css-clamp.js +5 -6
- package/mevebu/libs/js/tools/css-formatter.js +4 -7
- package/mevebu/libs/js/tools/css-grid.js +6 -7
- package/mevebu/libs/js/tools/css-minifier.js +5 -8
- package/mevebu/libs/js/tools/csv-to-json.js +2 -3
- package/mevebu/libs/js/tools/csv-viewer.js +9 -11
- package/mevebu/libs/js/tools/currency-format.js +5 -7
- package/mevebu/libs/js/tools/daily-planner.js +9 -10
- package/mevebu/libs/js/tools/data-masker.js +7 -9
- package/mevebu/libs/js/tools/data-storage.js +7 -9
- package/mevebu/libs/js/tools/data-validator.js +9 -11
- package/mevebu/libs/js/tools/date-difference.js +6 -7
- package/mevebu/libs/js/tools/date-format.js +11 -13
- package/mevebu/libs/js/tools/delimiter-converter.js +3 -5
- package/mevebu/libs/js/tools/discount-calculator.js +4 -5
- package/mevebu/libs/js/tools/doi-resolver.js +4 -5
- package/mevebu/libs/js/tools/env-formatter.js +3 -6
- package/mevebu/libs/js/tools/equation-formatter.js +5 -9
- package/mevebu/libs/js/tools/expense-split.js +6 -7
- package/mevebu/libs/js/tools/fake-data.js +20 -13
- package/mevebu/libs/js/tools/find-replace.js +3 -5
- package/mevebu/libs/js/tools/flashcard-creator.js +9 -10
- package/mevebu/libs/js/tools/font-pairing.js +6 -8
- package/mevebu/libs/js/tools/fraction-calculator.js +10 -13
- package/mevebu/libs/js/tools/git-command-builder.js +11 -22
- package/mevebu/libs/js/tools/gpa-calculator.js +8 -10
- package/mevebu/libs/js/tools/grade-calculator.js +7 -8
- package/mevebu/libs/js/tools/gradient-generator.js +10 -12
- package/mevebu/libs/js/tools/habit-tracker.js +8 -9
- package/mevebu/libs/js/tools/hash-generator.js +3 -5
- package/mevebu/libs/js/tools/hashtag-formatter.js +6 -8
- package/mevebu/libs/js/tools/hmac-generator.js +3 -5
- package/mevebu/libs/js/tools/hpp-calculator.js +4 -5
- package/mevebu/libs/js/tools/html-entity.js +2 -3
- package/mevebu/libs/js/tools/html-formatter.js +3 -6
- package/mevebu/libs/js/tools/http-status.js +5 -7
- package/mevebu/libs/js/tools/inventory-quantity.js +7 -8
- package/mevebu/libs/js/tools/json-formatter.js +3 -6
- package/mevebu/libs/js/tools/json-minifier.js +5 -8
- package/mevebu/libs/js/tools/json-to-csv.js +3 -5
- package/mevebu/libs/js/tools/json-to-typescript.js +2 -5
- package/mevebu/libs/js/tools/json-validator.js +2 -3
- package/mevebu/libs/js/tools/jwt-decoder.js +6 -7
- package/mevebu/libs/js/tools/jwt-expiry.js +5 -6
- package/mevebu/libs/js/tools/jwt-generator.js +4 -6
- package/mevebu/libs/js/tools/length-converter.js +7 -9
- package/mevebu/libs/js/tools/license-reference.js +10 -13
- package/mevebu/libs/js/tools/line-counter.js +5 -7
- package/mevebu/libs/js/tools/list-difference.js +5 -7
- package/mevebu/libs/js/tools/list-merger.js +4 -5
- package/mevebu/libs/js/tools/locale-number.js +5 -8
- package/mevebu/libs/js/tools/lorem-ipsum.js +4 -2
- package/mevebu/libs/js/tools/markdown-preview.js +5 -8
- package/mevebu/libs/js/tools/markdown-table.js +5 -7
- package/mevebu/libs/js/tools/markup-calculator.js +4 -5
- package/mevebu/libs/js/tools/meeting-agenda.js +8 -9
- package/mevebu/libs/js/tools/meta-preview.js +12 -13
- package/mevebu/libs/js/tools/meta-tags.js +6 -7
- package/mevebu/libs/js/tools/mime-types.js +5 -7
- package/mevebu/libs/js/tools/name-picker.js +2 -3
- package/mevebu/libs/js/tools/number-base-converter.js +9 -12
- package/mevebu/libs/js/tools/password-generator.js +9 -12
- package/mevebu/libs/js/tools/password-strength.js +21 -25
- package/mevebu/libs/js/tools/percent-to-fraction.js +6 -7
- package/mevebu/libs/js/tools/percentage-calculator.js +6 -9
- package/mevebu/libs/js/tools/percentage-change.js +4 -5
- package/mevebu/libs/js/tools/permutation-combination.js +9 -12
- package/mevebu/libs/js/tools/prefix-suffix.js +2 -3
- package/mevebu/libs/js/tools/priority-matrix.js +6 -10
- package/mevebu/libs/js/tools/profit-margin.js +4 -5
- package/mevebu/libs/js/tools/qr-generator.js +3 -4
- package/mevebu/libs/js/tools/qr-scanner.js +11 -12
- package/mevebu/libs/js/tools/quadratic-solver.js +7 -8
- package/mevebu/libs/js/tools/query-builder.js +2 -3
- package/mevebu/libs/js/tools/quiz-formatter.js +4 -6
- package/mevebu/libs/js/tools/ratio-calculator.js +7 -8
- package/mevebu/libs/js/tools/readability.js +12 -15
- package/mevebu/libs/js/tools/reading-time.js +5 -7
- package/mevebu/libs/js/tools/reference-completeness.js +6 -8
- package/mevebu/libs/js/tools/regex-tester.js +7 -10
- package/mevebu/libs/js/tools/remove-duplicate-lines.js +2 -3
- package/mevebu/libs/js/tools/remove-extra-spaces.js +3 -5
- package/mevebu/libs/js/tools/robots-txt.js +3 -4
- package/mevebu/libs/js/tools/roman-numeral-converter.js +6 -8
- package/mevebu/libs/js/tools/sales-commission.js +8 -9
- package/mevebu/libs/js/tools/scientific-calculator.js +10 -14
- package/mevebu/libs/js/tools/scientific-notation.js +8 -9
- package/mevebu/libs/js/tools/script-planner.js +8 -10
- package/mevebu/libs/js/tools/selling-price.js +4 -5
- package/mevebu/libs/js/tools/semver-helper.js +3 -5
- package/mevebu/libs/js/tools/sentence-length.js +9 -10
- package/mevebu/libs/js/tools/sequence-analyzer.js +7 -8
- package/mevebu/libs/js/tools/sitemap-xml.js +4 -7
- package/mevebu/libs/js/tools/slug-converter.js +3 -5
- package/mevebu/libs/js/tools/slug-preview.js +6 -7
- package/mevebu/libs/js/tools/sort-lines.js +3 -5
- package/mevebu/libs/js/tools/speed-converter.js +7 -9
- package/mevebu/libs/js/tools/spin-wheel.js +19 -17
- package/mevebu/libs/js/tools/sql-formatter.js +5 -9
- package/mevebu/libs/js/tools/sql-minifier.js +5 -8
- package/mevebu/libs/js/tools/standard-deviation.js +6 -9
- package/mevebu/libs/js/tools/stopwatch.js +4 -6
- package/mevebu/libs/js/tools/study-schedule.js +8 -9
- package/mevebu/libs/js/tools/subtitle-formatter.js +10 -11
- package/mevebu/libs/js/tools/table-to-markdown.js +8 -12
- package/mevebu/libs/js/tools/tax-calculator.js +4 -5
- package/mevebu/libs/js/tools/temperature-converter.js +8 -11
- package/mevebu/libs/js/tools/text-diff.js +11 -7
- package/mevebu/libs/js/tools/text-direction.js +3 -5
- package/mevebu/libs/js/tools/text-reverser.js +3 -5
- package/mevebu/libs/js/tools/time-duration.js +8 -9
- package/mevebu/libs/js/tools/timestamp-converter.js +4 -6
- package/mevebu/libs/js/tools/timezone-converter.js +9 -13
- package/mevebu/libs/js/tools/title-length.js +6 -7
- package/mevebu/libs/js/tools/unicode-inspector.js +4 -5
- package/mevebu/libs/js/tools/url-encoder.js +4 -2
- package/mevebu/libs/js/tools/url-inspector.js +9 -11
- package/mevebu/libs/js/tools/url-parser.js +12 -13
- package/mevebu/libs/js/tools/utf8-bytes.js +6 -9
- package/mevebu/libs/js/tools/uuid-generator.js +107 -15
- package/mevebu/libs/js/tools/version-compare.js +3 -4
- package/mevebu/libs/js/tools/video-duration.js +5 -7
- package/mevebu/libs/js/tools/volume-converter.js +7 -9
- package/mevebu/libs/js/tools/weekly-planner.js +8 -10
- package/mevebu/libs/js/tools/weight-converter.js +7 -9
- package/mevebu/libs/js/tools/weighted-average.js +4 -5
- package/mevebu/libs/js/tools/word-counter.js +10 -13
- package/mevebu/libs/js/tools/word-limit.js +4 -5
- package/mevebu/libs/js/tools/yaml-formatter.js +6 -7
- package/package.json +1 -1
package/mevebu/index.html
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
document.documentElement.dataset.theme = getTheme();
|
|
13
13
|
</script>
|
|
14
14
|
</head>
|
|
15
|
-
<body>
|
|
16
|
-
<noscript><p
|
|
15
|
+
<body class="flex min-h-dvh flex-col bg-void font-sans text-base text-ink antialiased">
|
|
16
|
+
<noscript><p class="p-6">mevebu needs JavaScript. Every tool runs in this page.</p></noscript>
|
|
17
17
|
<div id="app"></div>
|
|
18
18
|
<script type="module" src="./app/mevebu.js"></script>
|
|
19
19
|
</body>
|
package/mevebu/libs/js/core.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// Shared utilities for every mevebu tool: DOM building, storage, clipboard,
|
|
2
2
|
// crypto helpers, the bench controls, the modal, and the command palette.
|
|
3
|
-
//
|
|
3
|
+
//
|
|
4
|
+
// The interface is Tailwind, and this file is where the shared pieces of it live,
|
|
5
|
+
// so a control is written once instead of in 186 tools. Every class string below
|
|
6
|
+
// is a plain Tailwind utility list, and DESIGN.md section 3 pins the values they
|
|
7
|
+
// resolve to. A tool passes only the delta it needs: `class` is merged onto the
|
|
8
|
+
// helper's own class list, never substituted for it.
|
|
4
9
|
|
|
5
10
|
import { svg, hasIcon } from "./icons.js";
|
|
6
11
|
import { CATEGORIES, TOOLS, toolHref, getCategory } from "./registry.js";
|
|
@@ -46,7 +51,7 @@ export function clear(node) {
|
|
|
46
51
|
return node;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
export function icon(name, cls = "
|
|
54
|
+
export function icon(name, cls = "size-[18px] flex-none") {
|
|
50
55
|
return svg(name, cls);
|
|
51
56
|
}
|
|
52
57
|
|
|
@@ -152,33 +157,60 @@ export function download(filename, data, mime = "text/plain") {
|
|
|
152
157
|
let toastTimer;
|
|
153
158
|
|
|
154
159
|
export function toast(message) {
|
|
155
|
-
let node = $("#
|
|
160
|
+
let node = $("#toast");
|
|
156
161
|
if (!node) {
|
|
157
|
-
node = el("div", {
|
|
162
|
+
node = el("div", {
|
|
163
|
+
id: "toast",
|
|
164
|
+
class: "pointer-events-none fixed bottom-6 left-1/2 z-90 -translate-x-1/2 translate-y-3 rounded-r1 bg-ink px-4 py-2.5 text-sm font-semibold text-void opacity-0 transition-[opacity,translate] duration-150 data-[on]:translate-y-0 data-[on]:opacity-100",
|
|
165
|
+
role: "status",
|
|
166
|
+
"aria-live": "polite",
|
|
167
|
+
});
|
|
158
168
|
document.body.append(node);
|
|
159
169
|
}
|
|
160
170
|
node.textContent = message;
|
|
161
|
-
node.
|
|
171
|
+
node.dataset.on = "";
|
|
162
172
|
clearTimeout(toastTimer);
|
|
163
|
-
toastTimer = setTimeout(() => node.
|
|
173
|
+
toastTimer = setTimeout(() => delete node.dataset.on, 1800);
|
|
164
174
|
}
|
|
165
175
|
|
|
166
176
|
// ---------- modal ----------
|
|
167
177
|
|
|
168
178
|
let lastFocus = null;
|
|
169
179
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
180
|
+
/* One panel shape for the whole app: an optional sigil head, body copy, then an
|
|
181
|
+
action row that follows the same rule as a tool card (primary last, rightmost).
|
|
182
|
+
A close button is always in the corner, and Escape or a backdrop click does the
|
|
183
|
+
same thing. This is the only surface in the app that carries a shadow, because
|
|
184
|
+
it is the only one that genuinely floats above the page. */
|
|
185
|
+
const MODAL_BODY = "grid gap-2.5 [&_p]:text-pretty [&_p]:text-ink-2";
|
|
186
|
+
|
|
187
|
+
/** The head every modal opens with: a sigil for what the dialog is about, then its title. */
|
|
188
|
+
export function modalHead(iconName, title) {
|
|
189
|
+
return el(
|
|
190
|
+
"div",
|
|
191
|
+
{ class: "flex items-center gap-3 pe-10" },
|
|
192
|
+
el("span", { class: "grid size-11 flex-none place-items-center rounded-xl border border-orbit bg-jade-soft text-jade [&_svg]:size-[22px]" }, icon(iconName)),
|
|
193
|
+
el("h2", { class: "text-xl font-bold tracking-[-0.02em]" }, title)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function modal({ label, body, actions = [], onOpen, onClose, panelClass } = {}) {
|
|
175
198
|
lastFocus = document.activeElement;
|
|
176
|
-
const veil = el("div", {
|
|
199
|
+
const veil = el("div", {
|
|
200
|
+
class: "group fixed inset-0 z-80 grid items-start justify-items-center overflow-y-auto bg-[var(--scrim)] px-5 pt-[clamp(60px,9vh,110px)] pb-10 opacity-0 transition-opacity duration-200 data-[state=open]:opacity-100",
|
|
201
|
+
});
|
|
177
202
|
const panel = el(
|
|
178
203
|
"div",
|
|
179
|
-
{
|
|
180
|
-
|
|
181
|
-
|
|
204
|
+
{
|
|
205
|
+
class:
|
|
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)]"),
|
|
208
|
+
role: "dialog",
|
|
209
|
+
"aria-modal": "true",
|
|
210
|
+
"aria-label": label,
|
|
211
|
+
},
|
|
212
|
+
body ? el("div", { class: MODAL_BODY }, ...body) : null,
|
|
213
|
+
actions.length ? el("div", { class: "mt-1 flex flex-wrap items-center justify-end gap-2" }, ...actions) : null
|
|
182
214
|
);
|
|
183
215
|
veil.append(panel);
|
|
184
216
|
document.body.append(veil);
|
|
@@ -188,7 +220,7 @@ export function modal({ label, body, actions = [], onOpen, onClose, veilClass }
|
|
|
188
220
|
const close = () => {
|
|
189
221
|
if (closed) return;
|
|
190
222
|
closed = true;
|
|
191
|
-
veil.
|
|
223
|
+
delete veil.dataset.state;
|
|
192
224
|
onClose?.();
|
|
193
225
|
setTimeout(() => {
|
|
194
226
|
veil.remove();
|
|
@@ -198,6 +230,17 @@ export function modal({ label, body, actions = [], onOpen, onClose, veilClass }
|
|
|
198
230
|
if (lastFocus?.focus) lastFocus.focus();
|
|
199
231
|
};
|
|
200
232
|
|
|
233
|
+
const closeButton = el("button", {
|
|
234
|
+
class: "absolute top-3 end-3 inline-flex size-9 cursor-pointer items-center justify-center rounded-r1 border border-transparent bg-transparent text-ink-3 transition-colors duration-150 hover:bg-deck-2 hover:text-ink",
|
|
235
|
+
type: "button",
|
|
236
|
+
"aria-label": "Close",
|
|
237
|
+
onclick: () => close(),
|
|
238
|
+
}, icon("x"));
|
|
239
|
+
|
|
240
|
+
// The button is inside the panel, after its content, so it never lands in the
|
|
241
|
+
// tab order ahead of the dialog's own controls.
|
|
242
|
+
panel.append(closeButton);
|
|
243
|
+
|
|
201
244
|
const onKey = (event) => {
|
|
202
245
|
if (event.key === "Escape") close();
|
|
203
246
|
};
|
|
@@ -206,7 +249,9 @@ export function modal({ label, body, actions = [], onOpen, onClose, veilClass }
|
|
|
206
249
|
});
|
|
207
250
|
document.addEventListener("keydown", onKey);
|
|
208
251
|
|
|
209
|
-
requestAnimationFrame(() =>
|
|
252
|
+
requestAnimationFrame(() => {
|
|
253
|
+
veil.dataset.state = "open";
|
|
254
|
+
});
|
|
210
255
|
onOpen?.({ close, panel });
|
|
211
256
|
return close;
|
|
212
257
|
}
|
|
@@ -228,27 +273,32 @@ export function openPalette({ onPick } = {}) {
|
|
|
228
273
|
}));
|
|
229
274
|
|
|
230
275
|
const input = el("input", {
|
|
231
|
-
class: "
|
|
276
|
+
class: "h-[var(--ctl)] min-w-0 flex-1 border-0 bg-transparent text-md outline-none placeholder:text-ink-3",
|
|
232
277
|
type: "search",
|
|
233
278
|
placeholder: "Search tools",
|
|
234
279
|
"aria-label": "Search tools",
|
|
235
280
|
autocomplete: "off",
|
|
236
281
|
spellcheck: "false",
|
|
237
282
|
});
|
|
238
|
-
const list = el("ul", { class: "
|
|
239
|
-
const count = el("p", { class: "
|
|
283
|
+
const list = el("ul", { class: "m-0 mt-1.5 max-h-[min(60vh,360px)] overflow-y-auto px-2 pb-2", role: "listbox" });
|
|
284
|
+
const count = el("p", { class: "m-0 px-[18px] pt-2 text-2xs text-ink-3" });
|
|
240
285
|
const footer = el(
|
|
241
286
|
"p",
|
|
242
|
-
{ class: "
|
|
287
|
+
{ class: "m-0 flex flex-wrap items-center gap-2.5 border-t border-orbit px-[18px] py-2.5 text-2xs text-ink-3" },
|
|
243
288
|
"↑↓ navigate ",
|
|
244
|
-
el("kbd", { class: "
|
|
289
|
+
el("kbd", { class: "rounded-[5px] border border-orbit-hard px-[7px] py-px font-sans text-2xs leading-[1.45] text-ink-3" }, "↵"),
|
|
245
290
|
" open ",
|
|
246
|
-
el("kbd", { class: "
|
|
291
|
+
el("kbd", { class: "rounded-[5px] border border-orbit-hard px-[7px] py-px font-sans text-2xs leading-[1.45] text-ink-3" }, "esc"),
|
|
247
292
|
" close"
|
|
248
293
|
);
|
|
249
294
|
|
|
250
295
|
const body = [
|
|
251
|
-
el(
|
|
296
|
+
el(
|
|
297
|
+
"div",
|
|
298
|
+
{ class: "flex items-center gap-3 border-b border-orbit py-4 pe-12 ps-[18px]" },
|
|
299
|
+
icon("search", "size-[26px] flex-none text-ink-3"),
|
|
300
|
+
input
|
|
301
|
+
),
|
|
252
302
|
count,
|
|
253
303
|
list,
|
|
254
304
|
footer,
|
|
@@ -269,23 +319,24 @@ export function openPalette({ onPick } = {}) {
|
|
|
269
319
|
return el(
|
|
270
320
|
"li",
|
|
271
321
|
{
|
|
272
|
-
class:
|
|
322
|
+
class:
|
|
323
|
+
"group flex w-full cursor-pointer items-center gap-3 rounded-r1 border-0 bg-transparent px-3 py-2.5 text-start text-ink transition-colors duration-150 data-[on]:bg-jade-soft",
|
|
273
324
|
role: "option",
|
|
274
325
|
dataset: { href: toolHref(tool) },
|
|
275
326
|
"aria-selected": String(i === index),
|
|
276
327
|
onmousemove: () => setIndex(i),
|
|
277
328
|
onclick: () => pick(i),
|
|
278
329
|
},
|
|
279
|
-
el("span", { class: "
|
|
280
|
-
el("span", { class: "
|
|
281
|
-
el("span", { class: "
|
|
282
|
-
el("span", { class: "
|
|
283
|
-
el("span", { class: "
|
|
330
|
+
el("span", { class: "grid size-8 flex-none place-items-center rounded-lg bg-deck-2 text-jade [&_svg]:size-4" }, icon(tool.icon)),
|
|
331
|
+
el("span", { class: "grid min-w-0 flex-1 gap-px" },
|
|
332
|
+
el("span", { class: "text-sm font-semibold tracking-[-0.01em] group-data-[on]:text-jade" }, tool.name),
|
|
333
|
+
el("span", { class: "overflow-hidden text-2xs text-ellipsis whitespace-nowrap text-ink-2" }, tool.note)),
|
|
334
|
+
el("span", { class: "whitespace-nowrap text-2xs text-ink-3" }, category?.name ?? "")
|
|
284
335
|
);
|
|
285
336
|
})
|
|
286
337
|
);
|
|
287
338
|
if (!current.length) {
|
|
288
|
-
list.append(el("li", { class: "
|
|
339
|
+
list.append(el("li", { class: "p-[18px] text-sm text-ink-3" }, "No tool matches “", query, "”."));
|
|
289
340
|
}
|
|
290
341
|
count.textContent = q
|
|
291
342
|
? `${current.length} of ${tools.length} tools`
|
|
@@ -295,9 +346,12 @@ export function openPalette({ onPick } = {}) {
|
|
|
295
346
|
function setIndex(next) {
|
|
296
347
|
if (!current.length) return;
|
|
297
348
|
index = (next + current.length) % current.length;
|
|
298
|
-
for (const item of list.children) item.
|
|
299
|
-
list.children[index]
|
|
300
|
-
|
|
349
|
+
for (const item of list.children) delete item.dataset.on;
|
|
350
|
+
const active = list.children[index];
|
|
351
|
+
if (active) {
|
|
352
|
+
active.dataset.on = "";
|
|
353
|
+
active.scrollIntoView({ block: "nearest" });
|
|
354
|
+
}
|
|
301
355
|
}
|
|
302
356
|
|
|
303
357
|
function pick(at) {
|
|
@@ -310,6 +364,7 @@ export function openPalette({ onPick } = {}) {
|
|
|
310
364
|
const close = modal({
|
|
311
365
|
label: "Search tools",
|
|
312
366
|
body,
|
|
367
|
+
panelClass: "w-[min(640px,100%)] overflow-hidden p-0",
|
|
313
368
|
onOpen: ({ close: doClose, panel: modalPanel }) => {
|
|
314
369
|
input.focus();
|
|
315
370
|
input.addEventListener("input", () => paint(input.value));
|
|
@@ -321,7 +376,6 @@ export function openPalette({ onPick } = {}) {
|
|
|
321
376
|
void modalPanel;
|
|
322
377
|
void doClose;
|
|
323
378
|
},
|
|
324
|
-
veilClass: "mvb-veil--pal",
|
|
325
379
|
});
|
|
326
380
|
|
|
327
381
|
paint("");
|
|
@@ -363,29 +417,31 @@ export function debounce(fn, ms = 120) {
|
|
|
363
417
|
|
|
364
418
|
/** Two column bench: controls on the left, the result on the right. */
|
|
365
419
|
export function bench(side, main) {
|
|
366
|
-
return el(
|
|
420
|
+
return el(
|
|
421
|
+
"div",
|
|
422
|
+
{ class: "grid items-start gap-5 min-[60rem]:grid-cols-[minmax(0,21rem)_minmax(0,1fr)] min-[60rem]:gap-6" },
|
|
423
|
+
el("div", { class: "min-w-0" }, side),
|
|
424
|
+
el("div", { class: "min-w-0" }, main)
|
|
425
|
+
);
|
|
367
426
|
}
|
|
368
427
|
|
|
369
428
|
/** Single centred stage for tools whose whole interface is one object. */
|
|
370
429
|
export function stage(...nodes) {
|
|
371
|
-
return el("div", { class: "
|
|
430
|
+
return el("div", { class: "mx-auto grid max-w-[560px] justify-items-center gap-4 text-center max-[45rem]:justify-items-stretch max-[45rem]:text-start" }, ...nodes);
|
|
372
431
|
}
|
|
373
432
|
|
|
374
433
|
export function stack(...nodes) {
|
|
375
|
-
return el("div", { class: "
|
|
434
|
+
return el("div", { class: "grid content-start gap-4" }, ...nodes);
|
|
376
435
|
}
|
|
377
436
|
|
|
378
|
-
/**
|
|
379
|
-
* Row of controls. Pass anything; it sits in the row. Use `<.mvb-actions>` to
|
|
380
|
-
* mark "thumbs-up submit" slots that should sit on the right of a tool form.
|
|
381
|
-
*/
|
|
437
|
+
/** Row of controls. Pass anything; it sits in the row. */
|
|
382
438
|
export function row(...nodes) {
|
|
383
|
-
return el("div", { class: "
|
|
439
|
+
return el("div", { class: "flex min-w-0 flex-wrap items-center gap-2" }, ...nodes);
|
|
384
440
|
}
|
|
385
441
|
|
|
386
442
|
/** Right-aligned actions block. Use this for primary action buttons. */
|
|
387
443
|
export function actions(...nodes) {
|
|
388
|
-
return el("div", { class: "
|
|
444
|
+
return el("div", { class: "flex flex-wrap items-center justify-end gap-2" }, ...nodes);
|
|
389
445
|
}
|
|
390
446
|
|
|
391
447
|
// ---------- tool card ----------
|
|
@@ -400,39 +456,76 @@ export function actions(...nodes) {
|
|
|
400
456
|
* slot.append(card.node);
|
|
401
457
|
*/
|
|
402
458
|
export function toolcard({ head, body, foot } = {}) {
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
const
|
|
406
|
-
const
|
|
459
|
+
const band = "border-orbit bg-deck-2 px-[var(--card-pad)] py-[var(--band-pad)] max-[60rem]:px-[18px]";
|
|
460
|
+
const headNode = el("div", { class: `flex flex-wrap items-center gap-2 border-b ${band}`, hidden: !head }, head);
|
|
461
|
+
const bodyNode = el("div", { class: "flex-1 p-[var(--card-pad)] max-[60rem]:p-[18px]" }, body);
|
|
462
|
+
const footNode = el("div", { class: `flex flex-wrap items-center justify-end gap-2 border-t ${band}`, hidden: !foot }, foot);
|
|
463
|
+
const node = el("div", { class: "flex flex-col overflow-clip rounded-r2 border border-orbit bg-deck" }, headNode, bodyNode, footNode);
|
|
407
464
|
return { node, head: headNode, body: bodyNode, foot: footNode };
|
|
408
465
|
}
|
|
409
466
|
|
|
410
467
|
// ---------- controls ----------
|
|
411
468
|
|
|
412
|
-
/**
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
|
|
469
|
+
/** A panel field: label on top, the control, optional hint underneath.
|
|
470
|
+
* Pass a `state` ("error" | "good" | "warning") for status colours. The state is
|
|
471
|
+
* drawn as an outline, so it never fights the border or the focus ring. */
|
|
472
|
+
const HINT_TONE = { error: "text-danger", good: "text-jade", warning: "text-ink" };
|
|
473
|
+
const EDGE_TONE = {
|
|
474
|
+
error: "outline-2 -outline-offset-2 outline-danger",
|
|
475
|
+
good: "outline-2 -outline-offset-2 outline-jade",
|
|
476
|
+
warning: "outline-2 -outline-offset-2 outline-star",
|
|
477
|
+
};
|
|
478
|
+
|
|
416
479
|
export function panel(labelText, control, hint, state) {
|
|
480
|
+
if (state && EDGE_TONE[state] && control?.classList) control.classList.add(...EDGE_TONE[state].split(" "));
|
|
417
481
|
return el(
|
|
418
482
|
"label",
|
|
419
|
-
{ class: "
|
|
420
|
-
labelText ? el("span", { class: "
|
|
483
|
+
{ class: "grid min-w-0 content-start gap-1.5" },
|
|
484
|
+
labelText ? el("span", { class: "text-sm font-semibold text-ink-2" }, labelText) : null,
|
|
421
485
|
control,
|
|
422
|
-
hint ? el("span", { class: "
|
|
486
|
+
hint ? el("span", { class: `text-2xs ${HINT_TONE[state] ?? "text-ink-3"}` }, hint) : null
|
|
423
487
|
);
|
|
424
488
|
}
|
|
425
489
|
|
|
426
|
-
|
|
427
|
-
|
|
490
|
+
/* One field skin. You type on the raised deck surface, the same height as every
|
|
491
|
+
button beside it, and the focus ring is jade. Only the properties that never
|
|
492
|
+
change live in the base; width, padding and font size belong to the variant,
|
|
493
|
+
because two utilities for one property in the same layer resolve by sheet
|
|
494
|
+
order, not by the order they are written in the class list. */
|
|
495
|
+
const FIELD =
|
|
496
|
+
"min-w-0 rounded-r1 border border-orbit bg-deck text-ink transition-[border-color,box-shadow] duration-150 placeholder:text-ink-3 hover:border-orbit-hard focus:border-jade focus:outline-none focus:ring-3 focus:ring-jade-soft";
|
|
497
|
+
|
|
498
|
+
/** Merge a tool's own class onto a helper's class list. */
|
|
499
|
+
function withExtra(base, extra) {
|
|
500
|
+
return extra ? `${base} ${extra}` : base;
|
|
428
501
|
}
|
|
429
502
|
|
|
430
|
-
export function
|
|
431
|
-
return el("
|
|
503
|
+
export function textInput({ mono, class: extra, ...attrs } = {}) {
|
|
504
|
+
return el("input", {
|
|
505
|
+
class: withExtra(`${FIELD} w-full min-h-[var(--ctl)] px-3 py-2.5 ${mono ? "font-mono text-sm" : "text-base"}`, extra),
|
|
506
|
+
spellcheck: "false",
|
|
507
|
+
autocomplete: "off",
|
|
508
|
+
...attrs,
|
|
509
|
+
});
|
|
432
510
|
}
|
|
433
511
|
|
|
434
|
-
export function
|
|
435
|
-
|
|
512
|
+
export function textArea({ class: extra, rows = 6, ...attrs } = {}) {
|
|
513
|
+
return el("textarea", {
|
|
514
|
+
class: withExtra(`${FIELD} w-full min-h-24 resize-y px-3 py-2.5 font-mono text-sm leading-normal`, extra),
|
|
515
|
+
rows,
|
|
516
|
+
spellcheck: "false",
|
|
517
|
+
...attrs,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export function selectInput(options, { class: extra, ...attrs } = {}) {
|
|
522
|
+
const node = el("select", {
|
|
523
|
+
class: withExtra(
|
|
524
|
+
`${FIELD} w-full min-h-[var(--ctl)] cursor-pointer py-2.5 ps-3 pe-9 appearance-none bg-no-repeat bg-[position:right_12px_center] bg-[length:14px] bg-[image:var(--caret)] text-base`,
|
|
525
|
+
extra
|
|
526
|
+
),
|
|
527
|
+
...attrs,
|
|
528
|
+
});
|
|
436
529
|
for (const option of options) {
|
|
437
530
|
const { value, label } = typeof option === "string" ? { value: option, label: option } : option;
|
|
438
531
|
node.append(el("option", { value, selected: value === attrs.value }, label));
|
|
@@ -440,48 +533,85 @@ export function selectInput(options, attrs = {}) {
|
|
|
440
533
|
return node;
|
|
441
534
|
}
|
|
442
535
|
|
|
443
|
-
export function numberInput(attrs = {}) {
|
|
444
|
-
return el("input", {
|
|
536
|
+
export function numberInput({ class: extra, ...attrs } = {}) {
|
|
537
|
+
return el("input", {
|
|
538
|
+
class: withExtra(`${FIELD} w-24 min-h-[var(--ctl)] px-3 py-2.5 text-base`, extra),
|
|
539
|
+
type: "number",
|
|
540
|
+
inputmode: "numeric",
|
|
541
|
+
...attrs,
|
|
542
|
+
});
|
|
445
543
|
}
|
|
446
544
|
|
|
447
|
-
export function rangeInput(attrs = {}) {
|
|
448
|
-
return el("input", { class: "
|
|
545
|
+
export function rangeInput({ class: extra, ...attrs } = {}) {
|
|
546
|
+
return el("input", { class: withExtra("min-h-[var(--ctl)] w-full accent-[var(--jade)]", extra), type: "range", ...attrs });
|
|
449
547
|
}
|
|
450
548
|
|
|
451
|
-
|
|
452
|
-
|
|
549
|
+
const PICK =
|
|
550
|
+
"h-[var(--ctl)] w-[52px] flex-none cursor-pointer rounded-r1 border border-orbit bg-deck p-0.5 hover:border-orbit-hard focus:border-jade focus:outline-none focus:ring-3 focus:ring-jade-soft";
|
|
551
|
+
|
|
552
|
+
export function colorInput({ class: extra, ...attrs } = {}) {
|
|
553
|
+
return el("input", { class: withExtra(PICK, extra), type: "color", ...attrs });
|
|
453
554
|
}
|
|
454
555
|
|
|
455
|
-
export function fileInput(attrs = {}) {
|
|
456
|
-
return el("input", {
|
|
556
|
+
export function fileInput({ class: extra, ...attrs } = {}) {
|
|
557
|
+
return el("input", {
|
|
558
|
+
class: withExtra(
|
|
559
|
+
`${FIELD} w-full min-h-[var(--ctl)] p-1.5 text-sm [&::file-selector-button]:me-2.5 [&::file-selector-button]:cursor-pointer [&::file-selector-button]:rounded-r1 [&::file-selector-button]:border [&::file-selector-button]:border-orbit-hard [&::file-selector-button]:bg-deck-2 [&::file-selector-button]:px-2.5 [&::file-selector-button]:py-1 [&::file-selector-button]:text-2xs [&::file-selector-button]:font-semibold [&::file-selector-button]:text-ink`,
|
|
560
|
+
extra
|
|
561
|
+
),
|
|
562
|
+
type: "file",
|
|
563
|
+
...attrs,
|
|
564
|
+
});
|
|
457
565
|
}
|
|
458
566
|
|
|
459
|
-
export function passwordInput(attrs = {}) {
|
|
460
|
-
return el("input", {
|
|
567
|
+
export function passwordInput({ class: extra, ...attrs } = {}) {
|
|
568
|
+
return el("input", {
|
|
569
|
+
class: withExtra(`${FIELD} w-full min-h-[var(--ctl)] px-3 py-2.5 font-mono text-sm`, extra),
|
|
570
|
+
type: "password",
|
|
571
|
+
autocomplete: "new-password",
|
|
572
|
+
...attrs,
|
|
573
|
+
});
|
|
461
574
|
}
|
|
462
575
|
|
|
463
576
|
export function checkbox(labelText, attrs = {}) {
|
|
464
|
-
const box = el("input", { class: "
|
|
465
|
-
const node = el(
|
|
577
|
+
const box = el("input", { class: "m-0 size-4 accent-[var(--jade)]", type: "checkbox", ...attrs });
|
|
578
|
+
const node = el(
|
|
579
|
+
"label",
|
|
580
|
+
{ class: "inline-flex min-h-[var(--ctl)] cursor-pointer items-center gap-2 rounded-r1 border border-orbit bg-deck py-2 ps-2.5 pe-3 text-sm transition-[border-color,background-color] duration-150 has-checked:border-jade has-checked:bg-jade-soft" },
|
|
581
|
+
box,
|
|
582
|
+
el("span", {}, labelText)
|
|
583
|
+
);
|
|
466
584
|
node.input = box;
|
|
467
585
|
return node;
|
|
468
586
|
}
|
|
469
587
|
|
|
470
588
|
export function switchControl(labelText, attrs = {}) {
|
|
471
|
-
const box = el("input", { class: "
|
|
472
|
-
const track = el(
|
|
473
|
-
|
|
589
|
+
const box = el("input", { class: "peer pointer-events-none absolute opacity-0", type: "checkbox", role: "switch", ...attrs });
|
|
590
|
+
const track = el(
|
|
591
|
+
"span",
|
|
592
|
+
{ class: "relative h-[22px] w-[38px] rounded-full bg-track transition-colors duration-150 peer-checked:bg-jade peer-checked:[&>span]:translate-x-4 peer-focus-visible:ring-3 peer-focus-visible:ring-jade-soft" },
|
|
593
|
+
el("span", { class: "absolute top-0.5 left-0.5 size-[18px] rounded-full bg-deck shadow-[0_1px_2px_rgb(0_0_0/0.18)] transition-transform duration-150" })
|
|
594
|
+
);
|
|
595
|
+
const node = el("label", { class: "inline-flex min-h-[var(--ctl)] cursor-pointer items-center gap-2.5 select-none text-sm" }, box, track, el("span", {}, labelText));
|
|
474
596
|
node.input = box;
|
|
475
597
|
return node;
|
|
476
598
|
}
|
|
477
599
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
600
|
+
/* One button: one height, one padding, three weights. `fill` is the high
|
|
601
|
+
emphasis action and the only button that carries the accent. There is no size
|
|
602
|
+
modifier, because a tool that needs a smaller button needs fewer buttons. */
|
|
603
|
+
const BTN =
|
|
604
|
+
"inline-flex min-h-[var(--ctl)] cursor-pointer items-center justify-center gap-2 rounded-r1 border text-sm font-semibold whitespace-nowrap no-underline transition-[background-color,border-color,color,transform] duration-150 active:scale-[0.97] disabled:cursor-not-allowed disabled:opacity-45";
|
|
605
|
+
const BTN_KIND = {
|
|
606
|
+
fill: "border-jade bg-jade px-4 text-on-jade hover:border-jade-deep hover:bg-jade-deep",
|
|
607
|
+
plain: "border-orbit bg-deck px-4 text-ink hover:border-orbit-hard hover:bg-deck-2",
|
|
608
|
+
quiet: "border-transparent bg-transparent px-2.5 text-ink-2 hover:bg-deck-2 hover:text-ink",
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
export function button(labelText, { icon: iconName, kind = "plain", class: extra, ...rest } = {}) {
|
|
482
612
|
return el(
|
|
483
613
|
"button",
|
|
484
|
-
{ class:
|
|
614
|
+
{ class: withExtra(`${BTN} ${BTN_KIND[kind] ?? BTN_KIND.plain}`, extra), type: "button", ...rest },
|
|
485
615
|
iconName ? icon(iconName) : null,
|
|
486
616
|
el("span", {}, labelText)
|
|
487
617
|
);
|
|
@@ -514,23 +644,23 @@ export function downloadButton(getData, filename, labelText = "Download", mime =
|
|
|
514
644
|
* Single choice chips. `items` is [{ value, label }]. Returns the wrapper with
|
|
515
645
|
* a `.select(value)` method so the tool can read the current value.
|
|
516
646
|
*/
|
|
517
|
-
|
|
518
|
-
|
|
647
|
+
const CHIP =
|
|
648
|
+
"min-h-[var(--ctl)] cursor-pointer rounded-r1 border border-orbit bg-deck px-[13px] text-sm font-medium text-ink-2 transition-colors duration-150 hover:border-orbit-hard hover:text-ink data-[on]:border-jade data-[on]:bg-jade-soft data-[on]:text-ink";
|
|
649
|
+
|
|
650
|
+
export function chipRow(items, { value, onChange, label } = {}) {
|
|
651
|
+
const wrap = el("div", { class: "flex flex-wrap gap-1.5", role: "group", "aria-label": label || "Options" });
|
|
519
652
|
let current = value;
|
|
520
653
|
const paint = () => {
|
|
521
654
|
for (const chip of wrap.children) {
|
|
522
655
|
const item = items.find((entry) => entry.label === chip.textContent);
|
|
523
656
|
const on = item?.value === current;
|
|
524
|
-
chip.
|
|
657
|
+
if (on) chip.dataset.on = "";
|
|
658
|
+
else delete chip.dataset.on;
|
|
525
659
|
chip.setAttribute("aria-pressed", String(on));
|
|
526
660
|
}
|
|
527
661
|
};
|
|
528
662
|
for (const item of items) {
|
|
529
|
-
wrap.append(el(
|
|
530
|
-
"button",
|
|
531
|
-
{ class: "mvb-chip", type: "button", onclick: () => wrap.select(item.value) },
|
|
532
|
-
item.label
|
|
533
|
-
));
|
|
663
|
+
wrap.append(el("button", { class: CHIP, type: "button", onclick: () => wrap.select(item.value) }, item.label));
|
|
534
664
|
}
|
|
535
665
|
wrap.select = (next) => {
|
|
536
666
|
current = next;
|
|
@@ -544,7 +674,7 @@ export function chipRow(items, { value, onChange, label, size = "sm" } = {}) {
|
|
|
544
674
|
/** Independent toggles. Returns the wrapper with `.values()` reading the boxes. */
|
|
545
675
|
export function toggleRow(items) {
|
|
546
676
|
const boxes = new Map();
|
|
547
|
-
const wrap = el("div", { class: "
|
|
677
|
+
const wrap = el("div", { class: "flex flex-wrap gap-1.5" });
|
|
548
678
|
for (const item of items) {
|
|
549
679
|
const node = checkbox(item.label, { checked: item.checked !== false });
|
|
550
680
|
boxes.set(item.value, node.input);
|
|
@@ -556,10 +686,14 @@ export function toggleRow(items) {
|
|
|
556
686
|
|
|
557
687
|
// ---------- readouts ----------
|
|
558
688
|
|
|
689
|
+
/* The recessed half of the surface pair, so a result never reads as an input. */
|
|
690
|
+
const OUT =
|
|
691
|
+
"m-0 max-h-[340px] w-full overflow-auto rounded-r1 border border-orbit bg-deck-2 px-3.5 py-3 font-mono text-sm leading-normal whitespace-pre-wrap [overflow-wrap:anywhere]";
|
|
692
|
+
|
|
559
693
|
export function output({ value = "", rows = 6, tag = "pre", readonly = true } = {}) {
|
|
560
694
|
const node = tag === "pre"
|
|
561
|
-
? el("pre", { class:
|
|
562
|
-
: el("textarea", { class:
|
|
695
|
+
? el("pre", { class: `${OUT} [&:empty]:border-dashed [&:empty]:bg-transparent` }, value)
|
|
696
|
+
: el("textarea", { class: `${OUT} resize-y`, readonly, rows, value });
|
|
563
697
|
if (readonly) node.setAttribute("readonly", "");
|
|
564
698
|
// A textarea ignores textContent once its value has been assigned, so the
|
|
565
699
|
// readout has to write through `.value`.
|
|
@@ -571,8 +705,14 @@ export function output({ value = "", rows = 6, tag = "pre", readonly = true } =
|
|
|
571
705
|
}
|
|
572
706
|
|
|
573
707
|
/** A short line under a control. `kind` is one of error, warn, note, good. */
|
|
708
|
+
const NOTE_KIND = {
|
|
709
|
+
error: "border-s-danger text-danger",
|
|
710
|
+
warn: "border-s-star font-medium text-ink",
|
|
711
|
+
good: "border-s-jade text-jade",
|
|
712
|
+
};
|
|
713
|
+
|
|
574
714
|
export function notice(kind = "note") {
|
|
575
|
-
const node = el("p", { class: `
|
|
715
|
+
const node = el("p", { class: `border-s-[3px] ps-2 text-sm ${NOTE_KIND[kind] ?? "border-transparent text-ink-3"}`, role: kind === "error" ? "alert" : null });
|
|
576
716
|
node.write = (text) => {
|
|
577
717
|
node.textContent = text || "";
|
|
578
718
|
node.hidden = !text;
|
|
@@ -584,8 +724,8 @@ export function notice(kind = "note") {
|
|
|
584
724
|
export function blank(iconName, text) {
|
|
585
725
|
return el(
|
|
586
726
|
"div",
|
|
587
|
-
{ class: "
|
|
588
|
-
iconName ? icon(iconName, "
|
|
727
|
+
{ class: "grid justify-items-center gap-2.5 rounded-r2 border border-dashed border-orbit-hard bg-deck-2 px-4 py-[clamp(28px,6vw,56px)] text-center text-ink-3 [&_p]:max-w-[34ch]" },
|
|
728
|
+
iconName ? icon(iconName, "size-[26px] flex-none") : null,
|
|
589
729
|
el("p", {}, text)
|
|
590
730
|
);
|
|
591
731
|
}
|
|
@@ -593,7 +733,7 @@ export function blank(iconName, text) {
|
|
|
593
733
|
// ---------- version ----------
|
|
594
734
|
|
|
595
735
|
// Kept in one place. The CLI reads the same value from package.json.
|
|
596
|
-
export const VERSION = "1.
|
|
736
|
+
export const VERSION = "1.4.0";
|
|
597
737
|
export const PACKAGE_NAME = "mevebu";
|
|
598
738
|
export const REPO_SLUG = "mevebu/mevebu";
|
|
599
739
|
|
|
@@ -131,8 +131,8 @@ export const TOOLS = [
|
|
|
131
131
|
tutorial: {
|
|
132
132
|
intro: "Hash text with the SHA family. Hashing happens in this tab.",
|
|
133
133
|
steps: [
|
|
134
|
-
"Type or paste text
|
|
135
|
-
"Pick an algorithm with the chips
|
|
134
|
+
"Type or paste text.",
|
|
135
|
+
"Pick an algorithm with the chips.",
|
|
136
136
|
"The hash updates as you type. Press Copy hash to copy the result.",
|
|
137
137
|
],
|
|
138
138
|
},
|
|
@@ -272,16 +272,16 @@ export const TOOLS = [
|
|
|
272
272
|
name: "UUID Generator",
|
|
273
273
|
icon: "fingerprint-pattern",
|
|
274
274
|
note: "Generate UUIDs, one or a thousand.",
|
|
275
|
-
about: "
|
|
275
|
+
about: "Versions 1, 4, 6 and 7, plus the Nil and Max sentinels.",
|
|
276
276
|
tutorial: {
|
|
277
|
-
intro: "Generate
|
|
277
|
+
intro: "Generate UUIDs and take them as a list, as JSON, or as a file.",
|
|
278
278
|
steps: [
|
|
279
|
-
"
|
|
280
|
-
"
|
|
281
|
-
"Use Copy all, Copy JSON, or Save .txt
|
|
279
|
+
"Pick the version. v4 and v7 are random and v7 sorts by time. v1 and v6 carry a timestamp. Nil and Max are the two sentinels tests look for.",
|
|
280
|
+
"Set how many to generate, between 1 and 1000. Uppercase and No hyphens change the output only.",
|
|
281
|
+
"Use Copy all, Copy JSON, or Save .txt to take the list.",
|
|
282
282
|
],
|
|
283
283
|
},
|
|
284
|
-
keywords: "guid unique id v4 identifier",
|
|
284
|
+
keywords: "guid unique id v1 v4 v6 v7 nil max identifier",
|
|
285
285
|
load: () => import("./tools/uuid-generator.js"),
|
|
286
286
|
},
|
|
287
287
|
{
|
|
@@ -294,8 +294,8 @@ export const TOOLS = [
|
|
|
294
294
|
tutorial: {
|
|
295
295
|
intro: "Convert a color between hex, rgb, and hsl. The swatch and contrast reading update live.",
|
|
296
296
|
steps: [
|
|
297
|
-
"Pick a color with the picker
|
|
298
|
-
"The other formats, the swatch, and a contrast reading
|
|
297
|
+
"Pick a color with the picker, or type a hex, rgb, or hsl value.",
|
|
298
|
+
"The other formats, the swatch, and a contrast reading update as you type.",
|
|
299
299
|
"Click any of the four readouts to copy that value.",
|
|
300
300
|
],
|
|
301
301
|
},
|
|
@@ -315,7 +315,7 @@ export const TOOLS = [
|
|
|
315
315
|
steps: [
|
|
316
316
|
"Type or paste on either side. The other side updates as you type.",
|
|
317
317
|
"Edit the Base64 side directly to decode. Invalid Base64 shows an error.",
|
|
318
|
-
"Use Copy Base64 or Copy text
|
|
318
|
+
"Use Copy Base64 or Copy text to take either side.",
|
|
319
319
|
],
|
|
320
320
|
},
|
|
321
321
|
keywords: "encode decode binary data uri atob btoa",
|