normalize-metrics 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,24 +1,52 @@
1
1
  # normalize-metrics
2
2
 
3
- Rewrite font vertical metrics so a word sits in the box.
3
+ Fix font vertical metrics so a word sits in the box.
4
4
 
5
- The text within a button is not always centered. That is the font’s fault, not yours. You can already trim the line box in CSS with `text-box: trim-both cap alphabetic`. The default is still the untrimmed box, and most type ramps and component kits were written for that default.
5
+ The text within a button is not always centered. That is the font’s fault, not yours. You can trim the line box in CSS with `text-box: trim-both cap alphabetic`. The default is still the untrimmed box, and most type ramps and component kits were written for that default.
6
6
 
7
- This CLI rewrites the font for that default case. It only changes how the word sits in the box not the letters, the look of the font, or anything else.
7
+ Trim cuts descenders. An input, a textarea, or any box that clips overflow will cut g, p, and y once the line stops at the cap and the baseline. Turn trim off on those controls and you are back to the font’s real metrics.
8
+
9
+ For the web, fix those metrics in CSS and leave the file alone. Rewriting the font file is the other path, mostly for typographers who need that box everywhere the font is used, not only in one stylesheet.
10
+
11
+ ## Install
8
12
 
9
13
  ```
10
14
  npm i -g normalize-metrics
11
- normalize-metrics Inter-Regular.otf
12
- normalize-metrics ./fonts
15
+ ```
16
+
17
+ Or in a repo:
18
+
19
+ ```
20
+ npm i -D normalize-metrics
21
+ ```
22
+
23
+ ## CSS
24
+
25
+ For the web, write a stylesheet and leave the font files alone. `--css` saves `style.css` with `ascent-override`, `descent-override`, and `line-gap-override`.
26
+
27
+ A single file, a set of files, or a folder:
28
+
29
+ ```
30
+ normalize-metrics Inter-Regular.woff2 Inter-Regular.otf --css
31
+ normalize-metrics ./fonts --css
13
32
  ```
14
33
 
15
34
  Or once, without installing:
16
35
 
17
36
  ```
18
- npx normalize-metrics ./fonts
37
+ npx normalize-metrics ./fonts --css
19
38
  ```
20
39
 
21
- A folder means every `.otf`, `.ttf`, `.woff`, and `.woff2` inside it. It writes a normalized copy next to the original. Fonts that already have good metrics are left alone. Variable fonts and TTC collections are skipped in v1.
40
+ The stylesheet points at the original files. Do not add those descriptors to an already rewritten font.
41
+
42
+ ## Rewrite
43
+
44
+ For typographers, and for anywhere a stylesheet cannot go. A folder means every `.otf`, `.ttf`, `.woff`, and `.woff2` inside it. It writes a normalized copy next to the original. Fonts that already have good metrics are left alone. Variable fonts and TTC collections are skipped in v1.
45
+
46
+ ```
47
+ normalize-metrics Inter-Regular.woff2 Inter-Regular.otf
48
+ normalize-metrics ./fonts
49
+ ```
22
50
 
23
51
  Rewriting a licensed font and redistributing the result may violate the EULA. This tool does not legalize the file.
24
52
 
@@ -30,13 +58,14 @@ Rewriting a licensed font and redistributing the result may violate the EULA. Th
30
58
  ## Usage
31
59
 
32
60
  ```
33
- normalize-metrics <file|folder> [options]
61
+ normalize-metrics <file|folder> [more files] [options]
34
62
  ```
35
63
 
36
64
  | Option | What it does |
37
65
  | --- | --- |
38
66
  | `--check` | Report fonts that are still off; write nothing; exit 1 if any are off |
39
67
  | `--dry-run` | Same report; write nothing |
68
+ | `--css [file]` | Write `ascent-override`, `descent-override`, and `line-gap-override` for the original files. Default file is `style.css`. Does not rewrite fonts |
40
69
  | `--in-place` | Overwrite the original (opt-in) |
41
70
  | `--out-dir <dir>` | Write copies into this directory |
42
71
  | `--suffix <text>` | Filename suffix for copies (default: `-normalized`) |
@@ -44,11 +73,7 @@ normalize-metrics <file|folder> [options]
44
73
 
45
74
  Never overwrites unless you pass `--in-place`.
46
75
 
47
- In a repo, add it as a dev dependency and list the folders in the config. `--check` reports fonts that are still off and exits without writing — useful in CI.
48
-
49
- ```
50
- npm i -D normalize-metrics
51
- ```
76
+ In a repo, list the folders in the config. `--check` reports fonts that are still off and exits without writing — useful in CI.
52
77
 
53
78
  ## Config
54
79
 
package/dist/args.js CHANGED
@@ -1,9 +1,10 @@
1
1
  export const HELP = `normalize-metrics — rewrite vertical metrics so a word sits in the box
2
2
 
3
3
  Usage:
4
- normalize-metrics <file|folder> [options]
4
+ normalize-metrics <file|folder> [more files] [options]
5
5
  normalize-metrics [options]
6
6
 
7
+ Pass one file, a set of files, or a folder.
7
8
  A folder means every .otf, .ttf, .woff, and .woff2 inside it.
8
9
  Writes a normalized copy next to the original. Never overwrites unless --in-place.
9
10
  Fonts that already have good metrics are left alone.
@@ -11,6 +12,7 @@ Fonts that already have good metrics are left alone.
11
12
  Options:
12
13
  --check report fonts that are still off; write nothing; exit 1 if any are off
13
14
  --dry-run same report; write nothing
15
+ --css [file] write metric overrides for the original files (default: style.css); do not rewrite fonts
14
16
  --in-place overwrite the original (opt-in)
15
17
  --out-dir <dir> write copies into this directory
16
18
  --suffix <text> filename suffix for copies (default: -normalized)
@@ -23,19 +25,23 @@ Config (normalize-metrics.config.json or "normalize-metrics" in package.json):
23
25
 
24
26
  Examples:
25
27
  normalize-metrics Inter-Regular.otf
28
+ normalize-metrics Inter-Regular.woff2 GT-America-Regular.otf --css
26
29
  normalize-metrics ./fonts
30
+ normalize-metrics ./fonts --css
27
31
  npx normalize-metrics ./fonts --check
28
32
  `;
29
33
  const FLAGS_WITH_VALUE = new Set(["--out-dir", "--suffix", "--config"]);
34
+ export const DEFAULT_CSS_FILE = "style.css";
30
35
  export function parseArgs(argv) {
31
36
  const args = {
32
- path: null,
37
+ paths: [],
33
38
  check: false,
34
39
  dryRun: false,
35
40
  inPlace: false,
36
41
  outDir: null,
37
42
  suffix: null,
38
43
  configPath: null,
44
+ cssPath: null,
39
45
  help: false,
40
46
  version: false,
41
47
  };
@@ -61,6 +67,17 @@ export function parseArgs(argv) {
61
67
  args.inPlace = true;
62
68
  continue;
63
69
  }
70
+ if (token === "--css") {
71
+ const value = argv[i + 1];
72
+ if (value && !value.startsWith("-")) {
73
+ args.cssPath = value;
74
+ i += 1;
75
+ }
76
+ else {
77
+ args.cssPath = DEFAULT_CSS_FILE;
78
+ }
79
+ continue;
80
+ }
64
81
  if (FLAGS_WITH_VALUE.has(token)) {
65
82
  const value = argv[i + 1];
66
83
  if (!value || value.startsWith("-")) {
@@ -78,17 +95,22 @@ export function parseArgs(argv) {
78
95
  if (token.startsWith("-")) {
79
96
  throw new Error(`Unknown flag: ${token}`);
80
97
  }
81
- if (args.path) {
82
- throw new Error("Pass one file or one folder.");
83
- }
84
- args.path = token;
98
+ args.paths.push(token);
85
99
  }
86
100
  if (args.inPlace && args.outDir) {
87
101
  throw new Error("Use either --in-place or --out-dir, not both.");
88
102
  }
103
+ if (args.cssPath && !args.cssPath.toLowerCase().endsWith(".css")) {
104
+ throw new Error("--css needs a .css file.");
105
+ }
106
+ if (args.cssPath && (args.check || args.dryRun || args.inPlace || args.outDir)) {
107
+ throw new Error("--css writes a stylesheet and no font. Leave off --check, --dry-run, --in-place, and --out-dir.");
108
+ }
89
109
  return args;
90
110
  }
91
111
  export function modeOf(args) {
112
+ if (args.cssPath)
113
+ return "css";
92
114
  if (args.check)
93
115
  return "check";
94
116
  if (args.dryRun)
package/dist/css.js ADDED
@@ -0,0 +1,134 @@
1
+ import { dirname, extname, relative } from "node:path";
2
+ const HEADER = `/* Generated by normalize-metrics.
3
+ Line-box overrides for the original files. Does not rewrite the fonts. */`;
4
+ export function metricPercent(units, upm) {
5
+ if (!Number.isFinite(upm) || upm <= 0) {
6
+ throw new Error("Font has no units per em.");
7
+ }
8
+ const trimmed = ((Math.abs(units) / upm) * 100).toFixed(4).replace(/\.?0+$/, "");
9
+ return `${trimmed}%`;
10
+ }
11
+ export function fontSrc(cssFile, fontFile) {
12
+ const rel = relative(dirname(cssFile), fontFile).split("\\").join("/");
13
+ if (!rel || rel.startsWith("."))
14
+ return rel || ".";
15
+ return `./${rel}`;
16
+ }
17
+ export function fontFormat(fontFile) {
18
+ switch (extname(fontFile).toLowerCase()) {
19
+ case ".woff2":
20
+ return "woff2";
21
+ case ".woff":
22
+ return "woff";
23
+ case ".otf":
24
+ return "opentype";
25
+ case ".ttf":
26
+ return "truetype";
27
+ default:
28
+ return null;
29
+ }
30
+ }
31
+ function cssString(value) {
32
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r?\n/g, " ");
33
+ return `"${escaped}"`;
34
+ }
35
+ function cssStyle(style) {
36
+ if (style === "italic" || style === "oblique")
37
+ return style;
38
+ return "normal";
39
+ }
40
+ function cssWeight(weight) {
41
+ if (!weight || weight < 1 || weight > 1000)
42
+ return 400;
43
+ return Math.round(weight);
44
+ }
45
+ const FORMAT_ORDER = ["woff2", "woff", "truetype", "opentype"];
46
+ function faceKey(face) {
47
+ return [
48
+ face.family,
49
+ cssWeight(face.weight),
50
+ cssStyle(face.style),
51
+ face.ascent,
52
+ face.descent,
53
+ face.lineGap,
54
+ face.upm,
55
+ ].join("\0");
56
+ }
57
+ function srcRank(fontFile) {
58
+ const format = fontFormat(fontFile);
59
+ const index = format ? FORMAT_ORDER.indexOf(format) : -1;
60
+ return index === -1 ? FORMAT_ORDER.length : index;
61
+ }
62
+ export function groupFaces(faces) {
63
+ const groups = [];
64
+ const indexByKey = new Map();
65
+ for (const face of faces) {
66
+ const key = faceKey(face);
67
+ const existing = indexByKey.get(key);
68
+ if (existing == null) {
69
+ indexByKey.set(key, groups.length);
70
+ groups.push([face]);
71
+ continue;
72
+ }
73
+ groups[existing].push(face);
74
+ }
75
+ return groups.map((group) => [...group].sort((a, b) => srcRank(a.fontPath) - srcRank(b.fontPath)));
76
+ }
77
+ function srcList(faces, cssFile) {
78
+ return faces
79
+ .map((face) => {
80
+ const url = `url(${cssString(fontSrc(cssFile, face.fontPath))})`;
81
+ const format = fontFormat(face.fontPath);
82
+ return format ? `${url} format(${cssString(format)})` : url;
83
+ })
84
+ .join(",\n ");
85
+ }
86
+ export function formatFontFace(faces, cssFile) {
87
+ const face = faces[0];
88
+ if (!face)
89
+ throw new Error("No font faces to write.");
90
+ return [
91
+ "@font-face {",
92
+ ` font-family: ${cssString(face.family)};`,
93
+ ` src: ${srcList(faces, cssFile)};`,
94
+ ` font-weight: ${cssWeight(face.weight)};`,
95
+ ` font-style: ${cssStyle(face.style)};`,
96
+ ` ascent-override: ${metricPercent(face.ascent, face.upm)};`,
97
+ ` descent-override: ${metricPercent(face.descent, face.upm)};`,
98
+ ` line-gap-override: ${metricPercent(face.lineGap, face.upm)};`,
99
+ "}",
100
+ ].join("\n");
101
+ }
102
+ export function formatStylesheet(faces, cssFile) {
103
+ const blocks = groupFaces(faces).map((group) => formatFontFace(group, cssFile));
104
+ return `${HEADER}\n\n${blocks.join("\n\n")}\n`;
105
+ }
106
+ export function cssFaces(files, outcomes) {
107
+ const faces = [];
108
+ files.forEach((file, index) => {
109
+ const outcome = outcomes[index];
110
+ if (!outcome || outcome.status !== "css")
111
+ return;
112
+ const after = outcome.result.after;
113
+ if (!after)
114
+ return;
115
+ faces.push(faceFromResult(file, outcome.result));
116
+ });
117
+ return faces;
118
+ }
119
+ function faceFromResult(file, result) {
120
+ const after = result.after;
121
+ if (!after) {
122
+ throw new Error(`No metrics for ${file.name}`);
123
+ }
124
+ return {
125
+ family: result.family?.trim() || file.name,
126
+ weight: result.weight ?? 400,
127
+ style: result.style ?? "normal",
128
+ fontPath: file.abs,
129
+ ascent: after.ascent,
130
+ descent: after.descent,
131
+ lineGap: after.lineGap,
132
+ upm: after.upm,
133
+ };
134
+ }
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- import { basename, resolve } from "node:path";
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { basename, dirname, resolve } from "node:path";
2
3
  import { pathToFileURL } from "node:url";
3
4
  import { HELP, modeOf, parseArgs } from "./args.js";
4
5
  import { emptyConfig, findConfig, mergeConfig } from "./config.js";
@@ -7,6 +8,7 @@ import { runEngine } from "./engine.js";
7
8
  import { formatOffset, outputPath } from "./paths.js";
8
9
  import { formatReports } from "./report.js";
9
10
  import { ProgressStack } from "./progress.js";
11
+ import { cssFaces, formatStylesheet } from "./css.js";
10
12
  import { EULA, invocationDir, packageVersion, resolvePython } from "./runtime.js";
11
13
  import { pathExists } from "./fs-exists.js";
12
14
  function phaseFromEngine(phase) {
@@ -58,8 +60,18 @@ async function processFile(file, options) {
58
60
  function alreadyGoodDetail(result) {
59
61
  return `already good ${formatOffset(result.before?.offset ?? result.after?.offset)}`;
60
62
  }
63
+ const CSS_NOTICE = "This stylesheet overrides the line box in CSS. It does not rewrite the font file.";
61
64
  function finishResult(id, dest, result, mode, stack) {
62
65
  const offset = formatOffset(result.before?.offset);
66
+ if (mode === "css") {
67
+ const good = result.off === false;
68
+ stack.update(id, {
69
+ percent: 100,
70
+ phase: good ? "ok" : "done",
71
+ detail: good ? alreadyGoodDetail(result) : `${offset} → ${formatOffset(result.after?.offset)}`,
72
+ });
73
+ return { status: "css", result };
74
+ }
63
75
  if (result.off === false) {
64
76
  stack.update(id, { percent: 100, phase: "ok", detail: alreadyGoodDetail(result) });
65
77
  return { status: "ok", result };
@@ -88,6 +100,7 @@ function summarize(outcomes, mode) {
88
100
  wrote: outcomes.filter((item) => item.status === "wrote").length,
89
101
  would: outcomes.filter((item) => item.status === "would-write").length,
90
102
  ok: outcomes.filter((item) => item.status === "ok").length,
103
+ css: outcomes.filter((item) => item.status === "css").length,
91
104
  off: outcomes.filter((item) => item.status === "off").length,
92
105
  skipped: outcomes.filter((item) => item.status === "skipped").length,
93
106
  failed: outcomes.filter((item) => item.status === "failed").length,
@@ -107,6 +120,9 @@ function summarize(outcomes, mode) {
107
120
  parts.push(`${counts.off} off`);
108
121
  parts.push(`${counts.ok} ok`);
109
122
  }
123
+ if (mode === "css") {
124
+ parts.push(`${counts.css} measured`);
125
+ }
110
126
  if (counts.skipped)
111
127
  parts.push(`${counts.skipped} skipped`);
112
128
  if (counts.failed)
@@ -118,22 +134,35 @@ function exitCode(outcomes, mode) {
118
134
  return 1;
119
135
  if (mode === "check" && outcomes.some((item) => item.status === "off"))
120
136
  return 1;
137
+ if (mode === "css" && !outcomes.some((item) => item.status === "css"))
138
+ return 1;
121
139
  if (outcomes.length === 0)
122
140
  return 1;
123
141
  return 0;
124
142
  }
125
143
  async function resolveTargets(args, cwd, config) {
126
- if (args.path) {
127
- const target = resolve(cwd, args.path);
128
- if (!(await pathExists(target))) {
129
- throw new Error(`Not found: ${args.path}`);
144
+ if (args.paths.length) {
145
+ const found = [];
146
+ const seen = new Set();
147
+ for (const entry of args.paths) {
148
+ const target = resolve(cwd, entry);
149
+ if (!(await pathExists(target))) {
150
+ throw new Error(`Not found: ${entry}`);
151
+ }
152
+ const files = await discover(target, config);
153
+ for (const file of files) {
154
+ if (seen.has(file.abs))
155
+ continue;
156
+ seen.add(file.abs);
157
+ found.push(file);
158
+ }
130
159
  }
131
- return discover(target, config);
160
+ return found;
132
161
  }
133
162
  if (config.include.length) {
134
163
  return discoverFromInclude(cwd, config);
135
164
  }
136
- throw new Error("Point it at a file or a folder. See --help.");
165
+ throw new Error("Point it at a file, a set of files, or a folder. See --help.");
137
166
  }
138
167
  export async function main(argv = process.argv.slice(2)) {
139
168
  let args;
@@ -161,7 +190,7 @@ export async function main(argv = process.argv.slice(2)) {
161
190
  console.error(error instanceof Error ? error.message : error);
162
191
  return 2;
163
192
  }
164
- if (!config.suffix && !args.inPlace) {
193
+ if (!config.suffix && !args.inPlace && !args.cssPath) {
165
194
  console.error("A suffix is required unless you pass --in-place. Default is -normalized.");
166
195
  return 2;
167
196
  }
@@ -193,7 +222,7 @@ export async function main(argv = process.argv.slice(2)) {
193
222
  };
194
223
  process.on("SIGINT", onInterrupt);
195
224
  process.on("SIGTERM", onInterrupt);
196
- stack.identify(files.map((file) => ({ id: file.abs, label: files.length > 1 ? file.rel : file.name })), EULA);
225
+ stack.identify(files.map((file) => ({ id: file.abs, label: files.length > 1 ? file.rel : file.name })), mode === "css" ? CSS_NOTICE : EULA);
197
226
  const outcomes = [];
198
227
  for (const file of files) {
199
228
  outcomes.push(await processFile(file, { python, mode, inPlace: args.inPlace, config, stack }));
@@ -204,6 +233,19 @@ export async function main(argv = process.argv.slice(2)) {
204
233
  const reports = formatReports(outcomes);
205
234
  if (reports)
206
235
  process.stdout.write(`\n${reports}\n`);
236
+ if (mode === "css" && args.cssPath) {
237
+ const faces = cssFaces(files, outcomes);
238
+ if (!faces.length) {
239
+ console.error("No faces to write.");
240
+ }
241
+ else {
242
+ const cssFile = resolve(cwd, args.cssPath);
243
+ await mkdir(dirname(cssFile), { recursive: true });
244
+ await writeFile(cssFile, formatStylesheet(faces, cssFile), "utf8");
245
+ const wrote = process.stderr.isTTY ? process.stderr : process.stdout;
246
+ wrote.write(`\nWrote ${cssFile}\n`);
247
+ }
248
+ }
207
249
  const summary = summarize(outcomes, mode);
208
250
  if (summary) {
209
251
  const stream = process.stderr.isTTY ? process.stderr : process.stdout;
package/lib/engine.py CHANGED
@@ -86,6 +86,13 @@ def measure_bbox(font: TTFont, progress: Progress | None = None) -> tuple[int, i
86
86
  return y_min, y_max
87
87
 
88
88
 
89
+ def units_percent(units: float, upm: int) -> float:
90
+ """CSS ascent-override / descent-override: |units| as a percent of the em."""
91
+ if upm <= 0:
92
+ return 0.0
93
+ return round(abs(units) / upm * 100, 1)
94
+
95
+
89
96
  def snapshot(font: TTFont, cap: int, x_height: int) -> dict:
90
97
  upm = font["head"].unitsPerEm
91
98
  hhea = font["hhea"]
@@ -108,10 +115,35 @@ def snapshot(font: TTFont, cap: int, x_height: int) -> dict:
108
115
  "below": round(below / upm * 1000, 1),
109
116
  "offset": round(offset_pm, 1),
110
117
  "centered": centered,
118
+ "ascentOverride": units_percent(hhea.ascent, upm),
119
+ "descentOverride": units_percent(hhea.descent, upm),
111
120
  "grade": grade,
112
121
  }
113
122
 
114
123
 
124
+ ITALIC = 1 << 0
125
+ OBLIQUE = 1 << 9
126
+
127
+
128
+ def face_style(font: TTFont) -> str:
129
+ selection = int(getattr(font["OS/2"], "fsSelection", 0))
130
+ if selection & ITALIC:
131
+ return "italic"
132
+ if selection & OBLIQUE:
133
+ return "oblique"
134
+ subfamily = (font["name"].getDebugName(2) or "").lower()
135
+ if "italic" in subfamily:
136
+ return "italic"
137
+ if "oblique" in subfamily:
138
+ return "oblique"
139
+ return "normal"
140
+
141
+
142
+ def face_weight(font: TTFont) -> int:
143
+ weight = int(getattr(font["OS/2"], "usWeightClass", 400) or 400)
144
+ return weight
145
+
146
+
115
147
  def set_name_suffix(font: TTFont, suffix: str = " Normalized") -> None:
116
148
  name = font["name"]
117
149
  for rec in list(name.names):
@@ -225,6 +257,8 @@ def normalize(font: TTFont, progress: Progress | None = None) -> dict:
225
257
  family = font["name"].getDebugName(1) or "Font"
226
258
  return {
227
259
  "family": family.replace(" Normalized", ""),
260
+ "weight": face_weight(font),
261
+ "style": face_style(font),
228
262
  "before": before,
229
263
  "after": after,
230
264
  "off": off,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "normalize-metrics",
3
- "version": "0.1.1",
4
- "description": "Rewrite font vertical metrics so a word sits in the box",
3
+ "version": "0.2.0",
4
+ "description": "Fix font vertical metrics so a word sits in the box, as CSS overrides or a rewritten file",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Oles Gergun",
@@ -9,12 +9,12 @@
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "git+https://github.com/gerguno/normalize-metrics-cli.git"
12
+ "url": "git+https://github.com/gerguno/normalize-metrics.git"
13
13
  },
14
14
  "bugs": {
15
- "url": "https://github.com/gerguno/normalize-metrics-cli/issues"
15
+ "url": "https://github.com/gerguno/normalize-metrics/issues"
16
16
  },
17
- "homepage": "https://github.com/gerguno/normalize-metrics-cli#readme",
17
+ "homepage": "https://normalize-metrics.olesgergun.com",
18
18
  "keywords": [
19
19
  "font",
20
20
  "fonts",
@@ -23,6 +23,8 @@
23
23
  "otf",
24
24
  "ttf",
25
25
  "woff",
26
+ "woff2",
27
+ "css",
26
28
  "typography",
27
29
  "cli"
28
30
  ],
@@ -41,14 +43,27 @@
41
43
  "access": "public"
42
44
  },
43
45
  "scripts": {
46
+ "dev": "next dev",
47
+ "build": "next build",
48
+ "start": "next start",
49
+ "deploy": "next build && next start -p 3014",
44
50
  "cli": "node --experimental-strip-types --experimental-default-type=module --no-warnings=ExperimentalWarning cli/index.ts",
45
- "test": "node --experimental-strip-types --experimental-default-type=module --test cli/*.test.ts",
46
- "build": "tsc -p cli/tsconfig.build.json && printf '{\"type\":\"module\"}\\n' > dist/package.json",
47
- "prepack": "npm run build",
48
- "prepublishOnly": "npm test"
51
+ "test:cli": "node --experimental-strip-types --experimental-default-type=module --test cli/*.test.ts",
52
+ "build:cli": "tsc -p cli/tsconfig.build.json && printf '{\"type\":\"module\"}\\n' > dist/package.json",
53
+ "prepack": "npm run build:cli",
54
+ "prepublishOnly": "npm run test:cli"
49
55
  },
50
56
  "devDependencies": {
51
57
  "@types/node": "^22.18.1",
58
+ "@types/react": "^19.1.12",
59
+ "@types/react-dom": "^19.1.9",
60
+ "motion": "^13.4.0",
61
+ "next": "^15.5.3",
62
+ "overlayscrollbars": "^2.16.0",
63
+ "overlayscrollbars-react": "^0.5.6",
64
+ "react": "^19.1.0",
65
+ "react-dom": "^19.1.0",
66
+ "sass": "^1.104.1",
52
67
  "typescript": "^5.9.2"
53
68
  }
54
69
  }