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.
Files changed (167) hide show
  1. package/mevebu/app/mevebu.js +260 -208
  2. package/mevebu/app/tailwind.css +1639 -2077
  3. package/mevebu/app/tailwind.src.css +155 -1120
  4. package/mevebu/index.html +2 -2
  5. package/mevebu/libs/js/core.js +236 -96
  6. package/mevebu/libs/js/registry.js +11 -11
  7. package/mevebu/libs/js/tools/abstract-checklist.js +5 -7
  8. package/mevebu/libs/js/tools/add-subtract-days.js +9 -11
  9. package/mevebu/libs/js/tools/age-calculator.js +7 -8
  10. package/mevebu/libs/js/tools/api-request-builder.js +3 -4
  11. package/mevebu/libs/js/tools/area-converter.js +7 -9
  12. package/mevebu/libs/js/tools/aspect-ratio.js +8 -9
  13. package/mevebu/libs/js/tools/average-calculator.js +4 -5
  14. package/mevebu/libs/js/tools/base64.js +31 -11
  15. package/mevebu/libs/js/tools/binary-calculator.js +6 -7
  16. package/mevebu/libs/js/tools/border-radius.js +7 -9
  17. package/mevebu/libs/js/tools/box-shadow.js +6 -7
  18. package/mevebu/libs/js/tools/break-even.js +4 -5
  19. package/mevebu/libs/js/tools/business-days.js +7 -8
  20. package/mevebu/libs/js/tools/canonical-tag.js +5 -6
  21. package/mevebu/libs/js/tools/caption-counter.js +5 -7
  22. package/mevebu/libs/js/tools/case-converter.js +11 -12
  23. package/mevebu/libs/js/tools/case-inspector.js +14 -16
  24. package/mevebu/libs/js/tools/cash-denomination.js +7 -8
  25. package/mevebu/libs/js/tools/cash-flow.js +7 -8
  26. package/mevebu/libs/js/tools/cert-expiry.js +6 -10
  27. package/mevebu/libs/js/tools/changelog-generator.js +4 -6
  28. package/mevebu/libs/js/tools/character-counter.js +10 -13
  29. package/mevebu/libs/js/tools/checklist.js +6 -7
  30. package/mevebu/libs/js/tools/citation-number-check.js +10 -13
  31. package/mevebu/libs/js/tools/coin-flip.js +9 -11
  32. package/mevebu/libs/js/tools/color-converter.js +23 -17
  33. package/mevebu/libs/js/tools/color-palette.js +6 -8
  34. package/mevebu/libs/js/tools/color-picker.js +4 -5
  35. package/mevebu/libs/js/tools/content-calendar.js +3 -4
  36. package/mevebu/libs/js/tools/contrast-checker.js +5 -6
  37. package/mevebu/libs/js/tools/cron-builder.js +4 -6
  38. package/mevebu/libs/js/tools/css-clamp.js +5 -6
  39. package/mevebu/libs/js/tools/css-formatter.js +4 -7
  40. package/mevebu/libs/js/tools/css-grid.js +6 -7
  41. package/mevebu/libs/js/tools/css-minifier.js +5 -8
  42. package/mevebu/libs/js/tools/csv-to-json.js +2 -3
  43. package/mevebu/libs/js/tools/csv-viewer.js +9 -11
  44. package/mevebu/libs/js/tools/currency-format.js +5 -7
  45. package/mevebu/libs/js/tools/daily-planner.js +9 -10
  46. package/mevebu/libs/js/tools/data-masker.js +7 -9
  47. package/mevebu/libs/js/tools/data-storage.js +7 -9
  48. package/mevebu/libs/js/tools/data-validator.js +9 -11
  49. package/mevebu/libs/js/tools/date-difference.js +6 -7
  50. package/mevebu/libs/js/tools/date-format.js +11 -13
  51. package/mevebu/libs/js/tools/delimiter-converter.js +3 -5
  52. package/mevebu/libs/js/tools/discount-calculator.js +4 -5
  53. package/mevebu/libs/js/tools/doi-resolver.js +4 -5
  54. package/mevebu/libs/js/tools/env-formatter.js +3 -6
  55. package/mevebu/libs/js/tools/equation-formatter.js +5 -9
  56. package/mevebu/libs/js/tools/expense-split.js +6 -7
  57. package/mevebu/libs/js/tools/fake-data.js +20 -13
  58. package/mevebu/libs/js/tools/find-replace.js +3 -5
  59. package/mevebu/libs/js/tools/flashcard-creator.js +9 -10
  60. package/mevebu/libs/js/tools/font-pairing.js +6 -8
  61. package/mevebu/libs/js/tools/fraction-calculator.js +10 -13
  62. package/mevebu/libs/js/tools/git-command-builder.js +11 -22
  63. package/mevebu/libs/js/tools/gpa-calculator.js +8 -10
  64. package/mevebu/libs/js/tools/grade-calculator.js +7 -8
  65. package/mevebu/libs/js/tools/gradient-generator.js +10 -12
  66. package/mevebu/libs/js/tools/habit-tracker.js +8 -9
  67. package/mevebu/libs/js/tools/hash-generator.js +3 -5
  68. package/mevebu/libs/js/tools/hashtag-formatter.js +6 -8
  69. package/mevebu/libs/js/tools/hmac-generator.js +3 -5
  70. package/mevebu/libs/js/tools/hpp-calculator.js +4 -5
  71. package/mevebu/libs/js/tools/html-entity.js +2 -3
  72. package/mevebu/libs/js/tools/html-formatter.js +3 -6
  73. package/mevebu/libs/js/tools/http-status.js +5 -7
  74. package/mevebu/libs/js/tools/inventory-quantity.js +7 -8
  75. package/mevebu/libs/js/tools/json-formatter.js +3 -6
  76. package/mevebu/libs/js/tools/json-minifier.js +5 -8
  77. package/mevebu/libs/js/tools/json-to-csv.js +3 -5
  78. package/mevebu/libs/js/tools/json-to-typescript.js +2 -5
  79. package/mevebu/libs/js/tools/json-validator.js +2 -3
  80. package/mevebu/libs/js/tools/jwt-decoder.js +6 -7
  81. package/mevebu/libs/js/tools/jwt-expiry.js +5 -6
  82. package/mevebu/libs/js/tools/jwt-generator.js +4 -6
  83. package/mevebu/libs/js/tools/length-converter.js +7 -9
  84. package/mevebu/libs/js/tools/license-reference.js +10 -13
  85. package/mevebu/libs/js/tools/line-counter.js +5 -7
  86. package/mevebu/libs/js/tools/list-difference.js +5 -7
  87. package/mevebu/libs/js/tools/list-merger.js +4 -5
  88. package/mevebu/libs/js/tools/locale-number.js +5 -8
  89. package/mevebu/libs/js/tools/lorem-ipsum.js +4 -2
  90. package/mevebu/libs/js/tools/markdown-preview.js +5 -8
  91. package/mevebu/libs/js/tools/markdown-table.js +5 -7
  92. package/mevebu/libs/js/tools/markup-calculator.js +4 -5
  93. package/mevebu/libs/js/tools/meeting-agenda.js +8 -9
  94. package/mevebu/libs/js/tools/meta-preview.js +12 -13
  95. package/mevebu/libs/js/tools/meta-tags.js +6 -7
  96. package/mevebu/libs/js/tools/mime-types.js +5 -7
  97. package/mevebu/libs/js/tools/name-picker.js +2 -3
  98. package/mevebu/libs/js/tools/number-base-converter.js +9 -12
  99. package/mevebu/libs/js/tools/password-generator.js +9 -12
  100. package/mevebu/libs/js/tools/password-strength.js +21 -25
  101. package/mevebu/libs/js/tools/percent-to-fraction.js +6 -7
  102. package/mevebu/libs/js/tools/percentage-calculator.js +6 -9
  103. package/mevebu/libs/js/tools/percentage-change.js +4 -5
  104. package/mevebu/libs/js/tools/permutation-combination.js +9 -12
  105. package/mevebu/libs/js/tools/prefix-suffix.js +2 -3
  106. package/mevebu/libs/js/tools/priority-matrix.js +6 -10
  107. package/mevebu/libs/js/tools/profit-margin.js +4 -5
  108. package/mevebu/libs/js/tools/qr-generator.js +3 -4
  109. package/mevebu/libs/js/tools/qr-scanner.js +11 -12
  110. package/mevebu/libs/js/tools/quadratic-solver.js +7 -8
  111. package/mevebu/libs/js/tools/query-builder.js +2 -3
  112. package/mevebu/libs/js/tools/quiz-formatter.js +4 -6
  113. package/mevebu/libs/js/tools/ratio-calculator.js +7 -8
  114. package/mevebu/libs/js/tools/readability.js +12 -15
  115. package/mevebu/libs/js/tools/reading-time.js +5 -7
  116. package/mevebu/libs/js/tools/reference-completeness.js +6 -8
  117. package/mevebu/libs/js/tools/regex-tester.js +7 -10
  118. package/mevebu/libs/js/tools/remove-duplicate-lines.js +2 -3
  119. package/mevebu/libs/js/tools/remove-extra-spaces.js +3 -5
  120. package/mevebu/libs/js/tools/robots-txt.js +3 -4
  121. package/mevebu/libs/js/tools/roman-numeral-converter.js +6 -8
  122. package/mevebu/libs/js/tools/sales-commission.js +8 -9
  123. package/mevebu/libs/js/tools/scientific-calculator.js +10 -14
  124. package/mevebu/libs/js/tools/scientific-notation.js +8 -9
  125. package/mevebu/libs/js/tools/script-planner.js +8 -10
  126. package/mevebu/libs/js/tools/selling-price.js +4 -5
  127. package/mevebu/libs/js/tools/semver-helper.js +3 -5
  128. package/mevebu/libs/js/tools/sentence-length.js +9 -10
  129. package/mevebu/libs/js/tools/sequence-analyzer.js +7 -8
  130. package/mevebu/libs/js/tools/sitemap-xml.js +4 -7
  131. package/mevebu/libs/js/tools/slug-converter.js +3 -5
  132. package/mevebu/libs/js/tools/slug-preview.js +6 -7
  133. package/mevebu/libs/js/tools/sort-lines.js +3 -5
  134. package/mevebu/libs/js/tools/speed-converter.js +7 -9
  135. package/mevebu/libs/js/tools/spin-wheel.js +19 -17
  136. package/mevebu/libs/js/tools/sql-formatter.js +5 -9
  137. package/mevebu/libs/js/tools/sql-minifier.js +5 -8
  138. package/mevebu/libs/js/tools/standard-deviation.js +6 -9
  139. package/mevebu/libs/js/tools/stopwatch.js +4 -6
  140. package/mevebu/libs/js/tools/study-schedule.js +8 -9
  141. package/mevebu/libs/js/tools/subtitle-formatter.js +10 -11
  142. package/mevebu/libs/js/tools/table-to-markdown.js +8 -12
  143. package/mevebu/libs/js/tools/tax-calculator.js +4 -5
  144. package/mevebu/libs/js/tools/temperature-converter.js +8 -11
  145. package/mevebu/libs/js/tools/text-diff.js +11 -7
  146. package/mevebu/libs/js/tools/text-direction.js +3 -5
  147. package/mevebu/libs/js/tools/text-reverser.js +3 -5
  148. package/mevebu/libs/js/tools/time-duration.js +8 -9
  149. package/mevebu/libs/js/tools/timestamp-converter.js +4 -6
  150. package/mevebu/libs/js/tools/timezone-converter.js +9 -13
  151. package/mevebu/libs/js/tools/title-length.js +6 -7
  152. package/mevebu/libs/js/tools/unicode-inspector.js +4 -5
  153. package/mevebu/libs/js/tools/url-encoder.js +4 -2
  154. package/mevebu/libs/js/tools/url-inspector.js +9 -11
  155. package/mevebu/libs/js/tools/url-parser.js +12 -13
  156. package/mevebu/libs/js/tools/utf8-bytes.js +6 -9
  157. package/mevebu/libs/js/tools/uuid-generator.js +107 -15
  158. package/mevebu/libs/js/tools/version-compare.js +3 -4
  159. package/mevebu/libs/js/tools/video-duration.js +5 -7
  160. package/mevebu/libs/js/tools/volume-converter.js +7 -9
  161. package/mevebu/libs/js/tools/weekly-planner.js +8 -10
  162. package/mevebu/libs/js/tools/weight-converter.js +7 -9
  163. package/mevebu/libs/js/tools/weighted-average.js +4 -5
  164. package/mevebu/libs/js/tools/word-counter.js +10 -13
  165. package/mevebu/libs/js/tools/word-limit.js +4 -5
  166. package/mevebu/libs/js/tools/yaml-formatter.js +6 -7
  167. package/package.json +1 -1
@@ -8,15 +8,15 @@ const splitSentences = (text) => text.trim()
8
8
  const countWords = (text) => text.trim() ? text.trim().split(/\s+/).length : 0;
9
9
 
10
10
  function tile(label) {
11
- const value = el("span", { class: "mvb-stat__v" }, "0");
12
- const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
11
+ const value = el("span", { class: "wrap-anywhere text-xl font-bold tracking-[-0.02em] tabular-nums" }, "0");
12
+ 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));
13
13
  node.set = (text) => { value.textContent = text; };
14
14
  return node;
15
15
  }
16
16
 
17
- const kvRow = (key, value) => el("div", { class: "mvb-kv__row" },
18
- el("span", { class: "mvb-kv__k" }, key),
19
- el("span", { class: "mvb-kv__v" }, value));
17
+ const kvRow = (key, value) => el("div", { class: "grid gap-3 bg-deck px-3 py-[9px] [grid-template-columns:minmax(90px,34%)_minmax(0,1fr)]" },
18
+ el("span", { class: "wrap-anywhere text-2xs font-semibold text-ink-2" }, key),
19
+ el("span", { class: "wrap-anywhere font-mono text-2xs" }, value));
20
20
 
21
21
  export function mount(slot) {
22
22
  const source = textArea({ rows: 12, "aria-label": "Text", placeholder: "Type or paste text" });
@@ -26,9 +26,9 @@ export function mount(slot) {
26
26
  const shortestTile = tile("Shortest");
27
27
  const longestTile = tile("Longest");
28
28
  const averageTile = tile("Average");
29
- const stats = el("div", { class: "mvb-stats" }, sentencesTile, shortestTile, longestTile, averageTile);
29
+ const stats = el("div", { class: "grid gap-2.5 [grid-template-columns:repeat(auto-fit,minmax(120px,1fr))]" }, sentencesTile, shortestTile, longestTile, averageTile);
30
30
 
31
- const listWrap = el("div", { class: "mvb-scroll" });
31
+ const listWrap = el("div", { class: "max-h-80 overflow-auto" });
32
32
  const note = notice("note");
33
33
 
34
34
  let longText = "";
@@ -56,7 +56,7 @@ export function mount(slot) {
56
56
  if (!long.length) {
57
57
  listWrap.replaceChildren(blank("text-align-start", "No sentence runs past the threshold."));
58
58
  } else {
59
- listWrap.replaceChildren(el("div", { class: "mvb-kv" },
59
+ listWrap.replaceChildren(el("div", { class: "grid gap-px overflow-hidden rounded-r1 border border-orbit bg-orbit" },
60
60
  ...long.map((sentence) => kvRow(`${countWords(sentence)} words`, sentence))));
61
61
  }
62
62
  }
@@ -69,8 +69,7 @@ export function mount(slot) {
69
69
  stack(panel("Text", source), panel("Flag past", threshold, "Words")),
70
70
  stack(stats, note, panel("Long sentences", listWrap))
71
71
  ),
72
- foot: actions(copyButton(() => longText, "Copy long sentences", "fill")),
73
- });
72
+ foot: actions(copyButton(() => longText, "Copy long sentences", "fill"))});
74
73
  slot.append(card.node);
75
74
  run();
76
75
 
@@ -12,17 +12,17 @@ function parseNumbers(text) {
12
12
  }
13
13
 
14
14
  const kvRow = (key, value) =>
15
- el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
15
+ el("div", { class: "grid gap-3 bg-deck px-3 py-[9px] [grid-template-columns:minmax(90px,34%)_minmax(0,1fr)]" }, el("span", { class: "wrap-anywhere text-2xs font-semibold text-ink-2" }, key), el("span", { class: "wrap-anywhere font-mono text-2xs" }, value));
16
16
 
17
17
  export function mount(slot) {
18
18
  const source = textArea({ rows: 6, value: "2, 5, 8, 11", "aria-label": "Numbers", placeholder: "2, 5, 8, 11" });
19
19
  const error = notice("error");
20
20
 
21
- const terms = el("span", { class: "mvb-kv__v" }, "-");
22
- const differences = el("span", { class: "mvb-kv__v" }, "-");
23
- const pattern = el("span", { class: "mvb-kv__v" }, "-");
24
- const next = el("span", { class: "mvb-kv__v" }, "-");
25
- const kv = el("div", { class: "mvb-kv" },
21
+ const terms = el("span", { class: "wrap-anywhere font-mono text-2xs" }, "-");
22
+ const differences = el("span", { class: "wrap-anywhere font-mono text-2xs" }, "-");
23
+ const pattern = el("span", { class: "wrap-anywhere font-mono text-2xs" }, "-");
24
+ const next = el("span", { class: "wrap-anywhere font-mono text-2xs" }, "-");
25
+ const kv = el("div", { class: "grid gap-px overflow-hidden rounded-r1 border border-orbit bg-orbit" },
26
26
  kvRow("Terms", terms),
27
27
  kvRow("Differences", differences),
28
28
  kvRow("Pattern", pattern),
@@ -79,8 +79,7 @@ export function mount(slot) {
79
79
  stack(panel("Numbers", source), error),
80
80
  stack(panel("Analysis", kv))
81
81
  ),
82
- foot: actions(copyButton(() => text, "Copy result", "fill")),
83
- });
82
+ foot: actions(copyButton(() => text, "Copy result", "fill"))});
84
83
  slot.append(card.node);
85
84
  run();
86
85
 
@@ -15,7 +15,7 @@ const isUrl = (value) => {
15
15
 
16
16
  export function mount(slot) {
17
17
  const base = textInput({ value: "https://example.com", "aria-label": "Base URL" });
18
- const pages = textArea({ rows: 8, class: "mvb-in mvb-in--area mvb-in--mono", value: "/\n/about", placeholder: "/\n/about\n/blog", "aria-label": "Pages" });
18
+ const pages = textArea({ rows: 8, value: "/\n/about", placeholder: "/\n/about\n/blog", "aria-label": "Pages" });
19
19
  const freq = selectInput(["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"], { value: "weekly", "aria-label": "Change frequency" });
20
20
  const priority = numberInput({ value: "0.8", min: "0", max: "1", step: "0.1", "aria-label": "Priority" });
21
21
  const result = output({ rows: 14 });
@@ -43,15 +43,13 @@ export function mount(slot) {
43
43
  ` <lastmod>${today}</lastmod>`,
44
44
  ` <changefreq>${esc(freq.value)}</changefreq>`,
45
45
  ` <priority>${esc(priority.value || "0.5")}</priority>`,
46
- " </url>",
47
- ].join("\n");
46
+ " </url>"].join("\n");
48
47
  });
49
48
  result.write([
50
49
  '<?xml version="1.0" encoding="UTF-8"?>',
51
50
  '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
52
51
  ...entries,
53
- "</urlset>",
54
- ].join("\n"));
52
+ "</urlset>"].join("\n"));
55
53
  }
56
54
 
57
55
  for (const input of [base, pages, priority]) input.addEventListener("input", build);
@@ -71,8 +69,7 @@ export function mount(slot) {
71
69
  foot: actions(
72
70
  downloadButton(() => result.textContent, "sitemap.xml", "Save XML", "application/xml"),
73
71
  copyButton(() => result.textContent, "Copy XML", "fill")
74
- ),
75
- });
72
+ )});
76
73
  slot.append(card.node);
77
74
  build();
78
75
 
@@ -14,15 +14,14 @@ function slugify(text, sep) {
14
14
 
15
15
  export function mount(slot) {
16
16
  const source = textInput({ value: "Hello mevebu world", placeholder: "Title", "aria-label": "Title" });
17
- const result = textInput({ readonly: true, class: "mvb-in mvb-in--mono", "aria-label": "Slug" });
17
+ const result = textInput({ readonly: true, mono: true, "aria-label": "Slug" });
18
18
 
19
19
  let sep = "-";
20
20
  const chips = chipRow(
21
21
  [
22
22
  { value: "-", label: "Hyphen" },
23
23
  { value: "_", label: "Underscore" },
24
- { value: ".", label: "Dot" },
25
- ],
24
+ { value: ".", label: "Dot" }],
26
25
  { value: "-", label: "Separator", onChange: (value) => { sep = value; run(); } }
27
26
  );
28
27
 
@@ -37,8 +36,7 @@ export function mount(slot) {
37
36
  stack(panel("Title", source), panel("Separator", chips)),
38
37
  stack(panel("Slug", result))
39
38
  ),
40
- foot: actions(copyButton(() => result.value, "Copy slug", "fill")),
41
- });
39
+ foot: actions(copyButton(() => result.value, "Copy slug", "fill"))});
42
40
  slot.append(card.node);
43
41
  run();
44
42
 
@@ -20,19 +20,19 @@ function slugify(text) {
20
20
  }
21
21
 
22
22
  const kvRow = (key, value) =>
23
- el("div", { class: "mvb-kv__row" }, el("span", { class: "mvb-kv__k" }, key), el("span", { class: "mvb-kv__v" }, value));
23
+ el("div", { class: "grid gap-3 bg-deck px-3 py-[9px] [grid-template-columns:minmax(90px,34%)_minmax(0,1fr)]" }, el("span", { class: "wrap-anywhere text-2xs font-semibold text-ink-2" }, key), el("span", { class: "wrap-anywhere font-mono text-2xs" }, value));
24
24
 
25
25
  export function mount(slot) {
26
26
  const title = textInput({ value: "Best running shoes", "aria-label": "Title" });
27
27
  const base = textInput({ value: "https://example.com", "aria-label": "Base URL" });
28
28
  const error = notice("error");
29
29
 
30
- const slugValue = el("span", { class: "mvb-kv__v" }, "-");
31
- const urlValue = el("span", { class: "mvb-kv__v" }, "-");
32
- const lengthValue = el("span", { class: "mvb-kv__v" }, "-");
30
+ const slugValue = el("span", { class: "wrap-anywhere font-mono text-2xs" }, "-");
31
+ const urlValue = el("span", { class: "wrap-anywhere font-mono text-2xs" }, "-");
32
+ const lengthValue = el("span", { class: "wrap-anywhere font-mono text-2xs" }, "-");
33
33
  const kv = el(
34
34
  "div",
35
- { class: "mvb-kv" },
35
+ { class: "grid gap-px overflow-hidden rounded-r1 border border-orbit bg-orbit" },
36
36
  kvRow("Slug", slugValue),
37
37
  kvRow("URL", urlValue),
38
38
  kvRow("Length", lengthValue)
@@ -72,8 +72,7 @@ export function mount(slot) {
72
72
  foot: actions(
73
73
  copyButton(() => urlValue.textContent === "-" ? "" : urlValue.textContent, "Copy URL"),
74
74
  copyButton(() => slugValue.textContent === "-" ? "" : slugValue.textContent, "Copy slug", "fill")
75
- ),
76
- });
75
+ )});
77
76
  slot.append(card.node);
78
77
  run();
79
78
 
@@ -3,7 +3,7 @@ import { bench, stack, panel, textArea, chipRow, toggleRow, copyButton, actions,
3
3
 
4
4
  export function mount(slot) {
5
5
  const source = textArea({ rows: 12, "aria-label": "Text", placeholder: "Paste your list, one entry per line" });
6
- const result = textArea({ rows: 12, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Result" });
6
+ const result = textArea({ rows: 12, readonly: true, "aria-label": "Result" });
7
7
 
8
8
  let direction = "asc";
9
9
  const dirChips = chipRow(
@@ -12,8 +12,7 @@ export function mount(slot) {
12
12
  );
13
13
  const options = toggleRow([
14
14
  { value: "ignore", label: "Ignore case", checked: false },
15
- { value: "numeric", label: "Numeric", checked: false },
16
- ]);
15
+ { value: "numeric", label: "Numeric", checked: false }]);
17
16
 
18
17
  function run() {
19
18
  const text = source.value;
@@ -43,8 +42,7 @@ export function mount(slot) {
43
42
  stack(panel("Direction", dirChips), options),
44
43
  stack(panel("Result", result))
45
44
  ),
46
- foot: actions(copyButton(() => result.value, "Copy result", "fill")),
47
- });
45
+ foot: actions(copyButton(() => result.value, "Copy result", "fill"))});
48
46
  slot.append(card.node);
49
47
  run();
50
48
 
@@ -6,8 +6,7 @@ const UNITS = [
6
6
  { value: "kmh", label: "Kilometer per hour", factor: 0.2777777777777778 },
7
7
  { value: "mph", label: "Mile per hour", factor: 0.44704 },
8
8
  { value: "fps", label: "Foot per second", factor: 0.3048 },
9
- { value: "kn", label: "Knot", factor: 0.5144444444444445 },
10
- ];
9
+ { value: "kn", label: "Knot", factor: 0.5144444444444445 }];
11
10
 
12
11
  const byId = Object.fromEntries(UNITS.map((unit) => [unit.value, unit]));
13
12
  const options = UNITS.map(({ value, label }) => ({ value, label }));
@@ -24,11 +23,11 @@ export function mount(slot) {
24
23
  const value = numberInput({ value: "1", step: "any", "aria-label": "Value" });
25
24
  const from = selectInput(options, { value: "kmh", "aria-label": "From unit" });
26
25
  const to = selectInput(options, { value: "mph", "aria-label": "To unit" });
27
- const result = el("span", { class: "mvb-stat__v" }, "-");
28
- const stats = el("div", { class: "mvb-stats" }, el("div", { class: "mvb-stat" }, el("span", { class: "mvb-stat__k" }, "Result"), result));
29
- const factor = el("p", { class: "mvb-note" });
26
+ const result = el("span", { class: "wrap-anywhere text-xl font-bold tracking-[-0.02em] tabular-nums" }, "-");
27
+ const stats = el("div", { class: "grid gap-2.5 [grid-template-columns:repeat(auto-fit,minmax(120px,1fr))]" }, el("div", { class: "grid gap-0.5 rounded-r1 border border-orbit bg-deck-2 px-3.5 py-3" }, el("span", { class: "text-2xs font-semibold text-ink-3" }, "Result"), result));
28
+ const factor = el("p", { class: "border-s-[3px] border-transparent ps-2 text-sm text-ink-3" });
30
29
  const error = notice("error");
31
- const scroll = el("div", { class: "mvb-scroll" });
30
+ const scroll = el("div", { class: "max-h-80 overflow-auto" });
32
31
  let text = "";
33
32
 
34
33
  function run() {
@@ -48,7 +47,7 @@ export function mount(slot) {
48
47
  const out = (n * a.factor) / b.factor;
49
48
  result.textContent = `${fmt(out)} ${b.value}`;
50
49
  factor.textContent = `1 ${a.value} = ${fmt(a.factor / b.factor)} ${b.value}`;
51
- scroll.replaceChildren(el("table", { class: "mvb-ttable" },
50
+ scroll.replaceChildren(el("table", { class: "w-full border-collapse font-mono text-sm [&_th]:wrap-anywhere [&_td]:wrap-anywhere [&_th]:border-b [&_td]:border-b [&_th]:border-orbit [&_td]:border-orbit [&_th]:px-2.5 [&_td]:px-2.5 [&_th]:py-2 [&_td]:py-2 [&_th]:text-start [&_td]:text-start [&_th]:font-sans [&_th]:text-2xs [&_th]:font-semibold [&_th]:text-ink-2 [&_tbody>tr:last-child>th]:border-b-0 [&_tbody>tr:last-child>td]:border-b-0" },
52
51
  el("thead", {}, el("tr", {}, el("th", {}, "Unit"), el("th", {}, "Value"))),
53
52
  el("tbody", {}, ...UNITS.map((unit) => el("tr", {},
54
53
  el("td", {}, unit.label),
@@ -72,8 +71,7 @@ export function mount(slot) {
72
71
  stack(panel("Value", value), panel("From", from), panel("To", to), error),
73
72
  stack(panel("Result", stats), factor, panel("All units", scroll))
74
73
  ),
75
- foot: actions(swap, copyButton(() => text, "Copy result", "fill")),
76
- });
74
+ foot: actions(swap, copyButton(() => text, "Copy result", "fill"))});
77
75
  slot.append(card.node);
78
76
  run();
79
77
 
@@ -2,7 +2,9 @@
2
2
  // spin, driven by the browser random source, so every option has equal odds.
3
3
  import { el, toast, copyButton, randomInt, bench, stage, stack, panel, textArea, button, readPin, writePin, toolcard, actions } from "../core.js";
4
4
 
5
- const SEGMENTS = ["#2456D6", "#151D2E", "#5F8BFF", "#4B5871", "#1A43AD", "#69748C", "#1D2A4D", "#C2CBDF"];
5
+ // The slice palette. A green ramp that alternates dark and light, so two
6
+ // neighbouring slices never read as one wedge and every label stays legible.
7
+ const SEGMENTS = ["#0f6b4a", "#e0f0e7", "#0a5138", "#bfcac1", "#35805f", "#1d3b2c", "#8fd0b3", "#0b2a1e"];
6
8
 
7
9
  /**
8
10
  * The slice label, in whichever ink reads better on that fill. Every fill in the
@@ -12,7 +14,7 @@ const SEGMENTS = ["#2456D6", "#151D2E", "#5F8BFF", "#4B5871", "#1A43AD", "#69748
12
14
  function textOn(hex) {
13
15
  const linear = (value) => (value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4);
14
16
  const [r, g, b] = [1, 3, 5].map((at) => linear(parseInt(hex.slice(at, at + 2), 16) / 255));
15
- return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.2056 ? "#151D2E" : "#FFFFFF";
17
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.2056 ? "#101a15" : "#FFFFFF";
16
18
  }
17
19
 
18
20
  function entriesFrom(text) {
@@ -24,15 +26,15 @@ export function mount(slot) {
24
26
  const saved = readPin(PIN, { entries: "Ana\nBudi\nCitra\nDimas\nElena\nFajar" });
25
27
 
26
28
  const listField = textArea({ value: saved.entries ?? "", "aria-label": "Options, one per line", placeholder: "One option per line" });
27
- const canvas = el("canvas", { class: "mvb-wheel", width: 360, height: 360, role: "img", "aria-label": "The wheel" });
29
+ const canvas = el("canvas", { class: "h-auto w-[min(320px,100%)]", width: 360, height: 360, role: "img", "aria-label": "The wheel" });
28
30
  const context = canvas.getContext("2d");
29
- const historyBox = el("div", { class: "mvb-wheel__history", role: "list" });
31
+ const historyBox = el("div", { class: "flex max-w-[340px] flex-wrap justify-center gap-1.5", role: "list" });
30
32
 
31
33
  const spinButton = button("Spin", { icon: "rotate-cw", kind: "fill" });
32
34
  const clearButton = button("Clear past picks", { icon: "trash", kind: "quiet" });
33
35
  const copyWinner = copyButton(() => winner, "Copy winner");
34
36
 
35
- const status = el("div", { class: "mvb-wheel__status", "aria-live": "polite" }, "Add options, then spin.");
37
+ const status = el("div", { class: "text-balance text-xl font-bold tracking-[-0.01em]", "aria-live": "polite" }, "Add options, then spin.");
36
38
 
37
39
  let rotation = 0;
38
40
  let busy = false;
@@ -45,15 +47,17 @@ export function mount(slot) {
45
47
  const center = size / 2;
46
48
  const radius = center - 12;
47
49
  const root = getComputedStyle(document.documentElement);
48
- const ground = root.getPropertyValue("--mvb-void").trim() || "#fff";
49
- const orbit = root.getPropertyValue("--mvb-orbit-hard").trim() || "#888";
50
- const faint = root.getPropertyValue("--mvb-ink-3").trim() || "#888";
51
- const comet = root.getPropertyValue("--mvb-comet").trim() || "#2456D6";
50
+ const ground = root.getPropertyValue("--void").trim() || "#fff";
51
+ const orbit = root.getPropertyValue("--orbit-hard").trim() || "#888";
52
+ const faint = root.getPropertyValue("--ink-3").trim() || "#888";
53
+ const jade = root.getPropertyValue("--jade").trim() || "#0f6b4a";
54
+ // Canvas text is outside the stylesheet, so it reads the same stack the page uses.
55
+ const stack = root.getPropertyValue("--sans").trim() || "system-ui, sans-serif";
52
56
 
53
57
  context.clearRect(0, 0, size, size);
54
58
  if (entries.length < 2) {
55
59
  context.fillStyle = faint;
56
- context.font = "15px system-ui";
60
+ context.font = `15px ${stack}`;
57
61
  context.textAlign = "center";
58
62
  context.fillText(entries.length ? "Add one more option to spin" : "Type options to spin", center, center);
59
63
  status.textContent = entries.length ? "One more option to go." : "Add options, then spin.";
@@ -73,7 +77,7 @@ export function mount(slot) {
73
77
  context.rotate(a0 + slice / 2);
74
78
  context.textAlign = "right";
75
79
  context.fillStyle = textOn(SEGMENTS[i % SEGMENTS.length]);
76
- context.font = "600 13px system-ui, sans-serif";
80
+ context.font = `600 13px ${stack}`;
77
81
  const label = entry.length > 13 ? `${entry.slice(0, 12)}…` : entry;
78
82
  context.fillText(label, radius - 12, 4);
79
83
  context.restore();
@@ -90,7 +94,7 @@ export function mount(slot) {
90
94
  context.lineTo(center + 8, 2);
91
95
  context.lineTo(center, 20);
92
96
  context.closePath();
93
- context.fillStyle = comet;
97
+ context.fillStyle = jade;
94
98
  context.fill();
95
99
  if (status.dataset.winner !== "1") status.textContent = "Add options, then spin.";
96
100
  }
@@ -123,7 +127,7 @@ export function mount(slot) {
123
127
  status.dataset.winner = "1";
124
128
  winner = entries[winnerIndex];
125
129
  status.textContent = winner;
126
- const chip = el("span", { class: "mvb-wheel__chip", role: "listitem" }, winner);
130
+ const chip = el("span", { class: "rounded-full border border-orbit bg-deck-2 px-2.5 py-1 text-2xs font-semibold", role: "listitem" }, winner);
127
131
  historyBox.prepend(chip);
128
132
  while (historyBox.children.length > 12) historyBox.lastChild.remove();
129
133
  }
@@ -150,8 +154,7 @@ export function mount(slot) {
150
154
  stack(panel("Options, one per line", listField)),
151
155
  stage(canvas, status, historyBox)
152
156
  ),
153
- foot: actions(copyWinner, clearButton, spinButton),
154
- });
157
+ foot: actions(copyWinner, clearButton, spinButton)});
155
158
  slot.append(card.node);
156
159
  draw();
157
160
 
@@ -159,8 +162,7 @@ export function mount(slot) {
159
162
  unmount() {
160
163
  cancelAnimationFrame(frame);
161
164
  busy = false;
162
- },
163
- };
165
+ }};
164
166
  }
165
167
 
166
168
  export default { mount };
@@ -4,8 +4,7 @@ import { bench, stack, panel, textArea, notice, switchControl, copyButton, actio
4
4
  const CLAUSES = [
5
5
  "SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "HAVING", "LIMIT", "OFFSET", "VALUES", "SET",
6
6
  "INSERT INTO", "UPDATE", "DELETE FROM", "INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "FULL JOIN",
7
- "CROSS JOIN", "JOIN", "UNION ALL", "UNION", "CREATE TABLE", "ALTER TABLE", "DROP TABLE", "WITH", "RETURNING",
8
- ];
7
+ "CROSS JOIN", "JOIN", "UNION ALL", "UNION", "CREATE TABLE", "ALTER TABLE", "DROP TABLE", "WITH", "RETURNING"];
9
8
  const WORDS = [...CLAUSES, "AND", "OR", "ON", "AS", "IN", "IS", "NOT", "NULL", "LIKE", "BETWEEN", "DISTINCT", "ASC", "DESC", "CASE", "WHEN", "THEN", "ELSE", "END"];
10
9
 
11
10
  const toPattern = (word) => word.replace(/ /g, "\\s+");
@@ -33,18 +32,16 @@ function format(sql, upper) {
33
32
  out = out.replace(clauseRe, "\n$1");
34
33
  out = out.replace(/\s*\b(AND|OR)\b/gi, "\n $1");
35
34
  out = out.replace(/\(\s+/g, "(").replace(/\s+\)/g, ")");
36
- out = out.replace(/\n{3,}/g, "\n\n").trim();
35
+ out = out.replace(/\n{3}/g, "\n\n").trim();
37
36
  return restore(out, store);
38
37
  }
39
38
 
40
39
  export function mount(slot) {
41
40
  const source = textArea({
42
41
  rows: 12,
43
- class: "mvb-in mvb-in--area mvb-in--mono",
44
42
  "aria-label": "SQL",
45
- value: "select id, name from users where active = true and age > 18 order by name",
46
- });
47
- const result = textArea({ rows: 12, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Formatted SQL" });
43
+ value: "select id, name from users where active = true and age > 18 order by name"});
44
+ const result = textArea({ rows: 12, readonly: true, "aria-label": "Formatted SQL" });
48
45
  const error = notice("error");
49
46
  let upper = true;
50
47
 
@@ -67,8 +64,7 @@ export function mount(slot) {
67
64
  stack(panel("Options", casing), panel("SQL", source), error),
68
65
  stack(panel("Formatted", result))
69
66
  ),
70
- foot: actions(copyButton(() => result.value, "Copy query", "fill")),
71
- });
67
+ foot: actions(copyButton(() => result.value, "Copy query", "fill"))});
72
68
  slot.append(card.node);
73
69
  run();
74
70
 
@@ -4,7 +4,7 @@ import { el, bench, stack, panel, textArea, switchControl, copyButton, actions,
4
4
  const bytes = (text) => new TextEncoder().encode(text).length;
5
5
 
6
6
  const tile = (key, value) =>
7
- el("div", { class: "mvb-stat" }, el("span", { class: "mvb-stat__v" }, value), el("span", { class: "mvb-stat__k" }, key));
7
+ el("div", { class: "grid gap-0.5 rounded-r1 border border-orbit bg-deck-2 px-3.5 py-3" }, el("span", { class: "wrap-anywhere text-xl font-bold tracking-[-0.02em] tabular-nums" }, value), el("span", { class: "text-2xs font-semibold text-ink-3" }, key));
8
8
 
9
9
  function minify(sql, stripComments) {
10
10
  let out = "";
@@ -59,12 +59,10 @@ function minify(sql, stripComments) {
59
59
  export function mount(slot) {
60
60
  const source = textArea({
61
61
  rows: 12,
62
- class: "mvb-in mvb-in--area mvb-in--mono",
63
62
  "aria-label": "SQL",
64
- value: "SELECT id,\n name\n FROM users\n WHERE active = true\n AND age > 18;",
65
- });
66
- const result = textArea({ rows: 12, readonly: true, class: "mvb-in mvb-in--area mvb-in--mono", "aria-label": "Minified SQL" });
67
- const stats = el("div", { class: "mvb-stats" });
63
+ value: "SELECT id,\n name\n FROM users\n WHERE active = true\n AND age > 18;"});
64
+ const result = textArea({ rows: 12, readonly: true, "aria-label": "Minified SQL" });
65
+ const stats = el("div", { class: "grid gap-2.5 [grid-template-columns:repeat(auto-fit,minmax(120px,1fr))]" });
68
66
  let stripComments = true;
69
67
 
70
68
  const comments = switchControl("Remove comments", { checked: true });
@@ -91,8 +89,7 @@ export function mount(slot) {
91
89
  stack(panel("Options", comments), panel("SQL", source)),
92
90
  stack(panel("Minified", result), stats)
93
91
  ),
94
- foot: actions(copyButton(() => result.value, "Copy query", "fill")),
95
- });
92
+ foot: actions(copyButton(() => result.value, "Copy query", "fill"))});
96
93
  slot.append(card.node);
97
94
  run();
98
95
 
@@ -12,8 +12,8 @@ function parseNumbers(text) {
12
12
  }
13
13
 
14
14
  function tile(label) {
15
- const value = el("span", { class: "mvb-stat__v" }, "-");
16
- const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
15
+ const value = el("span", { class: "wrap-anywhere text-xl font-bold tracking-[-0.02em] tabular-nums" }, "-");
16
+ 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));
17
17
  node.set = (text) => { value.textContent = text; };
18
18
  return node;
19
19
  }
@@ -24,7 +24,7 @@ export function mount(slot) {
24
24
  const variance = tile("Variance");
25
25
  const deviation = tile("Std deviation");
26
26
  const count = tile("Count");
27
- const stats = el("div", { class: "mvb-stats" }, mean, variance, deviation, count);
27
+ const stats = el("div", { class: "grid gap-2.5 [grid-template-columns:repeat(auto-fit,minmax(120px,1fr))]" }, mean, variance, deviation, count);
28
28
  const error = notice("error");
29
29
  let kind = "population";
30
30
  let text = "";
@@ -66,16 +66,14 @@ export function mount(slot) {
66
66
  const chips = chipRow(
67
67
  [
68
68
  { value: "population", label: "Population" },
69
- { value: "sample", label: "Sample" },
70
- ],
69
+ { value: "sample", label: "Sample" }],
71
70
  {
72
71
  value: kind,
73
72
  label: "Data set",
74
73
  onChange: (value) => {
75
74
  kind = value;
76
75
  run();
77
- },
78
- }
76
+ }}
79
77
  );
80
78
 
81
79
  source.addEventListener("input", run);
@@ -85,8 +83,7 @@ export function mount(slot) {
85
83
  stack(panel("Numbers", source), panel("Data set", chips), error),
86
84
  stack(stats)
87
85
  ),
88
- foot: actions(copyButton(() => text, "Copy summary", "fill")),
89
- });
86
+ foot: actions(copyButton(() => text, "Copy summary", "fill"))});
90
87
  slot.append(card.node);
91
88
  run();
92
89
 
@@ -15,7 +15,7 @@ function format(ms) {
15
15
 
16
16
  export function mount(slot) {
17
17
  const display = output({ rows: 2 });
18
- const listWrap = el("div", { class: "mvb-scroll" });
18
+ const listWrap = el("div", { class: "max-h-80 overflow-auto" });
19
19
 
20
20
  const startButton = button("Start", { icon: "play", kind: "fill" });
21
21
  const lapButton = button("Lap", { icon: "plus" });
@@ -59,7 +59,7 @@ export function mount(slot) {
59
59
  el("td", {}, format(total))
60
60
  );
61
61
  });
62
- listWrap.replaceChildren(el("table", { class: "mvb-ttable" },
62
+ listWrap.replaceChildren(el("table", { class: "w-full border-collapse font-mono text-sm [&_th]:wrap-anywhere [&_td]:wrap-anywhere [&_th]:border-b [&_td]:border-b [&_th]:border-orbit [&_td]:border-orbit [&_th]:px-2.5 [&_td]:px-2.5 [&_th]:py-2 [&_td]:py-2 [&_th]:text-start [&_td]:text-start [&_th]:font-sans [&_th]:text-2xs [&_th]:font-semibold [&_th]:text-ink-2 [&_tbody>tr:last-child>th]:border-b-0 [&_tbody>tr:last-child>td]:border-b-0" },
63
63
  el("thead", {}, el("tr", {}, el("th", {}, "#"), el("th", {}, "Lap"), el("th", {}, "Total"))),
64
64
  el("tbody", {}, ...rows)
65
65
  ));
@@ -115,8 +115,7 @@ export function mount(slot) {
115
115
  stack(panel("Laps", listWrap)),
116
116
  stack(display)
117
117
  ),
118
- foot: actions(copyLaps, resetButton, lapButton, startButton),
119
- });
118
+ foot: actions(copyLaps, resetButton, lapButton, startButton)});
120
119
  slot.append(card.node);
121
120
  paint();
122
121
  renderLaps();
@@ -126,8 +125,7 @@ export function mount(slot) {
126
125
  unmount() {
127
126
  cancelAnimationFrame(frame);
128
127
  running = false;
129
- },
130
- };
128
+ }};
131
129
  }
132
130
 
133
131
  export default { mount };
@@ -9,16 +9,16 @@ export function mount(slot) {
9
9
  let list = Array.isArray(saved) ? saved : [];
10
10
 
11
11
  const name = textInput({ placeholder: "Topic", "aria-label": "Topic" });
12
- const date = el("input", { class: "mvb-in", type: "date", "aria-label": "Session date" });
13
- const listWrap = el("div", { class: "mvb-scroll" });
14
- const stats = el("div", { class: "mvb-stats" });
12
+ const date = el("input", { class: "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 w-full min-h-[var(--ctl)] px-3 py-2.5 text-base", type: "date", "aria-label": "Session date" });
13
+ const listWrap = el("div", { class: "max-h-80 overflow-auto" });
14
+ const stats = el("div", { class: "grid gap-2.5 [grid-template-columns:repeat(auto-fit,minmax(120px,1fr))]" });
15
15
 
16
16
  const addButton = button("Add", { icon: "plus", kind: "fill" });
17
17
 
18
18
  const save = () => writePin(PIN, list);
19
- const tile = (key, value) => el("div", { class: "mvb-stat" },
20
- el("span", { class: "mvb-stat__v" }, String(value)),
21
- el("span", { class: "mvb-stat__k" }, key));
19
+ const tile = (key, value) => el("div", { class: "grid gap-0.5 rounded-r1 border border-orbit bg-deck-2 px-3.5 py-3" },
20
+ el("span", { class: "wrap-anywhere text-xl font-bold tracking-[-0.02em] tabular-nums" }, String(value)),
21
+ el("span", { class: "text-2xs font-semibold text-ink-3" }, key));
22
22
 
23
23
  const sorted = () => list.slice().sort((a, b) => (a.date || "9999").localeCompare(b.date || "9999"));
24
24
 
@@ -50,7 +50,7 @@ export function mount(slot) {
50
50
  if (!rows.length) {
51
51
  listWrap.replaceChildren(blank("calendar-days", "No topics yet."));
52
52
  } else {
53
- listWrap.replaceChildren(el("table", { class: "mvb-ttable" },
53
+ listWrap.replaceChildren(el("table", { class: "w-full border-collapse font-mono text-sm [&_th]:wrap-anywhere [&_td]:wrap-anywhere [&_th]:border-b [&_td]:border-b [&_th]:border-orbit [&_td]:border-orbit [&_th]:px-2.5 [&_td]:px-2.5 [&_th]:py-2 [&_td]:py-2 [&_th]:text-start [&_td]:text-start [&_th]:font-sans [&_th]:text-2xs [&_th]:font-semibold [&_th]:text-ink-2 [&_tbody>tr:last-child>th]:border-b-0 [&_tbody>tr:last-child>td]:border-b-0" },
54
54
  el("thead", {}, el("tr", {}, el("th", {}, "Topic"), el("th", {}, "Date"), el("th", {}, ""))),
55
55
  el("tbody", {}, ...rows.map(rowNode))));
56
56
  }
@@ -77,8 +77,7 @@ export function mount(slot) {
77
77
  stack(panel("Topic", name), panel("Session date", date)),
78
78
  stack(stats, panel("Plan", listWrap))
79
79
  ),
80
- foot: actions(copyButton(() => sorted().map((item) => `${item.date || "No date"} ${item.done ? "[x]" : "[ ]"} ${item.name}`).join("\n"), "Copy plan"), addButton),
81
- });
80
+ foot: actions(copyButton(() => sorted().map((item) => `${item.date || "No date"} ${item.done ? "[x]" : "[ ]"} ${item.name}`).join("\n"), "Copy plan"), addButton)});
82
81
  slot.append(card.node);
83
82
  render();
84
83
 
@@ -5,8 +5,8 @@ const isTiming = (line) => line.includes("-->");
5
5
  const isIndex = (line) => /^\d+$/.test(line.trim());
6
6
 
7
7
  function tile(label) {
8
- const value = el("span", { class: "mvb-stat__v" }, "0");
9
- const node = el("div", { class: "mvb-stat" }, value, el("span", { class: "mvb-stat__k" }, label));
8
+ const value = el("span", { class: "wrap-anywhere text-xl font-bold tracking-[-0.02em] tabular-nums" }, "0");
9
+ 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));
10
10
  node.set = (text) => { value.textContent = text; };
11
11
  return node;
12
12
  }
@@ -18,9 +18,9 @@ export function mount(slot) {
18
18
  const linesTile = tile("Lines");
19
19
  const longTile = tile("Long lines");
20
20
  const longestTile = tile("Longest");
21
- const stats = el("div", { class: "mvb-stats" }, linesTile, longTile, longestTile);
21
+ const stats = el("div", { class: "grid gap-2.5 [grid-template-columns:repeat(auto-fit,minmax(120px,1fr))]" }, linesTile, longTile, longestTile);
22
22
 
23
- const longWrap = el("div", { class: "mvb-scroll" });
23
+ const longWrap = el("div", { class: "max-h-80 overflow-auto" });
24
24
  const out = output({ rows: 10 });
25
25
  const note = notice("note");
26
26
 
@@ -41,16 +41,16 @@ export function mount(slot) {
41
41
  if (!long.length) {
42
42
  longWrap.replaceChildren(blank("film", "No line runs past the limit."));
43
43
  } else {
44
- longWrap.replaceChildren(el("div", { class: "mvb-kv" },
45
- ...long.map((line) => el("div", { class: "mvb-kv__row" },
46
- el("span", { class: "mvb-kv__k" }, `${line.length}`),
47
- el("span", { class: "mvb-kv__v" }, line)))));
44
+ longWrap.replaceChildren(el("div", { class: "grid gap-px overflow-hidden rounded-r1 border border-orbit bg-orbit" },
45
+ ...long.map((line) => el("div", { class: "grid gap-3 bg-deck px-3 py-[9px] [grid-template-columns:minmax(90px,34%)_minmax(0,1fr)]" },
46
+ el("span", { class: "wrap-anywhere text-2xs font-semibold text-ink-2" }, `${line.length}`),
47
+ el("span", { class: "wrap-anywhere font-mono text-2xs" }, line)))));
48
48
  }
49
49
 
50
50
  tidied = raw
51
51
  .map((line) => line.replace(/\s+$/, ""))
52
52
  .join("\n")
53
- .replace(/\n{3,}/g, "\n\n")
53
+ .replace(/\n{3}/g, "\n\n")
54
54
  .replace(/^\n+|\n+$/g, "");
55
55
  out.write(tidied);
56
56
  }
@@ -63,8 +63,7 @@ export function mount(slot) {
63
63
  stack(panel("Subtitles", source), panel("Maximum line length", max), note),
64
64
  stack(stats, panel("Long lines", longWrap), panel("Tidied", out))
65
65
  ),
66
- foot: actions(copyButton(() => tidied, "Copy text", "fill")),
67
- });
66
+ foot: actions(copyButton(() => tidied, "Copy text", "fill"))});
68
67
  slot.append(card.node);
69
68
  run();
70
69