stablekit.ts 0.5.2 → 0.6.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 +8 -4
- package/dist/eslint.d.cts +4 -4
- package/dist/eslint.d.ts +4 -4
- package/dist/eslint.js +8 -4
- package/dist/stylelint.cjs +51 -4
- package/dist/stylelint.d.cts +7 -1
- package/dist/stylelint.d.ts +7 -1
- package/dist/stylelint.js +51 -4
- package/package.json +1 -1
package/dist/eslint.cjs
CHANGED
|
@@ -208,10 +208,14 @@ 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
|
-
// --- 4b.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
211
|
+
// --- 4b. Sibling hidden swap (geometric instability) ---
|
|
212
|
+
// Two sibling elements both using conditional hidden to swap
|
|
213
|
+
// visibility — neither reserves space for the other.
|
|
214
|
+
// A single hidden={condition} (e.g. error message at bottom of
|
|
215
|
+
// layout) is fine and intentionally not flagged.
|
|
216
|
+
{
|
|
217
|
+
selector: "JSXElement:has(JSXOpeningElement JSXAttribute[name.name='hidden'] > JSXExpressionContainer > :not(Literal)) ~ JSXElement:has(JSXOpeningElement JSXAttribute[name.name='hidden'] > JSXExpressionContainer > :not(Literal))",
|
|
218
|
+
message: "Sibling elements swap visibility with conditional hidden \u2014 neither reserves space for the other, causing layout shift. Use <StateSwap> for two states or <LayoutMap> for multiple states."
|
|
215
219
|
},
|
|
216
220
|
// --- 5. className on firewalled components ---
|
|
217
221
|
...classNameBlocked.length ? [
|
package/dist/eslint.d.cts
CHANGED
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
*
|
|
23
23
|
* 4. Geometric instability — conditional content in JSX children that
|
|
24
24
|
* causes layout shift: ternary swaps, && mounts, || / ?? fallbacks,
|
|
25
|
-
* interpolated template literals, and
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* interpolated template literals, and sibling hidden swaps (two+
|
|
26
|
+
* siblings using conditional hidden to toggle visibility without
|
|
27
|
+
* reserving space for each other). A single hidden={condition} is
|
|
28
|
+
* fine and not flagged. Always on.
|
|
29
29
|
*
|
|
30
30
|
* 5. className on custom components — passing className to a PascalCase
|
|
31
31
|
* component is a presentation leak across the Structure boundary.
|
package/dist/eslint.d.ts
CHANGED
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
*
|
|
23
23
|
* 4. Geometric instability — conditional content in JSX children that
|
|
24
24
|
* causes layout shift: ternary swaps, && mounts, || / ?? fallbacks,
|
|
25
|
-
* interpolated template literals, and
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* interpolated template literals, and sibling hidden swaps (two+
|
|
26
|
+
* siblings using conditional hidden to toggle visibility without
|
|
27
|
+
* reserving space for each other). A single hidden={condition} is
|
|
28
|
+
* fine and not flagged. Always on.
|
|
29
29
|
*
|
|
30
30
|
* 5. className on custom components — passing className to a PascalCase
|
|
31
31
|
* component is a presentation leak across the Structure boundary.
|
package/dist/eslint.js
CHANGED
|
@@ -184,10 +184,14 @@ 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
|
-
// --- 4b.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
// --- 4b. Sibling hidden swap (geometric instability) ---
|
|
188
|
+
// Two sibling elements both using conditional hidden to swap
|
|
189
|
+
// visibility — neither reserves space for the other.
|
|
190
|
+
// A single hidden={condition} (e.g. error message at bottom of
|
|
191
|
+
// layout) is fine and intentionally not flagged.
|
|
192
|
+
{
|
|
193
|
+
selector: "JSXElement:has(JSXOpeningElement JSXAttribute[name.name='hidden'] > JSXExpressionContainer > :not(Literal)) ~ JSXElement:has(JSXOpeningElement JSXAttribute[name.name='hidden'] > JSXExpressionContainer > :not(Literal))",
|
|
194
|
+
message: "Sibling elements swap visibility with conditional hidden \u2014 neither reserves space for the other, causing layout shift. Use <StateSwap> for two states or <LayoutMap> for multiple states."
|
|
191
195
|
},
|
|
192
196
|
// --- 5. className on firewalled components ---
|
|
193
197
|
...classNameBlocked.length ? [
|
package/dist/stylelint.cjs
CHANGED
|
@@ -25,6 +25,7 @@ __export(stylelint_exports, {
|
|
|
25
25
|
module.exports = __toCommonJS(stylelint_exports);
|
|
26
26
|
var pluginRuleName = "stablekit/no-functional-in-utility";
|
|
27
27
|
var descendantColorRuleName = "stablekit/no-descendant-color-in-state";
|
|
28
|
+
var duplicateRulesetRuleName = "stablekit/no-duplicate-ruleset";
|
|
28
29
|
function createFunctionalTokenPlugin(prefixes) {
|
|
29
30
|
const rule = (enabled) => {
|
|
30
31
|
return (root, result) => {
|
|
@@ -51,14 +52,17 @@ function createFunctionalTokenPlugin(prefixes) {
|
|
|
51
52
|
function createDescendantColorPlugin() {
|
|
52
53
|
const dataAttrWithDescendant = /\[data-[^\]]+\]\s+[.#\w]/;
|
|
53
54
|
const colorApplyPattern = /\btext-(?!xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl|left|right|center|justify|wrap|nowrap|ellipsis|clip|truncate)\w/;
|
|
54
|
-
const message = `
|
|
55
|
+
const message = `Visual property on a descendant inside a data-attribute selector. Set visual properties on the [data-*] container and let children inherit via currentColor.`;
|
|
55
56
|
const rule = (enabled) => {
|
|
56
57
|
return (root, result) => {
|
|
57
58
|
if (!enabled) return;
|
|
58
59
|
root.walkRules((ruleNode) => {
|
|
59
60
|
if (!dataAttrWithDescendant.test(ruleNode.selector)) return;
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
const visualProps = /^(color|background|background-color|border-color|outline-color|fill|stroke|opacity|box-shadow|text-shadow)$/;
|
|
62
|
+
ruleNode.walkDecls((decl) => {
|
|
63
|
+
if (visualProps.test(decl.prop)) {
|
|
64
|
+
result.warn(message, { node: decl });
|
|
65
|
+
}
|
|
62
66
|
});
|
|
63
67
|
ruleNode.walkAtRules("apply", (atRule) => {
|
|
64
68
|
if (colorApplyPattern.test(atRule.params)) {
|
|
@@ -73,13 +77,54 @@ function createDescendantColorPlugin() {
|
|
|
73
77
|
rule
|
|
74
78
|
};
|
|
75
79
|
}
|
|
80
|
+
function createDuplicateRulesetPlugin() {
|
|
81
|
+
function directFingerprint(ruleNode) {
|
|
82
|
+
const parts = [];
|
|
83
|
+
for (const child of ruleNode.nodes ?? []) {
|
|
84
|
+
if (child.type === "decl") {
|
|
85
|
+
parts.push(`${child.prop}: ${child.value}`);
|
|
86
|
+
} else if (child.type === "atrule" && child.name === "apply") {
|
|
87
|
+
parts.push(`@apply ${child.params}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return parts;
|
|
91
|
+
}
|
|
92
|
+
const rule = (enabled) => {
|
|
93
|
+
return (root, result) => {
|
|
94
|
+
if (!enabled) return;
|
|
95
|
+
const seen = /* @__PURE__ */ new Map();
|
|
96
|
+
root.walkRules((ruleNode) => {
|
|
97
|
+
if (ruleNode.parent?.type === "atrule" && ruleNode.parent.name === "keyframes") return;
|
|
98
|
+
const parts = directFingerprint(ruleNode);
|
|
99
|
+
if (parts.length < 2) return;
|
|
100
|
+
const fingerprint = parts.sort().join("; ");
|
|
101
|
+
const existing = seen.get(fingerprint);
|
|
102
|
+
if (existing) {
|
|
103
|
+
const sameSelector = existing === ruleNode.selector;
|
|
104
|
+
result.warn(
|
|
105
|
+
sameSelector ? `Redundant rule \u2014 "${ruleNode.selector}" is defined multiple times with identical declarations. Remove the duplicate.
|
|
106
|
+
Declarations: ${fingerprint}` : `Duplicate ruleset \u2014 "${ruleNode.selector}" has identical declarations to "${existing}". Consolidate under a shared class name.
|
|
107
|
+
Declarations: ${fingerprint}`,
|
|
108
|
+
{ node: ruleNode }
|
|
109
|
+
);
|
|
110
|
+
} else {
|
|
111
|
+
seen.set(fingerprint, ruleNode.selector);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
ruleName: duplicateRulesetRuleName,
|
|
118
|
+
rule
|
|
119
|
+
};
|
|
120
|
+
}
|
|
76
121
|
function createStyleLint(options = {}) {
|
|
77
122
|
const {
|
|
78
123
|
ignoreTypes = ["html", "body"],
|
|
79
124
|
functionalTokens = [],
|
|
80
125
|
files = ["src/**/*.css"]
|
|
81
126
|
} = options;
|
|
82
|
-
const plugins = [createDescendantColorPlugin()];
|
|
127
|
+
const plugins = [createDescendantColorPlugin(), createDuplicateRulesetPlugin()];
|
|
83
128
|
if (functionalTokens.length > 0) {
|
|
84
129
|
plugins.push(createFunctionalTokenPlugin(functionalTokens));
|
|
85
130
|
}
|
|
@@ -95,6 +140,8 @@ function createStyleLint(options = {}) {
|
|
|
95
140
|
"declaration-no-important": true,
|
|
96
141
|
// Ban color on descendants inside data-attribute selectors.
|
|
97
142
|
[descendantColorRuleName]: true,
|
|
143
|
+
// Flag selectors with identical declarations for consolidation.
|
|
144
|
+
[duplicateRulesetRuleName]: true,
|
|
98
145
|
// Ban animating layout properties — causes reflow on every frame.
|
|
99
146
|
// Use transform (scaleY, translateY) or opacity instead.
|
|
100
147
|
"declaration-property-value-disallowed-list": [
|
package/dist/stylelint.d.cts
CHANGED
|
@@ -18,13 +18,19 @@
|
|
|
18
18
|
* in scoped selectors like `.badge[data-status="paid"]`, not in
|
|
19
19
|
* utilities that can spread via @apply or className.
|
|
20
20
|
*
|
|
21
|
-
* 4. Don't set
|
|
21
|
+
* 4. Don't set visual properties on descendants inside data-attribute selectors.
|
|
22
22
|
* `.card[data-status="error"] .icon { color: red }` is wrong —
|
|
23
23
|
* set color on the container and let children inherit via currentColor.
|
|
24
|
+
* Also catches background, border-color, opacity, box-shadow, etc.
|
|
24
25
|
*
|
|
25
26
|
* 5. Don't animate layout properties (width, height, margin, padding,
|
|
26
27
|
* top/right/bottom/left). These trigger reflow on every frame.
|
|
27
28
|
* Use transform (scaleY, translateY) or opacity instead.
|
|
29
|
+
*
|
|
30
|
+
* 6. Don't duplicate rulesets — two selectors with byte-identical
|
|
31
|
+
* declarations (after sorting) should be consolidated under one
|
|
32
|
+
* shared class name. The warning includes the matched declarations
|
|
33
|
+
* so the developer can see why they were flagged.
|
|
28
34
|
*/
|
|
29
35
|
interface StyleLintOptions {
|
|
30
36
|
/** Element selectors to allow (e.g. in resets).
|
package/dist/stylelint.d.ts
CHANGED
|
@@ -18,13 +18,19 @@
|
|
|
18
18
|
* in scoped selectors like `.badge[data-status="paid"]`, not in
|
|
19
19
|
* utilities that can spread via @apply or className.
|
|
20
20
|
*
|
|
21
|
-
* 4. Don't set
|
|
21
|
+
* 4. Don't set visual properties on descendants inside data-attribute selectors.
|
|
22
22
|
* `.card[data-status="error"] .icon { color: red }` is wrong —
|
|
23
23
|
* set color on the container and let children inherit via currentColor.
|
|
24
|
+
* Also catches background, border-color, opacity, box-shadow, etc.
|
|
24
25
|
*
|
|
25
26
|
* 5. Don't animate layout properties (width, height, margin, padding,
|
|
26
27
|
* top/right/bottom/left). These trigger reflow on every frame.
|
|
27
28
|
* Use transform (scaleY, translateY) or opacity instead.
|
|
29
|
+
*
|
|
30
|
+
* 6. Don't duplicate rulesets — two selectors with byte-identical
|
|
31
|
+
* declarations (after sorting) should be consolidated under one
|
|
32
|
+
* shared class name. The warning includes the matched declarations
|
|
33
|
+
* so the developer can see why they were flagged.
|
|
28
34
|
*/
|
|
29
35
|
interface StyleLintOptions {
|
|
30
36
|
/** Element selectors to allow (e.g. in resets).
|
package/dist/stylelint.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/stylelint.ts
|
|
2
2
|
var pluginRuleName = "stablekit/no-functional-in-utility";
|
|
3
3
|
var descendantColorRuleName = "stablekit/no-descendant-color-in-state";
|
|
4
|
+
var duplicateRulesetRuleName = "stablekit/no-duplicate-ruleset";
|
|
4
5
|
function createFunctionalTokenPlugin(prefixes) {
|
|
5
6
|
const rule = (enabled) => {
|
|
6
7
|
return (root, result) => {
|
|
@@ -27,14 +28,17 @@ function createFunctionalTokenPlugin(prefixes) {
|
|
|
27
28
|
function createDescendantColorPlugin() {
|
|
28
29
|
const dataAttrWithDescendant = /\[data-[^\]]+\]\s+[.#\w]/;
|
|
29
30
|
const colorApplyPattern = /\btext-(?!xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl|left|right|center|justify|wrap|nowrap|ellipsis|clip|truncate)\w/;
|
|
30
|
-
const message = `
|
|
31
|
+
const message = `Visual property on a descendant inside a data-attribute selector. Set visual properties on the [data-*] container and let children inherit via currentColor.`;
|
|
31
32
|
const rule = (enabled) => {
|
|
32
33
|
return (root, result) => {
|
|
33
34
|
if (!enabled) return;
|
|
34
35
|
root.walkRules((ruleNode) => {
|
|
35
36
|
if (!dataAttrWithDescendant.test(ruleNode.selector)) return;
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
const visualProps = /^(color|background|background-color|border-color|outline-color|fill|stroke|opacity|box-shadow|text-shadow)$/;
|
|
38
|
+
ruleNode.walkDecls((decl) => {
|
|
39
|
+
if (visualProps.test(decl.prop)) {
|
|
40
|
+
result.warn(message, { node: decl });
|
|
41
|
+
}
|
|
38
42
|
});
|
|
39
43
|
ruleNode.walkAtRules("apply", (atRule) => {
|
|
40
44
|
if (colorApplyPattern.test(atRule.params)) {
|
|
@@ -49,13 +53,54 @@ function createDescendantColorPlugin() {
|
|
|
49
53
|
rule
|
|
50
54
|
};
|
|
51
55
|
}
|
|
56
|
+
function createDuplicateRulesetPlugin() {
|
|
57
|
+
function directFingerprint(ruleNode) {
|
|
58
|
+
const parts = [];
|
|
59
|
+
for (const child of ruleNode.nodes ?? []) {
|
|
60
|
+
if (child.type === "decl") {
|
|
61
|
+
parts.push(`${child.prop}: ${child.value}`);
|
|
62
|
+
} else if (child.type === "atrule" && child.name === "apply") {
|
|
63
|
+
parts.push(`@apply ${child.params}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return parts;
|
|
67
|
+
}
|
|
68
|
+
const rule = (enabled) => {
|
|
69
|
+
return (root, result) => {
|
|
70
|
+
if (!enabled) return;
|
|
71
|
+
const seen = /* @__PURE__ */ new Map();
|
|
72
|
+
root.walkRules((ruleNode) => {
|
|
73
|
+
if (ruleNode.parent?.type === "atrule" && ruleNode.parent.name === "keyframes") return;
|
|
74
|
+
const parts = directFingerprint(ruleNode);
|
|
75
|
+
if (parts.length < 2) return;
|
|
76
|
+
const fingerprint = parts.sort().join("; ");
|
|
77
|
+
const existing = seen.get(fingerprint);
|
|
78
|
+
if (existing) {
|
|
79
|
+
const sameSelector = existing === ruleNode.selector;
|
|
80
|
+
result.warn(
|
|
81
|
+
sameSelector ? `Redundant rule \u2014 "${ruleNode.selector}" is defined multiple times with identical declarations. Remove the duplicate.
|
|
82
|
+
Declarations: ${fingerprint}` : `Duplicate ruleset \u2014 "${ruleNode.selector}" has identical declarations to "${existing}". Consolidate under a shared class name.
|
|
83
|
+
Declarations: ${fingerprint}`,
|
|
84
|
+
{ node: ruleNode }
|
|
85
|
+
);
|
|
86
|
+
} else {
|
|
87
|
+
seen.set(fingerprint, ruleNode.selector);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
ruleName: duplicateRulesetRuleName,
|
|
94
|
+
rule
|
|
95
|
+
};
|
|
96
|
+
}
|
|
52
97
|
function createStyleLint(options = {}) {
|
|
53
98
|
const {
|
|
54
99
|
ignoreTypes = ["html", "body"],
|
|
55
100
|
functionalTokens = [],
|
|
56
101
|
files = ["src/**/*.css"]
|
|
57
102
|
} = options;
|
|
58
|
-
const plugins = [createDescendantColorPlugin()];
|
|
103
|
+
const plugins = [createDescendantColorPlugin(), createDuplicateRulesetPlugin()];
|
|
59
104
|
if (functionalTokens.length > 0) {
|
|
60
105
|
plugins.push(createFunctionalTokenPlugin(functionalTokens));
|
|
61
106
|
}
|
|
@@ -71,6 +116,8 @@ function createStyleLint(options = {}) {
|
|
|
71
116
|
"declaration-no-important": true,
|
|
72
117
|
// Ban color on descendants inside data-attribute selectors.
|
|
73
118
|
[descendantColorRuleName]: true,
|
|
119
|
+
// Flag selectors with identical declarations for consolidation.
|
|
120
|
+
[duplicateRulesetRuleName]: true,
|
|
74
121
|
// Ban animating layout properties — causes reflow on every frame.
|
|
75
122
|
// Use transform (scaleY, translateY) or opacity instead.
|
|
76
123
|
"declaration-property-value-disallowed-list": [
|
package/package.json
CHANGED