tempest-react-sdk 0.27.0 → 0.28.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 (107) hide show
  1. package/README.md +24 -0
  2. package/bin/lib/css/analyze.mjs +149 -0
  3. package/bin/lib/css/analyze.test.mjs +136 -0
  4. package/bin/lib/css/collect.mjs +152 -0
  5. package/bin/lib/css/findings.mjs +79 -0
  6. package/bin/lib/css/fix.e2e.test.mjs +99 -0
  7. package/bin/lib/css/fix.mjs +138 -0
  8. package/bin/lib/css/fix.test.mjs +108 -0
  9. package/bin/lib/css/index.mjs +16 -0
  10. package/bin/lib/css/parse.mjs +398 -0
  11. package/bin/lib/css/parse.test.mjs +146 -0
  12. package/bin/lib/css/properties.mjs +399 -0
  13. package/bin/lib/css/repetition.mjs +213 -0
  14. package/bin/lib/css/repetition.test.mjs +166 -0
  15. package/bin/lib/css/semantic.mjs +327 -0
  16. package/bin/lib/css/semantic.test.mjs +201 -0
  17. package/bin/lib/css/tokens.mjs +119 -0
  18. package/bin/lib/doctor/doctor.e2e.test.mjs +45 -0
  19. package/bin/tempest.mjs +169 -7
  20. package/dist/charts/scales.cjs +2 -0
  21. package/dist/charts/scales.cjs.map +1 -0
  22. package/dist/charts/scales.js +29 -0
  23. package/dist/charts/scales.js.map +1 -0
  24. package/dist/charts.cjs +1 -1
  25. package/dist/charts.d.ts +91 -0
  26. package/dist/charts.js +9 -8
  27. package/dist/components/BottomNavigation/BottomNavigation.module.cjs.map +1 -1
  28. package/dist/components/BottomNavigation/BottomNavigation.module.js.map +1 -1
  29. package/dist/components/Carousel/Carousel.module.cjs.map +1 -1
  30. package/dist/components/Carousel/Carousel.module.js.map +1 -1
  31. package/dist/components/CodeBlock/CodeBlock.cjs +3 -0
  32. package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -0
  33. package/dist/components/CodeBlock/CodeBlock.js +53 -0
  34. package/dist/components/CodeBlock/CodeBlock.js.map +1 -0
  35. package/dist/components/CodeBlock/CodeBlock.module.cjs +2 -0
  36. package/dist/components/CodeBlock/CodeBlock.module.cjs.map +1 -0
  37. package/dist/components/CodeBlock/CodeBlock.module.js +29 -0
  38. package/dist/components/CodeBlock/CodeBlock.module.js.map +1 -0
  39. package/dist/components/CodeBlock/tokenize.cjs +3 -0
  40. package/dist/components/CodeBlock/tokenize.cjs.map +1 -0
  41. package/dist/components/CodeBlock/tokenize.js +267 -0
  42. package/dist/components/CodeBlock/tokenize.js.map +1 -0
  43. package/dist/components/Navbar/Navbar.module.cjs.map +1 -1
  44. package/dist/components/Navbar/Navbar.module.js.map +1 -1
  45. package/dist/components/QRCode/QRCode.cjs +2 -0
  46. package/dist/components/QRCode/QRCode.cjs.map +1 -0
  47. package/dist/components/QRCode/QRCode.js +49 -0
  48. package/dist/components/QRCode/QRCode.js.map +1 -0
  49. package/dist/components/QRCode/QRCode.module.cjs +2 -0
  50. package/dist/components/QRCode/QRCode.module.cjs.map +1 -0
  51. package/dist/components/QRCode/QRCode.module.js +9 -0
  52. package/dist/components/QRCode/QRCode.module.js.map +1 -0
  53. package/dist/components/QRCode/qr-encode.cjs +2 -0
  54. package/dist/components/QRCode/qr-encode.cjs.map +1 -0
  55. package/dist/components/QRCode/qr-encode.js +295 -0
  56. package/dist/components/QRCode/qr-encode.js.map +1 -0
  57. package/dist/components/QRCode/qr-tables.cjs +2 -0
  58. package/dist/components/QRCode/qr-tables.cjs.map +1 -0
  59. package/dist/components/QRCode/qr-tables.js +366 -0
  60. package/dist/components/QRCode/qr-tables.js.map +1 -0
  61. package/dist/components/ScrollArea/ScrollArea.cjs +1 -1
  62. package/dist/components/ScrollArea/ScrollArea.cjs.map +1 -1
  63. package/dist/components/ScrollArea/ScrollArea.js +22 -16
  64. package/dist/components/ScrollArea/ScrollArea.js.map +1 -1
  65. package/dist/components/ScrollArea/ScrollArea.module.cjs.map +1 -1
  66. package/dist/components/ScrollArea/ScrollArea.module.js.map +1 -1
  67. package/dist/components/Sparkline/Sparkline.cjs +2 -0
  68. package/dist/components/Sparkline/Sparkline.cjs.map +1 -0
  69. package/dist/components/Sparkline/Sparkline.js +64 -0
  70. package/dist/components/Sparkline/Sparkline.js.map +1 -0
  71. package/dist/components/Sparkline/Sparkline.module.cjs +2 -0
  72. package/dist/components/Sparkline/Sparkline.module.cjs.map +1 -0
  73. package/dist/components/Sparkline/Sparkline.module.js +10 -0
  74. package/dist/components/Sparkline/Sparkline.module.js.map +1 -0
  75. package/dist/components/Sparkline/sparkline-geometry.cjs +2 -0
  76. package/dist/components/Sparkline/sparkline-geometry.cjs.map +1 -0
  77. package/dist/components/Sparkline/sparkline-geometry.js +56 -0
  78. package/dist/components/Sparkline/sparkline-geometry.js.map +1 -0
  79. package/dist/components/Table/Table.cjs +1 -1
  80. package/dist/components/Table/Table.cjs.map +1 -1
  81. package/dist/components/Table/Table.js +37 -30
  82. package/dist/components/Table/Table.js.map +1 -1
  83. package/dist/components/Table/Table.module.cjs.map +1 -1
  84. package/dist/components/Table/Table.module.js.map +1 -1
  85. package/dist/components/VirtualList/VirtualList.cjs +1 -1
  86. package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
  87. package/dist/components/VirtualList/VirtualList.js +13 -11
  88. package/dist/components/VirtualList/VirtualList.js.map +1 -1
  89. package/dist/components/VirtualList/VirtualList.module.cjs.map +1 -1
  90. package/dist/components/VirtualList/VirtualList.module.js.map +1 -1
  91. package/dist/hooks/use-scroll-overflow.cjs +2 -0
  92. package/dist/hooks/use-scroll-overflow.cjs.map +1 -0
  93. package/dist/hooks/use-scroll-overflow.js +22 -0
  94. package/dist/hooks/use-scroll-overflow.js.map +1 -0
  95. package/dist/styles.css +1 -1
  96. package/dist/tempest-react-sdk.cjs +1 -1
  97. package/dist/tempest-react-sdk.d.ts +445 -2
  98. package/dist/tempest-react-sdk.js +197 -190
  99. package/dist/theme/create-theme.cjs +3 -3
  100. package/dist/theme/create-theme.cjs.map +1 -1
  101. package/dist/theme/create-theme.js +52 -28
  102. package/dist/theme/create-theme.js.map +1 -1
  103. package/dist/theme/data-viz-ramps.cjs +2 -0
  104. package/dist/theme/data-viz-ramps.cjs.map +1 -0
  105. package/dist/theme/data-viz-ramps.js +58 -0
  106. package/dist/theme/data-viz-ramps.js.map +1 -0
  107. package/package.json +2 -1
@@ -0,0 +1,146 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { maskValue, normalizeSelectors, parseCss, stripComments } from "./parse.mjs";
4
+
5
+ const codes = (text) => parseCss(text).errors.map((e) => e.code);
6
+
7
+ describe("parseCss — structure", () => {
8
+ it("records rules, declarations and their lines", () => {
9
+ const { blocks } = parseCss(".a {\n color: red;\n padding: 0;\n}\n");
10
+ expect(blocks).toHaveLength(1);
11
+ expect(blocks[0]).toMatchObject({ kind: "rule", prelude: ".a", line: 1, endLine: 4 });
12
+ expect(blocks[0].decls.map((d) => [d.prop, d.value, d.line])).toEqual([
13
+ ["color", "red", 2],
14
+ ["padding", "0", 3],
15
+ ]);
16
+ });
17
+
18
+ it("keeps the last declaration when the block omits its semicolon", () => {
19
+ const { blocks, errors } = parseCss(".a { color: red }");
20
+ expect(errors).toEqual([]);
21
+ expect(blocks[0].decls).toHaveLength(1);
22
+ });
23
+
24
+ it("nests blocks and records the at-rule context", () => {
25
+ const { blocks } = parseCss("@media (min-width: 600px) {\n .a { color: red; }\n}\n");
26
+ expect(blocks[0]).toMatchObject({ kind: "at", children: 1 });
27
+ expect(blocks[1]).toMatchObject({
28
+ kind: "rule",
29
+ prelude: ".a",
30
+ context: ["@media (min-width: 600px)"],
31
+ });
32
+ });
33
+
34
+ it("collects at-statements with their params", () => {
35
+ const { statements } = parseCss('@import "x.css";\n@charset "utf-8";\n');
36
+ expect(statements).toEqual([
37
+ { name: "import", params: '"x.css"', line: 1 },
38
+ { name: "charset", params: '"utf-8"', line: 2 },
39
+ ]);
40
+ });
41
+
42
+ it("reports the declaration line, not the comment above it", () => {
43
+ const { blocks } = parseCss(".a {\n\n /* why */\n color: red;\n}\n");
44
+ expect(blocks[0].decls[0].line).toBe(4);
45
+ });
46
+
47
+ it("keeps a comment out of the parsed value", () => {
48
+ const { blocks } = parseCss(".a { color: red /* nope */; }");
49
+ expect(blocks[0].decls[0].value).toBe("red");
50
+ });
51
+
52
+ it("does not split on a semicolon inside url() or a string", () => {
53
+ const { blocks, errors } = parseCss('.a { background: url("a;b.png"); content: "x;y"; }');
54
+ expect(errors).toEqual([]);
55
+ expect(blocks[0].decls.map((d) => d.prop)).toEqual(["background", "content"]);
56
+ });
57
+
58
+ it("does not open a block on a brace inside a string", () => {
59
+ const { blocks, errors } = parseCss('.a { content: "{"; color: red; }');
60
+ expect(errors).toEqual([]);
61
+ expect(blocks).toHaveLength(1);
62
+ });
63
+
64
+ it("records offsets that point at the declaration text", () => {
65
+ const text = ".a {\n color: red;\n}\n";
66
+ const { blocks } = parseCss(text);
67
+ const decl = blocks[0].decls[0];
68
+ expect(text.slice(decl.start, decl.end)).toBe("color: red;");
69
+ expect(text.slice(blocks[0].start, blocks[0].end)).toBe(text.trimEnd());
70
+ });
71
+ });
72
+
73
+ describe("parseCss — syntax defects", () => {
74
+ it("flags a declaration with no colon", () => {
75
+ expect(codes(".a { color red; }")).toEqual(["missing-colon"]);
76
+ });
77
+
78
+ it("flags an empty value", () => {
79
+ expect(codes(".a { color: ; }")).toEqual(["empty-value"]);
80
+ });
81
+
82
+ it("flags a block that is never closed", () => {
83
+ expect(codes(".a {\n color: red;\n")).toEqual(["unclosed-block"]);
84
+ });
85
+
86
+ it("flags a stray closing brace and keeps parsing after it", () => {
87
+ const { blocks, errors } = parseCss("}\n.a { color: red; }\n");
88
+ expect(errors.map((e) => e.code)).toEqual(["unexpected-brace"]);
89
+ expect(blocks).toHaveLength(1);
90
+ });
91
+
92
+ it("flags an unterminated comment", () => {
93
+ expect(codes(".a { color: red; }\n/* forever")).toEqual(["unterminated-comment"]);
94
+ });
95
+
96
+ it("flags an unterminated string", () => {
97
+ expect(codes('.a { content: "oops;\n}')).toContain("unterminated-string");
98
+ });
99
+
100
+ it("flags an unbalanced paren", () => {
101
+ expect(codes(".a { width: calc(1px + 2px;\n}\n")).toContain("unterminated-paren");
102
+ });
103
+
104
+ it("flags a declaration outside any rule", () => {
105
+ expect(codes("color: red;\n")).toEqual(["declaration-outside-rule"]);
106
+ });
107
+
108
+ it("flags a selector with no block", () => {
109
+ expect(codes(".a { color: red; }\n.b\n")).toEqual(["missing-brace"]);
110
+ });
111
+
112
+ it("flags an empty selector", () => {
113
+ expect(codes("{ color: red; }")).toEqual(["empty-selector"]);
114
+ });
115
+
116
+ it("accepts a clean sheet", () => {
117
+ expect(codes(".a { color: red; }\n@media print { .a { color: black; } }\n")).toEqual([]);
118
+ });
119
+ });
120
+
121
+ describe("stripComments / maskValue", () => {
122
+ it("removes comments and keeps strings", () => {
123
+ expect(stripComments('a /* x */ b "c/* d */"')).toBe('a b "c/* d */"');
124
+ });
125
+
126
+ it("masks parens and strings without changing length, keeping the function name", () => {
127
+ const value = 'url("a:b") red';
128
+ expect(maskValue(value)).toHaveLength(value.length);
129
+ expect(maskValue(value)).not.toContain(":");
130
+ expect(maskValue(value).trim().startsWith("url")).toBe(true);
131
+ });
132
+
133
+ it("keeps newlines visible through the mask", () => {
134
+ expect(maskValue("a\nb")).toBe("a\nb");
135
+ });
136
+ });
137
+
138
+ describe("normalizeSelectors", () => {
139
+ it("sorts the list and collapses whitespace", () => {
140
+ expect(normalizeSelectors(".b > span , .a")).toEqual([".a", ".b > span"]);
141
+ });
142
+
143
+ it("does not split on a comma inside :is()", () => {
144
+ expect(normalizeSelectors(":is(.a, .b) span")).toEqual([":is(.a, .b) span"]);
145
+ });
146
+ });
@@ -0,0 +1,399 @@
1
+ // The known-name tables behind the typo checks, plus the nearest-match helper.
2
+ //
3
+ // The list is not exhaustive and does not need to be: `nearest()` only answers
4
+ // when a name is within edit distance 2 of something known, so a property this
5
+ // file has never heard of is reported as a typo **only** when a real property
6
+ // is one or two keystrokes away. A missing entry costs a missed typo, never a
7
+ // false accusation against valid CSS.
8
+
9
+ /** Properties the typo check compares against. */
10
+ export const KNOWN_PROPERTIES = new Set([
11
+ "accent-color",
12
+ "align-content",
13
+ "align-items",
14
+ "align-self",
15
+ "all",
16
+ "animation",
17
+ "animation-delay",
18
+ "animation-direction",
19
+ "animation-duration",
20
+ "animation-fill-mode",
21
+ "animation-iteration-count",
22
+ "animation-name",
23
+ "animation-play-state",
24
+ "animation-timing-function",
25
+ "appearance",
26
+ "aspect-ratio",
27
+ "backdrop-filter",
28
+ "backface-visibility",
29
+ "background",
30
+ "background-attachment",
31
+ "background-blend-mode",
32
+ "background-clip",
33
+ "background-color",
34
+ "background-image",
35
+ "background-origin",
36
+ "background-position",
37
+ "background-repeat",
38
+ "background-size",
39
+ "block-size",
40
+ "border",
41
+ "border-block",
42
+ "border-block-end",
43
+ "border-block-start",
44
+ "border-bottom",
45
+ "border-bottom-color",
46
+ "border-bottom-left-radius",
47
+ "border-bottom-right-radius",
48
+ "border-bottom-style",
49
+ "border-bottom-width",
50
+ "border-collapse",
51
+ "border-color",
52
+ "border-end-end-radius",
53
+ "border-end-start-radius",
54
+ "border-image",
55
+ "border-inline",
56
+ "border-inline-end",
57
+ "border-inline-start",
58
+ "border-left",
59
+ "border-left-color",
60
+ "border-left-style",
61
+ "border-left-width",
62
+ "border-radius",
63
+ "border-right",
64
+ "border-right-color",
65
+ "border-right-style",
66
+ "border-right-width",
67
+ "border-spacing",
68
+ "border-start-end-radius",
69
+ "border-start-start-radius",
70
+ "border-style",
71
+ "border-top",
72
+ "border-top-color",
73
+ "border-top-left-radius",
74
+ "border-top-right-radius",
75
+ "border-top-style",
76
+ "border-top-width",
77
+ "border-width",
78
+ "bottom",
79
+ "box-decoration-break",
80
+ "box-shadow",
81
+ "box-sizing",
82
+ "break-after",
83
+ "break-before",
84
+ "break-inside",
85
+ "caption-side",
86
+ "caret-color",
87
+ "clear",
88
+ "clip-path",
89
+ "color",
90
+ "color-scheme",
91
+ "column-count",
92
+ "column-gap",
93
+ "column-rule",
94
+ "column-span",
95
+ "column-width",
96
+ "columns",
97
+ "contain",
98
+ "container",
99
+ "container-name",
100
+ "container-type",
101
+ "content",
102
+ "content-visibility",
103
+ "counter-increment",
104
+ "counter-reset",
105
+ "counter-set",
106
+ "cursor",
107
+ "direction",
108
+ "display",
109
+ "empty-cells",
110
+ "field-sizing",
111
+ "filter",
112
+ "flex",
113
+ "flex-basis",
114
+ "flex-direction",
115
+ "flex-flow",
116
+ "flex-grow",
117
+ "flex-shrink",
118
+ "flex-wrap",
119
+ "float",
120
+ "font",
121
+ "font-family",
122
+ "font-feature-settings",
123
+ "font-kerning",
124
+ "font-optical-sizing",
125
+ "font-size",
126
+ "font-size-adjust",
127
+ "font-stretch",
128
+ "font-style",
129
+ "font-synthesis",
130
+ "font-variant",
131
+ "font-variant-numeric",
132
+ "font-variation-settings",
133
+ "font-weight",
134
+ "forced-color-adjust",
135
+ "gap",
136
+ "grid",
137
+ "grid-area",
138
+ "grid-auto-columns",
139
+ "grid-auto-flow",
140
+ "grid-auto-rows",
141
+ "grid-column",
142
+ "grid-column-end",
143
+ "grid-column-start",
144
+ "grid-row",
145
+ "grid-row-end",
146
+ "grid-row-start",
147
+ "grid-template",
148
+ "grid-template-areas",
149
+ "grid-template-columns",
150
+ "grid-template-rows",
151
+ "hanging-punctuation",
152
+ "height",
153
+ "hyphens",
154
+ "image-rendering",
155
+ "inline-size",
156
+ "inset",
157
+ "inset-block",
158
+ "inset-block-end",
159
+ "inset-block-start",
160
+ "inset-inline",
161
+ "inset-inline-end",
162
+ "inset-inline-start",
163
+ "isolation",
164
+ "justify-content",
165
+ "justify-items",
166
+ "justify-self",
167
+ "left",
168
+ "letter-spacing",
169
+ "line-break",
170
+ "line-clamp",
171
+ "line-height",
172
+ "list-style",
173
+ "list-style-image",
174
+ "list-style-position",
175
+ "list-style-type",
176
+ "margin",
177
+ "margin-block",
178
+ "margin-block-end",
179
+ "margin-block-start",
180
+ "margin-bottom",
181
+ "margin-inline",
182
+ "margin-inline-end",
183
+ "margin-inline-start",
184
+ "margin-left",
185
+ "margin-right",
186
+ "margin-top",
187
+ "mask",
188
+ "mask-image",
189
+ "mask-position",
190
+ "mask-repeat",
191
+ "mask-size",
192
+ "max-block-size",
193
+ "max-height",
194
+ "max-inline-size",
195
+ "max-width",
196
+ "min-block-size",
197
+ "min-height",
198
+ "min-inline-size",
199
+ "min-width",
200
+ "mix-blend-mode",
201
+ "object-fit",
202
+ "object-position",
203
+ "offset",
204
+ "opacity",
205
+ "order",
206
+ "outline",
207
+ "outline-color",
208
+ "outline-offset",
209
+ "outline-style",
210
+ "outline-width",
211
+ "overflow",
212
+ "overflow-anchor",
213
+ "overflow-block",
214
+ "overflow-clip-margin",
215
+ "overflow-inline",
216
+ "overflow-wrap",
217
+ "overflow-x",
218
+ "overflow-y",
219
+ "overscroll-behavior",
220
+ "overscroll-behavior-x",
221
+ "overscroll-behavior-y",
222
+ "padding",
223
+ "padding-block",
224
+ "padding-block-end",
225
+ "padding-block-start",
226
+ "padding-bottom",
227
+ "padding-inline",
228
+ "padding-inline-end",
229
+ "padding-inline-start",
230
+ "padding-left",
231
+ "padding-right",
232
+ "padding-top",
233
+ "page",
234
+ "page-break-after",
235
+ "page-break-before",
236
+ "page-break-inside",
237
+ "paint-order",
238
+ "perspective",
239
+ "perspective-origin",
240
+ "place-content",
241
+ "place-items",
242
+ "place-self",
243
+ "pointer-events",
244
+ "position",
245
+ "print-color-adjust",
246
+ "quotes",
247
+ "resize",
248
+ "right",
249
+ "rotate",
250
+ "row-gap",
251
+ "scale",
252
+ "scroll-behavior",
253
+ "scroll-margin",
254
+ "scroll-padding",
255
+ "scroll-snap-align",
256
+ "scroll-snap-stop",
257
+ "scroll-snap-type",
258
+ "scrollbar-color",
259
+ "scrollbar-gutter",
260
+ "scrollbar-width",
261
+ "shape-outside",
262
+ "tab-size",
263
+ "table-layout",
264
+ "text-align",
265
+ "text-align-last",
266
+ "text-decoration",
267
+ "text-decoration-color",
268
+ "text-decoration-line",
269
+ "text-decoration-style",
270
+ "text-decoration-thickness",
271
+ "text-emphasis",
272
+ "text-indent",
273
+ "text-justify",
274
+ "text-orientation",
275
+ "text-overflow",
276
+ "text-rendering",
277
+ "text-shadow",
278
+ "text-transform",
279
+ "text-underline-offset",
280
+ "text-underline-position",
281
+ "text-wrap",
282
+ "top",
283
+ "touch-action",
284
+ "transform",
285
+ "transform-box",
286
+ "transform-origin",
287
+ "transform-style",
288
+ "transition",
289
+ "transition-behavior",
290
+ "transition-delay",
291
+ "transition-duration",
292
+ "transition-property",
293
+ "transition-timing-function",
294
+ "translate",
295
+ "unicode-bidi",
296
+ "user-select",
297
+ "vertical-align",
298
+ "view-transition-name",
299
+ "visibility",
300
+ "white-space",
301
+ "white-space-collapse",
302
+ "widows",
303
+ "width",
304
+ "will-change",
305
+ "word-break",
306
+ "word-spacing",
307
+ "writing-mode",
308
+ "z-index",
309
+ "zoom",
310
+ // SVG presentation attributes usable as CSS properties.
311
+ "fill",
312
+ "fill-opacity",
313
+ "fill-rule",
314
+ "stroke",
315
+ "stroke-dasharray",
316
+ "stroke-dashoffset",
317
+ "stroke-linecap",
318
+ "stroke-linejoin",
319
+ "stroke-opacity",
320
+ "stroke-width",
321
+ ]);
322
+
323
+ /** At-rules the typo check compares against. */
324
+ export const KNOWN_AT_RULES = new Set([
325
+ "charset",
326
+ "container",
327
+ "counter-style",
328
+ "document",
329
+ "font-face",
330
+ "font-feature-values",
331
+ "font-palette-values",
332
+ "import",
333
+ "keyframes",
334
+ "layer",
335
+ "media",
336
+ "namespace",
337
+ "page",
338
+ "property",
339
+ "scope",
340
+ "starting-style",
341
+ "supports",
342
+ "view-transition",
343
+ ]);
344
+
345
+ /** Vendor prefixes: never flagged, and never suggested as a fix. */
346
+ const VENDOR = /^-(webkit|moz|ms|o|apple|epub|khtml)-/;
347
+
348
+ /** True when a property name is out of scope for the typo check. */
349
+ export function isCheckableProperty(prop) {
350
+ return !prop.startsWith("--") && !VENDOR.test(prop) && /^[a-z][a-z-]*$/.test(prop);
351
+ }
352
+
353
+ /**
354
+ * Levenshtein distance, abandoned as soon as it exceeds `max`.
355
+ *
356
+ * @param {string} a
357
+ * @param {string} b
358
+ * @param {number} max
359
+ * @returns {number} The distance, or `max + 1` when it is larger than `max`.
360
+ */
361
+ export function distance(a, b, max = 2) {
362
+ if (a === b) return 0;
363
+ if (Math.abs(a.length - b.length) > max) return max + 1;
364
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
365
+ for (let i = 1; i <= a.length; i += 1) {
366
+ const row = [i];
367
+ let best = i;
368
+ for (let j = 1; j <= b.length; j += 1) {
369
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
370
+ row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + cost);
371
+ best = Math.min(best, row[j]);
372
+ }
373
+ if (best > max) return max + 1;
374
+ prev = row;
375
+ }
376
+ return prev[b.length] > max ? max + 1 : prev[b.length];
377
+ }
378
+
379
+ /**
380
+ * Closest known name to `name`, when one is within `max` edits.
381
+ *
382
+ * @param {string} name - The name as written.
383
+ * @param {Iterable<string>} candidates - Known names to compare against.
384
+ * @param {number} [max] - Largest edit distance still considered a typo.
385
+ * @returns {string | null}
386
+ */
387
+ export function nearest(name, candidates, max = 2) {
388
+ let best = null;
389
+ let bestDistance = max + 1;
390
+ for (const candidate of candidates) {
391
+ const d = distance(name, candidate, max);
392
+ if (d < bestDistance) {
393
+ best = candidate;
394
+ bestDistance = d;
395
+ if (d === 1) break;
396
+ }
397
+ }
398
+ return bestDistance <= max ? best : null;
399
+ }