@cascivo/eslint-plugin 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 +31 -4
- package/package.json +2 -1
- package/readme.body.md +29 -2
- package/src/index.js +2 -1
- package/src/token-catalog.json +356 -0
- package/src/token-values.js +191 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!-- generated by scripts/readme/generate.ts — edit readme.body.md, not this file -->
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
|
-
<a href="https://cascivo.com"><img src="https://cascivo.com/
|
|
4
|
+
<a href="https://cascivo.com"><img src="https://cascivo.com/logo-mark-img-accent.svg" width="72" height="72" alt="cascivo logo"></a>
|
|
5
5
|
<h1>@cascivo/eslint-plugin</h1>
|
|
6
6
|
<p><strong>ESLint rule that turns cascivo's near-miss prop names into an actionable message — the wrong guess, the prop that exists, and why</strong></p>
|
|
7
7
|
|
|
@@ -16,9 +16,12 @@
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Two ESLint rules:
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
- **`cascivo/prop-vocabulary`** — reports the prop names an adopter is likely to guess wrong, and answers with the prop that exists and why.
|
|
22
|
+
- **`cascivo/token-values`** — reports a `--cascivo-*` custom property that does not exist, and names the one that does.
|
|
23
|
+
|
|
24
|
+
You probably do not need to install this directly — [`@cascivo/eslint-config`](../eslint-config) depends on it and enables both rules at `warn`.
|
|
22
25
|
|
|
23
26
|
## Why it exists
|
|
24
27
|
|
|
@@ -50,6 +53,30 @@ Property 'tone' does not exist on type 'TextProps'.
|
|
|
50
53
|
| `import { Dialog } from '@cascivo/react'` | `Dialog` is `Modal` | ✅ (unaliased imports only) |
|
|
51
54
|
| `<DataTable items={…}>` | it takes `rows` — the one component that does | — |
|
|
52
55
|
|
|
56
|
+
## `cascivo/token-values` — the name that does not exist
|
|
57
|
+
|
|
58
|
+
`prop-vocabulary` exists because a correct TypeScript error teaches nothing. This rule exists because there is **no error at all**.
|
|
59
|
+
|
|
60
|
+
CSS silently drops an unknown custom property. `--cascivo-color-acent: red` does not warn, does not fail the build, does not appear in DevTools, and does not throw. It has no effect, and the hunt for the cause starts in the component. React makes it worse: `CSSProperties` has no index signature for `--*` keys, so every custom property in a `style` prop reaches the DOM through a cast you wrote — and a cast launders a typo by definition.
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
`--cascivo-color-acent` is not a cascivo token — CSS drops an unknown custom property
|
|
64
|
+
silently, so this has no effect. Did you mean `--cascivo-color-accent`?
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
It also finds names written with the words in the wrong order, which plain edit distance never does:
|
|
68
|
+
|
|
69
|
+
| You wrote | It says |
|
|
70
|
+
| -------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
71
|
+
| `--cascivo-color-acent` | did you mean `--cascivo-color-accent` |
|
|
72
|
+
| `--cascivo-text-color` | did you mean `--cascivo-color-text` (`--cascivo-text-*` is the _size_ scale) |
|
|
73
|
+
| `color: 'var(--cascivo-color-txt)'` | same check, inside a `var()` reference |
|
|
74
|
+
| `background: '#3b82f6'` where exactly one token has that value | use `var(--cascivo-…)` so it follows the theme |
|
|
75
|
+
|
|
76
|
+
The name set is generated by `scripts/style-contract/generate.ts` from the same catalog the docs site and `cascivo audit --ai` read, so the three cannot disagree. `cascivo audit --ai` reports the same class at **error** — it also scans your CSS, which a lint rule cannot.
|
|
77
|
+
|
|
78
|
+
Your own custom properties are none of its business: only the `--cascivo-` namespace is checked.
|
|
79
|
+
|
|
53
80
|
## Install
|
|
54
81
|
|
|
55
82
|
```sh
|
|
@@ -96,4 +123,4 @@ pnpm add @cascivo/eslint-plugin
|
|
|
96
123
|
|
|
97
124
|
[cascivo.com](https://cascivo.com) · [Docs](https://cascivo.com/docs) · [Storybook](https://storybook.cascivo.com) · [GitHub](https://github.com/cascivo/cascivo) · AI agents: read [`llms.txt`](https://cascivo.com/llms.txt) (install steps + component index, plain text) or use [`@cascivo/mcp`](https://github.com/cascivo/cascivo/tree/main/packages/mcp) and [`registry.json`](https://github.com/cascivo/cascivo/blob/main/registry.json) · MIT
|
|
98
125
|
|
|
99
|
-
<div align="center"><a href="https://cascivo.com"><img src="https://cascivo.com/
|
|
126
|
+
<div align="center"><a href="https://cascivo.com"><img src="https://cascivo.com/logo-mark-img.svg" width="28" height="28" alt="cascivo"></a></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cascivo/eslint-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "ESLint rule that turns cascivo's near-miss prop names into an actionable message — the wrong guess, the prop that exists, and why",
|
|
6
6
|
"keywords": [
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"provenance": true
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
+
"@typescript-eslint/parser": "8.66.0",
|
|
41
42
|
"eslint": "10.8.0"
|
|
42
43
|
},
|
|
43
44
|
"peerDependencies": {
|
package/readme.body.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
Two ESLint rules:
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
- **`cascivo/prop-vocabulary`** — reports the prop names an adopter is likely to guess wrong, and answers with the prop that exists and why.
|
|
4
|
+
- **`cascivo/token-values`** — reports a `--cascivo-*` custom property that does not exist, and names the one that does.
|
|
5
|
+
|
|
6
|
+
You probably do not need to install this directly — [`@cascivo/eslint-config`](../eslint-config) depends on it and enables both rules at `warn`.
|
|
4
7
|
|
|
5
8
|
## Why it exists
|
|
6
9
|
|
|
@@ -32,6 +35,30 @@ Property 'tone' does not exist on type 'TextProps'.
|
|
|
32
35
|
| `import { Dialog } from '@cascivo/react'` | `Dialog` is `Modal` | ✅ (unaliased imports only) |
|
|
33
36
|
| `<DataTable items={…}>` | it takes `rows` — the one component that does | — |
|
|
34
37
|
|
|
38
|
+
## `cascivo/token-values` — the name that does not exist
|
|
39
|
+
|
|
40
|
+
`prop-vocabulary` exists because a correct TypeScript error teaches nothing. This rule exists because there is **no error at all**.
|
|
41
|
+
|
|
42
|
+
CSS silently drops an unknown custom property. `--cascivo-color-acent: red` does not warn, does not fail the build, does not appear in DevTools, and does not throw. It has no effect, and the hunt for the cause starts in the component. React makes it worse: `CSSProperties` has no index signature for `--*` keys, so every custom property in a `style` prop reaches the DOM through a cast you wrote — and a cast launders a typo by definition.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
`--cascivo-color-acent` is not a cascivo token — CSS drops an unknown custom property
|
|
46
|
+
silently, so this has no effect. Did you mean `--cascivo-color-accent`?
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
It also finds names written with the words in the wrong order, which plain edit distance never does:
|
|
50
|
+
|
|
51
|
+
| You wrote | It says |
|
|
52
|
+
| -------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
53
|
+
| `--cascivo-color-acent` | did you mean `--cascivo-color-accent` |
|
|
54
|
+
| `--cascivo-text-color` | did you mean `--cascivo-color-text` (`--cascivo-text-*` is the _size_ scale) |
|
|
55
|
+
| `color: 'var(--cascivo-color-txt)'` | same check, inside a `var()` reference |
|
|
56
|
+
| `background: '#3b82f6'` where exactly one token has that value | use `var(--cascivo-…)` so it follows the theme |
|
|
57
|
+
|
|
58
|
+
The name set is generated by `scripts/style-contract/generate.ts` from the same catalog the docs site and `cascivo audit --ai` read, so the three cannot disagree. `cascivo audit --ai` reports the same class at **error** — it also scans your CSS, which a lint rule cannot.
|
|
59
|
+
|
|
60
|
+
Your own custom properties are none of its business: only the `--cascivo-` namespace is checked.
|
|
61
|
+
|
|
35
62
|
## Install
|
|
36
63
|
|
|
37
64
|
```sh
|
package/src/index.js
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* who already installed the config gets the messages without changing anything.
|
|
8
8
|
*/
|
|
9
9
|
import propVocabulary from './prop-vocabulary.js'
|
|
10
|
+
import tokenValues from './token-values.js'
|
|
10
11
|
|
|
11
12
|
export default {
|
|
12
13
|
meta: { name: '@cascivo/eslint-plugin', version: '0.1.0' },
|
|
13
|
-
rules: { 'prop-vocabulary': propVocabulary },
|
|
14
|
+
rules: { 'prop-vocabulary': propVocabulary, 'token-values': tokenValues },
|
|
14
15
|
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "GENERATED by scripts/style-contract/generate.ts — run `pnpm regen`. Do not edit by hand. Names omit the shared `--cascivo-` prefix; the rule restores it.",
|
|
3
|
+
"tokens": [
|
|
4
|
+
"aspect-ratio",
|
|
5
|
+
"blue-100",
|
|
6
|
+
"blue-200",
|
|
7
|
+
"blue-300",
|
|
8
|
+
"blue-400",
|
|
9
|
+
"blue-50",
|
|
10
|
+
"blue-500",
|
|
11
|
+
"blue-600",
|
|
12
|
+
"blue-700",
|
|
13
|
+
"blue-800",
|
|
14
|
+
"blue-900",
|
|
15
|
+
"blue-950",
|
|
16
|
+
"border-default",
|
|
17
|
+
"border-strong",
|
|
18
|
+
"border-subtle",
|
|
19
|
+
"brand-accent",
|
|
20
|
+
"brand-gradient-end",
|
|
21
|
+
"brand-gradient-start",
|
|
22
|
+
"brand-ink",
|
|
23
|
+
"brand-paper",
|
|
24
|
+
"brand-primary",
|
|
25
|
+
"button-destructive-bg",
|
|
26
|
+
"button-destructive-bg-hover",
|
|
27
|
+
"button-ghost-bg",
|
|
28
|
+
"button-ghost-bg-hover",
|
|
29
|
+
"button-primary-bg",
|
|
30
|
+
"button-primary-bg-active",
|
|
31
|
+
"button-primary-bg-hover",
|
|
32
|
+
"button-radius",
|
|
33
|
+
"button-secondary-bg",
|
|
34
|
+
"button-secondary-bg-hover",
|
|
35
|
+
"calendar-bg",
|
|
36
|
+
"calendar-cell-size",
|
|
37
|
+
"calendar-day-selected-bg",
|
|
38
|
+
"calendar-day-selected-fg",
|
|
39
|
+
"calendar-day-today-color",
|
|
40
|
+
"calendar-radius",
|
|
41
|
+
"calendar-range-bg",
|
|
42
|
+
"chart-1",
|
|
43
|
+
"chart-2",
|
|
44
|
+
"chart-3",
|
|
45
|
+
"chart-4",
|
|
46
|
+
"chart-5",
|
|
47
|
+
"chart-6",
|
|
48
|
+
"chart-7",
|
|
49
|
+
"chart-8",
|
|
50
|
+
"chart-anim-dur",
|
|
51
|
+
"chart-anim-ease",
|
|
52
|
+
"chart-anim-props",
|
|
53
|
+
"chart-axis",
|
|
54
|
+
"chart-fill-opacity",
|
|
55
|
+
"chart-fill-opacity-overlap",
|
|
56
|
+
"chart-grid",
|
|
57
|
+
"color-accent",
|
|
58
|
+
"color-accent-active",
|
|
59
|
+
"color-accent-content",
|
|
60
|
+
"color-accent-foreground",
|
|
61
|
+
"color-accent-hover",
|
|
62
|
+
"color-accent-muted",
|
|
63
|
+
"color-accent-subtle",
|
|
64
|
+
"color-accent-text",
|
|
65
|
+
"color-accent-text-hover",
|
|
66
|
+
"color-active-bg",
|
|
67
|
+
"color-background",
|
|
68
|
+
"color-bg",
|
|
69
|
+
"color-bg-subtle",
|
|
70
|
+
"color-border",
|
|
71
|
+
"color-border-strong",
|
|
72
|
+
"color-destructive",
|
|
73
|
+
"color-destructive-content",
|
|
74
|
+
"color-destructive-foreground",
|
|
75
|
+
"color-destructive-hover",
|
|
76
|
+
"color-destructive-subtle",
|
|
77
|
+
"color-error",
|
|
78
|
+
"color-error-content",
|
|
79
|
+
"color-focus-ring",
|
|
80
|
+
"color-foreground",
|
|
81
|
+
"color-foreground-muted",
|
|
82
|
+
"color-info",
|
|
83
|
+
"color-info-content",
|
|
84
|
+
"color-info-foreground",
|
|
85
|
+
"color-info-subtle",
|
|
86
|
+
"color-picker-alpha-to",
|
|
87
|
+
"color-picker-area-size",
|
|
88
|
+
"color-primary",
|
|
89
|
+
"color-primary-active",
|
|
90
|
+
"color-primary-content",
|
|
91
|
+
"color-primary-fg",
|
|
92
|
+
"color-primary-hover",
|
|
93
|
+
"color-scrim",
|
|
94
|
+
"color-secondary",
|
|
95
|
+
"color-secondary-content",
|
|
96
|
+
"color-secondary-hover",
|
|
97
|
+
"color-secondary-subtle",
|
|
98
|
+
"color-success",
|
|
99
|
+
"color-success-content",
|
|
100
|
+
"color-success-foreground",
|
|
101
|
+
"color-success-subtle",
|
|
102
|
+
"color-surface",
|
|
103
|
+
"color-surface-2",
|
|
104
|
+
"color-surface-overlay",
|
|
105
|
+
"color-surface-raised",
|
|
106
|
+
"color-text",
|
|
107
|
+
"color-text-muted",
|
|
108
|
+
"color-text-on-accent",
|
|
109
|
+
"color-text-on-destructive",
|
|
110
|
+
"color-text-subtle",
|
|
111
|
+
"color-warning",
|
|
112
|
+
"color-warning-content",
|
|
113
|
+
"color-warning-foreground",
|
|
114
|
+
"color-warning-subtle",
|
|
115
|
+
"context-x",
|
|
116
|
+
"context-y",
|
|
117
|
+
"control-height-lg",
|
|
118
|
+
"control-height-md",
|
|
119
|
+
"control-height-sm",
|
|
120
|
+
"data-table-cell-gap",
|
|
121
|
+
"data-table-height",
|
|
122
|
+
"data-table-max-height",
|
|
123
|
+
"date-picker-day-today-color",
|
|
124
|
+
"dialog-body-gap",
|
|
125
|
+
"disabled-opacity",
|
|
126
|
+
"dropdown-anchor",
|
|
127
|
+
"duration-100",
|
|
128
|
+
"duration-150",
|
|
129
|
+
"duration-200",
|
|
130
|
+
"duration-300",
|
|
131
|
+
"duration-500",
|
|
132
|
+
"duration-75",
|
|
133
|
+
"duration-loop",
|
|
134
|
+
"duration-loop-fast",
|
|
135
|
+
"duration-loop-slow",
|
|
136
|
+
"ease-decel",
|
|
137
|
+
"ease-emphasized",
|
|
138
|
+
"ease-in",
|
|
139
|
+
"ease-in-out",
|
|
140
|
+
"ease-out",
|
|
141
|
+
"ease-spring",
|
|
142
|
+
"editor-bg",
|
|
143
|
+
"editor-border",
|
|
144
|
+
"editor-bracket",
|
|
145
|
+
"editor-caret-line",
|
|
146
|
+
"editor-current-line",
|
|
147
|
+
"editor-fg",
|
|
148
|
+
"editor-gutter-active",
|
|
149
|
+
"editor-gutter-bg",
|
|
150
|
+
"editor-gutter-fg",
|
|
151
|
+
"editor-match",
|
|
152
|
+
"editor-match-current",
|
|
153
|
+
"editor-selection",
|
|
154
|
+
"editor-syntax-attr",
|
|
155
|
+
"editor-syntax-boolean",
|
|
156
|
+
"editor-syntax-comment",
|
|
157
|
+
"editor-syntax-function",
|
|
158
|
+
"editor-syntax-keyword",
|
|
159
|
+
"editor-syntax-number",
|
|
160
|
+
"editor-syntax-operator",
|
|
161
|
+
"editor-syntax-property",
|
|
162
|
+
"editor-syntax-punctuation",
|
|
163
|
+
"editor-syntax-regexp",
|
|
164
|
+
"editor-syntax-string",
|
|
165
|
+
"editor-syntax-tag",
|
|
166
|
+
"editor-syntax-type",
|
|
167
|
+
"editor-syntax-variable",
|
|
168
|
+
"editor-tab-size",
|
|
169
|
+
"flash-tint",
|
|
170
|
+
"focus-ring",
|
|
171
|
+
"font-bold",
|
|
172
|
+
"font-display",
|
|
173
|
+
"font-medium",
|
|
174
|
+
"font-mono",
|
|
175
|
+
"font-normal",
|
|
176
|
+
"font-sans",
|
|
177
|
+
"font-semibold",
|
|
178
|
+
"font-size-2xl",
|
|
179
|
+
"font-size-3xl",
|
|
180
|
+
"font-size-4xl",
|
|
181
|
+
"font-size-base",
|
|
182
|
+
"font-size-lg",
|
|
183
|
+
"font-size-sm",
|
|
184
|
+
"font-size-xl",
|
|
185
|
+
"font-size-xs",
|
|
186
|
+
"gray-0",
|
|
187
|
+
"gray-100",
|
|
188
|
+
"gray-200",
|
|
189
|
+
"gray-300",
|
|
190
|
+
"gray-400",
|
|
191
|
+
"gray-50",
|
|
192
|
+
"gray-500",
|
|
193
|
+
"gray-600",
|
|
194
|
+
"gray-700",
|
|
195
|
+
"gray-800",
|
|
196
|
+
"gray-900",
|
|
197
|
+
"gray-950",
|
|
198
|
+
"green-100",
|
|
199
|
+
"green-200",
|
|
200
|
+
"green-400",
|
|
201
|
+
"green-50",
|
|
202
|
+
"green-500",
|
|
203
|
+
"green-600",
|
|
204
|
+
"green-700",
|
|
205
|
+
"green-900",
|
|
206
|
+
"header-bg",
|
|
207
|
+
"hover-opacity",
|
|
208
|
+
"kpi-padding",
|
|
209
|
+
"leading-none",
|
|
210
|
+
"leading-normal",
|
|
211
|
+
"leading-relaxed",
|
|
212
|
+
"leading-snug",
|
|
213
|
+
"leading-tight",
|
|
214
|
+
"link-color",
|
|
215
|
+
"logo-size",
|
|
216
|
+
"motion-attention",
|
|
217
|
+
"motion-drawer",
|
|
218
|
+
"motion-emphasis",
|
|
219
|
+
"motion-enter",
|
|
220
|
+
"motion-exit",
|
|
221
|
+
"orange-100",
|
|
222
|
+
"orange-400",
|
|
223
|
+
"orange-50",
|
|
224
|
+
"orange-500",
|
|
225
|
+
"orange-600",
|
|
226
|
+
"popover-anchor",
|
|
227
|
+
"progress-color",
|
|
228
|
+
"progress-value",
|
|
229
|
+
"radial-color",
|
|
230
|
+
"radial-progress",
|
|
231
|
+
"radial-size",
|
|
232
|
+
"radial-thickness",
|
|
233
|
+
"radius-2xl",
|
|
234
|
+
"radius-badge",
|
|
235
|
+
"radius-base",
|
|
236
|
+
"radius-button",
|
|
237
|
+
"radius-card",
|
|
238
|
+
"radius-component",
|
|
239
|
+
"radius-control",
|
|
240
|
+
"radius-field",
|
|
241
|
+
"radius-full",
|
|
242
|
+
"radius-indicator",
|
|
243
|
+
"radius-input",
|
|
244
|
+
"radius-item",
|
|
245
|
+
"radius-lg",
|
|
246
|
+
"radius-md",
|
|
247
|
+
"radius-modal",
|
|
248
|
+
"radius-none",
|
|
249
|
+
"radius-overlay",
|
|
250
|
+
"radius-pill",
|
|
251
|
+
"radius-sm",
|
|
252
|
+
"radius-surface",
|
|
253
|
+
"radius-xl",
|
|
254
|
+
"red-100",
|
|
255
|
+
"red-200",
|
|
256
|
+
"red-400",
|
|
257
|
+
"red-50",
|
|
258
|
+
"red-500",
|
|
259
|
+
"red-600",
|
|
260
|
+
"red-700",
|
|
261
|
+
"red-900",
|
|
262
|
+
"resizable-ratio",
|
|
263
|
+
"ring-color",
|
|
264
|
+
"ring-offset",
|
|
265
|
+
"ring-width",
|
|
266
|
+
"screen-lg",
|
|
267
|
+
"screen-md",
|
|
268
|
+
"screen-sm",
|
|
269
|
+
"screen-xl",
|
|
270
|
+
"scroll-area-height",
|
|
271
|
+
"scroll-area-width",
|
|
272
|
+
"search-width",
|
|
273
|
+
"shadow-lg",
|
|
274
|
+
"shadow-md",
|
|
275
|
+
"shadow-overlay",
|
|
276
|
+
"shadow-sm",
|
|
277
|
+
"shadow-xl",
|
|
278
|
+
"shadow-xs",
|
|
279
|
+
"shell-aside-inline-size",
|
|
280
|
+
"shell-header-block-size",
|
|
281
|
+
"shell-panel-inline-size",
|
|
282
|
+
"sidenav-bg",
|
|
283
|
+
"sidenav-inline-size",
|
|
284
|
+
"sidenav-rail-inline-size",
|
|
285
|
+
"skeleton-height",
|
|
286
|
+
"skeleton-width",
|
|
287
|
+
"space-0",
|
|
288
|
+
"space-1",
|
|
289
|
+
"space-10",
|
|
290
|
+
"space-12",
|
|
291
|
+
"space-16",
|
|
292
|
+
"space-2",
|
|
293
|
+
"space-20",
|
|
294
|
+
"space-24",
|
|
295
|
+
"space-3",
|
|
296
|
+
"space-4",
|
|
297
|
+
"space-5",
|
|
298
|
+
"space-6",
|
|
299
|
+
"space-8",
|
|
300
|
+
"stack-index",
|
|
301
|
+
"stack-offset",
|
|
302
|
+
"table-zebra-bg",
|
|
303
|
+
"target-min",
|
|
304
|
+
"target-min-coarse",
|
|
305
|
+
"text-2xl",
|
|
306
|
+
"text-2xl-fluid",
|
|
307
|
+
"text-3xl",
|
|
308
|
+
"text-3xl-fluid",
|
|
309
|
+
"text-4xl",
|
|
310
|
+
"text-4xl-fluid",
|
|
311
|
+
"text-base",
|
|
312
|
+
"text-body",
|
|
313
|
+
"text-body-sm",
|
|
314
|
+
"text-caption",
|
|
315
|
+
"text-code",
|
|
316
|
+
"text-display",
|
|
317
|
+
"text-heading-lg",
|
|
318
|
+
"text-heading-md",
|
|
319
|
+
"text-heading-sm",
|
|
320
|
+
"text-label",
|
|
321
|
+
"text-lg",
|
|
322
|
+
"text-sm",
|
|
323
|
+
"text-ui",
|
|
324
|
+
"text-xl",
|
|
325
|
+
"text-xs",
|
|
326
|
+
"textarea-max-block-size",
|
|
327
|
+
"tooltip-anchor",
|
|
328
|
+
"tracking-normal",
|
|
329
|
+
"tracking-tight",
|
|
330
|
+
"tracking-wide",
|
|
331
|
+
"tree-indent",
|
|
332
|
+
"tree-level",
|
|
333
|
+
"warm-100",
|
|
334
|
+
"warm-200",
|
|
335
|
+
"warm-300",
|
|
336
|
+
"warm-400",
|
|
337
|
+
"warm-50",
|
|
338
|
+
"warm-500",
|
|
339
|
+
"warm-600",
|
|
340
|
+
"warm-700",
|
|
341
|
+
"warm-800",
|
|
342
|
+
"warm-900",
|
|
343
|
+
"yellow-100",
|
|
344
|
+
"yellow-400",
|
|
345
|
+
"yellow-50",
|
|
346
|
+
"yellow-500",
|
|
347
|
+
"z-base",
|
|
348
|
+
"z-dock",
|
|
349
|
+
"z-dropdown",
|
|
350
|
+
"z-modal",
|
|
351
|
+
"z-overlay",
|
|
352
|
+
"z-raised",
|
|
353
|
+
"z-toast",
|
|
354
|
+
"z-tooltip"
|
|
355
|
+
]
|
|
356
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cascivo/token-values` — a `--cascivo-*` name that does not exist, reported where it is
|
|
3
|
+
* written.
|
|
4
|
+
*
|
|
5
|
+
* ## Why a lint rule, when there is already a token type and a token catalog
|
|
6
|
+
*
|
|
7
|
+
* There were three token surfaces before this rule and none of them could reject anything.
|
|
8
|
+
* `CascivoToken` was a generated union with no internal call site — an autocomplete aid an
|
|
9
|
+
* adopter had to opt into. `tokens.catalog.json` was data for the docs page. `cascivo audit`
|
|
10
|
+
* knew the token values but only ever looked for *literals that should be tokens*, never for
|
|
11
|
+
* *tokens that do not exist*.
|
|
12
|
+
*
|
|
13
|
+
* That left the single most expensive class of styling mistake completely unreported.
|
|
14
|
+
* **CSS silently drops an unknown custom property.** `style={{ '--cascivo-color-acent':
|
|
15
|
+
* 'red' }}` does not warn, does not fail the build, does not appear in DevTools, and does
|
|
16
|
+
* not throw — it has no effect, and the search for the cause starts in the component. Our
|
|
17
|
+
* own token docs say this in as many words about the `--cascivo-text-*` / `--cascivo-font-*`
|
|
18
|
+
* split, which is precisely the pair an agent gets wrong because it types from memory.
|
|
19
|
+
*
|
|
20
|
+
* TypeScript cannot cover this on its own: React's `CSSProperties` has no index signature
|
|
21
|
+
* for `--*` keys, so every custom property in a `style` prop reaches the DOM through a cast
|
|
22
|
+
* the adopter wrote, and a cast launders a typo by definition. `CascivoTokenStyle` +
|
|
23
|
+
* `satisfies` closes it for people who adopt the pattern; this rule closes it for everyone
|
|
24
|
+
* else, in the editor, before the build.
|
|
25
|
+
*
|
|
26
|
+
* ## Why `warn`, not `error`
|
|
27
|
+
*
|
|
28
|
+
* Same reasoning as `prop-vocabulary`, and it has not changed: a rule that fails somebody's
|
|
29
|
+
* build gets the whole config deleted, and that takes `react-hooks/immutability` with it.
|
|
30
|
+
* `cascivo audit --ai` reports the same finding at **error** level, because there it is an
|
|
31
|
+
* explicit gate somebody chose to run rather than something that appeared in their editor.
|
|
32
|
+
*
|
|
33
|
+
* ## Why the data is generated
|
|
34
|
+
*
|
|
35
|
+
* `token-catalog.json` comes from `scripts/style-contract/generate.ts`, which reads the same
|
|
36
|
+
* catalog the docs site and the CLI audit read. A token renamed in CSS updates this rule on
|
|
37
|
+
* the next `pnpm regen`; a hand-maintained list would eventually report a name that is
|
|
38
|
+
* correct, which is worse than reporting nothing.
|
|
39
|
+
*
|
|
40
|
+
* ## Why this rule does only one thing
|
|
41
|
+
*
|
|
42
|
+
* It does not also report "`#3b82f6` should be `var(--cascivo-blue-500)`". That is
|
|
43
|
+
* `cascivo audit --ai`'s `hardcoded-value`, which scopes by CSS property and reads CSS
|
|
44
|
+
* files — neither of which a lint rule looking at a custom-property key can do. Carrying a
|
|
45
|
+
* second, weaker copy here bought overlap and a false-positive surface, not coverage.
|
|
46
|
+
*/
|
|
47
|
+
import { createRequire } from 'node:module'
|
|
48
|
+
|
|
49
|
+
const require = createRequire(import.meta.url)
|
|
50
|
+
/**
|
|
51
|
+
* Names are stored without the shared `--cascivo-` prefix — 339 copies of a constant is most
|
|
52
|
+
* of this package's 8 KB bundle budget. Restored here, once.
|
|
53
|
+
*
|
|
54
|
+
* @type {{ tokens: string[] }}
|
|
55
|
+
*/
|
|
56
|
+
const catalog = require('./token-catalog.json')
|
|
57
|
+
|
|
58
|
+
const PREFIX = '--cascivo-'
|
|
59
|
+
const TOKENS = new Set(catalog.tokens.map((name) => PREFIX + name))
|
|
60
|
+
|
|
61
|
+
/** Levenshtein distance, capped — only used to pick a suggestion, never in a hot path. */
|
|
62
|
+
function distance(a, b) {
|
|
63
|
+
const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)])
|
|
64
|
+
for (let j = 0; j <= b.length; j++) rows[0][j] = j
|
|
65
|
+
for (let i = 1; i <= a.length; i++) {
|
|
66
|
+
for (let j = 1; j <= b.length; j++) {
|
|
67
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
|
68
|
+
rows[i][j] = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return rows[a.length][b.length]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Tokens keyed by their segments in sorted order, so a name written with the words in the
|
|
76
|
+
* wrong order finds its token.
|
|
77
|
+
*
|
|
78
|
+
* This is not a hypothetical shape. `docs/TOKENS.md` documents `--cascivo-text-color` vs
|
|
79
|
+
* `--cascivo-color-text` as a standing trap, and word-order swaps are exactly what an agent
|
|
80
|
+
* produces when it types a token from memory — but they are four or five edits apart, so
|
|
81
|
+
* plain edit distance never finds them.
|
|
82
|
+
*/
|
|
83
|
+
const BY_SORTED_SEGMENTS = new Map()
|
|
84
|
+
for (const name of catalog.tokens) {
|
|
85
|
+
const key = name.split('-').sort().join('-')
|
|
86
|
+
if (!BY_SORTED_SEGMENTS.has(key)) BY_SORTED_SEGMENTS.set(key, PREFIX + name)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The shipped token closest to what was written, or null when nothing is close enough.
|
|
91
|
+
*
|
|
92
|
+
* The threshold scales with name length so `--cascivo-color-acent` finds its token while a
|
|
93
|
+
* genuinely bespoke `--cascivo-my-app-thing` is left alone: guessing at a name the adopter
|
|
94
|
+
* invented on purpose is noise, and noise is how a rule gets switched off.
|
|
95
|
+
*/
|
|
96
|
+
function nearest(name) {
|
|
97
|
+
const reordered = BY_SORTED_SEGMENTS.get(name.replace(PREFIX, '').split('-').sort().join('-'))
|
|
98
|
+
if (reordered) return reordered
|
|
99
|
+
|
|
100
|
+
const budget = Math.min(4, Math.max(2, Math.floor(name.length / 8)))
|
|
101
|
+
let best = null
|
|
102
|
+
let bestDistance = Number.POSITIVE_INFINITY
|
|
103
|
+
for (const token of TOKENS) {
|
|
104
|
+
const d = distance(name, token)
|
|
105
|
+
if (d < bestDistance) {
|
|
106
|
+
bestDistance = d
|
|
107
|
+
best = token
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return bestDistance <= budget ? best : null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Every `--cascivo-*` name referenced by a `var()` inside a string value. */
|
|
114
|
+
function varReferences(value) {
|
|
115
|
+
return [...value.matchAll(/var\(\s*(--cascivo-[a-z0-9-]+)/gi)].map((m) => m[1])
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The object literal a `style={…}` attribute carries, looking through a cast. */
|
|
119
|
+
function styleObject(attribute) {
|
|
120
|
+
if (attribute.value?.type !== 'JSXExpressionContainer') return null
|
|
121
|
+
let expression = attribute.value.expression
|
|
122
|
+
// `style={{ … } as CSSProperties}` — the cast every adopter writes, because
|
|
123
|
+
// `CSSProperties` has no index signature for custom properties.
|
|
124
|
+
while (expression?.type === 'TSAsExpression' || expression?.type === 'TSSatisfiesExpression') {
|
|
125
|
+
expression = expression.expression
|
|
126
|
+
}
|
|
127
|
+
return expression?.type === 'ObjectExpression' ? expression : null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The literal text of a property key, whether written quoted or computed-with-a-string. */
|
|
131
|
+
function keyName(property) {
|
|
132
|
+
const key = property.key
|
|
133
|
+
if (!key) return null
|
|
134
|
+
if (key.type === 'Literal' && typeof key.value === 'string') return key.value
|
|
135
|
+
if (property.computed && key.type === 'Literal' && typeof key.value === 'string') return key.value
|
|
136
|
+
return null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const rule = {
|
|
140
|
+
meta: {
|
|
141
|
+
type: 'problem',
|
|
142
|
+
docs: {
|
|
143
|
+
description: 'Report a --cascivo-* custom property that does not exist',
|
|
144
|
+
url: 'https://github.com/cascivo/cascivo/tree/main/packages/eslint-plugin#readme',
|
|
145
|
+
},
|
|
146
|
+
schema: [],
|
|
147
|
+
messages: {
|
|
148
|
+
unknownToken:
|
|
149
|
+
'`{{wrote}}` is not a cascivo token — CSS drops an unknown custom property silently, so this has no effect. Did you mean `{{suggestion}}`?',
|
|
150
|
+
unknownTokenNoGuess:
|
|
151
|
+
'`{{wrote}}` is not a cascivo token — CSS drops an unknown custom property silently, so this has no effect. The shipped names are in `@cascivo/tokens/tokens.json`.',
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
create(context) {
|
|
156
|
+
/** @param {import('estree').Node} node @param {string} name */
|
|
157
|
+
function checkTokenName(node, name) {
|
|
158
|
+
if (TOKENS.has(name)) return
|
|
159
|
+
const suggestion = nearest(name)
|
|
160
|
+
context.report({
|
|
161
|
+
node,
|
|
162
|
+
messageId: suggestion ? 'unknownToken' : 'unknownTokenNoGuess',
|
|
163
|
+
data: { wrote: name, suggestion: suggestion ?? '' },
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
JSXAttribute(node) {
|
|
169
|
+
if (node.name?.type !== 'JSXIdentifier' || node.name.name !== 'style') return
|
|
170
|
+
const object = styleObject(node)
|
|
171
|
+
if (!object) return
|
|
172
|
+
|
|
173
|
+
for (const property of object.properties) {
|
|
174
|
+
if (property.type !== 'Property') continue
|
|
175
|
+
|
|
176
|
+
const key = keyName(property)
|
|
177
|
+
if (key?.startsWith('--cascivo-')) checkTokenName(property.key, key)
|
|
178
|
+
|
|
179
|
+
const value = property.value
|
|
180
|
+
if (value?.type !== 'Literal' || typeof value.value !== 'string') continue
|
|
181
|
+
|
|
182
|
+
for (const referenced of varReferences(value.value)) {
|
|
183
|
+
checkTokenName(value, referenced)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export default rule
|