@nadicodeai/ui 0.20.0 → 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 (64) hide show
  1. package/AGENTS.md +2 -0
  2. package/dist/components/brand-icons/apple.d.ts +18 -0
  3. package/dist/components/brand-icons/apple.d.ts.map +1 -0
  4. package/dist/components/brand-icons/apple.js +22 -0
  5. package/dist/components/brand-icons/brand-icon.d.ts +22 -0
  6. package/dist/components/brand-icons/brand-icon.d.ts.map +1 -0
  7. package/dist/components/brand-icons/brand-icon.js +26 -0
  8. package/dist/components/brand-icons/linux.d.ts +18 -0
  9. package/dist/components/brand-icons/linux.d.ts.map +1 -0
  10. package/dist/components/brand-icons/linux.js +25 -0
  11. package/dist/components/brand-icons/windows.d.ts +18 -0
  12. package/dist/components/brand-icons/windows.d.ts.map +1 -0
  13. package/dist/components/brand-icons/windows.js +22 -0
  14. package/dist/components/brand-icons.d.ts +5 -0
  15. package/dist/components/brand-icons.d.ts.map +1 -0
  16. package/dist/components/brand-icons.js +10 -0
  17. package/dist/components/button.js +1 -1
  18. package/dist/components/card-marketing.js +2 -2
  19. package/dist/components/card.d.ts +4 -3
  20. package/dist/components/card.d.ts.map +1 -1
  21. package/dist/components/card.js +12 -2
  22. package/dist/components/chart-breakdown-table.js +1 -1
  23. package/dist/components/chart-bullet-bar.d.ts +18 -2
  24. package/dist/components/chart-bullet-bar.d.ts.map +1 -1
  25. package/dist/components/chart-bullet-bar.js +62 -9
  26. package/dist/components/chart-date-range-control.d.ts +11 -1
  27. package/dist/components/chart-date-range-control.d.ts.map +1 -1
  28. package/dist/components/chart-date-range-control.js +13 -3
  29. package/dist/components/chart-entity-colors.d.ts +7 -1
  30. package/dist/components/chart-entity-colors.d.ts.map +1 -1
  31. package/dist/components/chart-entity-colors.js +7 -1
  32. package/dist/components/chart-kpi-stat.d.ts.map +1 -1
  33. package/dist/components/chart-kpi-stat.js +1 -1
  34. package/dist/components/chart-time-series.d.ts.map +1 -1
  35. package/dist/components/chart-time-series.js +62 -4
  36. package/dist/components/chart.js +2 -2
  37. package/dist/components/code-editor-mockup.js +1 -1
  38. package/dist/components/nav-bar.js +1 -1
  39. package/dist/components/radio-group.d.ts +1 -1
  40. package/dist/components/radio-group.d.ts.map +1 -1
  41. package/dist/components/radio-group.js +2 -2
  42. package/dist/components/theme-mode-switcher.d.ts +10 -3
  43. package/dist/components/theme-mode-switcher.d.ts.map +1 -1
  44. package/dist/components/theme-mode-switcher.js +14 -7
  45. package/dist/eslint/index.js +91 -0
  46. package/dist/eslint/rules/jsx-class-utility.js +231 -0
  47. package/dist/eslint/rules/no-deprecated-zod-api.js +156 -0
  48. package/dist/eslint/rules/no-design-system-src-import.js +11 -0
  49. package/dist/eslint/rules/no-invalid-token-utility.js +46 -0
  50. package/dist/eslint/rules/no-native-visible-control.js +86 -0
  51. package/dist/eslint/rules/no-raw-color-utility.js +109 -0
  52. package/dist/eslint/rules/no-raw-focus-ring-width.js +30 -0
  53. package/dist/eslint/rules/no-raw-logo-import.js +41 -0
  54. package/dist/eslint/rules/no-shadcn-appearance-override.js +191 -0
  55. package/dist/eslint/rules/no-token-category-bypass.js +53 -0
  56. package/dist/eslint/rules/no-ui-src-import.js +10 -0
  57. package/dist/eslint/rules/no-unpinned-vercel-functions-import.js +100 -0
  58. package/dist/eslint/rules/src-import-boundary.js +207 -0
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +1 -0
  62. package/docs/consuming-cross-repo.md +23 -0
  63. package/docs/contract.md +10 -0
  64. package/package.json +9 -3
@@ -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
+ };
@@ -0,0 +1,41 @@
1
+ import { createSrcImportRule } from "./src-import-boundary.js";
2
+
3
+ const BANNED_PATTERNS = [
4
+ "@nadicodeai/design-system/assets/logo-mark",
5
+ "@nadicodeai/design-system/assets/logo-wordmark",
6
+ "@nadicodeai/design-system/assets/logo-geometry",
7
+ "logo-mark.svg",
8
+ "logo-wordmark.svg",
9
+ ];
10
+
11
+ // Only the single brand adapter in @nadicodeai/ui may import the raw logo
12
+ // assets. A bare "/components/brand.tsx" suffix would also whitelist a future
13
+ // website/portal `components/brand.tsx`, letting an app bypass BrandMark —
14
+ // pin the full package-relative path instead.
15
+ const ALLOWED_SUFFIXES = ["@nadicodeai/ui/src/components/brand.tsx"];
16
+
17
+ function isAllowedFile(filename) {
18
+ const normalized = filename.replaceAll("\\", "/");
19
+ return ALLOWED_SUFFIXES.some((suffix) => normalized.endsWith(suffix));
20
+ }
21
+
22
+ function matchesBanned(value) {
23
+ return typeof value === "string" && BANNED_PATTERNS.some((pattern) => value.includes(pattern));
24
+ }
25
+
26
+ const rawLogoImportRule = createSrcImportRule({
27
+ description: "Disallow direct logo asset imports outside the BrandMark/BrandWordmark adapter",
28
+ bannedMessage:
29
+ "Import logo assets only through BrandMark/BrandWordmark from @nadicodeai/ui/components/brand (found {{value}}).",
30
+ isBanned: matchesBanned,
31
+ });
32
+
33
+ export const noRawLogoImport = {
34
+ ...rawLogoImportRule,
35
+ create(context) {
36
+ if (isAllowedFile(context.filename)) {
37
+ return {};
38
+ }
39
+ return rawLogoImportRule.create(context);
40
+ },
41
+ };
@@ -0,0 +1,191 @@
1
+ import {
2
+ classUtilities,
3
+ isPackageOwnedPrimitive,
4
+ openingElementClassUtilities,
5
+ } from "./jsx-class-utility.js";
6
+
7
+ function withoutVariants(utility) {
8
+ return utility.slice(utility.lastIndexOf(":") + 1);
9
+ }
10
+
11
+ function isTypographySize(utility) {
12
+ return /^text-(?:xs|sm|base|lg|xl|[2-9]xl|\[[\d.]+(?:px|rem|em|%|lh)\])$/.test(utility);
13
+ }
14
+
15
+ function isTextLayoutUtility(utility) {
16
+ return /^text-(?:left|center|right|justify|start|end|balance|pretty|clip|ellipsis)$/.test(
17
+ utility,
18
+ );
19
+ }
20
+
21
+ const COLOR_NAMES = new Set([
22
+ "black",
23
+ "white",
24
+ "slate",
25
+ "gray",
26
+ "zinc",
27
+ "neutral",
28
+ "stone",
29
+ "red",
30
+ "orange",
31
+ "amber",
32
+ "yellow",
33
+ "lime",
34
+ "green",
35
+ "emerald",
36
+ "teal",
37
+ "cyan",
38
+ "sky",
39
+ "blue",
40
+ "indigo",
41
+ "violet",
42
+ "purple",
43
+ "fuchsia",
44
+ "pink",
45
+ "rose",
46
+ "nc",
47
+ "background",
48
+ "foreground",
49
+ "card",
50
+ "popover",
51
+ "primary",
52
+ "secondary",
53
+ "muted",
54
+ "accent",
55
+ "destructive",
56
+ "border",
57
+ "input",
58
+ "ring",
59
+ "focus",
60
+ ]);
61
+
62
+ function hasColorValue(utility, prefix) {
63
+ if (!utility.startsWith(prefix)) {
64
+ return false;
65
+ }
66
+ const value = utility.slice(prefix.length).split("/")[0];
67
+ return value.startsWith("[") || COLOR_NAMES.has(value.split("-")[0]);
68
+ }
69
+
70
+ function isBorderMechanic(utility) {
71
+ return /^(?:border(?:-(?:[xy]|[trblse]{1,2}|[0-9]+|px|dashed|dotted|double|hidden|none))?)$/.test(
72
+ utility,
73
+ );
74
+ }
75
+
76
+ function isBorderRemoval(utility) {
77
+ return /^border(?:-[xytrblse]{1,2})?-(?:0|none|\[(?:length:)?0(?:px|rem|em|%)?\])$/.test(utility);
78
+ }
79
+
80
+ function isRingOrOutlineMechanic(utility) {
81
+ return /^(?:ring(?:-(?:[0-9]+|inset|offset(?:-[0-9]+)?))?|outline(?:-(?:none|[0-9]+|offset-[0-9]+))?)$/.test(
82
+ utility,
83
+ );
84
+ }
85
+
86
+ function isAppearanceUtility(utility) {
87
+ const base = withoutVariants(utility);
88
+ return (
89
+ /^appearance-/.test(base) ||
90
+ /^bg-/.test(base) ||
91
+ isBorderRemoval(base) ||
92
+ (base.startsWith("border") && !isBorderMechanic(base) && hasColorValue(base, "border-")) ||
93
+ /^shadow(?:-|$)/.test(base) ||
94
+ /^rounded(?:-|$)/.test(base) ||
95
+ ((base.startsWith("ring") || base.startsWith("outline")) &&
96
+ !isRingOrOutlineMechanic(base) &&
97
+ (hasColorValue(base, "ring-") || hasColorValue(base, "outline-"))) ||
98
+ /^opacity-/.test(base) ||
99
+ (/^text-/.test(base) && !isTypographySize(base) && !isTextLayoutUtility(base))
100
+ );
101
+ }
102
+
103
+ function isProhibitedAppearanceUtility(utility, { cardRoot = false } = {}) {
104
+ const base = withoutVariants(utility);
105
+ return isAppearanceUtility(utility) && (!isBorderRemoval(base) || cardRoot);
106
+ }
107
+
108
+ function targetsSharedSlot(utility) {
109
+ return utility.includes("data-slot") || utility.includes("data-[slot");
110
+ }
111
+
112
+ function targetsCardSlot(utility) {
113
+ return (
114
+ /data-slot=(?:card|["']card["'])(?=\])/.test(utility) ||
115
+ /data-\[slot=(?:card|["']card["'])\]/.test(utility)
116
+ );
117
+ }
118
+
119
+ export const noShadcnAppearanceOverride = {
120
+ meta: {
121
+ type: "problem",
122
+ docs: {
123
+ description:
124
+ "Disallow app callers repainting imported shadcn primitives; compose their structural layout only",
125
+ },
126
+ schema: [],
127
+ messages: {
128
+ appearanceOverride:
129
+ "Do not repaint an imported @nadicodeai/ui primitive with {{utility}}; use its semantic variant or package primitive.",
130
+ },
131
+ },
132
+ create(context) {
133
+ if (isPackageOwnedPrimitive(context.filename)) {
134
+ return {};
135
+ }
136
+
137
+ const sharedPrimitiveNames = new Set();
138
+ const sharedCardRootNames = new Set();
139
+ const isSharedPrimitive = (node) =>
140
+ node?.type === "JSXOpeningElement" &&
141
+ node.name.type === "JSXIdentifier" &&
142
+ sharedPrimitiveNames.has(node.name.name);
143
+ const isSharedCardRoot = (node) =>
144
+ node?.type === "JSXOpeningElement" &&
145
+ node.name.type === "JSXIdentifier" &&
146
+ sharedCardRootNames.has(node.name.name);
147
+ return {
148
+ ImportDeclaration(node) {
149
+ if (
150
+ typeof node.source.value !== "string" ||
151
+ !node.source.value.startsWith("@nadicodeai/ui/components/")
152
+ ) {
153
+ return;
154
+ }
155
+ for (const specifier of node.specifiers) {
156
+ sharedPrimitiveNames.add(specifier.local.name);
157
+ if (
158
+ node.source.value === "@nadicodeai/ui/components/card" &&
159
+ specifier.type === "ImportSpecifier" &&
160
+ specifier.imported.name === "Card"
161
+ ) {
162
+ sharedCardRootNames.add(specifier.local.name);
163
+ }
164
+ }
165
+ },
166
+ JSXOpeningElement(node) {
167
+ if (!isSharedPrimitive(node)) return;
168
+ for (const utility of openingElementClassUtilities(node, context)) {
169
+ if (
170
+ utility &&
171
+ isProhibitedAppearanceUtility(utility, { cardRoot: isSharedCardRoot(node) })
172
+ ) {
173
+ context.report({ node, messageId: "appearanceOverride", data: { utility } });
174
+ }
175
+ }
176
+ },
177
+ JSXAttribute(node) {
178
+ if (isSharedPrimitive(node.parent)) return;
179
+ for (const utility of classUtilities(node, context)) {
180
+ if (
181
+ !targetsSharedSlot(utility) ||
182
+ !isProhibitedAppearanceUtility(utility, { cardRoot: targetsCardSlot(utility) })
183
+ ) {
184
+ continue;
185
+ }
186
+ context.report({ node, messageId: "appearanceOverride", data: { utility } });
187
+ }
188
+ },
189
+ };
190
+ },
191
+ };