mevebu 1.3.0 → 1.4.1
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 +1658 -2079
- package/mevebu/app/tailwind.src.css +161 -1127
- package/mevebu/index.html +2 -2
- package/mevebu/libs/js/core.js +245 -96
- 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 +2 -3
- 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 +10 -11
- 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 +5 -7
- 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 +6 -7
- 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 +6 -8
- 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 +8 -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/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-inspector.js +8 -9
- 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 +5 -9
- 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: "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("");
|
|
@@ -361,31 +415,42 @@ export function debounce(fn, ms = 120) {
|
|
|
361
415
|
|
|
362
416
|
// ---------- bench layout ----------
|
|
363
417
|
|
|
364
|
-
/** Two column bench: controls on the left, the result on the right.
|
|
418
|
+
/** Two column bench: controls on the left, the result on the right.
|
|
419
|
+
* Above 60rem the controls pin under the header, so a long result can be read
|
|
420
|
+
* and scrolled without the controls leaving the screen. The cap keeps the
|
|
421
|
+
* bottom of a tall form reachable instead of pinning it out of view.
|
|
422
|
+
* The negative margin plus the matching padding give the focus halo room to
|
|
423
|
+
* paint: a scroll container clips its children's ink, and the fields sit flush
|
|
424
|
+
* with the column edges, so without the 4px gutter every ring loses its sides.
|
|
425
|
+
* The pair cancels out, so the fields keep their width and stay on the anchor line. */
|
|
365
426
|
export function bench(side, main) {
|
|
366
|
-
|
|
427
|
+
const sideColumn =
|
|
428
|
+
"min-w-0 min-[60rem]:sticky min-[60rem]:top-[var(--stick)] min-[60rem]:max-h-[var(--stick-max)] min-[60rem]:overflow-y-auto min-[60rem]:overscroll-contain min-[60rem]:-mx-1 min-[60rem]:px-1";
|
|
429
|
+
return el(
|
|
430
|
+
"div",
|
|
431
|
+
{ class: "grid items-start gap-5 min-[60rem]:grid-cols-[minmax(0,21rem)_minmax(0,1fr)] min-[60rem]:gap-6" },
|
|
432
|
+
el("div", { class: sideColumn }, side),
|
|
433
|
+
el("div", { class: "min-w-0" }, main)
|
|
434
|
+
);
|
|
367
435
|
}
|
|
368
436
|
|
|
369
437
|
/** Single centred stage for tools whose whole interface is one object. */
|
|
370
438
|
export function stage(...nodes) {
|
|
371
|
-
return el("div", { class: "
|
|
439
|
+
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
440
|
}
|
|
373
441
|
|
|
374
442
|
export function stack(...nodes) {
|
|
375
|
-
return el("div", { class: "
|
|
443
|
+
return el("div", { class: "grid content-start gap-4" }, ...nodes);
|
|
376
444
|
}
|
|
377
445
|
|
|
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
|
-
*/
|
|
446
|
+
/** Row of controls. Pass anything; it sits in the row. */
|
|
382
447
|
export function row(...nodes) {
|
|
383
|
-
return el("div", { class: "
|
|
448
|
+
return el("div", { class: "flex min-w-0 flex-wrap items-center gap-2" }, ...nodes);
|
|
384
449
|
}
|
|
385
450
|
|
|
386
451
|
/** Right-aligned actions block. Use this for primary action buttons. */
|
|
387
452
|
export function actions(...nodes) {
|
|
388
|
-
return el("div", { class: "
|
|
453
|
+
return el("div", { class: "flex flex-wrap items-center justify-end gap-2" }, ...nodes);
|
|
389
454
|
}
|
|
390
455
|
|
|
391
456
|
// ---------- tool card ----------
|
|
@@ -400,39 +465,76 @@ export function actions(...nodes) {
|
|
|
400
465
|
* slot.append(card.node);
|
|
401
466
|
*/
|
|
402
467
|
export function toolcard({ head, body, foot } = {}) {
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
const
|
|
406
|
-
const
|
|
468
|
+
const band = "border-orbit bg-deck-2 px-[var(--card-pad)] py-[var(--band-pad)] max-[60rem]:px-[18px]";
|
|
469
|
+
const headNode = el("div", { class: `flex flex-wrap items-center gap-2 border-b ${band}`, hidden: !head }, head);
|
|
470
|
+
const bodyNode = el("div", { class: "flex-1 p-[var(--card-pad)] max-[60rem]:p-[18px]" }, body);
|
|
471
|
+
const footNode = el("div", { class: `flex flex-wrap items-center justify-end gap-2 border-t ${band}`, hidden: !foot }, foot);
|
|
472
|
+
const node = el("div", { class: "flex flex-col overflow-clip rounded-r2 border border-orbit bg-deck" }, headNode, bodyNode, footNode);
|
|
407
473
|
return { node, head: headNode, body: bodyNode, foot: footNode };
|
|
408
474
|
}
|
|
409
475
|
|
|
410
476
|
// ---------- controls ----------
|
|
411
477
|
|
|
412
|
-
/**
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
|
|
478
|
+
/** A panel field: label on top, the control, optional hint underneath.
|
|
479
|
+
* Pass a `state` ("error" | "good" | "warning") for status colours. The state is
|
|
480
|
+
* drawn as an outline, so it never fights the border or the focus ring. */
|
|
481
|
+
const HINT_TONE = { error: "text-danger", good: "text-jade", warning: "text-ink" };
|
|
482
|
+
const EDGE_TONE = {
|
|
483
|
+
error: "outline-2 -outline-offset-2 outline-danger",
|
|
484
|
+
good: "outline-2 -outline-offset-2 outline-jade",
|
|
485
|
+
warning: "outline-2 -outline-offset-2 outline-star",
|
|
486
|
+
};
|
|
487
|
+
|
|
416
488
|
export function panel(labelText, control, hint, state) {
|
|
489
|
+
if (state && EDGE_TONE[state] && control?.classList) control.classList.add(...EDGE_TONE[state].split(" "));
|
|
417
490
|
return el(
|
|
418
491
|
"label",
|
|
419
|
-
{ class: "
|
|
420
|
-
labelText ? el("span", { class: "
|
|
492
|
+
{ class: "grid min-w-0 content-start gap-1.5" },
|
|
493
|
+
labelText ? el("span", { class: "text-sm font-semibold text-ink-2" }, labelText) : null,
|
|
421
494
|
control,
|
|
422
|
-
hint ? el("span", { class: "
|
|
495
|
+
hint ? el("span", { class: `text-2xs ${HINT_TONE[state] ?? "text-ink-3"}` }, hint) : null
|
|
423
496
|
);
|
|
424
497
|
}
|
|
425
498
|
|
|
426
|
-
|
|
427
|
-
|
|
499
|
+
/* One field skin. You type on the raised deck surface, the same height as every
|
|
500
|
+
button beside it, and the focus ring is jade. Only the properties that never
|
|
501
|
+
change live in the base; width, padding and font size belong to the variant,
|
|
502
|
+
because two utilities for one property in the same layer resolve by sheet
|
|
503
|
+
order, not by the order they are written in the class list. */
|
|
504
|
+
const FIELD =
|
|
505
|
+
"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";
|
|
506
|
+
|
|
507
|
+
/** Merge a tool's own class onto a helper's class list. */
|
|
508
|
+
function withExtra(base, extra) {
|
|
509
|
+
return extra ? `${base} ${extra}` : base;
|
|
428
510
|
}
|
|
429
511
|
|
|
430
|
-
export function
|
|
431
|
-
return el("
|
|
512
|
+
export function textInput({ mono, class: extra, ...attrs } = {}) {
|
|
513
|
+
return el("input", {
|
|
514
|
+
class: withExtra(`${FIELD} w-full min-h-[var(--ctl)] px-3 py-2.5 ${mono ? "font-mono text-sm" : "text-base"}`, extra),
|
|
515
|
+
spellcheck: "false",
|
|
516
|
+
autocomplete: "off",
|
|
517
|
+
...attrs,
|
|
518
|
+
});
|
|
432
519
|
}
|
|
433
520
|
|
|
434
|
-
export function
|
|
435
|
-
|
|
521
|
+
export function textArea({ class: extra, rows = 6, ...attrs } = {}) {
|
|
522
|
+
return el("textarea", {
|
|
523
|
+
class: withExtra(`${FIELD} w-full min-h-24 resize-y px-3 py-2.5 font-mono text-sm leading-normal`, extra),
|
|
524
|
+
rows,
|
|
525
|
+
spellcheck: "false",
|
|
526
|
+
...attrs,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function selectInput(options, { class: extra, ...attrs } = {}) {
|
|
531
|
+
const node = el("select", {
|
|
532
|
+
class: withExtra(
|
|
533
|
+
`${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`,
|
|
534
|
+
extra
|
|
535
|
+
),
|
|
536
|
+
...attrs,
|
|
537
|
+
});
|
|
436
538
|
for (const option of options) {
|
|
437
539
|
const { value, label } = typeof option === "string" ? { value: option, label: option } : option;
|
|
438
540
|
node.append(el("option", { value, selected: value === attrs.value }, label));
|
|
@@ -440,48 +542,85 @@ export function selectInput(options, attrs = {}) {
|
|
|
440
542
|
return node;
|
|
441
543
|
}
|
|
442
544
|
|
|
443
|
-
export function numberInput(attrs = {}) {
|
|
444
|
-
return el("input", {
|
|
545
|
+
export function numberInput({ class: extra, ...attrs } = {}) {
|
|
546
|
+
return el("input", {
|
|
547
|
+
class: withExtra(`${FIELD} w-24 min-h-[var(--ctl)] px-3 py-2.5 text-base`, extra),
|
|
548
|
+
type: "number",
|
|
549
|
+
inputmode: "numeric",
|
|
550
|
+
...attrs,
|
|
551
|
+
});
|
|
445
552
|
}
|
|
446
553
|
|
|
447
|
-
export function rangeInput(attrs = {}) {
|
|
448
|
-
return el("input", { class: "
|
|
554
|
+
export function rangeInput({ class: extra, ...attrs } = {}) {
|
|
555
|
+
return el("input", { class: withExtra("min-h-[var(--ctl)] w-full accent-[var(--jade)]", extra), type: "range", ...attrs });
|
|
449
556
|
}
|
|
450
557
|
|
|
451
|
-
|
|
452
|
-
|
|
558
|
+
const PICK =
|
|
559
|
+
"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";
|
|
560
|
+
|
|
561
|
+
export function colorInput({ class: extra, ...attrs } = {}) {
|
|
562
|
+
return el("input", { class: withExtra(PICK, extra), type: "color", ...attrs });
|
|
453
563
|
}
|
|
454
564
|
|
|
455
|
-
export function fileInput(attrs = {}) {
|
|
456
|
-
return el("input", {
|
|
565
|
+
export function fileInput({ class: extra, ...attrs } = {}) {
|
|
566
|
+
return el("input", {
|
|
567
|
+
class: withExtra(
|
|
568
|
+
`${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`,
|
|
569
|
+
extra
|
|
570
|
+
),
|
|
571
|
+
type: "file",
|
|
572
|
+
...attrs,
|
|
573
|
+
});
|
|
457
574
|
}
|
|
458
575
|
|
|
459
|
-
export function passwordInput(attrs = {}) {
|
|
460
|
-
return el("input", {
|
|
576
|
+
export function passwordInput({ class: extra, ...attrs } = {}) {
|
|
577
|
+
return el("input", {
|
|
578
|
+
class: withExtra(`${FIELD} w-full min-h-[var(--ctl)] px-3 py-2.5 font-mono text-sm`, extra),
|
|
579
|
+
type: "password",
|
|
580
|
+
autocomplete: "new-password",
|
|
581
|
+
...attrs,
|
|
582
|
+
});
|
|
461
583
|
}
|
|
462
584
|
|
|
463
585
|
export function checkbox(labelText, attrs = {}) {
|
|
464
|
-
const box = el("input", { class: "
|
|
465
|
-
const node = el(
|
|
586
|
+
const box = el("input", { class: "m-0 size-4 accent-[var(--jade)]", type: "checkbox", ...attrs });
|
|
587
|
+
const node = el(
|
|
588
|
+
"label",
|
|
589
|
+
{ 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" },
|
|
590
|
+
box,
|
|
591
|
+
el("span", {}, labelText)
|
|
592
|
+
);
|
|
466
593
|
node.input = box;
|
|
467
594
|
return node;
|
|
468
595
|
}
|
|
469
596
|
|
|
470
597
|
export function switchControl(labelText, attrs = {}) {
|
|
471
|
-
const box = el("input", { class: "
|
|
472
|
-
const track = el(
|
|
473
|
-
|
|
598
|
+
const box = el("input", { class: "peer pointer-events-none absolute opacity-0", type: "checkbox", role: "switch", ...attrs });
|
|
599
|
+
const track = el(
|
|
600
|
+
"span",
|
|
601
|
+
{ 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" },
|
|
602
|
+
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" })
|
|
603
|
+
);
|
|
604
|
+
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
605
|
node.input = box;
|
|
475
606
|
return node;
|
|
476
607
|
}
|
|
477
608
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
609
|
+
/* One button: one height, one padding, three weights. `fill` is the high
|
|
610
|
+
emphasis action and the only button that carries the accent. There is no size
|
|
611
|
+
modifier, because a tool that needs a smaller button needs fewer buttons. */
|
|
612
|
+
const BTN =
|
|
613
|
+
"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";
|
|
614
|
+
const BTN_KIND = {
|
|
615
|
+
fill: "border-jade bg-jade px-4 text-on-jade hover:border-jade-deep hover:bg-jade-deep",
|
|
616
|
+
plain: "border-orbit bg-deck px-4 text-ink hover:border-orbit-hard hover:bg-deck-2",
|
|
617
|
+
quiet: "border-transparent bg-transparent px-2.5 text-ink-2 hover:bg-deck-2 hover:text-ink",
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
export function button(labelText, { icon: iconName, kind = "plain", class: extra, ...rest } = {}) {
|
|
482
621
|
return el(
|
|
483
622
|
"button",
|
|
484
|
-
{ class:
|
|
623
|
+
{ class: withExtra(`${BTN} ${BTN_KIND[kind] ?? BTN_KIND.plain}`, extra), type: "button", ...rest },
|
|
485
624
|
iconName ? icon(iconName) : null,
|
|
486
625
|
el("span", {}, labelText)
|
|
487
626
|
);
|
|
@@ -514,23 +653,23 @@ export function downloadButton(getData, filename, labelText = "Download", mime =
|
|
|
514
653
|
* Single choice chips. `items` is [{ value, label }]. Returns the wrapper with
|
|
515
654
|
* a `.select(value)` method so the tool can read the current value.
|
|
516
655
|
*/
|
|
656
|
+
const CHIP =
|
|
657
|
+
"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";
|
|
658
|
+
|
|
517
659
|
export function chipRow(items, { value, onChange, label } = {}) {
|
|
518
|
-
const wrap = el("div", { class: "
|
|
660
|
+
const wrap = el("div", { class: "flex flex-wrap gap-1.5", role: "group", "aria-label": label || "Options" });
|
|
519
661
|
let current = value;
|
|
520
662
|
const paint = () => {
|
|
521
663
|
for (const chip of wrap.children) {
|
|
522
664
|
const item = items.find((entry) => entry.label === chip.textContent);
|
|
523
665
|
const on = item?.value === current;
|
|
524
|
-
chip.
|
|
666
|
+
if (on) chip.dataset.on = "";
|
|
667
|
+
else delete chip.dataset.on;
|
|
525
668
|
chip.setAttribute("aria-pressed", String(on));
|
|
526
669
|
}
|
|
527
670
|
};
|
|
528
671
|
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
|
-
));
|
|
672
|
+
wrap.append(el("button", { class: CHIP, type: "button", onclick: () => wrap.select(item.value) }, item.label));
|
|
534
673
|
}
|
|
535
674
|
wrap.select = (next) => {
|
|
536
675
|
current = next;
|
|
@@ -544,7 +683,7 @@ export function chipRow(items, { value, onChange, label } = {}) {
|
|
|
544
683
|
/** Independent toggles. Returns the wrapper with `.values()` reading the boxes. */
|
|
545
684
|
export function toggleRow(items) {
|
|
546
685
|
const boxes = new Map();
|
|
547
|
-
const wrap = el("div", { class: "
|
|
686
|
+
const wrap = el("div", { class: "flex flex-wrap gap-1.5" });
|
|
548
687
|
for (const item of items) {
|
|
549
688
|
const node = checkbox(item.label, { checked: item.checked !== false });
|
|
550
689
|
boxes.set(item.value, node.input);
|
|
@@ -556,10 +695,14 @@ export function toggleRow(items) {
|
|
|
556
695
|
|
|
557
696
|
// ---------- readouts ----------
|
|
558
697
|
|
|
698
|
+
/* The recessed half of the surface pair, so a result never reads as an input. */
|
|
699
|
+
const OUT =
|
|
700
|
+
"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]";
|
|
701
|
+
|
|
559
702
|
export function output({ value = "", rows = 6, tag = "pre", readonly = true } = {}) {
|
|
560
703
|
const node = tag === "pre"
|
|
561
|
-
? el("pre", { class:
|
|
562
|
-
: el("textarea", { class:
|
|
704
|
+
? el("pre", { class: `${OUT} [&:empty]:border-dashed [&:empty]:bg-transparent` }, value)
|
|
705
|
+
: el("textarea", { class: `${OUT} resize-y`, readonly, rows, value });
|
|
563
706
|
if (readonly) node.setAttribute("readonly", "");
|
|
564
707
|
// A textarea ignores textContent once its value has been assigned, so the
|
|
565
708
|
// readout has to write through `.value`.
|
|
@@ -571,8 +714,14 @@ export function output({ value = "", rows = 6, tag = "pre", readonly = true } =
|
|
|
571
714
|
}
|
|
572
715
|
|
|
573
716
|
/** A short line under a control. `kind` is one of error, warn, note, good. */
|
|
717
|
+
const NOTE_KIND = {
|
|
718
|
+
error: "border-s-danger text-danger",
|
|
719
|
+
warn: "border-s-star font-medium text-ink",
|
|
720
|
+
good: "border-s-jade text-jade",
|
|
721
|
+
};
|
|
722
|
+
|
|
574
723
|
export function notice(kind = "note") {
|
|
575
|
-
const node = el("p", { class: `
|
|
724
|
+
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
725
|
node.write = (text) => {
|
|
577
726
|
node.textContent = text || "";
|
|
578
727
|
node.hidden = !text;
|
|
@@ -584,8 +733,8 @@ export function notice(kind = "note") {
|
|
|
584
733
|
export function blank(iconName, text) {
|
|
585
734
|
return el(
|
|
586
735
|
"div",
|
|
587
|
-
{ class: "
|
|
588
|
-
iconName ? icon(iconName, "
|
|
736
|
+
{ 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]" },
|
|
737
|
+
iconName ? icon(iconName, "size-[26px] flex-none") : null,
|
|
589
738
|
el("p", {}, text)
|
|
590
739
|
);
|
|
591
740
|
}
|
|
@@ -593,7 +742,7 @@ export function blank(iconName, text) {
|
|
|
593
742
|
// ---------- version ----------
|
|
594
743
|
|
|
595
744
|
// Kept in one place. The CLI reads the same value from package.json.
|
|
596
|
-
export const VERSION = "1.
|
|
745
|
+
export const VERSION = "1.4.1";
|
|
597
746
|
export const PACKAGE_NAME = "mevebu";
|
|
598
747
|
export const REPO_SLUG = "mevebu/mevebu";
|
|
599
748
|
|
|
@@ -6,12 +6,11 @@ const PARTS = [
|
|
|
6
6
|
{ value: "aim", label: "Aim" },
|
|
7
7
|
{ value: "method", label: "Method" },
|
|
8
8
|
{ value: "results", label: "Results" },
|
|
9
|
-
{ value: "conclusion", label: "Conclusion" }
|
|
10
|
-
];
|
|
9
|
+
{ value: "conclusion", label: "Conclusion" }];
|
|
11
10
|
|
|
12
11
|
function tile(label) {
|
|
13
|
-
const value = el("span", { class: "
|
|
14
|
-
const node = el("div", { class: "
|
|
12
|
+
const value = el("span", { class: "wrap-anywhere text-xl font-bold tracking-[-0.02em] tabular-nums" }, "0");
|
|
13
|
+
const node = el("div", { class: "grid gap-0.5 rounded-r1 border border-orbit bg-deck-2 px-3.5 py-3" }, value, el("span", { class: "text-2xs font-semibold text-ink-3" }, label));
|
|
15
14
|
node.set = (text) => { value.textContent = text; };
|
|
16
15
|
return node;
|
|
17
16
|
}
|
|
@@ -23,7 +22,7 @@ export function mount(slot) {
|
|
|
23
22
|
const parts = toggleRow(PARTS.map((part) => ({ ...part, checked: false })));
|
|
24
23
|
const wordsTile = tile("Words");
|
|
25
24
|
const tickedTile = tile("Ticked");
|
|
26
|
-
const stats = el("div", { class: "
|
|
25
|
+
const stats = el("div", { class: "grid gap-2.5 [grid-template-columns:repeat(auto-fit,minmax(120px,1fr))]" }, wordsTile, tickedTile);
|
|
27
26
|
const note = notice("note");
|
|
28
27
|
note.write("This checks structure only, not quality.");
|
|
29
28
|
let summary = "";
|
|
@@ -43,8 +42,7 @@ export function mount(slot) {
|
|
|
43
42
|
stack(panel("Abstract", source), note),
|
|
44
43
|
stack(stats, panel("Parts found", parts))
|
|
45
44
|
),
|
|
46
|
-
foot: actions(copyButton(() => summary, "Copy checklist", "fill"))
|
|
47
|
-
});
|
|
45
|
+
foot: actions(copyButton(() => summary, "Copy checklist", "fill"))});
|
|
48
46
|
slot.append(card.node);
|
|
49
47
|
run();
|
|
50
48
|
|