stablekit.ts 0.6.0 → 0.6.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.
@@ -26,6 +26,7 @@ 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
28
  var duplicateRulesetRuleName = "stablekit/no-duplicate-ruleset";
29
+ var nearDuplicateRuleName = "stablekit/no-near-duplicate-ruleset";
29
30
  function createFunctionalTokenPlugin(prefixes) {
30
31
  const rule = (enabled) => {
31
32
  return (root, result) => {
@@ -118,13 +119,72 @@ Declarations: ${fingerprint}`,
118
119
  rule
119
120
  };
120
121
  }
122
+ function createNearDuplicatePlugin() {
123
+ function collectDirectDecls(ruleNode) {
124
+ const map = /* @__PURE__ */ new Map();
125
+ for (const child of ruleNode.nodes ?? []) {
126
+ if (child.type === "decl") {
127
+ map.set(child.prop, child.value);
128
+ } else if (child.type === "atrule" && child.name === "apply") {
129
+ map.set(`@apply`, child.params);
130
+ }
131
+ }
132
+ return map;
133
+ }
134
+ const rule = (enabled) => {
135
+ return (root, result) => {
136
+ if (!enabled) return;
137
+ const groups = /* @__PURE__ */ new Map();
138
+ root.walkRules((ruleNode) => {
139
+ if (ruleNode.parent?.type === "atrule" && ruleNode.parent.name === "keyframes") return;
140
+ const propMap = collectDirectDecls(ruleNode);
141
+ if (propMap.size < 2) return;
142
+ const propKey = [...propMap.keys()].sort().join(", ");
143
+ const entry = { selector: ruleNode.selector, propKey, propMap, node: ruleNode };
144
+ const group = groups.get(propKey);
145
+ if (group) {
146
+ for (const existing of group) {
147
+ let diffCount = 0;
148
+ let diffProp = "";
149
+ let diffOld = "";
150
+ let diffNew = "";
151
+ for (const [prop, value] of entry.propMap) {
152
+ const existingValue = existing.propMap.get(prop);
153
+ if (existingValue !== value) {
154
+ diffCount++;
155
+ diffProp = prop;
156
+ diffOld = existingValue ?? "(missing)";
157
+ diffNew = value;
158
+ }
159
+ }
160
+ if (diffCount === 0) continue;
161
+ if (diffCount === 1 && diffProp !== "@apply") {
162
+ result.warn(
163
+ `Near-duplicate ruleset \u2014 "${ruleNode.selector}" differs from "${existing.selector}" only in ${diffProp} (${diffOld} \u2192 ${diffNew}). Consider consolidating to a single class.`,
164
+ { node: ruleNode }
165
+ );
166
+ break;
167
+ }
168
+ }
169
+ group.push(entry);
170
+ } else {
171
+ groups.set(propKey, [entry]);
172
+ }
173
+ });
174
+ };
175
+ };
176
+ return {
177
+ ruleName: nearDuplicateRuleName,
178
+ rule
179
+ };
180
+ }
121
181
  function createStyleLint(options = {}) {
122
182
  const {
123
183
  ignoreTypes = ["html", "body"],
124
184
  functionalTokens = [],
125
185
  files = ["src/**/*.css"]
126
186
  } = options;
127
- const plugins = [createDescendantColorPlugin(), createDuplicateRulesetPlugin()];
187
+ const plugins = [createDescendantColorPlugin(), createDuplicateRulesetPlugin(), createNearDuplicatePlugin()];
128
188
  if (functionalTokens.length > 0) {
129
189
  plugins.push(createFunctionalTokenPlugin(functionalTokens));
130
190
  }
@@ -142,6 +202,8 @@ function createStyleLint(options = {}) {
142
202
  [descendantColorRuleName]: true,
143
203
  // Flag selectors with identical declarations for consolidation.
144
204
  [duplicateRulesetRuleName]: true,
205
+ // Flag near-duplicate rulesets (same props, differ by 1 value).
206
+ [nearDuplicateRuleName]: true,
145
207
  // Ban animating layout properties — causes reflow on every frame.
146
208
  // Use transform (scaleY, translateY) or opacity instead.
147
209
  "declaration-property-value-disallowed-list": [
package/dist/stylelint.js CHANGED
@@ -2,6 +2,7 @@
2
2
  var pluginRuleName = "stablekit/no-functional-in-utility";
3
3
  var descendantColorRuleName = "stablekit/no-descendant-color-in-state";
4
4
  var duplicateRulesetRuleName = "stablekit/no-duplicate-ruleset";
5
+ var nearDuplicateRuleName = "stablekit/no-near-duplicate-ruleset";
5
6
  function createFunctionalTokenPlugin(prefixes) {
6
7
  const rule = (enabled) => {
7
8
  return (root, result) => {
@@ -94,13 +95,72 @@ Declarations: ${fingerprint}`,
94
95
  rule
95
96
  };
96
97
  }
98
+ function createNearDuplicatePlugin() {
99
+ function collectDirectDecls(ruleNode) {
100
+ const map = /* @__PURE__ */ new Map();
101
+ for (const child of ruleNode.nodes ?? []) {
102
+ if (child.type === "decl") {
103
+ map.set(child.prop, child.value);
104
+ } else if (child.type === "atrule" && child.name === "apply") {
105
+ map.set(`@apply`, child.params);
106
+ }
107
+ }
108
+ return map;
109
+ }
110
+ const rule = (enabled) => {
111
+ return (root, result) => {
112
+ if (!enabled) return;
113
+ const groups = /* @__PURE__ */ new Map();
114
+ root.walkRules((ruleNode) => {
115
+ if (ruleNode.parent?.type === "atrule" && ruleNode.parent.name === "keyframes") return;
116
+ const propMap = collectDirectDecls(ruleNode);
117
+ if (propMap.size < 2) return;
118
+ const propKey = [...propMap.keys()].sort().join(", ");
119
+ const entry = { selector: ruleNode.selector, propKey, propMap, node: ruleNode };
120
+ const group = groups.get(propKey);
121
+ if (group) {
122
+ for (const existing of group) {
123
+ let diffCount = 0;
124
+ let diffProp = "";
125
+ let diffOld = "";
126
+ let diffNew = "";
127
+ for (const [prop, value] of entry.propMap) {
128
+ const existingValue = existing.propMap.get(prop);
129
+ if (existingValue !== value) {
130
+ diffCount++;
131
+ diffProp = prop;
132
+ diffOld = existingValue ?? "(missing)";
133
+ diffNew = value;
134
+ }
135
+ }
136
+ if (diffCount === 0) continue;
137
+ if (diffCount === 1 && diffProp !== "@apply") {
138
+ result.warn(
139
+ `Near-duplicate ruleset \u2014 "${ruleNode.selector}" differs from "${existing.selector}" only in ${diffProp} (${diffOld} \u2192 ${diffNew}). Consider consolidating to a single class.`,
140
+ { node: ruleNode }
141
+ );
142
+ break;
143
+ }
144
+ }
145
+ group.push(entry);
146
+ } else {
147
+ groups.set(propKey, [entry]);
148
+ }
149
+ });
150
+ };
151
+ };
152
+ return {
153
+ ruleName: nearDuplicateRuleName,
154
+ rule
155
+ };
156
+ }
97
157
  function createStyleLint(options = {}) {
98
158
  const {
99
159
  ignoreTypes = ["html", "body"],
100
160
  functionalTokens = [],
101
161
  files = ["src/**/*.css"]
102
162
  } = options;
103
- const plugins = [createDescendantColorPlugin(), createDuplicateRulesetPlugin()];
163
+ const plugins = [createDescendantColorPlugin(), createDuplicateRulesetPlugin(), createNearDuplicatePlugin()];
104
164
  if (functionalTokens.length > 0) {
105
165
  plugins.push(createFunctionalTokenPlugin(functionalTokens));
106
166
  }
@@ -118,6 +178,8 @@ function createStyleLint(options = {}) {
118
178
  [descendantColorRuleName]: true,
119
179
  // Flag selectors with identical declarations for consolidation.
120
180
  [duplicateRulesetRuleName]: true,
181
+ // Flag near-duplicate rulesets (same props, differ by 1 value).
182
+ [nearDuplicateRuleName]: true,
121
183
  // Ban animating layout properties — causes reflow on every frame.
122
184
  // Use transform (scaleY, translateY) or opacity instead.
123
185
  "declaration-property-value-disallowed-list": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stablekit.ts",
3
- "version": "0.6.0",
3
+ "version": "0.6.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",