stablekit.ts 0.3.2 → 0.4.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/dist/eslint.cjs CHANGED
@@ -87,7 +87,7 @@ function createArchitectureLint(options) {
87
87
  stateTokens,
88
88
  variantProps = [],
89
89
  banColorUtilities = true,
90
- classNamePassthrough = [],
90
+ classNameBlocked = [],
91
91
  loadingPassthrough = ["LoadingBoundary"],
92
92
  files = ["src/components/**/*.{tsx,jsx}"]
93
93
  } = options;
@@ -208,18 +208,13 @@ function createArchitectureLint(options) {
208
208
  selector: ":matches(JSXElement, JSXFragment) > JSXExpressionContainer > TemplateLiteral",
209
209
  message: "Interpolated text in JSX children. Extract to a const above the return (the linter won't flag a plain variable). For state-driven values, use <StableCounter> for numbers or <StateSwap> for text variants."
210
210
  },
211
- // --- 5. className on custom components ---
212
- ...classNamePassthrough.length ? [
211
+ // --- 5. className on firewalled components ---
212
+ ...classNameBlocked.length ? [
213
213
  {
214
- selector: `JSXOpeningElement[name.name=/^[A-Z]/]:not([name.name=/^(${classNamePassthrough.join("|")})$/]) > JSXAttribute[name.name='className']`,
215
- message: "className on a custom component. Components own their own styling \u2014 use a data-attribute, variant prop, or CSS class internally instead of passing Tailwind utilities from the consumer."
214
+ selector: `JSXOpeningElement[name.name=/^(${classNameBlocked.join("|")})$/] > JSXAttribute[name.name='className']`,
215
+ message: "className on a firewalled component. This component owns its own styling \u2014 use a data-attribute, variant prop, or CSS class internally instead of passing Tailwind utilities from the consumer."
216
216
  }
217
- ] : [
218
- {
219
- selector: "JSXOpeningElement[name.name=/^[A-Z]/] > JSXAttribute[name.name='className']",
220
- message: "className on a custom component. Components own their own styling \u2014 use a data-attribute, variant prop, or CSS class internally instead of passing Tailwind utilities from the consumer."
221
- }
222
- ]
217
+ ] : []
223
218
  ],
224
219
  "stablekit/no-loading-conflict": ["error", { passthrough: loadingPassthrough }]
225
220
  },
package/dist/eslint.d.cts CHANGED
@@ -49,11 +49,11 @@ interface ArchitectureLintOptions {
49
49
  * Colors must live in CSS — not in component classNames.
50
50
  * @default true */
51
51
  banColorUtilities?: boolean;
52
- /** Components that transparently pass className to their root element.
53
- * These are excluded from the className-on-component ban.
54
- * e.g. ["StableText", "StableCounter", "MediaSkeleton", "ChevronDown"]
55
- * @default [] */
56
- classNamePassthrough?: string[];
52
+ /** Components where className is banned (your firewalled primitives).
53
+ * Only these components are flagged everything else is left alone.
54
+ * e.g. ["Badge", "Button", "Card", "Input", "IconButton"]
55
+ * @default [] (rule is off when empty) */
56
+ classNameBlocked?: string[];
57
57
  /** Components where `loading` prop does NOT trigger a content swap
58
58
  * (e.g. LoadingBoundary controls opacity, not geometry).
59
59
  * These are excluded from the dual-paradigm conflict rule.
package/dist/eslint.d.ts CHANGED
@@ -49,11 +49,11 @@ interface ArchitectureLintOptions {
49
49
  * Colors must live in CSS — not in component classNames.
50
50
  * @default true */
51
51
  banColorUtilities?: boolean;
52
- /** Components that transparently pass className to their root element.
53
- * These are excluded from the className-on-component ban.
54
- * e.g. ["StableText", "StableCounter", "MediaSkeleton", "ChevronDown"]
55
- * @default [] */
56
- classNamePassthrough?: string[];
52
+ /** Components where className is banned (your firewalled primitives).
53
+ * Only these components are flagged everything else is left alone.
54
+ * e.g. ["Badge", "Button", "Card", "Input", "IconButton"]
55
+ * @default [] (rule is off when empty) */
56
+ classNameBlocked?: string[];
57
57
  /** Components where `loading` prop does NOT trigger a content swap
58
58
  * (e.g. LoadingBoundary controls opacity, not geometry).
59
59
  * These are excluded from the dual-paradigm conflict rule.
package/dist/eslint.js CHANGED
@@ -63,7 +63,7 @@ function createArchitectureLint(options) {
63
63
  stateTokens,
64
64
  variantProps = [],
65
65
  banColorUtilities = true,
66
- classNamePassthrough = [],
66
+ classNameBlocked = [],
67
67
  loadingPassthrough = ["LoadingBoundary"],
68
68
  files = ["src/components/**/*.{tsx,jsx}"]
69
69
  } = options;
@@ -184,18 +184,13 @@ function createArchitectureLint(options) {
184
184
  selector: ":matches(JSXElement, JSXFragment) > JSXExpressionContainer > TemplateLiteral",
185
185
  message: "Interpolated text in JSX children. Extract to a const above the return (the linter won't flag a plain variable). For state-driven values, use <StableCounter> for numbers or <StateSwap> for text variants."
186
186
  },
187
- // --- 5. className on custom components ---
188
- ...classNamePassthrough.length ? [
187
+ // --- 5. className on firewalled components ---
188
+ ...classNameBlocked.length ? [
189
189
  {
190
- selector: `JSXOpeningElement[name.name=/^[A-Z]/]:not([name.name=/^(${classNamePassthrough.join("|")})$/]) > JSXAttribute[name.name='className']`,
191
- message: "className on a custom component. Components own their own styling \u2014 use a data-attribute, variant prop, or CSS class internally instead of passing Tailwind utilities from the consumer."
190
+ selector: `JSXOpeningElement[name.name=/^(${classNameBlocked.join("|")})$/] > JSXAttribute[name.name='className']`,
191
+ message: "className on a firewalled component. This component owns its own styling \u2014 use a data-attribute, variant prop, or CSS class internally instead of passing Tailwind utilities from the consumer."
192
192
  }
193
- ] : [
194
- {
195
- selector: "JSXOpeningElement[name.name=/^[A-Z]/] > JSXAttribute[name.name='className']",
196
- message: "className on a custom component. Components own their own styling \u2014 use a data-attribute, variant prop, or CSS class internally instead of passing Tailwind utilities from the consumer."
197
- }
198
- ]
193
+ ] : []
199
194
  ],
200
195
  "stablekit/no-loading-conflict": ["error", { passthrough: loadingPassthrough }]
201
196
  },
package/llms.txt CHANGED
@@ -317,10 +317,10 @@ change to structure requires editing `.css`, a boundary has leaked.
317
317
  or use a StableKit component (for state-driven swaps — StateSwap,
318
318
  LayoutMap, LoadingBoundary, FadeTransition, StableField, StableCounter,
319
319
  LayoutGroup). These rules are always on.
320
- `className` on PascalCase components is banned — components own their own
321
- styling. Use `classNamePassthrough` to exempt transparent wrappers (e.g.
322
- `StableText`, `MediaSkeleton`, Lucide icons) that forward className to
323
- their root element.
320
+ `classNameBlocked` declares your firewalled primitives (e.g.
321
+ `["Badge", "Button", "Card", "Input"]`). Only those components are flagged
322
+ for className — everything else (Lucide icons, Radix, React Router, etc.)
323
+ is left alone. The rule is off when `classNameBlocked` is empty.
324
324
  A custom rule (`stablekit/no-loading-conflict`) catches dual-paradigm
325
325
  conflicts: if a component has a `loading` prop (which triggers an internal
326
326
  content swap via StateSwap), children must not be variables derived from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stablekit.ts",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "React toolkit for layout stability — zero-shift components for loading states, content swaps, and spatial containers.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",