@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.
Files changed (55) hide show
  1. package/README.md +64 -45
  2. package/dist/rules/no-protected-restyle.js +24 -14
  3. package/dist/rules/no-protected-restyle.js.map +1 -1
  4. package/docs/rules/consistent-token-usage.md +72 -0
  5. package/docs/rules/known-property.md +55 -0
  6. package/docs/rules/no-classname-prop.md +60 -0
  7. package/docs/rules/no-ignored-pseudo-element-state.md +97 -0
  8. package/docs/rules/no-important.md +25 -0
  9. package/docs/rules/no-nested-selector.md +39 -0
  10. package/docs/rules/no-nested-state-map.md +33 -0
  11. package/docs/rules/no-own-at-root.md +57 -0
  12. package/docs/rules/no-protected-restyle.md +192 -0
  13. package/docs/rules/no-raw-color-values.md +49 -0
  14. package/docs/rules/no-raw-motion-duration.md +50 -0
  15. package/docs/rules/no-raw-transition-duration.md +56 -0
  16. package/docs/rules/no-runtime-styles-mutation.md +58 -0
  17. package/docs/rules/no-state-in-selector.md +142 -0
  18. package/docs/rules/no-style-prop.md +127 -0
  19. package/docs/rules/no-style-spread.md +53 -0
  20. package/docs/rules/no-styles-prop.md +57 -0
  21. package/docs/rules/no-unknown-state-alias.md +45 -0
  22. package/docs/rules/prefer-auto-calc.md +30 -0
  23. package/docs/rules/prefer-custom-property-syntax.md +56 -0
  24. package/docs/rules/prefer-directional-shorthand.md +46 -0
  25. package/docs/rules/prefer-element-selector.md +77 -0
  26. package/docs/rules/prefer-fill-mix.md +119 -0
  27. package/docs/rules/prefer-hide.md +31 -0
  28. package/docs/rules/prefer-longhand-property.md +25 -0
  29. package/docs/rules/prefer-shorthand-property.md +194 -0
  30. package/docs/rules/prefer-state-negation.md +44 -0
  31. package/docs/rules/require-default-state.md +50 -0
  32. package/docs/rules/static-no-dynamic-values.md +32 -0
  33. package/docs/rules/static-valid-selector.md +36 -0
  34. package/docs/rules/valid-boolean-property.md +39 -0
  35. package/docs/rules/valid-color-token.md +64 -0
  36. package/docs/rules/valid-custom-property.md +36 -0
  37. package/docs/rules/valid-custom-unit.md +34 -0
  38. package/docs/rules/valid-default-state-order.md +44 -0
  39. package/docs/rules/valid-directional-modifier.md +46 -0
  40. package/docs/rules/valid-preset.md +35 -0
  41. package/docs/rules/valid-radius-shape.md +31 -0
  42. package/docs/rules/valid-recipe.md +35 -0
  43. package/docs/rules/valid-state-definition.md +46 -0
  44. package/docs/rules/valid-state-key.md +62 -0
  45. package/docs/rules/valid-styles-structure.md +48 -0
  46. package/docs/rules/valid-sub-element.md +100 -0
  47. package/docs/rules/valid-transition.md +46 -0
  48. package/docs/rules/valid-value.md +65 -0
  49. package/harness/dev.harnesspm/hooks.json +12 -0
  50. package/harness/dev.harnesspm/rules/usage.md +11 -0
  51. package/harness/dev.harnesspm/scripts/check-edit.mjs +127 -0
  52. package/harness/dev.harnesspm/scripts/check.mjs +159 -0
  53. package/harness/dev.harnesspm/scripts/lint.mjs +236 -0
  54. package/harness/plugin.json +23 -0
  55. package/package.json +11 -7
package/README.md CHANGED
@@ -226,59 +226,61 @@ not matched.
226
226
 
227
227
  ## Rules
228
228
 
229
+ Every rule has a page in [`docs/rules/`](docs/rules/) with examples and options. The pages ship in the package.
230
+
229
231
  ### Recommended
230
232
 
231
233
  | Rule | Severity | Description |
232
234
  |------|----------|-------------|
233
- | `tasty/known-property` | warn | Unknown style property names (checked against the full MDN/W3C CSS property set) |
234
- | `tasty/valid-value` | error | Malformed style values (unbalanced parens, !important) |
235
- | `tasty/valid-color-token` | error | Invalid color token syntax or unknown tokens |
236
- | `tasty/valid-custom-unit` | error | Unknown custom units |
237
- | `tasty/valid-boolean-property` | error | `true` on properties that don't support it, in a state map as well as on a direct value. Includes `fill`, which tasty documents as taking `true` but currently renders as `background-color: true` |
238
- | `tasty/valid-state-key` | error | Invalid state key syntax in style mappings (including misuse of the `_` fallback floor) |
239
- | `tasty/valid-styles-structure` | error | Invalid styles object structure, and the v2 at-rule key spellings (auto-fixable) |
240
- | `tasty/no-nested-state-map` | error | Nested state maps (not supported) |
241
- | `tasty/no-important` | error | `!important` usage (breaks tasty specificity) |
242
- | `tasty/valid-sub-element` | error | Sub-element values must be style objects; `$` selectors must not target the root element ([details](docs/rules/valid-sub-element.md)) |
243
- | `tasty/valid-directional-modifier` | error | Directional modifiers on wrong properties, and more than one value in a group that names directions. Physical properties take `top`/`right`/`bottom`/`left`, logical ones `start`/`end`, and mixing the two is reported in either direction |
244
- | `tasty/valid-radius-shape` | error | Unknown radius shape keywords |
245
- | `tasty/valid-preset` | error | Unknown preset names |
246
- | `tasty/valid-recipe` | error | Unknown recipe names |
247
- | `tasty/valid-transition` | warn | Unknown transition names, and camelCase CSS names that Tasty would emit as no property (`gridTemplateColumns` → `grid-template-columns`) |
248
- | `tasty/no-nested-selector` | warn | `&`-prefixed nested selectors (use sub-elements) |
249
- | `tasty/no-state-in-selector` | warn | Pseudo-class and attribute conditions in sub-element `$` selectors; put conditions in property state maps ([details](docs/rules/no-state-in-selector.md)) |
250
- | `tasty/prefer-element-selector` | warn | Prefer element names such as `Primary > Search` over exact `data-element` attributes in sub-element `$` selectors (safely autofixable; [details](docs/rules/prefer-element-selector.md)) |
251
- | `tasty/static-no-dynamic-values` | error | Dynamic values in `tastyStatic()` |
252
- | `tasty/static-valid-selector` | error | Invalid selector in `tastyStatic(selector, ...)` |
253
- | `tasty/require-default-state` | error | Missing default (`''`) or fallback floor (`_`) key in state mappings (skipped for extending calls and maps that spread another) |
254
- | `tasty/no-own-at-root` | warn | `@own()` used at root level where it is redundant (not in shared style constants, which may be spread into a sub-element) |
255
- | `tasty/no-ignored-pseudo-element-state` | error | States Tasty ignores on a pseudo-element, such as `@own([open])` on `$: 'a::before'`: attributes, modifiers, classes, and tree-structural pseudo-classes ([details](docs/rules/no-ignored-pseudo-element-state.md)) |
256
- | `tasty/valid-default-state-order` | warn | Misplaced default (`''`) or redundant `''` when only `_` is present |
257
- | `tasty/prefer-shorthand-property` | warn | Use Tasty shorthand instead of native CSS properties (`backgroundColor` → `fill`, `paddingBlock` → `blockPadding`, etc.). Report-only warnings explain how to override one part through a token, with concrete min/max dimension examples. Extension warnings point at the base component and are skipped over bases you cannot edit (see `ownedSources`; [details](docs/rules/prefer-shorthand-property.md)) |
258
- | `tasty/no-raw-color-values` | warn | Raw hex/rgb/`okhsl`/`okhst`/`oklch`/named colors instead of `#color` tokens |
259
- | `tasty/no-raw-transition-duration` | warn | Hardcoded `transition` duration (`fill 0.2s`) in Tasty styles or a local `tasty()` component's `transition` prop. Suggests a configured duration token or the implicit per-name timing; zero and delays are left alone |
260
- | `tasty/no-runtime-styles-mutation` | warn | Render-time JavaScript in Tasty style values (props, state, hook results, parameters, local bindings, calls, computed keys); use states and tokens instead. A style object no function encloses is built once at load and not checked; inside a function, module-scope constants, imports, `undefined` and static `mergeStyles(...)` calls are fixed at load and allowed |
261
- | `tasty/no-style-spread` | warn | Object or array spreads inside runtime Tasty styles; recommends `mergeStyles` for multiple root spreads; intentional spreads need a per-line suppression and reason |
262
- | `tasty/no-style-prop` | warn | `style` on recognized Tasty components; use token references and the `tokens` prop. The `elements` option covers every component or element, so it can replace React's `style` restriction; `values: 'static'` reports only static styles. Tasty syntax in `style` is always reported ([details](docs/rules/no-style-prop.md)) |
263
- | `tasty/no-classname-prop` | warn | `className` on recognized Tasty components; keep styling in Tasty or use `data-element` for sub-elements ([details](docs/rules/no-classname-prop.md)) |
264
- | `tasty/no-styles-prop` | warn | Instance `styles` on recognized Tasty components; use `tokens`, `mods`, exposed props/variants, or `tasty(Component, { styles })` ([details](docs/rules/no-styles-prop.md)) |
265
- | `tasty/no-protected-restyle` | warn | Restyling a design system's `protectedComponents`: instance `styles` and `*Styles`, appearance style props, and `tasty()` wrappers that restyle them, followed through aliases, wrappers and relative re-exports. Inactive without the config ([details](docs/rules/no-protected-restyle.md)) |
266
- | `tasty/consistent-token-usage` | warn | Nonzero raw pixel lengths, including compounds, expressions and numeric inputs to enhanced length handlers. The `pixels: 'replaceable'` option reports only lengths with an exact unit, such as `8px` → `1x` ([details](docs/rules/consistent-token-usage.md)) |
267
- | `tasty/prefer-auto-calc` | warn | `calc(...)` instead of Tasty auto-calc `(...)` |
268
- | `tasty/prefer-state-negation` | warn | Top-level `:not(...)` in state keys instead of the `!` prefix (autofixable; nested CSS selectors stay intact) |
269
- | `tasty/prefer-custom-property-syntax` | warn | `var(--prop)` / `$x-color` / `transparent` / `currentColor` instead of `$prop` / `#color` / `#clear` / `#current` |
270
- | `tasty/prefer-hide` | warn | `display: 'none'` instead of `hide: true` |
271
- | `tasty/prefer-fill-mix` | warn | Deprecated two-color `fill`, space-separated (`'#surface #dark.04'`) or comma-mixed (`'#dark 4%, #surface'`), instead of a compound color (`'#surface+#dark.04'`) or `color-mix()`, autofixed when the result is the same color; mix parts weighted by their opacity (`'#dark.04, #surface'`). Needs `@tenphi/tasty` 3.15, and 3.16 for compound colors ([details](docs/rules/prefer-fill-mix.md)) |
272
- | `tasty/prefer-directional-shorthand` | warn | 4-value `margin`/`padding`/`inset`/`radius`/`fade`/`border` with placeholder positions instead of directional form |
273
- | `tasty/prefer-longhand-property` | error | Lossy `flex` shorthand instead of `flexGrow` / `flexShrink` / `flexBasis` |
235
+ | [`tasty/known-property`](docs/rules/known-property.md) | warn | Unknown style property names (checked against the full MDN/W3C CSS property set) |
236
+ | [`tasty/valid-value`](docs/rules/valid-value.md) | error | Malformed style values (unbalanced parens, !important) |
237
+ | [`tasty/valid-color-token`](docs/rules/valid-color-token.md) | error | Invalid color token syntax or unknown tokens |
238
+ | [`tasty/valid-custom-unit`](docs/rules/valid-custom-unit.md) | error | Unknown custom units |
239
+ | [`tasty/valid-boolean-property`](docs/rules/valid-boolean-property.md) | error | `true` on properties that don't support it, in a state map as well as on a direct value. Includes `fill`, which tasty documents as taking `true` but currently renders as `background-color: true` |
240
+ | [`tasty/valid-state-key`](docs/rules/valid-state-key.md) | error | Invalid state key syntax in style mappings (including misuse of the `_` fallback floor) |
241
+ | [`tasty/valid-styles-structure`](docs/rules/valid-styles-structure.md) | error | Invalid styles object structure, and the v2 at-rule key spellings (auto-fixable) |
242
+ | [`tasty/no-nested-state-map`](docs/rules/no-nested-state-map.md) | error | Nested state maps (not supported) |
243
+ | [`tasty/no-important`](docs/rules/no-important.md) | error | `!important` usage (breaks tasty specificity) |
244
+ | [`tasty/valid-sub-element`](docs/rules/valid-sub-element.md) | error | Sub-element values must be style objects; `$` selectors must not target the root element |
245
+ | [`tasty/valid-directional-modifier`](docs/rules/valid-directional-modifier.md) | error | Directional modifiers on wrong properties, and more than one value in a group that names directions. Physical properties take `top`/`right`/`bottom`/`left`, logical ones `start`/`end`, and mixing the two is reported in either direction |
246
+ | [`tasty/valid-radius-shape`](docs/rules/valid-radius-shape.md) | error | Unknown radius shape keywords |
247
+ | [`tasty/valid-preset`](docs/rules/valid-preset.md) | error | Unknown preset names |
248
+ | [`tasty/valid-recipe`](docs/rules/valid-recipe.md) | error | Unknown recipe names |
249
+ | [`tasty/valid-transition`](docs/rules/valid-transition.md) | warn | Unknown transition names, and camelCase CSS names that Tasty would emit as no property (`gridTemplateColumns` → `grid-template-columns`) |
250
+ | [`tasty/no-nested-selector`](docs/rules/no-nested-selector.md) | warn | `&`-prefixed nested selectors (use sub-elements) |
251
+ | [`tasty/no-state-in-selector`](docs/rules/no-state-in-selector.md) | warn | Pseudo-class and attribute conditions in sub-element `$` selectors; put conditions in property state maps |
252
+ | [`tasty/prefer-element-selector`](docs/rules/prefer-element-selector.md) | warn | Prefer element names such as `Primary > Search` over exact `data-element` attributes in sub-element `$` selectors (safely autofixable) |
253
+ | [`tasty/static-no-dynamic-values`](docs/rules/static-no-dynamic-values.md) | error | Dynamic values in `tastyStatic()` |
254
+ | [`tasty/static-valid-selector`](docs/rules/static-valid-selector.md) | error | Invalid selector in `tastyStatic(selector, ...)` |
255
+ | [`tasty/require-default-state`](docs/rules/require-default-state.md) | error | Missing default (`''`) or fallback floor (`_`) key in state mappings (skipped for extending calls and maps that spread another) |
256
+ | [`tasty/no-own-at-root`](docs/rules/no-own-at-root.md) | warn | `@own()` used at root level where it is redundant (not in shared style constants, which may be spread into a sub-element) |
257
+ | [`tasty/no-ignored-pseudo-element-state`](docs/rules/no-ignored-pseudo-element-state.md) | error | States Tasty ignores on a pseudo-element, such as `@own([open])` on `$: 'a::before'`: attributes, modifiers, classes, and tree-structural pseudo-classes |
258
+ | [`tasty/valid-default-state-order`](docs/rules/valid-default-state-order.md) | warn | Misplaced default (`''`) or redundant `''` when only `_` is present |
259
+ | [`tasty/prefer-shorthand-property`](docs/rules/prefer-shorthand-property.md) | warn | Use Tasty shorthand instead of native CSS properties (`backgroundColor` → `fill`, `paddingBlock` → `blockPadding`, etc.). Report-only warnings explain how to override one part through a token, with concrete min/max dimension examples. Extension warnings point at the base component and are skipped over bases you cannot edit (see `ownedSources`) |
260
+ | [`tasty/no-raw-color-values`](docs/rules/no-raw-color-values.md) | warn | Raw hex/rgb/`okhsl`/`okhst`/`oklch`/named colors instead of `#color` tokens |
261
+ | [`tasty/no-raw-transition-duration`](docs/rules/no-raw-transition-duration.md) | warn | Hardcoded `transition` duration (`fill 0.2s`) in Tasty styles or a local `tasty()` component's `transition` prop. Suggests a configured duration token or the implicit per-name timing; zero and delays are left alone |
262
+ | [`tasty/no-runtime-styles-mutation`](docs/rules/no-runtime-styles-mutation.md) | warn | Render-time JavaScript in Tasty style values (props, state, hook results, parameters, local bindings, calls, computed keys); use states and tokens instead. A style object no function encloses is built once at load and not checked; inside a function, module-scope constants, imports, `undefined` and static `mergeStyles(...)` calls are fixed at load and allowed |
263
+ | [`tasty/no-style-spread`](docs/rules/no-style-spread.md) | warn | Object or array spreads inside runtime Tasty styles; recommends `mergeStyles` for multiple root spreads; intentional spreads need a per-line suppression and reason |
264
+ | [`tasty/no-style-prop`](docs/rules/no-style-prop.md) | warn | `style` on recognized Tasty components; use token references and the `tokens` prop. The `elements` option covers every component or element, so it can replace React's `style` restriction; `values: 'static'` reports only static styles. Tasty syntax in `style` is always reported |
265
+ | [`tasty/no-classname-prop`](docs/rules/no-classname-prop.md) | warn | `className` on recognized Tasty components; keep styling in Tasty or use `data-element` for sub-elements |
266
+ | [`tasty/no-styles-prop`](docs/rules/no-styles-prop.md) | warn | Instance `styles` on recognized Tasty components; use `tokens`, `mods`, exposed props/variants, or `tasty(Component, { styles })` |
267
+ | [`tasty/no-protected-restyle`](docs/rules/no-protected-restyle.md) | warn | Restyling a design system's `protectedComponents`: instance `styles` and `*Styles`, appearance style props, and `tasty()` wrappers that restyle them, followed through aliases, wrappers and relative re-exports. `usages: 'wrappers'` reports only the wrappers. Inactive without the config |
268
+ | [`tasty/consistent-token-usage`](docs/rules/consistent-token-usage.md) | warn | Nonzero raw pixel lengths, including compounds, expressions and numeric inputs to enhanced length handlers. The `pixels: 'replaceable'` option reports only lengths with an exact unit, such as `8px` → `1x` |
269
+ | [`tasty/prefer-auto-calc`](docs/rules/prefer-auto-calc.md) | warn | `calc(...)` instead of Tasty auto-calc `(...)` |
270
+ | [`tasty/prefer-state-negation`](docs/rules/prefer-state-negation.md) | warn | Top-level `:not(...)` in state keys instead of the `!` prefix (autofixable; nested CSS selectors stay intact) |
271
+ | [`tasty/prefer-custom-property-syntax`](docs/rules/prefer-custom-property-syntax.md) | warn | `var(--prop)` / `$x-color` / `transparent` / `currentColor` instead of `$prop` / `#color` / `#clear` / `#current` |
272
+ | [`tasty/prefer-hide`](docs/rules/prefer-hide.md) | warn | `display: 'none'` instead of `hide: true` |
273
+ | [`tasty/prefer-fill-mix`](docs/rules/prefer-fill-mix.md) | warn | Deprecated two-color `fill`, space-separated (`'#surface #dark.04'`) or comma-mixed (`'#dark 4%, #surface'`), instead of a compound color (`'#surface+#dark.04'`) or `color-mix()`, autofixed when the result is the same color; mix parts weighted by their opacity (`'#dark.04, #surface'`). Needs `@tenphi/tasty` 3.15, and 3.16 for compound colors |
274
+ | [`tasty/prefer-directional-shorthand`](docs/rules/prefer-directional-shorthand.md) | warn | 4-value `margin`/`padding`/`inset`/`radius`/`fade`/`border` with placeholder positions instead of directional form |
275
+ | [`tasty/prefer-longhand-property`](docs/rules/prefer-longhand-property.md) | error | Lossy `flex` shorthand instead of `flexGrow` / `flexShrink` / `flexBasis` |
274
276
 
275
277
  ### Strict (includes all recommended rules)
276
278
 
277
279
  | Rule | Severity | Description |
278
280
  |------|----------|-------------|
279
- | `tasty/valid-custom-property` | warn | Unknown `$name` custom properties |
280
- | `tasty/valid-state-definition` | warn | Invalid state definition values in `configure()` or `tasty.config` |
281
- | `tasty/no-unknown-state-alias` | warn | Unknown `@name` state aliases |
281
+ | [`tasty/valid-custom-property`](docs/rules/valid-custom-property.md) | warn | Unknown `$name` custom properties |
282
+ | [`tasty/valid-state-definition`](docs/rules/valid-state-definition.md) | warn | Invalid state definition values in `configure()` or `tasty.config` |
283
+ | [`tasty/no-unknown-state-alias`](docs/rules/no-unknown-state-alias.md) | warn | Unknown `@name` state aliases |
282
284
 
283
285
  ### Structural selectors and state maps
284
286
 
@@ -360,7 +362,7 @@ suppressed, run ESLint with `--max-warnings=0`.
360
362
 
361
363
  ### Optional motion duration rule
362
364
 
363
- Enable `tasty/no-raw-motion-duration` as a warning if your design system uses
365
+ Enable [`tasty/no-raw-motion-duration`](docs/rules/no-raw-motion-duration.md) as a warning if your design system uses
364
366
  tokens for animation timing and explicit CSS durations:
365
367
 
366
368
  ```js
@@ -635,6 +637,23 @@ your installed version — and on 3.7 and below `blockPadding` is not a style ta
635
637
  (an unhandled camelCase key renders through to `block-padding`, which the browser drops).
636
638
  The peer range floor is 3.8.0 for that reason.
637
639
 
640
+ ## AI agents
641
+
642
+ The package ships an agent harness for [harnesspm](https://www.npmjs.com/package/@tenphi/harnesspm), as plugin `tasty-lint`. In a project that uses harnesspm, agents get:
643
+
644
+ - a short always-on rule: lint the files you change and fix every `tasty/*` problem;
645
+ - the rule pages of the installed version, in the docs index;
646
+ - `npx harnesspm run tasty-lint:check <files> [--strict] [--json]`, which runs only the `tasty/*` rules with the project's `tasty.config.*` and prints each problem with its rule's page;
647
+ - a Claude Code hook that lints each file the agent edits, when it imports Tasty or a configured `importSources` module, and hands the new problems back to the agent.
648
+
649
+ The tool and the hook run only in projects that trust the package, in `harnesspm.json`:
650
+
651
+ ```json
652
+ { "trust": { "@tenphi/eslint-plugin-tasty": true } }
653
+ ```
654
+
655
+ They need `eslint`, and `typescript-eslint` or `@typescript-eslint/parser` for TypeScript files. They do not read the project's ESLint or Oxlint config, so other plugins, rule options and settings such as `typeAwareJSX` do not apply there.
656
+
638
657
  ## License
639
658
 
640
659
  MIT
@@ -38,10 +38,17 @@ var no_protected_restyle_default = createRule({
38
38
  restyleProp: "'{{prop}}' restyles {{component}}, a protected {{source}} component. Customize it through its own props, 'tokens' or 'mods'. If this override is intended, explicitly disable tasty/no-protected-restyle for this usage with an ESLint comment and a reason.",
39
39
  restyleWrapper: "This tasty() wrapper restyles {{component}}, a protected {{source}} component, through '{{prop}}'. Set its own props, 'tokens' or 'mods' instead. If this override is intended, explicitly disable tasty/no-protected-restyle for this usage with an ESLint comment and a reason."
40
40
  },
41
- schema: []
41
+ schema: [{
42
+ type: "object",
43
+ properties: { usages: {
44
+ type: "string",
45
+ enum: ["all", "wrappers"]
46
+ } },
47
+ additionalProperties: false
48
+ }]
42
49
  },
43
- defaultOptions: [],
44
- create(context) {
50
+ defaultOptions: [{ usages: "all" }],
51
+ create(context, [{ usages = "all" }]) {
45
52
  const ctx = new TastyContext(context);
46
53
  const components = new ProtectedComponents(context, ctx);
47
54
  if (!components.enabled) return {};
@@ -87,18 +94,21 @@ var no_protected_restyle_default = createRule({
87
94
  if (name === "variants" || restyles(name, prop.value, target)) report(prop, "restyleWrapper", name, target);
88
95
  }
89
96
  }
97
+ const jsx = jsxElementPropListeners(ctx, (node) => {
98
+ const target = components.resolveTag(node.name);
99
+ return target && {
100
+ ...target,
101
+ reference: context.sourceCode.getText(node.name)
102
+ };
103
+ }, (name, node, target) => {
104
+ if (restyles(name, node.type === "Property" ? node.value : node.value?.type === "JSXExpressionContainer" ? node.value.expression : node.value, target, node.type === "JSXAttribute" ? node : void 0)) report(node, "restyleProp", name, target);
105
+ });
90
106
  return {
91
- ...jsxElementPropListeners(ctx, (node) => {
92
- const target = components.resolveTag(node.name);
93
- return target && {
94
- ...target,
95
- reference: context.sourceCode.getText(node.name)
96
- };
97
- }, (name, node, target) => {
98
- if (restyles(name, node.type === "Property" ? node.value : node.value?.type === "JSXExpressionContainer" ? node.value.expression : node.value, target, node.type === "JSXAttribute" ? node : void 0)) report(node, "restyleProp", name, target);
99
- }),
100
- "Program:exit"() {
101
- ctx.finishJSXAnalysis();
107
+ ...usages === "wrappers" ? { ImportDeclaration: jsx.ImportDeclaration } : {
108
+ ...jsx,
109
+ "Program:exit"() {
110
+ ctx.finishJSXAnalysis();
111
+ }
102
112
  },
103
113
  CallExpression(node) {
104
114
  const [base, options] = node.arguments;
@@ -1 +1 @@
1
- {"version":3,"file":"no-protected-restyle.js","names":[],"sources":["../../src/rules/no-protected-restyle.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { KNOWN_TASTY_PROPERTIES } from '../constants.js';\nimport {\n type ProtectedComponent,\n ProtectedComponents,\n} from '../protected-components.js';\nimport { getKeyName, unwrapExpression } from '../utils.js';\nimport { jsxElementPropListeners } from './tasty-component-props.js';\n\ntype MessageIds = 'restyleProp' | 'restyleWrapper';\n\n/** `tasty(Base, options)` keys that configure the wrapper instead of setting a default prop. */\nconst WRAPPER_OPTIONS = new Set([\n 'as',\n 'element',\n 'elements',\n 'mods',\n 'modProps',\n 'qa',\n 'qaVal',\n 'styleProps',\n 'tokenProps',\n 'tokens',\n 'variant',\n]);\n\ninterface Target extends ProtectedComponent {\n /** The tag or base as written: `UI.Button`, `CopyButton`. */\n reference: string;\n}\n\n/** `inputStyles`, `triggerStyles`, ...: styles for one of the component's inner elements. */\nfunction isInnerStylesProp(name: string): boolean {\n return /^[a-z][A-Za-z0-9]*Styles$/.test(name);\n}\n\n/** A value that cannot be a style value: a callback or an element. */\nfunction isNeverStyleValue(node: TSESTree.Node): boolean {\n const value = unwrapExpression(node);\n return (\n value.type === 'ArrowFunctionExpression' ||\n value.type === 'FunctionExpression' ||\n value.type === 'JSXElement' ||\n value.type === 'JSXFragment'\n );\n}\n\nexport default createRule<[], MessageIds>({\n name: 'no-protected-restyle',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n \"Warn when a design system's protected components are restyled instead of customized through their props\",\n },\n messages: {\n restyleProp:\n \"'{{prop}}' restyles {{component}}, a protected {{source}} component. Customize it through its own props, 'tokens' or 'mods'. If this override is intended, explicitly disable tasty/no-protected-restyle for this usage with an ESLint comment and a reason.\",\n restyleWrapper:\n \"This tasty() wrapper restyles {{component}}, a protected {{source}} component, through '{{prop}}'. Set its own props, 'tokens' or 'mods' instead. If this override is intended, explicitly disable tasty/no-protected-restyle for this usage with an ESLint comment and a reason.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const components = new ProtectedComponents(context, ctx);\n if (!components.enabled) return {};\n\n const styleProps = new Set([\n ...KNOWN_TASTY_PROPERTIES,\n ...ctx.config.styles,\n ]);\n\n /** Root `styles` that only places or sizes the component, through allowed keys. */\n function placesOnly(value: TSESTree.Node | null, target: Target): boolean {\n const object = value && unwrapExpression(value);\n return (\n object?.type === 'ObjectExpression' &&\n object.properties.every((prop) => {\n if (prop.type !== 'Property' || prop.computed) return false;\n const key = getKeyName(prop.key);\n return key !== null && target.allowedStyleProps.has(key);\n })\n );\n }\n\n function restyles(\n name: string,\n value: TSESTree.Node | null,\n target: Target,\n attribute?: TSESTree.JSXAttribute,\n ): boolean {\n if (name === 'styles') return !placesOnly(value, target);\n if (isInnerStylesProp(name)) return true;\n if (!styleProps.has(name) || target.allowedStyleProps.has(name)) {\n return false;\n }\n if (value && isNeverStyleValue(value)) return false;\n // With typeAwareJSX, a prop the component declares itself is not a style prop.\n return !(attribute && ctx.isComponentJSXProp(attribute));\n }\n\n function report(\n node: TSESTree.Node,\n messageId: MessageIds,\n prop: string,\n target: Target,\n ): void {\n context.report({\n node,\n messageId,\n data: {\n prop,\n component:\n target.reference === target.name\n ? `'${target.name}'`\n : `'${target.reference}' (based on '${target.name}')`,\n source: `'${target.source}'`,\n },\n });\n }\n\n function checkWrapperOptions(\n options: TSESTree.ObjectExpression,\n target: Target,\n ): void {\n for (const prop of options.properties) {\n if (prop.type === 'SpreadElement') {\n const spread = unwrapExpression(prop.argument);\n if (spread.type === 'ObjectExpression') {\n checkWrapperOptions(spread, target);\n }\n continue;\n }\n const key = prop.computed ? unwrapExpression(prop.key) : prop.key;\n if (prop.computed && key.type !== 'Literal') continue;\n const name = getKeyName(key);\n if (name === null || WRAPPER_OPTIONS.has(name)) continue;\n if (name === 'variants' || restyles(name, prop.value, target)) {\n report(prop, 'restyleWrapper', name, target);\n }\n }\n }\n\n const jsx = jsxElementPropListeners(\n ctx,\n (node): Target | null => {\n const target = components.resolveTag(node.name);\n return (\n target && {\n ...target,\n reference: context.sourceCode.getText(node.name),\n }\n );\n },\n (name, node, target) => {\n const value =\n node.type === 'Property'\n ? node.value\n : node.value?.type === 'JSXExpressionContainer'\n ? node.value.expression\n : node.value;\n const attribute = node.type === 'JSXAttribute' ? node : undefined;\n if (restyles(name, value, target, attribute)) {\n report(node, 'restyleProp', name, target);\n }\n },\n );\n\n return {\n ...jsx,\n 'Program:exit'() {\n ctx.finishJSXAnalysis();\n },\n CallExpression(node) {\n const [base, options] = node.arguments;\n if (!options || !components.isWrapperCall(node)) return;\n const component = components.resolveExpression(base);\n const object = unwrapExpression(options);\n if (!component || object.type !== 'ObjectExpression') return;\n checkWrapperOptions(object, {\n ...component,\n reference: context.sourceCode.getText(unwrapExpression(base)),\n });\n },\n };\n },\n});\n"],"mappings":";;;;;;;;;AAcA,MAAM,kBAAkB,IAAI,IAAI;CAC9B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;AAQF,SAAS,kBAAkB,MAAuB;AAChD,QAAO,4BAA4B,KAAK,KAAK;;;AAI/C,SAAS,kBAAkB,MAA8B;CACvD,MAAM,QAAQ,iBAAiB,KAAK;AACpC,QACE,MAAM,SAAS,6BACf,MAAM,SAAS,wBACf,MAAM,SAAS,gBACf,MAAM,SAAS;;AAInB,mCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,2GACH;EACD,UAAU;GACR,aACE;GACF,gBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,aAAa,IAAI,oBAAoB,SAAS,IAAI;AACxD,MAAI,CAAC,WAAW,QAAS,QAAO,EAAE;EAElC,MAAM,aAAa,IAAI,IAAI,CACzB,GAAG,wBACH,GAAG,IAAI,OAAO,OACf,CAAC;;EAGF,SAAS,WAAW,OAA6B,QAAyB;GACxE,MAAM,SAAS,SAAS,iBAAiB,MAAM;AAC/C,UACE,QAAQ,SAAS,sBACjB,OAAO,WAAW,OAAO,SAAS;AAChC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU,QAAO;IACtD,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,WAAO,QAAQ,QAAQ,OAAO,kBAAkB,IAAI,IAAI;KACxD;;EAIN,SAAS,SACP,MACA,OACA,QACA,WACS;AACT,OAAI,SAAS,SAAU,QAAO,CAAC,WAAW,OAAO,OAAO;AACxD,OAAI,kBAAkB,KAAK,CAAE,QAAO;AACpC,OAAI,CAAC,WAAW,IAAI,KAAK,IAAI,OAAO,kBAAkB,IAAI,KAAK,CAC7D,QAAO;AAET,OAAI,SAAS,kBAAkB,MAAM,CAAE,QAAO;AAE9C,UAAO,EAAE,aAAa,IAAI,mBAAmB,UAAU;;EAGzD,SAAS,OACP,MACA,WACA,MACA,QACM;AACN,WAAQ,OAAO;IACb;IACA;IACA,MAAM;KACJ;KACA,WACE,OAAO,cAAc,OAAO,OACxB,IAAI,OAAO,KAAK,KAChB,IAAI,OAAO,UAAU,eAAe,OAAO,KAAK;KACtD,QAAQ,IAAI,OAAO,OAAO;KAC3B;IACF,CAAC;;EAGJ,SAAS,oBACP,SACA,QACM;AACN,QAAK,MAAM,QAAQ,QAAQ,YAAY;AACrC,QAAI,KAAK,SAAS,iBAAiB;KACjC,MAAM,SAAS,iBAAiB,KAAK,SAAS;AAC9C,SAAI,OAAO,SAAS,mBAClB,qBAAoB,QAAQ,OAAO;AAErC;;IAEF,MAAM,MAAM,KAAK,WAAW,iBAAiB,KAAK,IAAI,GAAG,KAAK;AAC9D,QAAI,KAAK,YAAY,IAAI,SAAS,UAAW;IAC7C,MAAM,OAAO,WAAW,IAAI;AAC5B,QAAI,SAAS,QAAQ,gBAAgB,IAAI,KAAK,CAAE;AAChD,QAAI,SAAS,cAAc,SAAS,MAAM,KAAK,OAAO,OAAO,CAC3D,QAAO,MAAM,kBAAkB,MAAM,OAAO;;;AA8BlD,SAAO;GACL,GA1BU,wBACV,MACC,SAAwB;IACvB,MAAM,SAAS,WAAW,WAAW,KAAK,KAAK;AAC/C,WACE,UAAU;KACR,GAAG;KACH,WAAW,QAAQ,WAAW,QAAQ,KAAK,KAAK;KACjD;OAGJ,MAAM,MAAM,WAAW;AAQtB,QAAI,SAAS,MANX,KAAK,SAAS,aACV,KAAK,QACL,KAAK,OAAO,SAAS,2BACnB,KAAK,MAAM,aACX,KAAK,OAEa,QADR,KAAK,SAAS,iBAAiB,OAAO,OACZ,CAC1C,QAAO,MAAM,eAAe,MAAM,OAAO;KAG9C;GAIC,iBAAiB;AACf,QAAI,mBAAmB;;GAEzB,eAAe,MAAM;IACnB,MAAM,CAAC,MAAM,WAAW,KAAK;AAC7B,QAAI,CAAC,WAAW,CAAC,WAAW,cAAc,KAAK,CAAE;IACjD,MAAM,YAAY,WAAW,kBAAkB,KAAK;IACpD,MAAM,SAAS,iBAAiB,QAAQ;AACxC,QAAI,CAAC,aAAa,OAAO,SAAS,mBAAoB;AACtD,wBAAoB,QAAQ;KAC1B,GAAG;KACH,WAAW,QAAQ,WAAW,QAAQ,iBAAiB,KAAK,CAAC;KAC9D,CAAC;;GAEL;;CAEJ,CAAC"}
1
+ {"version":3,"file":"no-protected-restyle.js","names":[],"sources":["../../src/rules/no-protected-restyle.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { KNOWN_TASTY_PROPERTIES } from '../constants.js';\nimport {\n type ProtectedComponent,\n ProtectedComponents,\n} from '../protected-components.js';\nimport { getKeyName, unwrapExpression } from '../utils.js';\nimport { jsxElementPropListeners } from './tasty-component-props.js';\n\ntype MessageIds = 'restyleProp' | 'restyleWrapper';\n\n/** Which usages the rule checks: instance props and `tasty()` wrappers, or the wrappers alone. */\ntype Usages = 'all' | 'wrappers';\n\n/** `tasty(Base, options)` keys that configure the wrapper instead of setting a default prop. */\nconst WRAPPER_OPTIONS = new Set([\n 'as',\n 'element',\n 'elements',\n 'mods',\n 'modProps',\n 'qa',\n 'qaVal',\n 'styleProps',\n 'tokenProps',\n 'tokens',\n 'variant',\n]);\n\ninterface Target extends ProtectedComponent {\n /** The tag or base as written: `UI.Button`, `CopyButton`. */\n reference: string;\n}\n\n/** `inputStyles`, `triggerStyles`, ...: styles for one of the component's inner elements. */\nfunction isInnerStylesProp(name: string): boolean {\n return /^[a-z][A-Za-z0-9]*Styles$/.test(name);\n}\n\n/** A value that cannot be a style value: a callback or an element. */\nfunction isNeverStyleValue(node: TSESTree.Node): boolean {\n const value = unwrapExpression(node);\n return (\n value.type === 'ArrowFunctionExpression' ||\n value.type === 'FunctionExpression' ||\n value.type === 'JSXElement' ||\n value.type === 'JSXFragment'\n );\n}\n\nexport default createRule<[{ usages?: Usages }], MessageIds>({\n name: 'no-protected-restyle',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n \"Warn when a design system's protected components are restyled instead of customized through their props\",\n },\n messages: {\n restyleProp:\n \"'{{prop}}' restyles {{component}}, a protected {{source}} component. Customize it through its own props, 'tokens' or 'mods'. If this override is intended, explicitly disable tasty/no-protected-restyle for this usage with an ESLint comment and a reason.\",\n restyleWrapper:\n \"This tasty() wrapper restyles {{component}}, a protected {{source}} component, through '{{prop}}'. Set its own props, 'tokens' or 'mods' instead. If this override is intended, explicitly disable tasty/no-protected-restyle for this usage with an ESLint comment and a reason.\",\n },\n schema: [\n {\n type: 'object',\n properties: {\n usages: { type: 'string', enum: ['all', 'wrappers'] },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ usages: 'all' }],\n create(context, [{ usages = 'all' }]) {\n const ctx = new TastyContext(context);\n const components = new ProtectedComponents(context, ctx);\n if (!components.enabled) return {};\n\n const styleProps = new Set([\n ...KNOWN_TASTY_PROPERTIES,\n ...ctx.config.styles,\n ]);\n\n /** Root `styles` that only places or sizes the component, through allowed keys. */\n function placesOnly(value: TSESTree.Node | null, target: Target): boolean {\n const object = value && unwrapExpression(value);\n return (\n object?.type === 'ObjectExpression' &&\n object.properties.every((prop) => {\n if (prop.type !== 'Property' || prop.computed) return false;\n const key = getKeyName(prop.key);\n return key !== null && target.allowedStyleProps.has(key);\n })\n );\n }\n\n function restyles(\n name: string,\n value: TSESTree.Node | null,\n target: Target,\n attribute?: TSESTree.JSXAttribute,\n ): boolean {\n if (name === 'styles') return !placesOnly(value, target);\n if (isInnerStylesProp(name)) return true;\n if (!styleProps.has(name) || target.allowedStyleProps.has(name)) {\n return false;\n }\n if (value && isNeverStyleValue(value)) return false;\n // With typeAwareJSX, a prop the component declares itself is not a style prop.\n return !(attribute && ctx.isComponentJSXProp(attribute));\n }\n\n function report(\n node: TSESTree.Node,\n messageId: MessageIds,\n prop: string,\n target: Target,\n ): void {\n context.report({\n node,\n messageId,\n data: {\n prop,\n component:\n target.reference === target.name\n ? `'${target.name}'`\n : `'${target.reference}' (based on '${target.name}')`,\n source: `'${target.source}'`,\n },\n });\n }\n\n function checkWrapperOptions(\n options: TSESTree.ObjectExpression,\n target: Target,\n ): void {\n for (const prop of options.properties) {\n if (prop.type === 'SpreadElement') {\n const spread = unwrapExpression(prop.argument);\n if (spread.type === 'ObjectExpression') {\n checkWrapperOptions(spread, target);\n }\n continue;\n }\n const key = prop.computed ? unwrapExpression(prop.key) : prop.key;\n if (prop.computed && key.type !== 'Literal') continue;\n const name = getKeyName(key);\n if (name === null || WRAPPER_OPTIONS.has(name)) continue;\n if (name === 'variants' || restyles(name, prop.value, target)) {\n report(prop, 'restyleWrapper', name, target);\n }\n }\n }\n\n const jsx = jsxElementPropListeners(\n ctx,\n (node): Target | null => {\n const target = components.resolveTag(node.name);\n return (\n target && {\n ...target,\n reference: context.sourceCode.getText(node.name),\n }\n );\n },\n (name, node, target) => {\n const value =\n node.type === 'Property'\n ? node.value\n : node.value?.type === 'JSXExpressionContainer'\n ? node.value.expression\n : node.value;\n const attribute = node.type === 'JSXAttribute' ? node : undefined;\n if (restyles(name, value, target, attribute)) {\n report(node, 'restyleProp', name, target);\n }\n },\n );\n\n // `usages: 'wrappers'` drops the JSX checks, so instance props pass. The\n // wrapper check still needs the imports tracked.\n const instances =\n usages === 'wrappers'\n ? { ImportDeclaration: jsx.ImportDeclaration }\n : {\n ...jsx,\n 'Program:exit'() {\n ctx.finishJSXAnalysis();\n },\n };\n\n return {\n ...instances,\n CallExpression(node) {\n const [base, options] = node.arguments;\n if (!options || !components.isWrapperCall(node)) return;\n const component = components.resolveExpression(base);\n const object = unwrapExpression(options);\n if (!component || object.type !== 'ObjectExpression') return;\n checkWrapperOptions(object, {\n ...component,\n reference: context.sourceCode.getText(unwrapExpression(base)),\n });\n },\n };\n },\n});\n"],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ # tasty/consistent-token-usage
2
+
3
+ Warn about pixel lengths in Tasty styles.
4
+
5
+ **Presets:** warning in recommended and strict. **Fix:** editor suggestions.
6
+
7
+ ## Why
8
+
9
+ A design system sizes things with its units (`x`, `r`, `bw`) and tokens, so a layout follows its scale. A raw `8px` stays put when the scale changes.
10
+
11
+ ## Checks
12
+
13
+ The rule reports every nonzero pixel length in recognized style objects and JSX
14
+ style props: compound values, comma-separated groups, function arguments, and
15
+ numeric inputs to enhanced handlers that convert them to pixels, such as
16
+ `gap={17}`. Unitless properties such as `opacity` and `zIndex` are not checked.
17
+ Zero, token definitions (`$name` and `#name` keys), quoted CSS text, and URLs
18
+ are allowed.
19
+
20
+ A pixel length with an exact unit gets a suggestion that replaces it:
21
+
22
+ | Pixels | Replacement |
23
+ | --------------------------- | --------------- |
24
+ | `8px` to `64px`, step `8px` | `1x` to `8x` |
25
+ | `6px` in `radius` | `1r` |
26
+ | `1px` in a border width | `1bw` |
27
+
28
+ These replacements assume Tasty's default scale. Review them against your
29
+ design system. Any other pixel length, such as `17px` or `520px`, is reported
30
+ without a suggestion: the rule does not invent a token. A `fontSize`,
31
+ `lineHeight` or `letterSpacing` length never gets an `x` suggestion: it is on
32
+ the type scale, which presets set, and `x` is the spacing scale.
33
+
34
+ A suggestion keeps the rest of the value as written. It is withheld when
35
+ JavaScript escapes or JSX entities make the source differ from the value.
36
+
37
+ ```tsx
38
+ import { tasty } from '@tenphi/tasty';
39
+
40
+ // ❌ '1x' and '1bw' replace these exactly.
41
+ const Card = tasty({ styles: { padding: '8px', border: '1px solid #border' } });
42
+
43
+ // ✅ Preferred.
44
+ const Card = tasty({ styles: { padding: '1x', border: '1bw solid #border' } });
45
+ ```
46
+
47
+ For an intentional pixel length, add an explicit local ignore with a reason:
48
+
49
+ ```tsx
50
+ const Rule = tasty({
51
+ styles: {
52
+ // eslint-disable-next-line tasty/consistent-token-usage -- aligns with the 3px focus ring
53
+ inset: '-3px',
54
+ },
55
+ });
56
+ ```
57
+
58
+ ## Options
59
+
60
+ ```js
61
+ 'tasty/consistent-token-usage': ['error', { pixels: 'replaceable' }],
62
+ ```
63
+
64
+ | `pixels` | Reports |
65
+ | ----------------- | ------------------------------------------------------------------------------ |
66
+ | `'all'` (default) | Every nonzero pixel length |
67
+ | `'replaceable'` | Only a pixel length with an exact unit in the table above, with its suggestion |
68
+
69
+ Use `'replaceable'` when your layout sizes are in pixels on purpose, such as
70
+ `width: 'max 520px'`. The rule then reports only a length that a unit replaces
71
+ exactly, so every finding has a fix and the rule can run as an error. In
72
+ `padding: '8px 17px'` it reports `8px` and leaves `17px` alone.
@@ -0,0 +1,55 @@
1
+ # tasty/known-property
2
+
3
+ Reports a style property name that is neither a Tasty style nor a CSS property, such as a typo.
4
+
5
+ **Presets:** warning in recommended and strict. **Fix:** none.
6
+
7
+ ## Why
8
+
9
+ Tasty passes an unknown key through as a CSS declaration, which the browser drops. A typo such as `colour` or `boarder` fails silently at runtime.
10
+
11
+ ## Examples
12
+
13
+ ```js
14
+ // ✅ Valid
15
+ tasty({ styles: { fill: '#purple', padding: '2x' } });
16
+
17
+ // ❌ Unknown style property 'colour'
18
+ tasty({ styles: { colour: '#purple' } });
19
+
20
+ // ❌ Unknown style property 'boarder'
21
+ tasty({ styles: { boarder: true } });
22
+ ```
23
+
24
+ ## What counts as known
25
+
26
+ - **Tasty styles:** `display`, `font`, `preset`, `hide`, `whiteSpace`, `opacity`, `transition`, `gridArea`, `order`, `gridColumn`, `gridRow`, `placeSelf`, `alignSelf`, `justifySelf`, `zIndex`, `margin`, `inset`, `position`, `padding`, `overflow`, `scrollbar`, `textAlign`, `scrollMargin`, `scrollPadding`, `border`, `radius`, `shadow`, `outline`, `color`, `fill`, `svgFill`, `fade`, `image`, `textTransform`, `fontWeight`, `fontStyle`, `width`, `height`, `flexBasis`, `flexGrow`, `flexShrink`, `flex`, `flow`, `placeItems`, `placeContent`, `alignItems`, `alignContent`, `justifyItems`, `justifyContent`, `align`, `justify`, `gap`, `columnGap`, `rowGap`, `gridColumns`, `gridRows`, `gridTemplate`, `gridAreas`, `recipe`, `textOverflow`.
27
+ - **Enhanced logical styles** (Tasty 3.8 and later), one per axis and category:
28
+
29
+ | Category | Block axis | Inline axis |
30
+ | --- | --- | --- |
31
+ | Size | `blockSize` (+ `minBlockSize`, `maxBlockSize`) | `inlineSize` (+ `minInlineSize`, `maxInlineSize`) |
32
+ | Padding | `blockPadding` | `inlinePadding` |
33
+ | Margin | `blockMargin` | `inlineMargin` |
34
+ | Inset | `blockInset` | `inlineInset` |
35
+ | Scroll margin | `blockScrollMargin` | `inlineScrollMargin` |
36
+ | Scroll padding | `blockScrollPadding` | `inlineScrollPadding` |
37
+ | Border | `blockBorder` | `inlineBorder` |
38
+
39
+ - **CSS properties.** The list is generated from `known-css-properties`, the MDN/W3C dataset that stylelint uses for `property-no-unknown`. It covers modern properties (`maskSize`, `anchorName`, `fieldSizing`, `viewTransitionName`, `textBox`) and SVG presentation and geometry properties. A small hand-kept supplement adds properties that ship before the dataset has them. At-rule descriptors that the specs (through `@webref/css`) do not also define as properties, such as `src`, `prefix`, `suffix`, `syntax`, `symbols` and `unicodeRange`, are left out: a style rule cannot declare them. Regenerate the list with `pnpm generate:css-properties`.
40
+ - **Names from your config:** the `styles` list in `tasty.config.*`, for handlers added through `configure({ handlers })`.
41
+
42
+ The native logical spellings (`paddingBlock`, `insetInlineStart`, `borderBlockColor`, …) stay valid as CSS properties. They are no longer Tasty styles: Tasty 3.8 stopped reading `paddingBlock`, `paddingInline`, `insetBlock` and `insetInline` in its physical handlers. [`tasty/prefer-shorthand-property`](prefer-shorthand-property.md) reports them with a migration.
43
+
44
+ ## Keys this rule skips
45
+
46
+ - A key that starts with an uppercase letter is a sub-element ([`tasty/valid-sub-element`](valid-sub-element.md)).
47
+ - A key that starts with `@` is an at-rule key (`@keyframes`, `@property`, …) or a local state alias.
48
+ - A key that starts with `$` or `$$` defines a custom property (`$name` is `--name`).
49
+ - A key that starts with `#` or `##` defines a color token (`#name` is `--name-color`).
50
+ - `--name` custom properties and vendor-prefixed properties (`-webkit-…`).
51
+ - A key that starts with `&` is a nested selector, which [`tasty/no-nested-selector`](no-nested-selector.md) reports.
52
+
53
+ ## Options
54
+
55
+ None.
@@ -0,0 +1,60 @@
1
+ # tasty/no-classname-prop
2
+
3
+ Warn about `className` on recognized Tasty components.
4
+
5
+ **Presets:** warning in recommended and strict. **Fix:** none.
6
+
7
+ Keep styling in Tasty rather than attaching external styling classes. When a
8
+ class identifies a sub-element for its parent to style, replace that identity
9
+ with `data-element="Name"` and a matching capitalized sub-element key:
10
+
11
+ ```tsx
12
+ import { tasty } from '@tenphi/tasty';
13
+
14
+ const Card = tasty({ styles: { Body: { padding: '2x' } } });
15
+ const Box = tasty({ as: 'div' });
16
+
17
+ // ❌ Avoid external class-based styling.
18
+ <Card>
19
+ <Box className="card-body" />
20
+ </Card>;
21
+
22
+ // ✅ Preferred: Card's Body styles target this element.
23
+ <Card>
24
+ <Box data-element="Body" />
25
+ </Card>;
26
+ ```
27
+
28
+ For general root styling, define styles on the component or use a styled wrapper.
29
+ `data-element` is an identity for parent sub-element styling, not a replacement
30
+ for an arbitrary list of CSS classes.
31
+
32
+ The rule checks every explicit attribute value and visible `className` keys in
33
+ inline object spreads. It has no automatic fix because external classes can have
34
+ multiple styling or behavior roles.
35
+
36
+ A third-party library may require a class for its stylesheet or behavior. Keep
37
+ that integration and acknowledge the exception with an explicit local ignore and
38
+ a reason:
39
+
40
+ ```tsx
41
+ const libraryElement = (
42
+ // eslint-disable-next-line tasty/no-classname-prop -- library stylesheet requires this class
43
+ <Box className={libraryClassName} />
44
+ );
45
+ ```
46
+
47
+ Place the directive immediately before the reported attribute line. It suppresses
48
+ only this rule for that line. The warning message includes this exception path;
49
+ the plugin does not silently exempt library-provided values or enforce reasons.
50
+
51
+ Recognition uses local `const` Tasty/configured-options factory results, their
52
+ `const` aliases/sub-elements, and imports from `importSources`. Exports of relative modules (`import { Panel } from './styled'`) count when they
53
+ resolve to one of these, through re-exports too; following them needs the
54
+ `typescript` package. Native elements,
55
+ unrelated components, type-only imports, shadowed bindings, mutable aliases, and
56
+ opaque spread bindings are excluded.
57
+
58
+ ## Options
59
+
60
+ None.
@@ -0,0 +1,97 @@
1
+ # tasty/no-ignored-pseudo-element-state
2
+
3
+ Report states that Tasty ignores because they test a pseudo-element for
4
+ something it cannot have. The reported state never applies.
5
+
6
+ **Presets:** error in recommended and strict. **Fix:** none.
7
+
8
+ When a sub-element's `$` ends in a pseudo-element, Tasty applies `@own(...)` to
9
+ the pseudo-element itself: `$: '@::part(label)'` with `@own(:focus)` becomes
10
+ `::part(label):where(:focus)`. A pseudo-element has no attributes, classes, IDs,
11
+ or place in the DOM tree, so a state that tests one of those can never match.
12
+ Tasty drops such a state at runtime with a `PSEUDO_ELEMENT_STATE_IGNORED`
13
+ warning. This rule reports the same state keys while you edit.
14
+
15
+ ## Reported
16
+
17
+ ```js
18
+ const Pagination = tasty({
19
+ styles: {
20
+ NextArrow: {
21
+ $: 'a::before',
22
+ content: {
23
+ '': 'none',
24
+ '@own([rel="next"])': '"→"', // attributes
25
+ },
26
+ },
27
+ Marker: {
28
+ $: 'li::marker',
29
+ color: {
30
+ '': '#text',
31
+ '@own(:last-child)': '#accent', // position in the DOM tree
32
+ '@own(selected)': '#active', // modifiers
33
+ },
34
+ },
35
+ },
36
+ });
37
+ ```
38
+
39
+ Tasty ignores these states on a pseudo-element:
40
+
41
+ - Modifiers: `@own(hovered)`, `@own(theme=dark)`.
42
+ - Attributes: `@own([open])`, `@own(:not([open]))`.
43
+ - Classes and IDs: `@own(.active)`.
44
+ - Tree-structural pseudo-classes: `:root`, `:scope`, `:empty`, `:has()`,
45
+ `:first-child`, `:last-child`, `:only-child`, their `-of-type` forms, and
46
+ `:nth-*()`.
47
+ - Any key that combines one of these with other states, such as
48
+ `@own(:hover | [open])` or `:focus & @own([open])`. Tasty drops the whole key.
49
+
50
+ ## Preferred
51
+
52
+ Write a condition on the element the pseudo-element belongs to in `$`, before
53
+ the pseudo-element. `tasty/no-state-in-selector` accepts these selectors.
54
+
55
+ ```js
56
+ const Pagination = tasty({
57
+ styles: {
58
+ NextArrow: { $: 'a[rel="next"]::before', content: '"→"' },
59
+ Marker: { $: 'li:last-child::marker', color: '#accent' },
60
+ Label: {
61
+ $: '@::part(label)',
62
+ color: { '': '#text', '@own(:hover)': '#active' },
63
+ },
64
+ },
65
+ });
66
+ ```
67
+
68
+ A pseudo-element can be in a pseudo-class state such as `:hover` or `:focus`, so
69
+ those still belong in `@own(...)`. Root states, `@parent(...)`, `@root(...)`, and
70
+ at-rule conditions describe other elements and are not reported.
71
+
72
+ ## Where it applies
73
+
74
+ - A sub-element `$` item that ends in a pseudo-element: the first `::name`, or a
75
+ legacy `:before`, `:after`, `:first-line`, or `:first-letter`, in its last
76
+ compound. Each item of a `$` list is judged on its own. With
77
+ `$: 'summary::marker, summary'`, Tasty ignores `@own([open])` only on
78
+ `summary::marker`, and the message names that item.
79
+ - `&` keys and `&`-prefixed `$` values extend their owner's selector. `'&::before'`
80
+ at the root, and `'&:hover'` under `$: 'a::before'`, are pseudo-element targets.
81
+ - A selector-mode call whose literal selector ends in a pseudo-element, such as
82
+ `useGlobalStyles('.card::before', styles)` or `tastyStatic('.card::before',
83
+ styles)`. There, root states describe the pseudo-element too, so
84
+ `[data-on]`, `selected`, and `.dark` are reported, while `:hover` is not.
85
+
86
+ ## Limits
87
+
88
+ - `@name` state aliases are not resolved. Tasty may still ignore a state an
89
+ alias expands to.
90
+ - Dynamic `$` values and selector arguments are skipped. Malformed state keys
91
+ are left to `tasty/valid-state-key`.
92
+ - The rule has no autofix. Moving a condition into `$` changes which element
93
+ every property of that sub-element applies to, so the author must choose.
94
+
95
+ ## Options
96
+
97
+ None.
@@ -0,0 +1,25 @@
1
+ # tasty/no-important
2
+
3
+ Reports `!important` in a style value.
4
+
5
+ **Presets:** error in recommended and strict. **Fix:** autofix (removes `!important`).
6
+
7
+ ## Why
8
+
9
+ Tasty manages specificity itself, through doubled class selectors and the order of states. `!important` overrides that system: states and extensions can no longer win where they should.
10
+
11
+ ## Examples
12
+
13
+ ```js
14
+ // ❌ Do not use !important in tasty styles.
15
+ fill: '#red !important'
16
+
17
+ // ✅ After the autofix
18
+ fill: '#red'
19
+ ```
20
+
21
+ To make a state win, give it a later place in the state map, or combine conditions in its key.
22
+
23
+ ## Options
24
+
25
+ None.
@@ -0,0 +1,39 @@
1
+ # tasty/no-nested-selector
2
+
3
+ Reports `&`-prefixed nested selectors, such as `'& img'` or `'&:hover'`, as keys of a style object.
4
+
5
+ **Presets:** error in recommended and strict. **Fix:** none.
6
+
7
+ ## Why
8
+
9
+ Nested selectors are legacy syntax. Sub-elements (capitalized keys that target `data-element`) and state maps are more explicit and compose: Tasty resolves their priority, and `mergeStyles` and extensions merge them per key.
10
+
11
+ ## Examples
12
+
13
+ ```js
14
+ // ❌ Avoid nested selectors. Use sub-element styling instead.
15
+ tasty({
16
+ styles: {
17
+ '& img': { width: '100%' },
18
+ '&:hover': { fill: '#blue' },
19
+ },
20
+ });
21
+
22
+ // ✅ A sub-element
23
+ tasty({
24
+ styles: {
25
+ Image: { width: '100%' },
26
+ },
27
+ });
28
+
29
+ // ✅ A state map for a pseudo-class
30
+ tasty({
31
+ styles: {
32
+ fill: { '': '#white', ':hover': '#blue' },
33
+ },
34
+ });
35
+ ```
36
+
37
+ ## Options
38
+
39
+ None.