stablekit.ts 0.3.0 → 0.3.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.
package/dist/eslint.cjs CHANGED
@@ -23,6 +23,65 @@ __export(eslint_exports, {
23
23
  createArchitectureLint: () => createArchitectureLint
24
24
  });
25
25
  module.exports = __toCommonJS(eslint_exports);
26
+ function findVariable(scope, name) {
27
+ let current = scope;
28
+ while (current) {
29
+ const variable = current.set.get(name);
30
+ if (variable) return variable;
31
+ current = current.upper;
32
+ }
33
+ return null;
34
+ }
35
+ var noLoadingConflictRule = {
36
+ meta: {
37
+ type: "problem",
38
+ schema: [
39
+ {
40
+ type: "object",
41
+ properties: {
42
+ passthrough: { type: "array", items: { type: "string" } }
43
+ },
44
+ additionalProperties: false
45
+ }
46
+ ],
47
+ messages: {
48
+ conflict: "Conditional variable as children of a component with a loading prop. The loading prop triggers an internal content swap \u2014 if children also change, both sides shrink simultaneously and pre-allocation is defeated. Use static children with the loading prop, or handle the entire swap explicitly with <StateSwap> in the caller."
49
+ }
50
+ },
51
+ create(context) {
52
+ const passthrough = context.options[0]?.passthrough ?? [];
53
+ return {
54
+ JSXElement(node) {
55
+ const opening = node.openingElement;
56
+ if (opening.name?.type !== "JSXIdentifier") return;
57
+ const name = opening.name.name;
58
+ if (!/^[A-Z]/.test(name)) return;
59
+ if (passthrough.includes(name)) return;
60
+ const hasLoading = opening.attributes.some(
61
+ (attr) => attr.type === "JSXAttribute" && attr.name?.name === "loading"
62
+ );
63
+ if (!hasLoading) return;
64
+ for (const child of node.children) {
65
+ if (child.type !== "JSXExpressionContainer") continue;
66
+ const expr = child.expression;
67
+ if (expr.type !== "Identifier") continue;
68
+ const scope = context.sourceCode ? context.sourceCode.getScope(node) : context.getScope();
69
+ const variable = findVariable(scope, expr.name);
70
+ if (!variable) continue;
71
+ for (const def of variable.defs) {
72
+ if (def.type === "Parameter") continue;
73
+ if (def.type === "Variable" && def.node.init) {
74
+ const initType = def.node.init.type;
75
+ if (initType === "ConditionalExpression" || initType === "LogicalExpression" || initType === "TemplateLiteral") {
76
+ context.report({ node: expr, messageId: "conflict" });
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+ };
83
+ }
84
+ };
26
85
  function createArchitectureLint(options) {
27
86
  const {
28
87
  stateTokens,
@@ -160,13 +219,16 @@ function createArchitectureLint(options) {
160
219
  selector: "JSXOpeningElement[name.name=/^[A-Z]/] > JSXAttribute[name.name='className']",
161
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."
162
221
  }
163
- ],
164
- // --- 6. Dual-paradigm conflict (loading + variable children) ---
165
- {
166
- selector: `JSXElement:has(JSXOpeningElement[name.name=/^[A-Z]/]:not([name.name=/^(${loadingPassthrough.join("|")})$/]) > JSXAttribute[name.name='loading']) > JSXExpressionContainer > Identifier`,
167
- message: "Variable children inside a component with a loading prop. The loading prop triggers an internal content swap \u2014 if children also change, both sides shrink simultaneously and pre-allocation is defeated. Use static children with the loading prop, or handle the entire swap explicitly with <StateSwap> in the caller."
222
+ ]
223
+ ],
224
+ "stablekit/no-loading-conflict": ["error", { passthrough: loadingPassthrough }]
225
+ },
226
+ plugins: {
227
+ stablekit: {
228
+ rules: {
229
+ "no-loading-conflict": noLoadingConflictRule
168
230
  }
169
- ]
231
+ }
170
232
  }
171
233
  };
172
234
  }
package/dist/eslint.d.cts CHANGED
@@ -30,10 +30,12 @@
30
30
  * component is a presentation leak across the Structure boundary.
31
31
  * The component should own its own styling. Always on.
32
32
  *
33
- * 6. Dual-paradigm conflict a component with a `loading` prop does
34
- * internal content swapping (StateSwap). If children are also variable,
35
- * both sides of the swap change simultaneously, defeating pre-allocation.
36
- * Children of loading-swappable components must be static.
33
+ * 6. Dual-paradigm conflict (custom rule with scope analysis) a component
34
+ * with a `loading` prop does internal content swapping (StateSwap). If
35
+ * children are a variable derived from a conditional expression, both
36
+ * sides of the swap change simultaneously, defeating pre-allocation.
37
+ * Only flags locals initialized with ternaries/logical expressions.
38
+ * Props (function parameters) are allowed — they're static per mount.
37
39
  */
38
40
  interface ArchitectureLintOptions {
39
41
  /** State token names that should never appear in JS as Tailwind classes.
@@ -68,6 +70,16 @@ declare function createArchitectureLint(options: ArchitectureLintOptions): {
68
70
  selector: string;
69
71
  message: string;
70
72
  })[];
73
+ "stablekit/no-loading-conflict": (string | {
74
+ passthrough: string[];
75
+ })[];
76
+ };
77
+ plugins: {
78
+ stablekit: {
79
+ rules: {
80
+ "no-loading-conflict": any;
81
+ };
82
+ };
71
83
  };
72
84
  };
73
85
 
package/dist/eslint.d.ts CHANGED
@@ -30,10 +30,12 @@
30
30
  * component is a presentation leak across the Structure boundary.
31
31
  * The component should own its own styling. Always on.
32
32
  *
33
- * 6. Dual-paradigm conflict a component with a `loading` prop does
34
- * internal content swapping (StateSwap). If children are also variable,
35
- * both sides of the swap change simultaneously, defeating pre-allocation.
36
- * Children of loading-swappable components must be static.
33
+ * 6. Dual-paradigm conflict (custom rule with scope analysis) a component
34
+ * with a `loading` prop does internal content swapping (StateSwap). If
35
+ * children are a variable derived from a conditional expression, both
36
+ * sides of the swap change simultaneously, defeating pre-allocation.
37
+ * Only flags locals initialized with ternaries/logical expressions.
38
+ * Props (function parameters) are allowed — they're static per mount.
37
39
  */
38
40
  interface ArchitectureLintOptions {
39
41
  /** State token names that should never appear in JS as Tailwind classes.
@@ -68,6 +70,16 @@ declare function createArchitectureLint(options: ArchitectureLintOptions): {
68
70
  selector: string;
69
71
  message: string;
70
72
  })[];
73
+ "stablekit/no-loading-conflict": (string | {
74
+ passthrough: string[];
75
+ })[];
76
+ };
77
+ plugins: {
78
+ stablekit: {
79
+ rules: {
80
+ "no-loading-conflict": any;
81
+ };
82
+ };
71
83
  };
72
84
  };
73
85
 
package/dist/eslint.js CHANGED
@@ -1,4 +1,63 @@
1
1
  // src/eslint.ts
2
+ function findVariable(scope, name) {
3
+ let current = scope;
4
+ while (current) {
5
+ const variable = current.set.get(name);
6
+ if (variable) return variable;
7
+ current = current.upper;
8
+ }
9
+ return null;
10
+ }
11
+ var noLoadingConflictRule = {
12
+ meta: {
13
+ type: "problem",
14
+ schema: [
15
+ {
16
+ type: "object",
17
+ properties: {
18
+ passthrough: { type: "array", items: { type: "string" } }
19
+ },
20
+ additionalProperties: false
21
+ }
22
+ ],
23
+ messages: {
24
+ conflict: "Conditional variable as children of a component with a loading prop. The loading prop triggers an internal content swap \u2014 if children also change, both sides shrink simultaneously and pre-allocation is defeated. Use static children with the loading prop, or handle the entire swap explicitly with <StateSwap> in the caller."
25
+ }
26
+ },
27
+ create(context) {
28
+ const passthrough = context.options[0]?.passthrough ?? [];
29
+ return {
30
+ JSXElement(node) {
31
+ const opening = node.openingElement;
32
+ if (opening.name?.type !== "JSXIdentifier") return;
33
+ const name = opening.name.name;
34
+ if (!/^[A-Z]/.test(name)) return;
35
+ if (passthrough.includes(name)) return;
36
+ const hasLoading = opening.attributes.some(
37
+ (attr) => attr.type === "JSXAttribute" && attr.name?.name === "loading"
38
+ );
39
+ if (!hasLoading) return;
40
+ for (const child of node.children) {
41
+ if (child.type !== "JSXExpressionContainer") continue;
42
+ const expr = child.expression;
43
+ if (expr.type !== "Identifier") continue;
44
+ const scope = context.sourceCode ? context.sourceCode.getScope(node) : context.getScope();
45
+ const variable = findVariable(scope, expr.name);
46
+ if (!variable) continue;
47
+ for (const def of variable.defs) {
48
+ if (def.type === "Parameter") continue;
49
+ if (def.type === "Variable" && def.node.init) {
50
+ const initType = def.node.init.type;
51
+ if (initType === "ConditionalExpression" || initType === "LogicalExpression" || initType === "TemplateLiteral") {
52
+ context.report({ node: expr, messageId: "conflict" });
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
58
+ };
59
+ }
60
+ };
2
61
  function createArchitectureLint(options) {
3
62
  const {
4
63
  stateTokens,
@@ -136,13 +195,16 @@ function createArchitectureLint(options) {
136
195
  selector: "JSXOpeningElement[name.name=/^[A-Z]/] > JSXAttribute[name.name='className']",
137
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."
138
197
  }
139
- ],
140
- // --- 6. Dual-paradigm conflict (loading + variable children) ---
141
- {
142
- selector: `JSXElement:has(JSXOpeningElement[name.name=/^[A-Z]/]:not([name.name=/^(${loadingPassthrough.join("|")})$/]) > JSXAttribute[name.name='loading']) > JSXExpressionContainer > Identifier`,
143
- message: "Variable children inside a component with a loading prop. The loading prop triggers an internal content swap \u2014 if children also change, both sides shrink simultaneously and pre-allocation is defeated. Use static children with the loading prop, or handle the entire swap explicitly with <StateSwap> in the caller."
198
+ ]
199
+ ],
200
+ "stablekit/no-loading-conflict": ["error", { passthrough: loadingPassthrough }]
201
+ },
202
+ plugins: {
203
+ stablekit: {
204
+ rules: {
205
+ "no-loading-conflict": noLoadingConflictRule
144
206
  }
145
- ]
207
+ }
146
208
  }
147
209
  };
148
210
  }
package/llms.txt CHANGED
@@ -317,6 +317,19 @@ 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.
324
+ A custom rule (`stablekit/no-loading-conflict`) catches dual-paradigm
325
+ conflicts: if a component has a `loading` prop (which triggers an internal
326
+ content swap via StateSwap), children must not be variables derived from
327
+ conditional expressions. The `loading` prop controls geometry — if children
328
+ also change, both sides shrink simultaneously and pre-allocation is
329
+ defeated. Props (function parameters) are allowed because they are static
330
+ per mount. Use `loadingPassthrough` to exempt components where `loading`
331
+ does not trigger a content swap (e.g. `LoadingBoundary` controls opacity,
332
+ not geometry). Default passthrough: `["LoadingBoundary"]`.
320
333
  4. **Stylelint** (`stablekit/stylelint`) — `createStyleLint({ functionalTokens })`
321
334
  bans element selectors in CSS (`& svg`, `& span`), bans `!important`, and
322
335
  bans functional color tokens inside `@utility` blocks. `functionalTokens`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stablekit.ts",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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",