@tenphi/eslint-plugin-tasty 1.18.0 → 1.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +72 -45
  2. package/docs/rules/consistent-token-usage.md +74 -0
  3. package/docs/rules/known-property.md +55 -0
  4. package/docs/rules/no-classname-prop.md +62 -0
  5. package/docs/rules/no-ignored-pseudo-element-state.md +97 -0
  6. package/docs/rules/no-important.md +25 -0
  7. package/docs/rules/no-nested-selector.md +39 -0
  8. package/docs/rules/no-nested-state-map.md +33 -0
  9. package/docs/rules/no-own-at-root.md +57 -0
  10. package/docs/rules/no-protected-restyle.md +194 -0
  11. package/docs/rules/no-raw-color-values.md +51 -0
  12. package/docs/rules/no-raw-motion-duration.md +50 -0
  13. package/docs/rules/no-raw-transition-duration.md +56 -0
  14. package/docs/rules/no-runtime-styles-mutation.md +58 -0
  15. package/docs/rules/no-state-in-selector.md +142 -0
  16. package/docs/rules/no-style-prop.md +129 -0
  17. package/docs/rules/no-style-spread.md +55 -0
  18. package/docs/rules/no-styles-prop.md +59 -0
  19. package/docs/rules/no-unknown-state-alias.md +45 -0
  20. package/docs/rules/prefer-auto-calc.md +30 -0
  21. package/docs/rules/prefer-custom-property-syntax.md +56 -0
  22. package/docs/rules/prefer-directional-shorthand.md +46 -0
  23. package/docs/rules/prefer-element-selector.md +77 -0
  24. package/docs/rules/prefer-fill-mix.md +119 -0
  25. package/docs/rules/prefer-hide.md +31 -0
  26. package/docs/rules/prefer-longhand-property.md +25 -0
  27. package/docs/rules/prefer-shorthand-property.md +194 -0
  28. package/docs/rules/prefer-state-negation.md +44 -0
  29. package/docs/rules/require-default-state.md +50 -0
  30. package/docs/rules/static-no-dynamic-values.md +32 -0
  31. package/docs/rules/static-valid-selector.md +36 -0
  32. package/docs/rules/valid-boolean-property.md +39 -0
  33. package/docs/rules/valid-color-token.md +64 -0
  34. package/docs/rules/valid-custom-property.md +36 -0
  35. package/docs/rules/valid-custom-unit.md +34 -0
  36. package/docs/rules/valid-default-state-order.md +44 -0
  37. package/docs/rules/valid-directional-modifier.md +46 -0
  38. package/docs/rules/valid-preset.md +35 -0
  39. package/docs/rules/valid-radius-shape.md +31 -0
  40. package/docs/rules/valid-recipe.md +35 -0
  41. package/docs/rules/valid-state-definition.md +46 -0
  42. package/docs/rules/valid-state-key.md +62 -0
  43. package/docs/rules/valid-styles-structure.md +48 -0
  44. package/docs/rules/valid-sub-element.md +100 -0
  45. package/docs/rules/valid-transition.md +46 -0
  46. package/docs/rules/valid-value.md +65 -0
  47. package/harness/dev.harnesspm/hooks.json +12 -0
  48. package/harness/dev.harnesspm/rules/usage.md +11 -0
  49. package/harness/dev.harnesspm/scripts/check-edit.mjs +163 -0
  50. package/harness/dev.harnesspm/scripts/check.mjs +254 -0
  51. package/harness/dev.harnesspm/scripts/lint.mjs +318 -0
  52. package/harness/dev.harnesspm/scripts/project-config.mjs +316 -0
  53. package/harness/plugin.json +23 -0
  54. package/package.json +11 -7
@@ -0,0 +1,194 @@
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
+ With Oxlint, `// oxlint-disable-next-line` works the same way.
187
+
188
+ ## Relation to `no-styles-prop`
189
+
190
+ `tasty/no-styles-prop` reports instance `styles` on every recognized Tasty
191
+ component. Once a design system lists its protected components, this rule
192
+ covers those with a message that names the component. A project that relies on
193
+ it can turn `no-styles-prop` off, so layout primitives keep `styles` as their
194
+ expected API.
@@ -0,0 +1,51 @@
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
+ With Oxlint, `// oxlint-disable-next-line` works the same way.
48
+
49
+ ## Options
50
+
51
+ 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,129 @@
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
+ With Oxlint, `// oxlint-disable-next-line` works the same way.
41
+
42
+ Place the directive immediately before the reported attribute line. It suppresses
43
+ only this rule for that line. The warning message includes this exception path;
44
+ the plugin does not silently exempt library-provided values or enforce reasons.
45
+
46
+ ## Options
47
+
48
+ ```js
49
+ 'tasty/no-style-prop': ['warn', { elements: 'all' }],
50
+ ```
51
+
52
+ | `elements` | Reports `style` on |
53
+ | ------------------- | ----------------------------------------------------------------------------------------------------------- |
54
+ | `'tasty'` (default) | Recognized Tasty components |
55
+ | `'components'` | Every component: Tasty ones, other capitalized tags, and member tags such as `<UI.Panel>` or `<motion.div>` |
56
+ | `'all'` | Every component and native element, such as `<div>`, `<my-element>` or `<svg:rect>` |
57
+
58
+ On a Tasty component the message suggests token references and `tokens`. On any
59
+ other element it suggests moving the styles into Tasty: render a `tasty()`
60
+ element, or wrap the component with `tasty(Component, { styles })`.
61
+
62
+ ```js
63
+ 'tasty/no-style-prop': ['warn', { values: 'static' }],
64
+ ```
65
+
66
+ | `values` | Reports |
67
+ | ----------------- | ------------------------------------------------------------------------------------------------------- |
68
+ | `'all'` (default) | Every `style` |
69
+ | `'static'` | A `style` whose every value is a literal, written inline or as a local `const` object, and Tasty syntax |
70
+
71
+ A static `style` always has a Tasty form: style props or the component's styles,
72
+ where Tasty units, tokens and states apply. A runtime value (a measured
73
+ position, a size from a prop, a user's theme) is the same inline custom property
74
+ whether it is written in `style` or in `tokens`, so `'static'` leaves it to the
75
+ author. A `style` built from a spread, a call, a conditional, a template with
76
+ substitutions or a computed key is not static.
77
+
78
+ ### Tasty syntax in `style`
79
+
80
+ `style` hands its values to the browser unparsed, so a Tasty unit (`'4x'`,
81
+ `'1bw'`), a color token (`'#dark'`) or a `$` token in a literal value is an
82
+ invalid declaration that the browser drops. The rule reports it under either
83
+ `values` setting, even when the other values are dynamic, and names the value.
84
+ A raw hex color, `var()` and CSS units, `lh` included, are left alone.
85
+
86
+ ## Replacing React's `style` restriction
87
+
88
+ `react/forbid-component-props` and `react/forbid-dom-props` are each one rule
89
+ for every prop they forbid, and a disable directive cannot target a single
90
+ forbidden prop. When they forbid `style`, a `style` exception on a Tasty
91
+ component has to name the React rule as well as this one, and the React part
92
+ also hides any other forbidden prop, such as `className`, on that line.
93
+
94
+ Let this rule handle `style` instead, and keep the React rules for the other
95
+ props:
96
+
97
+ ```js
98
+ rules: {
99
+ // Without options, forbid-component-props forbids both className and style.
100
+ 'react/forbid-component-props': ['error', { forbid: ['className'] }],
101
+ // Remove 'style' from forbid-dom-props, or turn the rule off if 'style' was
102
+ // its only entry.
103
+ 'react/forbid-dom-props': 'off',
104
+ 'tasty/no-style-prop': ['warn', { elements: 'all' }],
105
+ },
106
+ ```
107
+
108
+ The rule names are the same in an Oxlint config. Use `elements: 'components'`
109
+ instead to leave native elements to `react/forbid-dom-props`. A `style`
110
+ exception then names this rule only, and leaves every other check on the line in
111
+ place:
112
+
113
+ ```tsx
114
+ // eslint-disable-next-line tasty/no-style-prop -- positioning library supplies these inline styles
115
+ <Popover style={positioningStyles} />
116
+ ```
117
+
118
+ After the switch, existing `style` exceptions that name a React rule no longer
119
+ suppress anything. ESLint reports them as unused disable directives (a warning
120
+ by default, see `linterOptions.reportUnusedDisableDirectives`); Oxlint does with
121
+ `--report-unused-disable-directives`. Rename each to `tasty/no-style-prop`.
122
+
123
+ Recognition uses local `const` components created by imported `tasty()` or
124
+ configured options factories, their `const` aliases/sub-elements, and components
125
+ from configured `importSources`. Exports of relative modules (`import { Panel } from './styled'`) count when they
126
+ resolve to one of these, through re-exports too; following them needs the
127
+ `typescript` package. List the design-system module
128
+ in `importSources` for components imported from a package. Native elements, unrelated components, shadowed bindings,
129
+ mutable aliases, and opaque spread bindings are excluded.