@nadicodeai/ui 0.19.2 → 0.21.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 (69) hide show
  1. package/AGENTS.md +3 -1
  2. package/README.md +0 -2
  3. package/dist/components/brand-icons/apple.d.ts +18 -0
  4. package/dist/components/brand-icons/apple.d.ts.map +1 -0
  5. package/dist/components/brand-icons/apple.js +22 -0
  6. package/dist/components/brand-icons/brand-icon.d.ts +22 -0
  7. package/dist/components/brand-icons/brand-icon.d.ts.map +1 -0
  8. package/dist/components/brand-icons/brand-icon.js +26 -0
  9. package/dist/components/brand-icons/linux.d.ts +18 -0
  10. package/dist/components/brand-icons/linux.d.ts.map +1 -0
  11. package/dist/components/brand-icons/linux.js +25 -0
  12. package/dist/components/brand-icons/windows.d.ts +18 -0
  13. package/dist/components/brand-icons/windows.d.ts.map +1 -0
  14. package/dist/components/brand-icons/windows.js +22 -0
  15. package/dist/components/brand-icons.d.ts +5 -0
  16. package/dist/components/brand-icons.d.ts.map +1 -0
  17. package/dist/components/brand-icons.js +10 -0
  18. package/dist/components/button.js +1 -1
  19. package/dist/components/card-marketing.js +2 -2
  20. package/dist/components/card.d.ts +4 -3
  21. package/dist/components/card.d.ts.map +1 -1
  22. package/dist/components/card.js +12 -2
  23. package/dist/components/chart-breakdown-table.js +1 -1
  24. package/dist/components/chart-bullet-bar.d.ts +18 -2
  25. package/dist/components/chart-bullet-bar.d.ts.map +1 -1
  26. package/dist/components/chart-bullet-bar.js +62 -9
  27. package/dist/components/chart-date-range-control.d.ts +11 -1
  28. package/dist/components/chart-date-range-control.d.ts.map +1 -1
  29. package/dist/components/chart-date-range-control.js +13 -3
  30. package/dist/components/chart-entity-colors.d.ts +7 -1
  31. package/dist/components/chart-entity-colors.d.ts.map +1 -1
  32. package/dist/components/chart-entity-colors.js +7 -1
  33. package/dist/components/chart-kpi-stat.d.ts.map +1 -1
  34. package/dist/components/chart-kpi-stat.js +1 -1
  35. package/dist/components/chart-time-series.d.ts.map +1 -1
  36. package/dist/components/chart-time-series.js +62 -4
  37. package/dist/components/chart.js +2 -2
  38. package/dist/components/code-editor-mockup.js +1 -1
  39. package/dist/components/nav-bar.js +1 -1
  40. package/dist/components/radio-group.d.ts +1 -1
  41. package/dist/components/radio-group.d.ts.map +1 -1
  42. package/dist/components/radio-group.js +2 -2
  43. package/dist/components/theme-mode-switcher.d.ts +10 -3
  44. package/dist/components/theme-mode-switcher.d.ts.map +1 -1
  45. package/dist/components/theme-mode-switcher.js +14 -7
  46. package/dist/eslint/index.js +91 -0
  47. package/dist/eslint/rules/jsx-class-utility.js +231 -0
  48. package/dist/eslint/rules/no-deprecated-zod-api.js +156 -0
  49. package/dist/eslint/rules/no-design-system-src-import.js +11 -0
  50. package/dist/eslint/rules/no-invalid-token-utility.js +46 -0
  51. package/dist/eslint/rules/no-native-visible-control.js +86 -0
  52. package/dist/eslint/rules/no-raw-color-utility.js +109 -0
  53. package/dist/eslint/rules/no-raw-focus-ring-width.js +30 -0
  54. package/dist/eslint/rules/no-raw-logo-import.js +41 -0
  55. package/dist/eslint/rules/no-shadcn-appearance-override.js +191 -0
  56. package/dist/eslint/rules/no-token-category-bypass.js +53 -0
  57. package/dist/eslint/rules/no-ui-src-import.js +10 -0
  58. package/dist/eslint/rules/no-unpinned-vercel-functions-import.js +100 -0
  59. package/dist/eslint/rules/src-import-boundary.js +207 -0
  60. package/dist/index.d.ts +1 -0
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +1 -0
  63. package/docs/agents/nadicodeai-ui.md +0 -2
  64. package/docs/consuming-cross-repo.md +31 -3
  65. package/docs/contract.md +11 -1
  66. package/llms.txt +0 -1
  67. package/package.json +9 -3
  68. package/skills/nadicodeai-ui/SKILL.md +1 -1
  69. package/docs/migration/nadia-consumer-readiness.md +0 -180
@@ -0,0 +1,231 @@
1
+ const PACKAGE_PRIMITIVE_PATH = "@nadicodeai/ui/src/";
2
+
3
+ export function isPackageOwnedPrimitive(filename) {
4
+ return filename.replaceAll("\\", "/").includes(PACKAGE_PRIMITIVE_PATH);
5
+ }
6
+
7
+ function variableInitializer(node, context) {
8
+ let scope = context.sourceCode.getScope(node);
9
+ while (scope) {
10
+ const variable = scope.set.get(node.name);
11
+ const definition = variable?.defs.find(
12
+ (candidate) => candidate.type === "Variable" && candidate.node?.type === "VariableDeclarator",
13
+ );
14
+ if (definition?.node.init) return definition.node.init;
15
+ scope = scope.upper;
16
+ }
17
+ return undefined;
18
+ }
19
+
20
+ function collectStaticClassFragments(
21
+ node,
22
+ fragments,
23
+ context,
24
+ seen = new Set(),
25
+ includeObjectKeys = true,
26
+ ) {
27
+ if (!node) return;
28
+
29
+ if (seen.has(node)) return;
30
+ seen.add(node);
31
+
32
+ if (node.type === "Literal" && typeof node.value === "string") {
33
+ fragments.push(node.value);
34
+ return;
35
+ }
36
+ if (node.type === "JSXExpressionContainer" || node.type === "ChainExpression") {
37
+ collectStaticClassFragments(node.expression, fragments, context, seen, includeObjectKeys);
38
+ return;
39
+ }
40
+ if (node.type === "TemplateLiteral") {
41
+ for (const quasi of node.quasis) fragments.push(quasi.value.cooked ?? quasi.value.raw);
42
+ for (const expression of node.expressions) {
43
+ collectStaticClassFragments(expression, fragments, context, seen, includeObjectKeys);
44
+ }
45
+ return;
46
+ }
47
+ if (node.type === "TaggedTemplateExpression") {
48
+ collectStaticClassFragments(node.quasi, fragments, context, seen, includeObjectKeys);
49
+ return;
50
+ }
51
+ if (node.type === "ConditionalExpression") {
52
+ collectStaticClassFragments(node.consequent, fragments, context, seen, includeObjectKeys);
53
+ collectStaticClassFragments(node.alternate, fragments, context, seen, includeObjectKeys);
54
+ return;
55
+ }
56
+ if (node.type === "LogicalExpression" || node.type === "BinaryExpression") {
57
+ collectStaticClassFragments(node.left, fragments, context, seen, includeObjectKeys);
58
+ collectStaticClassFragments(node.right, fragments, context, seen, includeObjectKeys);
59
+ return;
60
+ }
61
+ if (node.type === "CallExpression" || node.type === "NewExpression") {
62
+ for (const argument of node.arguments) {
63
+ collectStaticClassFragments(
64
+ argument.type === "SpreadElement" ? argument.argument : argument,
65
+ fragments,
66
+ context,
67
+ seen,
68
+ includeObjectKeys,
69
+ );
70
+ }
71
+ return;
72
+ }
73
+ if (node.type === "ArrayExpression" || node.type === "SequenceExpression") {
74
+ for (const element of node.elements ?? node.expressions) {
75
+ collectStaticClassFragments(
76
+ element?.type === "SpreadElement" ? element.argument : element,
77
+ fragments,
78
+ context,
79
+ seen,
80
+ includeObjectKeys,
81
+ );
82
+ }
83
+ return;
84
+ }
85
+ if (node.type === "ObjectExpression") {
86
+ for (const property of node.properties) {
87
+ if (property.type === "SpreadElement") {
88
+ collectStaticClassFragments(property.argument, fragments, context, seen, includeObjectKeys);
89
+ continue;
90
+ }
91
+ if (includeObjectKeys && !property.computed) {
92
+ collectStaticClassFragments(property.key, fragments, context, seen, includeObjectKeys);
93
+ }
94
+ collectStaticClassFragments(property.value, fragments, context, seen, includeObjectKeys);
95
+ }
96
+ return;
97
+ }
98
+ if (node.type === "Identifier") {
99
+ collectStaticClassFragments(
100
+ variableInitializer(node, context),
101
+ fragments,
102
+ context,
103
+ seen,
104
+ includeObjectKeys,
105
+ );
106
+ return;
107
+ }
108
+ if (node.type === "MemberExpression") {
109
+ // A dynamic lookup can select any value in a local class table. Resolve the
110
+ // entire object so no palette entry can hide behind `classes[current]`.
111
+ collectStaticClassFragments(node.object, fragments, context, seen, includeObjectKeys);
112
+ return;
113
+ }
114
+ if (
115
+ node.type === "TSAsExpression" ||
116
+ node.type === "TSSatisfiesExpression" ||
117
+ node.type === "TSNonNullExpression" ||
118
+ node.type === "TypeCastExpression"
119
+ ) {
120
+ collectStaticClassFragments(node.expression, fragments, context, seen, includeObjectKeys);
121
+ }
122
+ }
123
+
124
+ function staticPropertyName(property) {
125
+ if (property.key.type === "Identifier" && !property.computed) return property.key.name;
126
+ if (property.key.type === "Literal" && typeof property.key.value === "string") {
127
+ return property.key.value;
128
+ }
129
+ return undefined;
130
+ }
131
+
132
+ function collectStaticSpreadClassFragments(node, fragments, context, seen = new Set()) {
133
+ if (!node || seen.has(node)) return;
134
+ seen.add(node);
135
+
136
+ if (node.type === "Identifier") {
137
+ collectStaticSpreadClassFragments(variableInitializer(node, context), fragments, context, seen);
138
+ return;
139
+ }
140
+ if (node.type === "ObjectExpression") {
141
+ for (const property of node.properties) {
142
+ if (property.type === "SpreadElement") {
143
+ collectStaticSpreadClassFragments(property.argument, fragments, context, seen);
144
+ continue;
145
+ }
146
+ if (staticPropertyName(property) === "className") {
147
+ collectStaticClassFragments(property.value, fragments, context, seen);
148
+ }
149
+ }
150
+ return;
151
+ }
152
+ if (node.type === "ConditionalExpression") {
153
+ collectStaticSpreadClassFragments(node.consequent, fragments, context, seen);
154
+ collectStaticSpreadClassFragments(node.alternate, fragments, context, seen);
155
+ return;
156
+ }
157
+ if (node.type === "LogicalExpression") {
158
+ collectStaticSpreadClassFragments(node.left, fragments, context, seen);
159
+ collectStaticSpreadClassFragments(node.right, fragments, context, seen);
160
+ return;
161
+ }
162
+ if (
163
+ node.type === "TSAsExpression" ||
164
+ node.type === "TSSatisfiesExpression" ||
165
+ node.type === "TSNonNullExpression" ||
166
+ node.type === "TypeCastExpression"
167
+ ) {
168
+ collectStaticSpreadClassFragments(node.expression, fragments, context, seen);
169
+ }
170
+ }
171
+
172
+ export function classUtilities(attribute, context) {
173
+ if (attribute?.type !== "JSXAttribute" || attribute.name.name !== "className") return [];
174
+ return staticClassUtilities(attribute.value, context);
175
+ }
176
+
177
+ export function staticStringFragments(node, context) {
178
+ const fragments = [];
179
+ collectStaticClassFragments(node, fragments, context);
180
+ return fragments;
181
+ }
182
+
183
+ function staticClassUtilities(node, context) {
184
+ return staticStringFragments(node, context).flatMap((fragment) =>
185
+ fragment.split(/\s+/).filter(Boolean),
186
+ );
187
+ }
188
+
189
+ function staticValueClassUtilities(node, context) {
190
+ const fragments = [];
191
+ collectStaticClassFragments(node, fragments, context, new Set(), false);
192
+ return fragments.flatMap((fragment) => fragment.split(/\s+/).filter(Boolean));
193
+ }
194
+
195
+ export function tokenUtilityVisitors(context, onUtility) {
196
+ const cvaNames = new Set();
197
+
198
+ function visitUtilities(node, utilities) {
199
+ for (const utility of utilities) onUtility(node, utility);
200
+ }
201
+
202
+ return {
203
+ ImportDeclaration(node) {
204
+ if (node.source.value !== "class-variance-authority") return;
205
+ for (const specifier of node.specifiers) {
206
+ if (specifier.type === "ImportSpecifier" && specifier.imported.name === "cva") {
207
+ cvaNames.add(specifier.local.name);
208
+ }
209
+ }
210
+ },
211
+ JSXAttribute(node) {
212
+ visitUtilities(node, classUtilities(node, context));
213
+ },
214
+ CallExpression(node) {
215
+ if (node.callee.type !== "Identifier" || !cvaNames.has(node.callee.name)) return;
216
+ visitUtilities(node, staticValueClassUtilities(node, context));
217
+ },
218
+ };
219
+ }
220
+
221
+ export function openingElementClassUtilities(node, context) {
222
+ const fragments = [];
223
+ for (const attribute of node.attributes) {
224
+ if (attribute.type === "JSXAttribute" && attribute.name.name === "className") {
225
+ fragments.push(...staticStringFragments(attribute.value, context));
226
+ } else if (attribute.type === "JSXSpreadAttribute") {
227
+ collectStaticSpreadClassFragments(attribute.argument, fragments, context);
228
+ }
229
+ }
230
+ return fragments.flatMap((fragment) => fragment.split(/\s+/).filter(Boolean));
231
+ }
@@ -0,0 +1,156 @@
1
+ const LEGACY_FACTORIES = new Map([
2
+ ["nativeEnum", "z.enum()"],
3
+ ["promise", "await the value before parsing it"],
4
+ ]);
5
+
6
+ const LEGACY_METHODS = {
7
+ number: new Map([
8
+ ["finite", "remove it; z.number() already rejects infinite values"],
9
+ ["safe", ".int()"],
10
+ ["step", ".multipleOf()"],
11
+ ]),
12
+ object: new Map([
13
+ ["merge", ".extend(other.shape)"],
14
+ ["passthrough", "z.looseObject()"],
15
+ ["strict", "z.strictObject()"],
16
+ ["strip", "z.object(), which strips unknown keys by default"],
17
+ ]),
18
+ string: new Map(
19
+ [
20
+ "base64",
21
+ "base64url",
22
+ "cuid",
23
+ "cuid2",
24
+ "e164",
25
+ "email",
26
+ "emoji",
27
+ "guid",
28
+ "ipv4",
29
+ "ipv6",
30
+ "jwt",
31
+ "ksuid",
32
+ "nanoid",
33
+ "ulid",
34
+ "url",
35
+ "uuid",
36
+ "uuidv4",
37
+ "uuidv6",
38
+ "uuidv7",
39
+ "xid",
40
+ ].map((name) => [name, `z.${name.startsWith("uuidv") ? "uuid" : name}()`]),
41
+ ),
42
+ };
43
+
44
+ for (const name of ["date", "datetime", "duration", "time"]) {
45
+ LEGACY_METHODS.string.set(name, `z.iso.${name}()`);
46
+ }
47
+ for (const name of ["cidrv4", "cidrv6"]) {
48
+ LEGACY_METHODS.string.set(name, `z.${name}()`);
49
+ }
50
+
51
+ function memberName(node) {
52
+ if (!node.computed && node.property.type === "Identifier") {
53
+ return node.property.name;
54
+ }
55
+ if (node.computed && node.property.type === "Literal") {
56
+ return typeof node.property.value === "string" ? node.property.value : undefined;
57
+ }
58
+ return undefined;
59
+ }
60
+
61
+ export const noDeprecatedZodApi = {
62
+ meta: {
63
+ type: "problem",
64
+ docs: {
65
+ description: "Disallow deprecated and legacy Zod APIs when a Zod 4 replacement exists",
66
+ },
67
+ schema: [],
68
+ messages: {
69
+ legacyFactory: "{{api}} is deprecated in Zod 4; use {{replacement}}.",
70
+ legacyMethod: "{{api}} is deprecated or legacy in Zod 4; use {{replacement}}.",
71
+ },
72
+ },
73
+
74
+ create(context) {
75
+ const sourceCode = context.sourceCode;
76
+ const zodBindings = new Set();
77
+
78
+ function isZodBinding(identifier) {
79
+ for (let scope = sourceCode.getScope(identifier); scope; scope = scope.upper) {
80
+ const variable = scope.set.get(identifier.name);
81
+ if (variable) {
82
+ return zodBindings.has(variable);
83
+ }
84
+ }
85
+ return false;
86
+ }
87
+
88
+ function rootFactory(call) {
89
+ if (call.type !== "CallExpression" || call.callee.type !== "MemberExpression") {
90
+ return undefined;
91
+ }
92
+ const factory = memberName(call.callee);
93
+ if (!factory) {
94
+ return undefined;
95
+ }
96
+ if (call.callee.object.type === "Identifier" && isZodBinding(call.callee.object)) {
97
+ return factory;
98
+ }
99
+ return rootFactory(call.callee.object);
100
+ }
101
+
102
+ return {
103
+ ImportDeclaration(node) {
104
+ if (node.source.value !== "zod") {
105
+ return;
106
+ }
107
+ for (const specifier of node.specifiers) {
108
+ const importsNamespace = specifier.type === "ImportNamespaceSpecifier";
109
+ const importsZ =
110
+ specifier.type === "ImportSpecifier" &&
111
+ ((specifier.imported.type === "Identifier" && specifier.imported.name === "z") ||
112
+ (specifier.imported.type === "Literal" && specifier.imported.value === "z"));
113
+ if (!importsNamespace && !importsZ) {
114
+ continue;
115
+ }
116
+ for (const variable of sourceCode.getDeclaredVariables(specifier)) {
117
+ zodBindings.add(variable);
118
+ }
119
+ }
120
+ },
121
+
122
+ CallExpression(node) {
123
+ if (node.callee.type !== "MemberExpression") {
124
+ return;
125
+ }
126
+ const method = memberName(node.callee);
127
+ if (!method) {
128
+ return;
129
+ }
130
+
131
+ if (node.callee.object.type === "Identifier" && isZodBinding(node.callee.object)) {
132
+ const replacement = LEGACY_FACTORIES.get(method);
133
+ if (replacement) {
134
+ context.report({
135
+ node: node.callee.property,
136
+ messageId: "legacyFactory",
137
+ data: { api: `z.${method}()`, replacement },
138
+ });
139
+ }
140
+ return;
141
+ }
142
+
143
+ const factory = rootFactory(node.callee.object);
144
+ const family = factory === "strictObject" || factory === "looseObject" ? "object" : factory;
145
+ const replacement = LEGACY_METHODS[family]?.get(method);
146
+ if (replacement) {
147
+ context.report({
148
+ node: node.callee.property,
149
+ messageId: "legacyMethod",
150
+ data: { api: `z.${factory}().${method}()`, replacement },
151
+ });
152
+ }
153
+ },
154
+ };
155
+ },
156
+ };
@@ -0,0 +1,11 @@
1
+ import { bannedSourceRootMatcher, createSrcImportRule } from "./src-import-boundary.js";
2
+
3
+ /** @type {import('eslint').Rule.RuleModule} */
4
+ export const noDesignSystemSrcImport = createSrcImportRule({
5
+ description:
6
+ "Disallow deep imports from @nadicodeai/design-system source paths in apps and UI consumers",
7
+ bannedMessage: "Import @nadicodeai/design-system through published exports only, not {{value}}.",
8
+ // The package dir is `design-system/`, so relative paths carry the shorter
9
+ // `design-system/src` marker rather than the scoped specifier.
10
+ isBanned: bannedSourceRootMatcher("design-system/src"),
11
+ });
@@ -0,0 +1,46 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ import { tokenUtilityVisitors } from "./jsx-class-utility.js";
4
+
5
+ const require = createRequire(import.meta.url);
6
+ const generatedTheme = require("@nadicodeai/design-system/tailwind").theme.extend;
7
+ const typographyUtilities = new Set(
8
+ Object.keys(generatedTheme.fontSize).map((name) => `nc-type-${name}`),
9
+ );
10
+ const colorTokens = new Set(Object.keys(generatedTheme.colors));
11
+ const COLOR_UTILITY =
12
+ /^(?:bg|text|border|outline|ring|fill|stroke|decoration|accent|caret|divide|from|via|to|shadow)-nc-([^/]+)(?:\/.*)?$/;
13
+
14
+ function withoutVariants(utility) {
15
+ return utility.slice(utility.lastIndexOf(":") + 1);
16
+ }
17
+
18
+ function isInvalidGeneratedUtility(utility) {
19
+ const unqualified = withoutVariants(utility);
20
+ if (unqualified.startsWith("nc-type-")) {
21
+ return !typographyUtilities.has(unqualified);
22
+ }
23
+
24
+ const color = COLOR_UTILITY.exec(unqualified);
25
+ return color ? !colorTokens.has(color[1]) : false;
26
+ }
27
+
28
+ export const noInvalidTokenUtility = {
29
+ meta: {
30
+ type: "problem",
31
+ docs: {
32
+ description: "Disallow token utilities that are absent from generated design-system output",
33
+ },
34
+ schema: [],
35
+ messages: {
36
+ invalidTokenUtility:
37
+ "{{utility}} is not generated by the installed NadicodeAI design system. Use a utility from @nadicodeai/design-system/tailwind.",
38
+ },
39
+ },
40
+ create(context) {
41
+ return tokenUtilityVisitors(context, (node, utility) => {
42
+ if (!isInvalidGeneratedUtility(utility)) return;
43
+ context.report({ node, messageId: "invalidTokenUtility", data: { utility } });
44
+ });
45
+ },
46
+ };
@@ -0,0 +1,86 @@
1
+ import { isPackageOwnedPrimitive } from "./jsx-class-utility.js";
2
+
3
+ const NATIVE_CONTROLS = new Set(["button", "input", "select", "textarea"]);
4
+
5
+ function staticValue(node) {
6
+ if (node?.type === "Literal") {
7
+ return node.value;
8
+ }
9
+ if (node?.type === "JSXExpressionContainer") {
10
+ return staticValue(node.expression);
11
+ }
12
+ if (node?.type === "TemplateLiteral" && node.expressions.length === 0) {
13
+ return node.quasis[0]?.value.cooked;
14
+ }
15
+ return undefined;
16
+ }
17
+
18
+ function hiddenInput(node) {
19
+ const typeAttribute = node.attributes.find(
20
+ (attribute) => attribute.type === "JSXAttribute" && attribute.name.name === "type",
21
+ );
22
+ return staticValue(typeAttribute?.value) === "hidden";
23
+ }
24
+
25
+ function isPrimitiveRenderElement(node, packagePrimitiveNames) {
26
+ const expression = node.parent;
27
+ const attribute = expression?.parent;
28
+ const openingElement = attribute?.parent;
29
+ return (
30
+ expression?.type === "JSXExpressionContainer" &&
31
+ attribute?.type === "JSXAttribute" &&
32
+ attribute.name.name === "render" &&
33
+ openingElement?.type === "JSXOpeningElement" &&
34
+ openingElement.name.type === "JSXIdentifier" &&
35
+ packagePrimitiveNames.has(openingElement.name.name)
36
+ );
37
+ }
38
+
39
+ export const noNativeVisibleControl = {
40
+ meta: {
41
+ type: "problem",
42
+ docs: {
43
+ description: "Disallow visible native controls in app source; use @nadicodeai/ui primitives",
44
+ },
45
+ schema: [],
46
+ messages: {
47
+ nativeVisibleControl:
48
+ "Use the @nadicodeai/ui primitive instead of native visible <{{element}}>.",
49
+ },
50
+ },
51
+ create(context) {
52
+ if (isPackageOwnedPrimitive(context.filename)) {
53
+ return {};
54
+ }
55
+ const packagePrimitiveNames = new Set();
56
+ return {
57
+ ImportDeclaration(node) {
58
+ if (
59
+ typeof node.source.value !== "string" ||
60
+ !node.source.value.startsWith("@nadicodeai/ui")
61
+ ) {
62
+ return;
63
+ }
64
+ for (const specifier of node.specifiers) {
65
+ packagePrimitiveNames.add(specifier.local.name);
66
+ }
67
+ },
68
+ JSXOpeningElement(node) {
69
+ if (node.name.type !== "JSXIdentifier" || !NATIVE_CONTROLS.has(node.name.name)) {
70
+ return;
71
+ }
72
+ if (node.name.name === "input" && hiddenInput(node)) {
73
+ return;
74
+ }
75
+ if (isPrimitiveRenderElement(node.parent, packagePrimitiveNames)) {
76
+ return;
77
+ }
78
+ context.report({
79
+ node: node.name,
80
+ messageId: "nativeVisibleControl",
81
+ data: { element: node.name.name },
82
+ });
83
+ },
84
+ };
85
+ },
86
+ };
@@ -0,0 +1,109 @@
1
+ import {
2
+ classUtilities,
3
+ isPackageOwnedPrimitive,
4
+ staticStringFragments,
5
+ } from "./jsx-class-utility.js";
6
+
7
+ const COLOR_NAMES = new Set([
8
+ "black",
9
+ "white",
10
+ "slate",
11
+ "gray",
12
+ "zinc",
13
+ "neutral",
14
+ "stone",
15
+ "red",
16
+ "orange",
17
+ "amber",
18
+ "yellow",
19
+ "lime",
20
+ "green",
21
+ "emerald",
22
+ "teal",
23
+ "cyan",
24
+ "sky",
25
+ "blue",
26
+ "indigo",
27
+ "violet",
28
+ "purple",
29
+ "fuchsia",
30
+ "pink",
31
+ "rose",
32
+ "nc",
33
+ ]);
34
+ const COLOR_UTILITY =
35
+ /^(?:bg|text|border|outline|ring|fill|stroke|decoration|accent|caret|divide|from|via|to|shadow)-(\[.+\]|[^/]+)(?:\/.*)?$/;
36
+ const COLOR_VALUE_PROPERTIES = new Set([
37
+ "accentColor",
38
+ "background",
39
+ "backgroundColor",
40
+ "borderColor",
41
+ "caretColor",
42
+ "color",
43
+ "fill",
44
+ "outlineColor",
45
+ "stopColor",
46
+ "stroke",
47
+ "textDecorationColor",
48
+ ]);
49
+
50
+ function withoutVariants(utility) {
51
+ return utility.slice(utility.lastIndexOf(":") + 1);
52
+ }
53
+
54
+ function isRawColorUtility(utility) {
55
+ const match = COLOR_UTILITY.exec(withoutVariants(utility));
56
+ if (!match) {
57
+ return false;
58
+ }
59
+ const color = match[1];
60
+ if (color.startsWith("[")) {
61
+ return (
62
+ /var\(--nc-/i.test(color) ||
63
+ /^\[(?:#|(?:rgba?|hsla?|oklch|oklab|hwb|color-mix)\(|(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|black|white)\])/i.test(
64
+ color,
65
+ )
66
+ );
67
+ }
68
+ return COLOR_NAMES.has(color.split("-")[0]);
69
+ }
70
+
71
+ function propertyName(node) {
72
+ if (!node || node.computed) return undefined;
73
+ if (node.key.type === "Identifier") return node.key.name;
74
+ if (node.key.type === "Literal" && typeof node.key.value === "string") return node.key.value;
75
+ return undefined;
76
+ }
77
+
78
+ export const noRawColorUtility = {
79
+ meta: {
80
+ type: "problem",
81
+ docs: {
82
+ description:
83
+ "Disallow raw Tailwind colors and canonical color values in app source; use semantic design tokens",
84
+ },
85
+ schema: [],
86
+ messages: {
87
+ rawColorUtility: "Use a semantic color role instead of raw color value {{utility}}.",
88
+ },
89
+ },
90
+ create(context) {
91
+ if (isPackageOwnedPrimitive(context.filename)) return {};
92
+
93
+ return {
94
+ JSXAttribute(node) {
95
+ for (const utility of classUtilities(node, context)) {
96
+ if (!isRawColorUtility(utility)) continue;
97
+ context.report({ node, messageId: "rawColorUtility", data: { utility } });
98
+ }
99
+ },
100
+ Property(node) {
101
+ if (!COLOR_VALUE_PROPERTIES.has(propertyName(node))) return;
102
+ for (const value of staticStringFragments(node.value, context)) {
103
+ if (!/var\(--nc-/i.test(value)) continue;
104
+ context.report({ node, messageId: "rawColorUtility", data: { utility: value } });
105
+ }
106
+ },
107
+ };
108
+ },
109
+ };
@@ -0,0 +1,30 @@
1
+ import { classUtilities } from "./jsx-class-utility.js";
2
+
3
+ function isRawFocusRingWidth(utility) {
4
+ const base = utility.slice(utility.lastIndexOf(":") + 1);
5
+ return utility.includes("focus") && /^ring-(?:[1-9]\d*|px)$/.test(base);
6
+ }
7
+
8
+ export const noRawFocusRingWidth = {
9
+ meta: {
10
+ type: "problem",
11
+ docs: {
12
+ description: "Require the design-system focus-ring width on every focus surface",
13
+ },
14
+ schema: [],
15
+ messages: {
16
+ rawFocusRingWidth:
17
+ "Use ring-[length:var(--ring-width-focus)] instead of raw focus-ring width {{utility}}.",
18
+ },
19
+ },
20
+ create(context) {
21
+ return {
22
+ JSXAttribute(node) {
23
+ for (const utility of classUtilities(node, context)) {
24
+ if (!isRawFocusRingWidth(utility)) continue;
25
+ context.report({ node, messageId: "rawFocusRingWidth", data: { utility } });
26
+ }
27
+ },
28
+ };
29
+ },
30
+ };