@tenphi/eslint-plugin-tasty 1.17.1 → 1.19.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 +64 -45
- package/dist/rules/no-protected-restyle.js +24 -14
- package/dist/rules/no-protected-restyle.js.map +1 -1
- package/docs/rules/consistent-token-usage.md +72 -0
- package/docs/rules/known-property.md +55 -0
- package/docs/rules/no-classname-prop.md +60 -0
- package/docs/rules/no-ignored-pseudo-element-state.md +97 -0
- package/docs/rules/no-important.md +25 -0
- package/docs/rules/no-nested-selector.md +39 -0
- package/docs/rules/no-nested-state-map.md +33 -0
- package/docs/rules/no-own-at-root.md +57 -0
- package/docs/rules/no-protected-restyle.md +192 -0
- package/docs/rules/no-raw-color-values.md +49 -0
- package/docs/rules/no-raw-motion-duration.md +50 -0
- package/docs/rules/no-raw-transition-duration.md +56 -0
- package/docs/rules/no-runtime-styles-mutation.md +58 -0
- package/docs/rules/no-state-in-selector.md +142 -0
- package/docs/rules/no-style-prop.md +127 -0
- package/docs/rules/no-style-spread.md +53 -0
- package/docs/rules/no-styles-prop.md +57 -0
- package/docs/rules/no-unknown-state-alias.md +45 -0
- package/docs/rules/prefer-auto-calc.md +30 -0
- package/docs/rules/prefer-custom-property-syntax.md +56 -0
- package/docs/rules/prefer-directional-shorthand.md +46 -0
- package/docs/rules/prefer-element-selector.md +77 -0
- package/docs/rules/prefer-fill-mix.md +119 -0
- package/docs/rules/prefer-hide.md +31 -0
- package/docs/rules/prefer-longhand-property.md +25 -0
- package/docs/rules/prefer-shorthand-property.md +194 -0
- package/docs/rules/prefer-state-negation.md +44 -0
- package/docs/rules/require-default-state.md +50 -0
- package/docs/rules/static-no-dynamic-values.md +32 -0
- package/docs/rules/static-valid-selector.md +36 -0
- package/docs/rules/valid-boolean-property.md +39 -0
- package/docs/rules/valid-color-token.md +64 -0
- package/docs/rules/valid-custom-property.md +36 -0
- package/docs/rules/valid-custom-unit.md +34 -0
- package/docs/rules/valid-default-state-order.md +44 -0
- package/docs/rules/valid-directional-modifier.md +46 -0
- package/docs/rules/valid-preset.md +35 -0
- package/docs/rules/valid-radius-shape.md +31 -0
- package/docs/rules/valid-recipe.md +35 -0
- package/docs/rules/valid-state-definition.md +46 -0
- package/docs/rules/valid-state-key.md +62 -0
- package/docs/rules/valid-styles-structure.md +48 -0
- package/docs/rules/valid-sub-element.md +100 -0
- package/docs/rules/valid-transition.md +46 -0
- package/docs/rules/valid-value.md +65 -0
- package/harness/dev.harnesspm/hooks.json +12 -0
- package/harness/dev.harnesspm/rules/usage.md +11 -0
- package/harness/dev.harnesspm/scripts/check-edit.mjs +127 -0
- package/harness/dev.harnesspm/scripts/check.mjs +159 -0
- package/harness/dev.harnesspm/scripts/lint.mjs +236 -0
- package/harness/plugin.json +23 -0
- package/package.json +11 -7
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# tasty/no-nested-state-map
|
|
2
|
+
|
|
3
|
+
Reports a state map inside another state map.
|
|
4
|
+
|
|
5
|
+
**Presets:** error in recommended and strict. **Fix:** none.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
A state map's keys are states and its values are style values. Tasty does not read a map as a value, so the nested map renders nothing. Combine the states in one key instead.
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// ❌ Nested state maps are not allowed. Use combined state keys instead.
|
|
15
|
+
fill: {
|
|
16
|
+
'': '#white',
|
|
17
|
+
hovered: {
|
|
18
|
+
'': '#blue',
|
|
19
|
+
pressed: '#red',
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ✅ Combined states
|
|
24
|
+
fill: {
|
|
25
|
+
'': '#white',
|
|
26
|
+
hovered: '#blue',
|
|
27
|
+
'hovered & pressed': '#red',
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Options
|
|
32
|
+
|
|
33
|
+
None.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# tasty/no-own-at-root
|
|
2
|
+
|
|
3
|
+
Reports `@own(...)` in a state key of the root styles, where it is redundant.
|
|
4
|
+
|
|
5
|
+
**Presets:** warning in recommended and strict. **Fix:** autofix.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
`@own(...)` says "this state belongs to the sub-element itself, not to the root". At the root, the element is the root, so `@own(:hover)` means the same as `:hover`. Writing it there suggests a difference that does not exist.
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// ❌ @own() is redundant outside sub-element styles.
|
|
15
|
+
tasty({
|
|
16
|
+
styles: {
|
|
17
|
+
fill: { '': '#white', '@own(:hover)': '#blue' },
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// ✅ After the autofix
|
|
22
|
+
tasty({
|
|
23
|
+
styles: {
|
|
24
|
+
fill: { '': '#white', ':hover': '#blue' },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// ✅ In a sub-element, @own() is the sub-element's own state
|
|
29
|
+
tasty({
|
|
30
|
+
styles: {
|
|
31
|
+
Icon: {
|
|
32
|
+
fill: { '': '#white', '@own(:hover)': '#blue' },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Autofix
|
|
39
|
+
|
|
40
|
+
The fix unwraps every `@own(...)` term of the key and keeps a term with operators inside grouped:
|
|
41
|
+
|
|
42
|
+
| Before | After |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `'@parent(:hover) & !@own(:hover)'` | `'@parent(:hover) & !:hover'` |
|
|
45
|
+
| `'@own(pin=start \| pin=end)'` | `'pin=start \| pin=end'` |
|
|
46
|
+
| `'@own(hovered \| focused) & active'` | `'(hovered \| focused) & active'` |
|
|
47
|
+
|
|
48
|
+
A key with a comma inside `@own(...)` is reported without a fix.
|
|
49
|
+
|
|
50
|
+
## Not reported
|
|
51
|
+
|
|
52
|
+
- Sub-element styles, at any depth.
|
|
53
|
+
- Shared style objects, such as a `const CELL_STYLES: Styles = { … }` that may be spread into a sub-element, where `@own(...)` matters.
|
|
54
|
+
|
|
55
|
+
## Options
|
|
56
|
+
|
|
57
|
+
None.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# tasty/no-protected-restyle
|
|
2
|
+
|
|
3
|
+
Warn when a design system's protected components are restyled rather than
|
|
4
|
+
customized through their props. It does nothing until a `tasty.config.*` lists
|
|
5
|
+
`protectedComponents`.
|
|
6
|
+
|
|
7
|
+
**Presets:** warning in recommended and strict. **Fix:** none.
|
|
8
|
+
|
|
9
|
+
A design system built on Tasty exposes `styles` and `*Styles` props on its
|
|
10
|
+
components, but for most of them the look is a contract: a consumer picks a
|
|
11
|
+
`type`, `theme` or `size` rather than restyling a `Button`. This rule lets the
|
|
12
|
+
design system say which components those are, and asks an override to explain
|
|
13
|
+
itself.
|
|
14
|
+
|
|
15
|
+
## Configuration
|
|
16
|
+
|
|
17
|
+
The design system ships the list in its own `tasty.config.*`, and consumers
|
|
18
|
+
inherit it through `extends`:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// @my-org/ui tasty.config.ts
|
|
22
|
+
import type { TastyValidationConfig } from '@tenphi/eslint-plugin-tasty';
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
importSources: ['@my-org/ui'],
|
|
26
|
+
protectedComponents: [
|
|
27
|
+
{
|
|
28
|
+
source: '@my-org/ui',
|
|
29
|
+
exclude: ['Block', 'Flex', 'Grid', 'Space', 'Text', 'Title', '*Icon'],
|
|
30
|
+
allowedStyleProps: ['position', 'dimension'],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
} satisfies TastyValidationConfig;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// the consumer's tasty.config.ts
|
|
38
|
+
export default {
|
|
39
|
+
extends: '@my-org/ui',
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| Key | Meaning |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `source` | The package the components are imported from. `*` matches any run of characters. |
|
|
46
|
+
| `include` | Exported names or member paths to protect. Defaults to every export. |
|
|
47
|
+
| `exclude` | Exported names or member paths to leave open, usually layout and typography primitives. `*` matches any run of characters, so `*Icon` covers every icon. |
|
|
48
|
+
| `allowedStyleProps` | Style props that place or size the root and stay allowed. `'position'` and `'dimension'` stand for Tasty's `POSITION_STYLES` (`gridArea`, `margin`, `inset`, `zIndex`, …) and `DIMENSION_STYLES` (`width`, `height`, `flex`, …); anything else is a property name. Defaults to none. |
|
|
49
|
+
|
|
50
|
+
A component matches the first entry whose `source`, `include` and `exclude` fit
|
|
51
|
+
it, so a second entry for the same source can give a few components their own
|
|
52
|
+
`allowedStyleProps`. Entries from an extended config come first; a config can
|
|
53
|
+
add its own, or set `protectedComponents: false` to drop the inherited ones.
|
|
54
|
+
|
|
55
|
+
### Sub-components
|
|
56
|
+
|
|
57
|
+
A sub-component such as `<Button.Primary>` follows the entry of the export it
|
|
58
|
+
hangs off, which suits one that shares its parent's contract. Some are separate
|
|
59
|
+
components attached for convenience, though: `Button.Group` may be a layout
|
|
60
|
+
primitive. A name with a dot in `include` or `exclude` is a member path
|
|
61
|
+
(`'Button.Group'`, `'Menu.*'`), and an entry that names a component's member
|
|
62
|
+
path decides for it before any entry that matches only its export:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
protectedComponents: [
|
|
66
|
+
{
|
|
67
|
+
source: '@my-org/ui',
|
|
68
|
+
exclude: ['Space', 'ButtonGroup', 'Button.Group'],
|
|
69
|
+
allowedStyleProps: ['position', 'dimension'],
|
|
70
|
+
},
|
|
71
|
+
// `Button.Split` gets its own allowance; `Button` and `Button.Primary` don't.
|
|
72
|
+
{ source: '@my-org/ui', include: ['Button.Split'], allowedStyleProps: ['direction'] },
|
|
73
|
+
],
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
<Button.Group gap="1x" flow="row" />; // passes: Button.Group is excluded
|
|
78
|
+
<Button.Split direction="column" />; // passes
|
|
79
|
+
<Button gap="1x" />; // reported: Button keeps its contract
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The longest named path wins, so `'Button.Group'` also covers
|
|
83
|
+
`<Button.Group.Item>`, and the first entry that names a path decides it:
|
|
84
|
+
excluding the path leaves it open, including it protects it with that entry's
|
|
85
|
+
`allowedStyleProps`. Patterns without a dot, `*` included, only ever match the
|
|
86
|
+
export name.
|
|
87
|
+
|
|
88
|
+
For `extends: '@my-org/ui'` to find the list, the package has to publish its
|
|
89
|
+
`tasty.config.*` at the package root.
|
|
90
|
+
|
|
91
|
+
The design system's own source imports its components by relative path, which
|
|
92
|
+
does not match `source`, so the design system itself is not held to the rule.
|
|
93
|
+
|
|
94
|
+
## Options
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
'tasty/no-protected-restyle': ['error', { usages: 'wrappers' }],
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
| `usages` | Reports |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| `'all'` (default) | Instance `styles`, `*Styles` and style props, and `tasty()` wrappers |
|
|
103
|
+
| `'wrappers'` | `tasty()` wrappers that restyle a protected component, and wrappers of those wrappers |
|
|
104
|
+
|
|
105
|
+
A wrapper defines a restyled component once, and every use of it inherits the
|
|
106
|
+
restyle. `'wrappers'` lets a project enforce the rule on those definitions while
|
|
107
|
+
its instance overrides are still being worked through.
|
|
108
|
+
|
|
109
|
+
## What it reports
|
|
110
|
+
|
|
111
|
+
On a protected component:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import { tasty } from '@tenphi/tasty';
|
|
115
|
+
import { Button, ComboBox } from '@my-org/ui';
|
|
116
|
+
|
|
117
|
+
// Instance styles: `styles` and every `*Styles` prop.
|
|
118
|
+
<Button styles={{ fill: '#danger' }} />;
|
|
119
|
+
<ComboBox inputStyles={{ padding: '2x' }} />;
|
|
120
|
+
|
|
121
|
+
// Appearance style props.
|
|
122
|
+
<Button fill="#danger" radius="round" />;
|
|
123
|
+
|
|
124
|
+
// Wrappers that restyle it: `styles`, `variants`, or `*Styles` and appearance
|
|
125
|
+
// style props among their default props.
|
|
126
|
+
const DangerButton = tasty(Button, { styles: { fill: '#danger' } });
|
|
127
|
+
|
|
128
|
+
// Wrappers of wrappers still restyle `Button`.
|
|
129
|
+
const CopyButton = tasty(Button, { type: 'clear' });
|
|
130
|
+
<CopyButton styles={{ fill: '#danger' }} />;
|
|
131
|
+
tasty(CopyButton, { fill: '#danger' });
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
And not:
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
// Its own props, tokens and mods.
|
|
138
|
+
<Button type="primary" theme="danger" size="small" tokens={{ $gap: '1x' }} />;
|
|
139
|
+
|
|
140
|
+
// Allowed style props, and root `styles` whose keys are all allowed.
|
|
141
|
+
<Button width="100%" gridArea="actions" />;
|
|
142
|
+
<Button styles={{ width: '100%', gridArea: 'actions' }} />;
|
|
143
|
+
|
|
144
|
+
// A wrapper that only sets semantic defaults.
|
|
145
|
+
const CopyButton = tasty(Button, { type: 'clear', size: 'small' });
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Root `styles` passes only as an inline object whose every key is an allowed
|
|
149
|
+
style prop. A sub-element key, a spread, or a variable holding the styles is
|
|
150
|
+
reported. `*Styles` props style the component's inner elements, so they are
|
|
151
|
+
always reported.
|
|
152
|
+
|
|
153
|
+
A style prop is a Tasty style property or an entry in `config.styles`. Plain
|
|
154
|
+
CSS property names are left out, because some of them (`size`, `orientation`)
|
|
155
|
+
are common semantic props. With
|
|
156
|
+
[`typeAwareJSX`](../../README.md#experimental-type-aware-jsx-props) on, a prop
|
|
157
|
+
the component declares with its own type (such as a `position: 'before' |
|
|
158
|
+
'after'` prop) is not a style prop either. Callbacks and JSX values are never
|
|
159
|
+
style values.
|
|
160
|
+
|
|
161
|
+
## Recognition
|
|
162
|
+
|
|
163
|
+
A component resolves to the name its source exports it under, so:
|
|
164
|
+
|
|
165
|
+
- an aliased import (`import { Button as UIButton }`) still counts;
|
|
166
|
+
- a namespace member (`<UI.Button>`) is `Button`;
|
|
167
|
+
- a sub-component (`<Text.Strong>`) follows `Text`, so excluding `Text` leaves
|
|
168
|
+
`Text.Strong` open too, unless an entry names the member path (see
|
|
169
|
+
[Sub-components](#sub-components));
|
|
170
|
+
- `const` aliases and `tasty()` wrappers are followed, within the file and
|
|
171
|
+
across relative imports, along with relative re-exports and `export *` from a
|
|
172
|
+
protected source. Following relative modules needs the `typescript` package.
|
|
173
|
+
|
|
174
|
+
Shadowed bindings, `let` aliases, type-only imports and native elements are not
|
|
175
|
+
protected components.
|
|
176
|
+
|
|
177
|
+
## Intentional overrides
|
|
178
|
+
|
|
179
|
+
A justified override takes a reasoned disable on its line:
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
// eslint-disable-next-line tasty/no-protected-restyle -- campaign banner needs the brand fill
|
|
183
|
+
<Button fill="#brand" />;
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Relation to `no-styles-prop`
|
|
187
|
+
|
|
188
|
+
`tasty/no-styles-prop` reports instance `styles` on every recognized Tasty
|
|
189
|
+
component. Once a design system lists its protected components, this rule
|
|
190
|
+
covers those with a message that names the component. A project that relies on
|
|
191
|
+
it can turn `no-styles-prop` off, so layout primitives keep `styles` as their
|
|
192
|
+
expected API.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# tasty/no-raw-color-values
|
|
2
|
+
|
|
3
|
+
Reports raw colors in style values: hex colors, color functions such as `rgb()` or `okhsl()`, and named colors such as `red`. Use a color token instead.
|
|
4
|
+
|
|
5
|
+
**Presets:** warning in recommended and strict. **Fix:** none.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
A design system owns its colors in one place. A raw color skips the palette, so it does not follow themes, dark mode or high contrast, and it drifts from the tokens over time.
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// ❌ Use a color token instead of raw hex color '#ff0000'.
|
|
15
|
+
fill: '#ff0000'
|
|
16
|
+
|
|
17
|
+
// ❌ Use a color token instead of raw rgb() color.
|
|
18
|
+
color: 'rgb(0 0 0)'
|
|
19
|
+
|
|
20
|
+
// ❌ Use a color token instead of raw named color 'red'.
|
|
21
|
+
border: '1bw solid red'
|
|
22
|
+
|
|
23
|
+
// ✅ A token
|
|
24
|
+
fill: '#danger'
|
|
25
|
+
|
|
26
|
+
// ✅ A token definition may hold a raw color
|
|
27
|
+
'#accent': '#ff0000'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Checks
|
|
31
|
+
|
|
32
|
+
1. **Hex colors:** `#` followed by 3, 4, 6 or 8 hex digits. A token name made only of those characters (`#add`, `#beef`) reads as a hex color too.
|
|
33
|
+
2. **Color functions:** `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()`, `lab()`, `lch()`, `oklab()`, `oklch()`, `okhsl()`, `okhsv()`, `okhst()`, `color()`, `device-cmyk()` and `light-dark()`.
|
|
34
|
+
3. **Named colors** (`red`, `rebeccapurple`, …) in properties that take colors. `transparent` and `currentColor` are left to [`tasty/prefer-custom-property-syntax`](prefer-custom-property-syntax.md), which suggests `#clear` and `#current`.
|
|
35
|
+
|
|
36
|
+
Not reported: values of `#name` and `$name` keys, which define tokens, values inside `tastyStatic(':root', { … })`, quoted strings and URLs.
|
|
37
|
+
|
|
38
|
+
## Exceptions
|
|
39
|
+
|
|
40
|
+
For an intentional one-off color, disable the rule for that line, with a reason:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
// eslint-disable-next-line tasty/no-raw-color-values -- brand color from the partner's guidelines
|
|
44
|
+
fill: '#1db954',
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Options
|
|
48
|
+
|
|
49
|
+
None.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# tasty/no-raw-motion-duration
|
|
2
|
+
|
|
3
|
+
Reports a hardcoded duration in `animation`, `animationDuration` and `transitionDuration`, such as `'spin 2s linear infinite'`. Use a duration token instead.
|
|
4
|
+
|
|
5
|
+
**Presets:** none. Turn it on in your config. **Fix:** editor suggestions.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
When a design system keeps its animation timing in tokens, a raw `2s` drifts from it. This rule extends [`tasty/no-raw-transition-duration`](no-raw-transition-duration.md) to the CSS animation and duration properties. It is opt-in because not every design system has motion tokens.
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// ❌ animation duration '2s' is hardcoded. Use a duration token.
|
|
15
|
+
animation: 'spin 2s linear infinite'
|
|
16
|
+
|
|
17
|
+
// ❌ transitionDuration duration '150ms' is hardcoded. Use a duration token.
|
|
18
|
+
transitionDuration: '150ms'
|
|
19
|
+
|
|
20
|
+
// ✅ A token
|
|
21
|
+
animationDuration: '$fast-duration'
|
|
22
|
+
|
|
23
|
+
// ✅ A delay: the second time in an animation shorthand
|
|
24
|
+
animation: 'spin $spin-duration linear 2s'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Checks
|
|
28
|
+
|
|
29
|
+
- The duration in `animation`, and the values of `animationDuration` and `transitionDuration`, in Tasty style objects and JSX style props.
|
|
30
|
+
- A second time in an animation shorthand is a delay and is left alone.
|
|
31
|
+
- Zero and token-based timing are fine.
|
|
32
|
+
|
|
33
|
+
The editor suggests each configured duration token whose name fits the property, such as a `$…duration` token. It never suggests removing the duration: unlike Tasty's `transition`, these CSS properties have no Tasty timing to fall back to.
|
|
34
|
+
|
|
35
|
+
Lengths and other numbers are outside this rule. [`tasty/consistent-token-usage`](consistent-token-usage.md) covers pixel lengths.
|
|
36
|
+
|
|
37
|
+
## Turning it on
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import tasty from '@tenphi/eslint-plugin-tasty';
|
|
41
|
+
|
|
42
|
+
export default [
|
|
43
|
+
tasty.configs.recommended,
|
|
44
|
+
{ rules: { 'tasty/no-raw-motion-duration': 'warn' } },
|
|
45
|
+
];
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Options
|
|
49
|
+
|
|
50
|
+
None.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# tasty/no-raw-transition-duration
|
|
2
|
+
|
|
3
|
+
Reports a hardcoded duration in a `transition` value, such as `'fill 0.2s'`. Use a duration token, or leave the duration out to get the design system's timing.
|
|
4
|
+
|
|
5
|
+
**Presets:** warning in recommended and strict. **Fix:** editor suggestions.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
A design system that owns its motion timing owns it in one place. A `0.2s` sprinkled through components drifts the same way a raw hex color does. Without a duration, Tasty uses `var(--<name>-transition, var(--transition))`, so the timing follows the design system.
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// ❌ Transition duration '0.2s' is hardcoded. Use a duration token ($transition),
|
|
15
|
+
// or omit it to inherit $fill-transition (falling back to $transition).
|
|
16
|
+
transition: 'fill 0.2s'
|
|
17
|
+
|
|
18
|
+
// ❌ Transition duration '(0.2s * 2)' contains a hardcoded time.
|
|
19
|
+
transition: 'fill (0.2s * 2)'
|
|
20
|
+
|
|
21
|
+
// ✅ A token
|
|
22
|
+
transition: 'fill $transition'
|
|
23
|
+
|
|
24
|
+
// ✅ No duration: Tasty uses the per-name timing
|
|
25
|
+
transition: 'fill'
|
|
26
|
+
transition: 'fill ease-in'
|
|
27
|
+
|
|
28
|
+
// ✅ A delay, not a duration
|
|
29
|
+
transition: 'fill ease-in 0.1s'
|
|
30
|
+
|
|
31
|
+
// ✅ Zero turns the motion off
|
|
32
|
+
transition: 'fill 0s'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Checks
|
|
36
|
+
|
|
37
|
+
The rule checks only the **duration**: the second word of each comma group, when it is not an easing. A Tasty transition is `[name] [duration] [easing] [delay]` or `[name] [easing] [delay]`, so a later time is a delay and is left alone. Leaving out a delay means "no delay", a real change, while leaving out a duration falls back to the design system's timing.
|
|
38
|
+
|
|
39
|
+
Hardcoded means a nonzero CSS time (`0.2s`, `200ms`, `.15s`), or an expression with a raw time and no token, such as `(0.2s * 2)`. Tokens and expressions built on them are fine: `$transition`, `var(--transition)`, `calc(var(--transition) * 2)`.
|
|
40
|
+
|
|
41
|
+
The rule also checks the `transition` JSX prop of a local `const` component created by an imported `tasty()` call. Other components' props are not assumed to use Tasty's transition syntax.
|
|
42
|
+
|
|
43
|
+
## Suggestions
|
|
44
|
+
|
|
45
|
+
For a literal duration, the editor offers:
|
|
46
|
+
|
|
47
|
+
1. each duration token in the project config: a `tokens` entry named `$transition`, ending in `-transition`, or containing `duration`;
|
|
48
|
+
2. removing the duration, to inherit `var(--<name>-transition, var(--transition))`.
|
|
49
|
+
|
|
50
|
+
Neither is an autofix: both change what the browser renders. An expression with a raw time gets no suggestion, since replacing or removing it may change what it computes.
|
|
51
|
+
|
|
52
|
+
`@tenphi/tasty` ships a `tasty.config.ts` that lists `$transition`, and the plugin reads it as the base of the config chain, so there is always at least one token to suggest.
|
|
53
|
+
|
|
54
|
+
## Options
|
|
55
|
+
|
|
56
|
+
None.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# tasty/no-runtime-styles-mutation
|
|
2
|
+
|
|
3
|
+
Reports style values and keys computed at render time: props, state, hook results, parameters, local bindings, calls, conditionals, computed keys and template interpolation.
|
|
4
|
+
|
|
5
|
+
**Presets:** warning in recommended and strict. **Fix:** none.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Tasty generates CSS once per distinct styles object. Values that change on every render generate new CSS each time they change, and they hide what the component can look like. Express dynamic behavior with modifiers (`mods`), tokens (`tokens`) or CSS custom properties, and keep style values static.
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// ❌ Prefer a static style value for 'fill'. Use states with mods, tokens, or styleProps.
|
|
15
|
+
function Tab({ isActive }) {
|
|
16
|
+
return <Box styles={{ fill: isActive ? '#blue' : '#gray' }} />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ✅ A state map and a modifier
|
|
20
|
+
const TabBox = tasty({
|
|
21
|
+
styles: {
|
|
22
|
+
fill: {
|
|
23
|
+
'': '#gray',
|
|
24
|
+
active: '#blue',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function Tab({ isActive }) {
|
|
30
|
+
return <TabBox mods={{ active: isActive }} />;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## What is reported
|
|
35
|
+
|
|
36
|
+
The rule reports the specific value inside a state map or sub-element.
|
|
37
|
+
|
|
38
|
+
- A style object that no function encloses is built once, while the module loads, so none of its values is reported, calls and unresolved names included. An instance field does not count as module level: it runs for every instance.
|
|
39
|
+
- Inside a function, values fixed once the module has loaded are allowed, since Tasty generates their CSS once, as for a literal: `undefined`, module-scope `const` bindings, enums and imports (and their members, including computed members with such keys), and templates, operators, conditionals and `mergeStyles(...)` calls built only from those. The same test applies to computed keys.
|
|
40
|
+
- Anything that reaches render-time scope is reported: props, state, hook results, parameters, a local or `let` binding, any other call.
|
|
41
|
+
- A `mergeStyles` object-literal argument is a style object of its own and is checked where it stands.
|
|
42
|
+
- Literal strings, `null`, `false` and `true` are fine.
|
|
43
|
+
|
|
44
|
+
Style objects are found where they reach Tasty, not only where they are written: an object behind a conditional or logical operator (`mergeStyles(cond ? { … } : null, styles)`), and an object held in a render-scope variable that is read as styles (`const local = { … }` or `styles = { ...styles, … }`, then `styles={local}`, `mergeStyles(styles, local)` or `Icon: local`). A render-scope `const` bound to an object literal is not reported where it is passed on: its values are checked where it is written.
|
|
45
|
+
|
|
46
|
+
Not covered here:
|
|
47
|
+
|
|
48
|
+
- Spreads, which [`tasty/no-style-spread`](no-style-spread.md) reports, so each can be disabled on its own.
|
|
49
|
+
- `tastyStatic()`, which [`tasty/static-no-dynamic-values`](static-no-dynamic-values.md) checks.
|
|
50
|
+
- At-rule definition objects (`@keyframes`, `@font-face`, …), which may be shared JavaScript data.
|
|
51
|
+
|
|
52
|
+
## Exceptions
|
|
53
|
+
|
|
54
|
+
For JavaScript computation that is really needed, disable the rule for that line, with a reason.
|
|
55
|
+
|
|
56
|
+
## Options
|
|
57
|
+
|
|
58
|
+
None.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# tasty/no-state-in-selector
|
|
2
|
+
|
|
3
|
+
Keep sub-element `$` selectors structural. Express state conditions in property
|
|
4
|
+
state maps so Tasty can resolve their priority and make competing states
|
|
5
|
+
exclusive.
|
|
6
|
+
|
|
7
|
+
**Presets:** warning in recommended and strict. **Fix:** none.
|
|
8
|
+
|
|
9
|
+
## Reported
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const Content = tasty({
|
|
13
|
+
styles: {
|
|
14
|
+
ResponsiveWidth: {
|
|
15
|
+
$: 'img:not([width]), :where(picture), video:not([width])',
|
|
16
|
+
inlineSize: 'max 100%',
|
|
17
|
+
},
|
|
18
|
+
Link: {
|
|
19
|
+
$: 'a:hover',
|
|
20
|
+
color: '#active',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The rule reports one warning per `$` value, pointing out its first condition.
|
|
27
|
+
Attribute selectors (such as `[width]`, `[disabled]`, `[data-active]`, and
|
|
28
|
+
`[aria-expanded="true"]`) and pseudo-classes (including `:hover`, `:not()`,
|
|
29
|
+
`:has()`, and `:last-child`) are conditions. Conditions are also reported inside
|
|
30
|
+
structural grouping functions such as `:is()` and `:where()`.
|
|
31
|
+
|
|
32
|
+
## Preferred
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
const Content = tasty({
|
|
36
|
+
styles: {
|
|
37
|
+
Link: {
|
|
38
|
+
$: 'a',
|
|
39
|
+
color: {
|
|
40
|
+
'': '#text',
|
|
41
|
+
'@own(:hover)': '#active',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
Picture: {
|
|
45
|
+
$: ':where(picture)',
|
|
46
|
+
inlineSize: 'max 100%',
|
|
47
|
+
},
|
|
48
|
+
Before: {
|
|
49
|
+
$: '&::before',
|
|
50
|
+
content: '""',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use `@own(...)` when the state belongs to the selected sub-element, for example
|
|
57
|
+
`@own(![width])`. Ordinary state keys such as `:hover` refer to the root component.
|
|
58
|
+
Conditions on ancestors need the appropriate root/parent state expression.
|
|
59
|
+
When a selector list mixes conditions with unconditional targets, keep their
|
|
60
|
+
different behavior explicit. For example, `picture` in the reported media list
|
|
61
|
+
has no width condition and can own a separate unconditional style definition.
|
|
62
|
+
|
|
63
|
+
Choose a default or fallback for each property state map deliberately. Removing
|
|
64
|
+
a condition from `$` broadens the selector: every property in that sub-element
|
|
65
|
+
must retain its intended condition, and default values can affect elements that
|
|
66
|
+
previously received no rule. The rule provides no automatic fixes or editor
|
|
67
|
+
suggestions because a safe migration depends on that intent.
|
|
68
|
+
|
|
69
|
+
## Allowed structural selectors
|
|
70
|
+
|
|
71
|
+
- Tags, universal selectors, Tasty element names and `@` placeholders.
|
|
72
|
+
- Combinators, classes and IDs that identify the component's anatomy.
|
|
73
|
+
- Exact, non-namespaced `[data-element="Name"]` selectors with a nonempty name.
|
|
74
|
+
- Pseudo-elements, including the legacy spellings `:before`, `:after`,
|
|
75
|
+
`:first-line`, and `:first-letter`.
|
|
76
|
+
- `:is()` and `:where()` containing only structural selectors.
|
|
77
|
+
|
|
78
|
+
## Selectors that end in a pseudo-element
|
|
79
|
+
|
|
80
|
+
When a `$` item ends in a pseudo-element, Tasty treats the pseudo-element as
|
|
81
|
+
the sub-element: `@own(...)` states go after it, and states a pseudo-element
|
|
82
|
+
cannot have (attributes, classes, `:first-child`, `:nth-*()`, `:empty`,
|
|
83
|
+
`:has()`, and modifiers) are dropped with a warning. A condition on the element
|
|
84
|
+
the pseudo-element belongs to must therefore stay in `$`, before the
|
|
85
|
+
pseudo-element, and the rule accepts it:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
const Pagination = tasty({
|
|
89
|
+
styles: {
|
|
90
|
+
NextArrow: {
|
|
91
|
+
$: 'a[rel="next"]::before',
|
|
92
|
+
content: '"→"',
|
|
93
|
+
},
|
|
94
|
+
Marker: {
|
|
95
|
+
$: 'li:last-child::marker',
|
|
96
|
+
color: '#text',
|
|
97
|
+
},
|
|
98
|
+
Label: {
|
|
99
|
+
// Not '@::part(label):hover': @own(:hover) is the part's own state.
|
|
100
|
+
$: '@::part(label)',
|
|
101
|
+
color: { '': '#text', '@own(:hover)': '#active' },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Moving such a condition into `@own(...)` would not work: Tasty ignores it, and
|
|
108
|
+
[`tasty/no-ignored-pseudo-element-state`](no-ignored-pseudo-element-state.md)
|
|
109
|
+
reports it. Pseudo-classes written after the pseudo-element are its own states,
|
|
110
|
+
which `@own(...)` expresses exactly, so they are still reported. Conditions after it
|
|
111
|
+
that `@own(...)` would drop, such as `::part(label):first-child`, are not.
|
|
112
|
+
Conditions inside the pseudo-element, such as `::slotted(img:hover)`, are part
|
|
113
|
+
of what it selects and stay in `$`. A condition in an earlier compound, such as
|
|
114
|
+
`:last-child` in `li:last-child > a::before`, describes an ancestor and is
|
|
115
|
+
still reported.
|
|
116
|
+
|
|
117
|
+
"Ends in a pseudo-element" means the first `::name` or legacy single-colon
|
|
118
|
+
spelling in the item's last compound. Each item of a selector list is checked
|
|
119
|
+
on its own, so `summary::marker, li:last-child` still reports `:last-child`.
|
|
120
|
+
|
|
121
|
+
Class and ID names are treated as identities; the rule cannot infer whether a
|
|
122
|
+
particular class name represents application state. Keep such state in maps too.
|
|
123
|
+
|
|
124
|
+
## Scope and limits
|
|
125
|
+
|
|
126
|
+
This rule checks literal strings and templates without interpolation in
|
|
127
|
+
sub-element `$` values. It uses the plugin's existing Tasty style detection,
|
|
128
|
+
including `tasty()`, `tastyStatic()`, `useStyles()`, `useGlobalStyles()`, typed/shared
|
|
129
|
+
style objects, JSX/Storybook styles, and configured `styleFunctions` helpers.
|
|
130
|
+
TypeScript wrappers and statically computed `$` keys are supported.
|
|
131
|
+
|
|
132
|
+
Root selector arguments to `tastyStatic()` and `useGlobalStyles()` are outside
|
|
133
|
+
this rule's scope, as are unrelated JavaScript objects and dynamic selectors.
|
|
134
|
+
Malformed selectors that cannot be parsed are left to syntax validation; this
|
|
135
|
+
rule does not validate CSS syntax. Other rules may flag dynamic style values.
|
|
136
|
+
|
|
137
|
+
For an intentional exception, disable only this rule on the selector's line and
|
|
138
|
+
explain why its condition must remain part of the structural selection.
|
|
139
|
+
|
|
140
|
+
## Options
|
|
141
|
+
|
|
142
|
+
None.
|