@ksp-gonogo/ui-kit 0.1.0 → 0.2.0-rc.10

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 (132) hide show
  1. package/LICENSE +21 -596
  2. package/README.md +151 -0
  3. package/dist/chunk-G3PMV62Z.js +36 -0
  4. package/dist/chunk-G3PMV62Z.js.map +1 -0
  5. package/dist/chunk-KPESGDJJ.js +8611 -0
  6. package/dist/chunk-KPESGDJJ.js.map +1 -0
  7. package/dist/chunk-SHAX2WJJ.js +18 -0
  8. package/dist/chunk-SHAX2WJJ.js.map +1 -0
  9. package/dist/dist-JSEJWZKX.js +546 -0
  10. package/dist/dist-JSEJWZKX.js.map +1 -0
  11. package/dist/gridUnits.d.ts +34 -0
  12. package/dist/gridUnits.js +14 -0
  13. package/dist/gridUnits.js.map +1 -0
  14. package/dist/guards.d.ts +110 -0
  15. package/dist/guards.js +171 -0
  16. package/dist/guards.js.map +1 -0
  17. package/dist/index.d.ts +9318 -25
  18. package/dist/index.js +8098 -28
  19. package/dist/index.js.map +1 -1
  20. package/dist/magic-string.es-J7BYFTTJ.js +1309 -0
  21. package/dist/magic-string.es-J7BYFTTJ.js.map +1 -0
  22. package/dist/testing.d.ts +470 -0
  23. package/dist/testing.js +16747 -0
  24. package/dist/testing.js.map +1 -0
  25. package/dist/tokens.css +1063 -29
  26. package/dist/units-Cl-8ZdJc.d.ts +270 -0
  27. package/package.json +57 -15
  28. package/dist/ActionButton.d.ts +0 -18
  29. package/dist/ActionButton.d.ts.map +0 -1
  30. package/dist/ActionButton.js +0 -56
  31. package/dist/ActionButton.js.map +0 -1
  32. package/dist/Badge.d.ts +0 -15
  33. package/dist/Badge.d.ts.map +0 -1
  34. package/dist/Badge.js +0 -60
  35. package/dist/Badge.js.map +0 -1
  36. package/dist/Box.d.ts +0 -19
  37. package/dist/Box.d.ts.map +0 -1
  38. package/dist/Box.js +0 -27
  39. package/dist/Box.js.map +0 -1
  40. package/dist/Card.d.ts +0 -11
  41. package/dist/Card.d.ts.map +0 -1
  42. package/dist/Card.js +0 -13
  43. package/dist/Card.js.map +0 -1
  44. package/dist/Cluster.d.ts +0 -18
  45. package/dist/Cluster.d.ts.map +0 -1
  46. package/dist/Cluster.js +0 -24
  47. package/dist/Cluster.js.map +0 -1
  48. package/dist/EmptyState.d.ts +0 -16
  49. package/dist/EmptyState.d.ts.map +0 -1
  50. package/dist/EmptyState.js +0 -37
  51. package/dist/EmptyState.js.map +0 -1
  52. package/dist/GonogoTokens.d.ts +0 -16
  53. package/dist/GonogoTokens.d.ts.map +0 -1
  54. package/dist/GonogoTokens.js +0 -108
  55. package/dist/GonogoTokens.js.map +0 -1
  56. package/dist/Grid.d.ts +0 -25
  57. package/dist/Grid.d.ts.map +0 -1
  58. package/dist/Grid.js +0 -23
  59. package/dist/Grid.js.map +0 -1
  60. package/dist/Inline.d.ts +0 -20
  61. package/dist/Inline.d.ts.map +0 -1
  62. package/dist/Inline.js +0 -17
  63. package/dist/Inline.js.map +0 -1
  64. package/dist/Panel.d.ts +0 -14
  65. package/dist/Panel.d.ts.map +0 -1
  66. package/dist/Panel.js +0 -145
  67. package/dist/Panel.js.map +0 -1
  68. package/dist/ProgressBar.d.ts +0 -15
  69. package/dist/ProgressBar.d.ts.map +0 -1
  70. package/dist/ProgressBar.js +0 -31
  71. package/dist/ProgressBar.js.map +0 -1
  72. package/dist/Readout.d.ts +0 -35
  73. package/dist/Readout.d.ts.map +0 -1
  74. package/dist/Readout.js +0 -114
  75. package/dist/Readout.js.map +0 -1
  76. package/dist/Row.d.ts +0 -22
  77. package/dist/Row.d.ts.map +0 -1
  78. package/dist/Row.js +0 -32
  79. package/dist/Row.js.map +0 -1
  80. package/dist/Section.d.ts +0 -15
  81. package/dist/Section.d.ts.map +0 -1
  82. package/dist/Section.js +0 -21
  83. package/dist/Section.js.map +0 -1
  84. package/dist/Spinner.d.ts +0 -18
  85. package/dist/Spinner.d.ts.map +0 -1
  86. package/dist/Spinner.js +0 -22
  87. package/dist/Spinner.js.map +0 -1
  88. package/dist/Stack.d.ts +0 -15
  89. package/dist/Stack.d.ts.map +0 -1
  90. package/dist/Stack.js +0 -17
  91. package/dist/Stack.js.map +0 -1
  92. package/dist/StatusIndicator.d.ts +0 -27
  93. package/dist/StatusIndicator.d.ts.map +0 -1
  94. package/dist/StatusIndicator.js +0 -78
  95. package/dist/StatusIndicator.js.map +0 -1
  96. package/dist/Truncate.d.ts +0 -7
  97. package/dist/Truncate.d.ts.map +0 -1
  98. package/dist/Truncate.js +0 -14
  99. package/dist/Truncate.js.map +0 -1
  100. package/dist/Value.d.ts +0 -24
  101. package/dist/Value.d.ts.map +0 -1
  102. package/dist/Value.js +0 -42
  103. package/dist/Value.js.map +0 -1
  104. package/dist/WidgetHeader.d.ts +0 -16
  105. package/dist/WidgetHeader.d.ts.map +0 -1
  106. package/dist/WidgetHeader.js +0 -40
  107. package/dist/WidgetHeader.js.map +0 -1
  108. package/dist/defaultDarkTheme.d.ts +0 -16
  109. package/dist/defaultDarkTheme.d.ts.map +0 -1
  110. package/dist/defaultDarkTheme.js +0 -96
  111. package/dist/defaultDarkTheme.js.map +0 -1
  112. package/dist/format.d.ts +0 -11
  113. package/dist/format.d.ts.map +0 -1
  114. package/dist/format.js +0 -12
  115. package/dist/format.js.map +0 -1
  116. package/dist/index.d.ts.map +0 -1
  117. package/dist/science/ScienceExperimentRow.d.ts +0 -43
  118. package/dist/science/ScienceExperimentRow.d.ts.map +0 -1
  119. package/dist/science/ScienceExperimentRow.js +0 -61
  120. package/dist/science/ScienceExperimentRow.js.map +0 -1
  121. package/dist/test/axe.d.ts +0 -3
  122. package/dist/test/axe.d.ts.map +0 -1
  123. package/dist/test/axe.js +0 -5
  124. package/dist/test/axe.js.map +0 -1
  125. package/dist/test/setup.d.ts +0 -2
  126. package/dist/test/setup.d.ts.map +0 -1
  127. package/dist/test/setup.js +0 -12
  128. package/dist/test/setup.js.map +0 -1
  129. package/dist/theme.d.ts +0 -116
  130. package/dist/theme.d.ts.map +0 -1
  131. package/dist/theme.js +0 -15
  132. package/dist/theme.js.map +0 -1
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Build-time guards an Uplink can run against its own source.
3
+ *
4
+ * Published as `@ksp-gonogo/ui-kit/guards`, separate from `./testing` because
5
+ * it reads the filesystem (`node:fs`), which would break a browser test runner.
6
+ *
7
+ * ## Why
8
+ *
9
+ * A widget that writes
10
+ *
11
+ * `${closingSpeed.toFixed(1)} m/s`
12
+ *
13
+ * type-checks and renders, but the symbol cannot be dimmed, cannot be kept off
14
+ * a line break, is announced as the letters "m", "slash", "s", and does not
15
+ * follow when the value's ladder changes rung. `<Unit>` solves all four.
16
+ *
17
+ * ## Using it
18
+ *
19
+ * One test file, anywhere your test runner will find it:
20
+ *
21
+ * ```ts
22
+ * import { expectNoHandTypedUnits } from "@ksp-gonogo/ui-kit/guards";
23
+ * import { it } from "vitest";
24
+ *
25
+ * it("renders units through <Unit>, not by typing the symbol", () => {
26
+ * expectNoHandTypedUnits({ dir: "src" });
27
+ * });
28
+ * ```
29
+ *
30
+ * On an existing codebase with offenders already in it, seed a `baseline` and
31
+ * lower it as you convert: a guessed conversion is worse than a hand-typed
32
+ * symbol, because it renders a confident wrong label.
33
+ */
34
+ /**
35
+ * The unit symbols the check looks for by default. It is a chosen list and
36
+ * not every unit: a symbol nobody types beside a number, or one short enough
37
+ * to turn up in ordinary prose, is left out.
38
+ *
39
+ * Pass your own through `symbols` when your Uplink registers a unit of its own.
40
+ *
41
+ * @category Testing
42
+ */
43
+ declare const HAND_TYPED_SYMBOLS: readonly string[];
44
+ /**
45
+ * Somewhere a unit symbol was typed next to a number.
46
+ *
47
+ * @category Testing
48
+ */
49
+ interface HandTypedUnit {
50
+ /** Path relative to the scanned directory, with `/` separators. */
51
+ file: string;
52
+ /** 1-based, so it is clickable in a terminal. */
53
+ line: number;
54
+ /** The offending source line, trimmed. */
55
+ source: string;
56
+ /** The symbol that matched. */
57
+ symbol: string;
58
+ }
59
+ /**
60
+ * Options for {@link findHandTypedUnits} and {@link expectNoHandTypedUnits}.
61
+ *
62
+ * @category Testing
63
+ */
64
+ interface HandTypedUnitOptions {
65
+ /** Directory to scan. Defaults to `src` under the current directory. */
66
+ dir?: string;
67
+ /** Override the symbols looked for. See {@link HAND_TYPED_SYMBOLS}. */
68
+ symbols?: readonly string[];
69
+ /**
70
+ * Per-file allowance, keyed by the path as it appears in a finding. A file
71
+ * at its entry passes, and a file below it throws, so an allowance left
72
+ * higher than needed is reported rather than left open.
73
+ */
74
+ baseline?: Readonly<Record<string, number>>;
75
+ /**
76
+ * Extra paths to skip, matched against the relative path. Tests, snapshots,
77
+ * `node_modules` and build output are already skipped.
78
+ */
79
+ ignore?: (file: string) => boolean;
80
+ }
81
+ /**
82
+ * Every place a unit symbol is typed next to a number, under `dir`.
83
+ *
84
+ * Returns findings rather than throwing, so a caller can report them their own
85
+ * way. {@link expectNoHandTypedUnits} is the assertion built on top.
86
+ *
87
+ * @category Testing
88
+ */
89
+ declare function findHandTypedUnits(options?: HandTypedUnitOptions): HandTypedUnit[];
90
+ /**
91
+ * Throws when a unit symbol is typed next to a number, with the fix in the
92
+ * message. Silent for a file at its `baseline` entry; a file below its entry
93
+ * throws, so an allowance left higher than needed cannot let the symbol come
94
+ * back unnoticed.
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * import { expectNoHandTypedUnits } from "@ksp-gonogo/ui-kit/guards";
99
+ * import { it } from "vitest";
100
+ *
101
+ * it("renders units through <Unit>, not by typing the symbol", () => {
102
+ * expectNoHandTypedUnits({ dir: "src" });
103
+ * });
104
+ * ```
105
+ *
106
+ * @category Testing
107
+ */
108
+ declare function expectNoHandTypedUnits(options?: HandTypedUnitOptions): void;
109
+
110
+ export { HAND_TYPED_SYMBOLS, type HandTypedUnit, type HandTypedUnitOptions, expectNoHandTypedUnits, findHandTypedUnits };
package/dist/guards.js ADDED
@@ -0,0 +1,171 @@
1
+ import "./chunk-G3PMV62Z.js";
2
+
3
+ // src/guards.ts
4
+ import { readdirSync, readFileSync, statSync } from "fs";
5
+ import { join, relative, sep } from "path";
6
+ var HAND_TYPED_SYMBOLS = [
7
+ "m/s\xB2",
8
+ "m/s",
9
+ "km/h",
10
+ "km",
11
+ "Mm",
12
+ "Gm",
13
+ "mm",
14
+ "cm",
15
+ "kPa",
16
+ "MPa",
17
+ "kN",
18
+ "MN",
19
+ "kW",
20
+ "MW",
21
+ "GW",
22
+ "kg",
23
+ "\xB0C",
24
+ "\xB0",
25
+ "%",
26
+ "m",
27
+ "s",
28
+ "t",
29
+ "N",
30
+ "W",
31
+ "f",
32
+ "sci",
33
+ "rep",
34
+ "Mit",
35
+ "deg",
36
+ "rad",
37
+ /*
38
+ * Matching is case-sensitive, since single letters like `m` and `W` would
39
+ * otherwise match prose and CSS, so a unit conventionally written both ways
40
+ * (`rpm`, `RPM`) is listed in both spellings.
41
+ */
42
+ "rpm",
43
+ "RPM",
44
+ "dB",
45
+ "rad/s",
46
+ "bit/s",
47
+ "J/s",
48
+ "N\xB7m",
49
+ "m\xB2",
50
+ "Pa",
51
+ "kg/m\xB3",
52
+ "g/m\xB3"
53
+ ];
54
+ var CSS_PROPERTY = /(width|height|left|top|right|bottom|transform|translate|inset|margin|padding|gap|flex|stroke|offset|dasharray|dashoffset|hsl|hsla|rgb|rgba)\s*[:(={]/i;
55
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
56
+ "node_modules",
57
+ "dist",
58
+ "build",
59
+ "coverage",
60
+ ".git",
61
+ ".turbo",
62
+ "__snapshots__"
63
+ ]);
64
+ var SOURCE = /\.(ts|tsx|js|jsx)$/;
65
+ function stripComments(source) {
66
+ const out = [];
67
+ let inBlock = false;
68
+ for (const line of source.split("\n")) {
69
+ let kept = "";
70
+ let i = 0;
71
+ while (i < line.length) {
72
+ if (inBlock) {
73
+ if (line.startsWith("*/", i)) {
74
+ inBlock = false;
75
+ i += 2;
76
+ } else {
77
+ i += 1;
78
+ }
79
+ continue;
80
+ }
81
+ if (line.startsWith("/*", i)) {
82
+ inBlock = true;
83
+ i += 2;
84
+ continue;
85
+ }
86
+ if (line.startsWith("//", i)) break;
87
+ kept += line[i];
88
+ i += 1;
89
+ }
90
+ out.push(kept);
91
+ }
92
+ return out;
93
+ }
94
+ function isTest(file) {
95
+ return file.includes(".test.") || file.includes(".spec.");
96
+ }
97
+ function walk(root, dir, out) {
98
+ for (const entry of readdirSync(dir)) {
99
+ const full = join(dir, entry);
100
+ if (statSync(full).isDirectory()) {
101
+ if (!SKIP_DIRS.has(entry)) walk(root, full, out);
102
+ continue;
103
+ }
104
+ if (SOURCE.test(entry)) out.push(relative(root, full).split(sep).join("/"));
105
+ }
106
+ }
107
+ function patternFor(symbols) {
108
+ const alternatives = [...symbols].sort((a, b) => b.length - a.length);
109
+ const escaped = alternatives.map(
110
+ (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
111
+ );
112
+ return new RegExp(`\\}\\s?(${escaped.join("|")})([^A-Za-z0-9_/]|\`|$)`);
113
+ }
114
+ function findHandTypedUnits(options = {}) {
115
+ const dir = options.dir ?? "src";
116
+ const pattern = patternFor(options.symbols ?? HAND_TYPED_SYMBOLS);
117
+ const files = [];
118
+ walk(dir, dir, files);
119
+ const found = [];
120
+ for (const file of files) {
121
+ if (isTest(file)) continue;
122
+ if (options.ignore?.(file)) continue;
123
+ const raw = readFileSync(join(dir, file), "utf8").split("\n");
124
+ stripComments(raw.join("\n")).forEach((code, index) => {
125
+ const match = pattern.exec(code);
126
+ if (match === null) return;
127
+ if (CSS_PROPERTY.test(code)) return;
128
+ found.push({
129
+ file,
130
+ line: index + 1,
131
+ // The raw line, not the blanked form the scan matched against.
132
+ source: raw[index].trim(),
133
+ symbol: match[1]
134
+ });
135
+ });
136
+ }
137
+ return found;
138
+ }
139
+ function expectNoHandTypedUnits(options = {}) {
140
+ const baseline = options.baseline ?? {};
141
+ const found = findHandTypedUnits(options);
142
+ const counts = {};
143
+ for (const one of found) counts[one.file] = (counts[one.file] ?? 0) + 1;
144
+ const over = found.filter(
145
+ (one) => (counts[one.file] ?? 0) > (baseline[one.file] ?? 0)
146
+ );
147
+ if (over.length > 0) {
148
+ throw new Error(
149
+ `A unit symbol was typed next to a number in ${over.length} place(s). Render <Unit value={x} /> instead, so the symbol keeps its styling, follows the value's ladder, and is announced as a word rather than as letters.
150
+
151
+ Where a string is genuinely required: speakQuantity for an accessible name, writeQuantity for visible text that is MEASURED (an SVG <text>, a canvas label), and nothing else.
152
+
153
+ ` + over.map((one) => ` ${one.file}:${one.line} ${one.source}`).join("\n")
154
+ );
155
+ }
156
+ const stale = Object.keys(baseline).filter(
157
+ (file) => (counts[file] ?? 0) < baseline[file]
158
+ );
159
+ if (stale.length > 0) {
160
+ throw new Error(
161
+ `These are below their baseline, which is good news. Lower or remove the entry so the gain is locked in, otherwise the symbol is free to come back:
162
+ ${stale.map((f) => ` ${f}: now ${counts[f] ?? 0}, baseline ${baseline[f]}`).join("\n")}`
163
+ );
164
+ }
165
+ }
166
+ export {
167
+ HAND_TYPED_SYMBOLS,
168
+ expectNoHandTypedUnits,
169
+ findHandTypedUnits
170
+ };
171
+ //# sourceMappingURL=guards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/guards.ts"],"sourcesContent":["import { readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { join, relative, sep } from \"node:path\";\n\n/**\n * Build-time guards an Uplink can run against its own source.\n *\n * Published as `@ksp-gonogo/ui-kit/guards`, separate from `./testing` because\n * it reads the filesystem (`node:fs`), which would break a browser test runner.\n *\n * ## Why\n *\n * A widget that writes\n *\n * `${closingSpeed.toFixed(1)} m/s`\n *\n * type-checks and renders, but the symbol cannot be dimmed, cannot be kept off\n * a line break, is announced as the letters \"m\", \"slash\", \"s\", and does not\n * follow when the value's ladder changes rung. `<Unit>` solves all four.\n *\n * ## Using it\n *\n * One test file, anywhere your test runner will find it:\n *\n * ```ts\n * import { expectNoHandTypedUnits } from \"@ksp-gonogo/ui-kit/guards\";\n * import { it } from \"vitest\";\n *\n * it(\"renders units through <Unit>, not by typing the symbol\", () => {\n * expectNoHandTypedUnits({ dir: \"src\" });\n * });\n * ```\n *\n * On an existing codebase with offenders already in it, seed a `baseline` and\n * lower it as you convert: a guessed conversion is worse than a hand-typed\n * symbol, because it renders a confident wrong label.\n */\n\n/**\n * The unit symbols the check looks for by default. It is a chosen list and\n * not every unit: a symbol nobody types beside a number, or one short enough\n * to turn up in ordinary prose, is left out.\n *\n * Pass your own through `symbols` when your Uplink registers a unit of its own.\n *\n * @category Testing\n */\nexport const HAND_TYPED_SYMBOLS: readonly string[] = [\n \"m/s²\",\n \"m/s\",\n \"km/h\",\n \"km\",\n \"Mm\",\n \"Gm\",\n \"mm\",\n \"cm\",\n \"kPa\",\n \"MPa\",\n \"kN\",\n \"MN\",\n \"kW\",\n \"MW\",\n \"GW\",\n \"kg\",\n \"°C\",\n \"°\",\n \"%\",\n \"m\",\n \"s\",\n \"t\",\n \"N\",\n \"W\",\n \"f\",\n \"sci\",\n \"rep\",\n \"Mit\",\n \"deg\",\n \"rad\",\n /*\n * Matching is case-sensitive, since single letters like `m` and `W` would\n * otherwise match prose and CSS, so a unit conventionally written both ways\n * (`rpm`, `RPM`) is listed in both spellings.\n */\n \"rpm\",\n \"RPM\",\n \"dB\",\n \"rad/s\",\n \"bit/s\",\n \"J/s\",\n \"N·m\",\n \"m²\",\n \"Pa\",\n \"kg/m³\",\n \"g/m³\",\n];\n\n/**\n * A CSS length or colour is not a readout. `[:(={]` covers a declaration\n * (`width: ...`), a call (`translate(...)`) and an attribute (`offset={...}`);\n * the colour functions cover `hsl(${h}deg ${s}% ${l}%)`.\n */\nconst CSS_PROPERTY =\n /(width|height|left|top|right|bottom|transform|translate|inset|margin|padding|gap|flex|stroke|offset|dasharray|dashoffset|hsl|hsla|rgb|rgba)\\s*[:(={]/i;\n\n/**\n * Somewhere a unit symbol was typed next to a number.\n *\n * @category Testing\n */\nexport interface HandTypedUnit {\n /** Path relative to the scanned directory, with `/` separators. */\n file: string;\n /** 1-based, so it is clickable in a terminal. */\n line: number;\n /** The offending source line, trimmed. */\n source: string;\n /** The symbol that matched. */\n symbol: string;\n}\n\n/**\n * Options for {@link findHandTypedUnits} and {@link expectNoHandTypedUnits}.\n *\n * @category Testing\n */\nexport interface HandTypedUnitOptions {\n /** Directory to scan. Defaults to `src` under the current directory. */\n dir?: string;\n /** Override the symbols looked for. See {@link HAND_TYPED_SYMBOLS}. */\n symbols?: readonly string[];\n /**\n * Per-file allowance, keyed by the path as it appears in a finding. A file\n * at its entry passes, and a file below it throws, so an allowance left\n * higher than needed is reported rather than left open.\n */\n baseline?: Readonly<Record<string, number>>;\n /**\n * Extra paths to skip, matched against the relative path. Tests, snapshots,\n * `node_modules` and build output are already skipped.\n */\n ignore?: (file: string) => boolean;\n}\n\nconst SKIP_DIRS = new Set([\n \"node_modules\",\n \"dist\",\n \"build\",\n \"coverage\",\n \".git\",\n \".turbo\",\n \"__snapshots__\",\n]);\n\nconst SOURCE = /\\.(ts|tsx|js|jsx)$/;\n\n/**\n * Blank out comments, keeping line structure so reported line numbers still\n * point at the source, so prose about the rule is not a breach of it. Not a\n * parser: over-blanking would at worst hide a symbol inside a string literal.\n */\nfunction stripComments(source: string): string[] {\n const out: string[] = [];\n let inBlock = false;\n for (const line of source.split(\"\\n\")) {\n let kept = \"\";\n let i = 0;\n while (i < line.length) {\n if (inBlock) {\n if (line.startsWith(\"*/\", i)) {\n inBlock = false;\n i += 2;\n } else {\n i += 1;\n }\n continue;\n }\n if (line.startsWith(\"/*\", i)) {\n inBlock = true;\n i += 2;\n continue;\n }\n if (line.startsWith(\"//\", i)) break;\n kept += line[i];\n i += 1;\n }\n out.push(kept);\n }\n return out;\n}\n\nfunction isTest(file: string): boolean {\n return file.includes(\".test.\") || file.includes(\".spec.\");\n}\n\nfunction walk(root: string, dir: string, out: string[]): void {\n for (const entry of readdirSync(dir)) {\n const full = join(dir, entry);\n if (statSync(full).isDirectory()) {\n if (!SKIP_DIRS.has(entry)) walk(root, full, out);\n continue;\n }\n if (SOURCE.test(entry)) out.push(relative(root, full).split(sep).join(\"/\"));\n }\n}\n\n/**\n * The pattern every offender takes: a `${...}` interpolation, then optionally\n * one space, then a unit symbol ending at a non-word boundary.\n *\n * Longest symbol first, so `m/s` cannot be matched as a bare `m` and reported\n * with the wrong symbol.\n */\nfunction patternFor(symbols: readonly string[]): RegExp {\n const alternatives = [...symbols].sort((a, b) => b.length - a.length);\n const escaped = alternatives.map((s) =>\n s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"),\n );\n return new RegExp(`\\\\}\\\\s?(${escaped.join(\"|\")})([^A-Za-z0-9_/]|\\`|$)`);\n}\n\n/**\n * Every place a unit symbol is typed next to a number, under `dir`.\n *\n * Returns findings rather than throwing, so a caller can report them their own\n * way. {@link expectNoHandTypedUnits} is the assertion built on top.\n *\n * @category Testing\n */\nexport function findHandTypedUnits(\n options: HandTypedUnitOptions = {},\n): HandTypedUnit[] {\n const dir = options.dir ?? \"src\";\n const pattern = patternFor(options.symbols ?? HAND_TYPED_SYMBOLS);\n const files: string[] = [];\n walk(dir, dir, files);\n\n const found: HandTypedUnit[] = [];\n for (const file of files) {\n if (isTest(file)) continue;\n if (options.ignore?.(file)) continue;\n const raw = readFileSync(join(dir, file), \"utf8\").split(\"\\n\");\n stripComments(raw.join(\"\\n\")).forEach((code, index) => {\n const match = pattern.exec(code);\n if (match === null) return;\n if (CSS_PROPERTY.test(code)) return;\n found.push({\n file,\n line: index + 1,\n // The raw line, not the blanked form the scan matched against.\n source: raw[index].trim(),\n symbol: match[1],\n });\n });\n }\n return found;\n}\n\n/**\n * Throws when a unit symbol is typed next to a number, with the fix in the\n * message. Silent for a file at its `baseline` entry; a file below its entry\n * throws, so an allowance left higher than needed cannot let the symbol come\n * back unnoticed.\n *\n * @example\n * ```ts\n * import { expectNoHandTypedUnits } from \"@ksp-gonogo/ui-kit/guards\";\n * import { it } from \"vitest\";\n *\n * it(\"renders units through <Unit>, not by typing the symbol\", () => {\n * expectNoHandTypedUnits({ dir: \"src\" });\n * });\n * ```\n *\n * @category Testing\n */\nexport function expectNoHandTypedUnits(\n options: HandTypedUnitOptions = {},\n): void {\n const baseline = options.baseline ?? {};\n const found = findHandTypedUnits(options);\n\n const counts: Record<string, number> = {};\n for (const one of found) counts[one.file] = (counts[one.file] ?? 0) + 1;\n\n const over = found.filter(\n (one) => (counts[one.file] ?? 0) > (baseline[one.file] ?? 0),\n );\n if (over.length > 0) {\n throw new Error(\n `A unit symbol was typed next to a number in ${over.length} place(s). ` +\n \"Render <Unit value={x} /> instead, so the symbol keeps its styling, \" +\n \"follows the value's ladder, and is announced as a word rather than \" +\n \"as letters.\\n\\nWhere a string is genuinely required: speakQuantity \" +\n \"for an accessible name, writeQuantity for visible text that is \" +\n \"MEASURED (an SVG <text>, a canvas label), and nothing else.\\n\\n\" +\n over\n .map((one) => ` ${one.file}:${one.line} ${one.source}`)\n .join(\"\\n\"),\n );\n }\n\n const stale = Object.keys(baseline).filter(\n (file) => (counts[file] ?? 0) < baseline[file],\n );\n if (stale.length > 0) {\n throw new Error(\n \"These are below their baseline, which is good news. Lower or remove \" +\n \"the entry so the gain is locked in, otherwise the symbol is free to \" +\n `come back:\\n${stale\n .map((f) => ` ${f}: now ${counts[f] ?? 0}, baseline ${baseline[f]}`)\n .join(\"\\n\")}`,\n );\n }\n}\n"],"mappings":"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","names":[]}