@tenphi/eslint-plugin-tasty 1.18.0 → 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/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,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.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# tasty/no-style-prop
|
|
2
|
+
|
|
3
|
+
Warn about React's `style` prop on recognized Tasty components. The [`elements`](#options) option extends the check to
|
|
4
|
+
every component or element, so this rule can replace React's `style`
|
|
5
|
+
restriction.
|
|
6
|
+
|
|
7
|
+
**Presets:** warning in recommended and strict. **Fix:** none.
|
|
8
|
+
|
|
9
|
+
Define token references in the component's styles and use `tokens` for dynamic
|
|
10
|
+
per-instance values:
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { tasty } from '@tenphi/tasty';
|
|
14
|
+
|
|
15
|
+
const Progress = tasty({ styles: { width: '$progress' } });
|
|
16
|
+
|
|
17
|
+
// ❌ Move the dynamic value to a token consumed by the definition.
|
|
18
|
+
<Progress style={{ width: `${percent}%` }} />;
|
|
19
|
+
|
|
20
|
+
// ✅ Preferred.
|
|
21
|
+
<Progress tokens={{ $progress: `${percent}%` }} />;
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The warning covers literals, variables, calls, conditional expressions, null,
|
|
25
|
+
and visible `style` keys in inline JSX object spreads. It does not rewrite the
|
|
26
|
+
prop: choosing a token and declaring its reference requires the component's
|
|
27
|
+
styling contract.
|
|
28
|
+
|
|
29
|
+
A third-party positioning or animation library may require applying its inline
|
|
30
|
+
styles through `style`. Keep that integration and acknowledge the exception with
|
|
31
|
+
an explicit local ignore and a reason:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
const floating = (
|
|
35
|
+
// eslint-disable-next-line tasty/no-style-prop -- positioning library supplies these inline styles
|
|
36
|
+
<Progress style={positioningStyles} />
|
|
37
|
+
);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Place the directive immediately before the reported attribute line. It suppresses
|
|
41
|
+
only this rule for that line. The warning message includes this exception path;
|
|
42
|
+
the plugin does not silently exempt library-provided values or enforce reasons.
|
|
43
|
+
|
|
44
|
+
## Options
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
'tasty/no-style-prop': ['warn', { elements: 'all' }],
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| `elements` | Reports `style` on |
|
|
51
|
+
| ------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
52
|
+
| `'tasty'` (default) | Recognized Tasty components |
|
|
53
|
+
| `'components'` | Every component: Tasty ones, other capitalized tags, and member tags such as `<UI.Panel>` or `<motion.div>` |
|
|
54
|
+
| `'all'` | Every component and native element, such as `<div>`, `<my-element>` or `<svg:rect>` |
|
|
55
|
+
|
|
56
|
+
On a Tasty component the message suggests token references and `tokens`. On any
|
|
57
|
+
other element it suggests moving the styles into Tasty: render a `tasty()`
|
|
58
|
+
element, or wrap the component with `tasty(Component, { styles })`.
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
'tasty/no-style-prop': ['warn', { values: 'static' }],
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| `values` | Reports |
|
|
65
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------- |
|
|
66
|
+
| `'all'` (default) | Every `style` |
|
|
67
|
+
| `'static'` | A `style` whose every value is a literal, written inline or as a local `const` object, and Tasty syntax |
|
|
68
|
+
|
|
69
|
+
A static `style` always has a Tasty form: style props or the component's styles,
|
|
70
|
+
where Tasty units, tokens and states apply. A runtime value (a measured
|
|
71
|
+
position, a size from a prop, a user's theme) is the same inline custom property
|
|
72
|
+
whether it is written in `style` or in `tokens`, so `'static'` leaves it to the
|
|
73
|
+
author. A `style` built from a spread, a call, a conditional, a template with
|
|
74
|
+
substitutions or a computed key is not static.
|
|
75
|
+
|
|
76
|
+
### Tasty syntax in `style`
|
|
77
|
+
|
|
78
|
+
`style` hands its values to the browser unparsed, so a Tasty unit (`'4x'`,
|
|
79
|
+
`'1bw'`), a color token (`'#dark'`) or a `$` token in a literal value is an
|
|
80
|
+
invalid declaration that the browser drops. The rule reports it under either
|
|
81
|
+
`values` setting, even when the other values are dynamic, and names the value.
|
|
82
|
+
A raw hex color, `var()` and CSS units, `lh` included, are left alone.
|
|
83
|
+
|
|
84
|
+
## Replacing React's `style` restriction
|
|
85
|
+
|
|
86
|
+
`react/forbid-component-props` and `react/forbid-dom-props` are each one rule
|
|
87
|
+
for every prop they forbid, and a disable directive cannot target a single
|
|
88
|
+
forbidden prop. When they forbid `style`, a `style` exception on a Tasty
|
|
89
|
+
component has to name the React rule as well as this one, and the React part
|
|
90
|
+
also hides any other forbidden prop, such as `className`, on that line.
|
|
91
|
+
|
|
92
|
+
Let this rule handle `style` instead, and keep the React rules for the other
|
|
93
|
+
props:
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
rules: {
|
|
97
|
+
// Without options, forbid-component-props forbids both className and style.
|
|
98
|
+
'react/forbid-component-props': ['error', { forbid: ['className'] }],
|
|
99
|
+
// Remove 'style' from forbid-dom-props, or turn the rule off if 'style' was
|
|
100
|
+
// its only entry.
|
|
101
|
+
'react/forbid-dom-props': 'off',
|
|
102
|
+
'tasty/no-style-prop': ['warn', { elements: 'all' }],
|
|
103
|
+
},
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The rule names are the same in an Oxlint config. Use `elements: 'components'`
|
|
107
|
+
instead to leave native elements to `react/forbid-dom-props`. A `style`
|
|
108
|
+
exception then names this rule only, and leaves every other check on the line in
|
|
109
|
+
place:
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
// eslint-disable-next-line tasty/no-style-prop -- positioning library supplies these inline styles
|
|
113
|
+
<Popover style={positioningStyles} />
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
After the switch, existing `style` exceptions that name a React rule no longer
|
|
117
|
+
suppress anything. ESLint reports them as unused disable directives (a warning
|
|
118
|
+
by default, see `linterOptions.reportUnusedDisableDirectives`); Oxlint does with
|
|
119
|
+
`--report-unused-disable-directives`. Rename each to `tasty/no-style-prop`.
|
|
120
|
+
|
|
121
|
+
Recognition uses local `const` components created by imported `tasty()` or
|
|
122
|
+
configured options factories, their `const` aliases/sub-elements, and components
|
|
123
|
+
from configured `importSources`. Exports of relative modules (`import { Panel } from './styled'`) count when they
|
|
124
|
+
resolve to one of these, through re-exports too; following them needs the
|
|
125
|
+
`typescript` package. List the design-system module
|
|
126
|
+
in `importSources` for components imported from a package. Native elements, unrelated components, shadowed bindings,
|
|
127
|
+
mutable aliases, and opaque spread bindings are excluded.
|