@nadicodeai/ui 1.0.2 → 1.1.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 (133) hide show
  1. package/AGENTS.md +13 -13
  2. package/README.md +1 -1
  3. package/dist/agents.d.ts +8 -8
  4. package/dist/components/accordion.js +1 -1
  5. package/dist/components/action-band.js +1 -1
  6. package/dist/components/agent-card.d.ts +1 -1
  7. package/dist/components/agent-visual.d.ts +1 -1
  8. package/dist/components/alert-dialog.d.ts +1 -1
  9. package/dist/components/automation-section.d.ts +2 -2
  10. package/dist/components/avatar.d.ts.map +1 -1
  11. package/dist/components/avatar.js +3 -1
  12. package/dist/components/badge.js +1 -1
  13. package/dist/components/brand-icons/apple.d.ts +1 -1
  14. package/dist/components/brand-icons/linux.d.ts +1 -1
  15. package/dist/components/brand-icons/windows.d.ts +1 -1
  16. package/dist/components/brand-icons.d.ts +4 -4
  17. package/dist/components/button-group.d.ts +1 -1
  18. package/dist/components/button.js +1 -1
  19. package/dist/components/calendar.d.ts +1 -1
  20. package/dist/components/calendar.js +1 -1
  21. package/dist/components/card.d.ts.map +1 -1
  22. package/dist/components/card.js +2 -4
  23. package/dist/components/carousel.d.ts +1 -1
  24. package/dist/components/carousel.d.ts.map +1 -1
  25. package/dist/components/chart-breakdown-table.d.ts +2 -2
  26. package/dist/components/chart-bullet-bar.js +5 -5
  27. package/dist/components/chart-date-range-control.d.ts +1 -1
  28. package/dist/components/chart-kpi-stat.d.ts +1 -1
  29. package/dist/components/chart-time-series.d.ts +1 -1
  30. package/dist/components/chart-time-series.d.ts.map +1 -1
  31. package/dist/components/chart-time-series.js +10 -9
  32. package/dist/components/chart.d.ts.map +1 -1
  33. package/dist/components/chart.js +13 -3
  34. package/dist/components/checkbox.js +1 -1
  35. package/dist/components/combobox.js +2 -2
  36. package/dist/components/command.d.ts +1 -1
  37. package/dist/components/command.d.ts.map +1 -1
  38. package/dist/components/command.js +2 -2
  39. package/dist/components/context-menu.js +1 -1
  40. package/dist/components/dashboard-shell.d.ts +1 -1
  41. package/dist/components/disclosure-row.d.ts.map +1 -1
  42. package/dist/components/disclosure-row.js +1 -3
  43. package/dist/components/dropdown-menu.js +1 -1
  44. package/dist/components/faq-item.d.ts +1 -1
  45. package/dist/components/faq-item.d.ts.map +1 -1
  46. package/dist/components/faq-item.js +2 -2
  47. package/dist/components/feedback-state.d.ts +1 -1
  48. package/dist/components/field.d.ts +1 -1
  49. package/dist/components/footer.d.ts.map +1 -1
  50. package/dist/components/footer.js +1 -1
  51. package/dist/components/form.d.ts +1 -1
  52. package/dist/components/glass-control.js +1 -1
  53. package/dist/components/hero.js +1 -1
  54. package/dist/components/input-group.d.ts +1 -1
  55. package/dist/components/input-group.js +3 -3
  56. package/dist/components/input-otp.d.ts.map +1 -1
  57. package/dist/components/input-otp.js +6 -3
  58. package/dist/components/input.js +1 -1
  59. package/dist/components/integration-flow.d.ts.map +1 -1
  60. package/dist/components/integration-flow.js +5 -5
  61. package/dist/components/item.d.ts +1 -1
  62. package/dist/components/item.js +1 -1
  63. package/dist/components/label-system.js +1 -1
  64. package/dist/components/live-demo.js +2 -2
  65. package/dist/components/menubar.d.ts +1 -1
  66. package/dist/components/menubar.js +1 -1
  67. package/dist/components/native-select.js +1 -1
  68. package/dist/components/nav-bar.d.ts.map +1 -1
  69. package/dist/components/nav-bar.js +1 -1
  70. package/dist/components/navigation-menu.d.ts.map +1 -1
  71. package/dist/components/navigation-menu.js +5 -5
  72. package/dist/components/pagination.d.ts +1 -1
  73. package/dist/components/plugin-slot-boundary.d.ts.map +1 -1
  74. package/dist/components/plugin-slot-boundary.js +1 -1
  75. package/dist/components/popover.js +1 -1
  76. package/dist/components/radio-group.js +1 -1
  77. package/dist/components/resizable.d.ts.map +1 -1
  78. package/dist/components/resizable.js +4 -2
  79. package/dist/components/scroll-area.js +1 -1
  80. package/dist/components/seam-grid.d.ts +1 -1
  81. package/dist/components/select.js +4 -4
  82. package/dist/components/settings-section.d.ts +1 -1
  83. package/dist/components/sidebar.d.ts +4 -4
  84. package/dist/components/sidebar.d.ts.map +1 -1
  85. package/dist/components/sidebar.js +6 -7
  86. package/dist/components/slider.js +1 -1
  87. package/dist/components/status-dot.js +2 -2
  88. package/dist/components/switch.js +1 -1
  89. package/dist/components/tabs.js +1 -1
  90. package/dist/components/textarea.js +1 -1
  91. package/dist/components/texture-strip.d.ts.map +1 -1
  92. package/dist/components/texture-strip.js +3 -0
  93. package/dist/components/theme-mode-switcher.d.ts +2 -2
  94. package/dist/components/toggle-group.d.ts +1 -1
  95. package/dist/components/toggle.js +1 -1
  96. package/dist/eslint/index.js +23 -17
  97. package/dist/eslint/rules/confirm-destructive-action.js +177 -0
  98. package/dist/eslint/rules/jsx-class-utility.js +162 -176
  99. package/dist/eslint/rules/no-app-feature-component.js +100 -0
  100. package/dist/eslint/rules/no-deprecated-zod-api.js +63 -15
  101. package/dist/eslint/rules/no-elevated-edge.js +144 -0
  102. package/dist/eslint/rules/no-mono-title-register.js +73 -0
  103. package/dist/eslint/rules/no-native-visible-control.js +6 -3
  104. package/dist/eslint/rules/no-package-component-redefinition.js +175 -0
  105. package/dist/eslint/rules/no-paired-size-utility.js +66 -0
  106. package/dist/eslint/rules/no-repainted-focus.js +80 -0
  107. package/dist/eslint/rules/no-resting-settings-control.js +87 -0
  108. package/dist/eslint/rules/no-shadcn-appearance-override.js +30 -56
  109. package/dist/eslint/rules/no-unpinned-vercel-functions-import.js +25 -23
  110. package/dist/eslint/rules/server-action-discipline.js +182 -0
  111. package/dist/eslint/rules/shadcn-composition.js +131 -0
  112. package/dist/hooks/index.d.ts +3 -3
  113. package/dist/index.d.ts +132 -132
  114. package/dist/internal/agent-visual/poster.d.ts +1 -1
  115. package/dist/internal/agent-visual/renderer.d.ts +1 -1
  116. package/dist/internal/assistant-ui/attachment.js +1 -1
  117. package/dist/internal/assistant-ui/follow-up-suggestions.d.ts.map +1 -1
  118. package/dist/internal/assistant-ui/follow-up-suggestions.js +5 -1
  119. package/dist/internal/assistant-ui/reasoning.d.ts +1 -1
  120. package/dist/internal/assistant-ui/tool-fallback.d.ts +1 -1
  121. package/dist/internal/assistant-ui/tool-group.d.ts +1 -1
  122. package/dist/internal/assistant-ui/tooltip-icon-button.d.ts +1 -1
  123. package/dist/styles/globals.css +37 -12
  124. package/docs/consuming-cross-repo.md +2 -2
  125. package/docs/contract.md +30 -20
  126. package/docs/migration/base-ui-migration.md +2 -2
  127. package/package.json +7 -6
  128. package/dist/eslint/rules/no-design-system-src-import.js +0 -11
  129. package/dist/eslint/rules/no-raw-color-utility.js +0 -109
  130. package/dist/eslint/rules/no-raw-focus-ring-width.js +0 -30
  131. package/dist/eslint/rules/no-raw-logo-import.js +0 -41
  132. package/dist/eslint/rules/no-ui-src-import.js +0 -10
  133. package/dist/eslint/rules/src-import-boundary.js +0 -207
@@ -1,204 +1,181 @@
1
- const PACKAGE_PRIMITIVE_PATH = "@nadicodeai/ui/src/";
1
+ /*
2
+ * The one className reader the plugin's class rules share.
3
+ *
4
+ * It reads LITERALS: string literals and template quasis anywhere inside a
5
+ * `className` expression, plus the class strings a `cva()` call carries. On top
6
+ * of that it takes EXACTLY ONE HOP: an identifier resolves to a same-file
7
+ * `const` string literal, and an identifier or a `TABLE.key` lookup resolves to
8
+ * a same-file `const` object whose string values are then read as classes. A
9
+ * `{...props}` spread resolves the same way and contributes that object's
10
+ * `className` value alone.
11
+ *
12
+ * One hop, and no further: nothing chains through a second binding, nothing
13
+ * crosses a file, nothing evaluates a condition or a template, and no value the
14
+ * hop cannot see is guessed at. That is the minimum that holds the package
15
+ * contract's repaint ban (`packages/ui/docs/contract.md`) against the way
16
+ * this repository actually authors classes — a tone table beside its component
17
+ * and a lookup at the call site, as in
18
+ * `apps/portal/src/components/portal-overview/overview-usage-tone.ts`. Without the
19
+ * hop, `<Card className={tones.warning} />` repaints a primitive and no rule
20
+ * sees it; with it, the table's own values are read.
21
+ *
22
+ * Standard tool checked (ADR 0057 tier 2): `eslint-plugin-better-tailwindcss`
23
+ * reads lookup tables through a `variable` selector with an `objectValues`
24
+ * matcher, and the root config uses exactly that so a table's classes are
25
+ * checked for existence. What it cannot do is say WHERE a value lands: its
26
+ * rules judge a class on its own, never against the JSX element it is applied
27
+ * to. "Not on an imported primitive" is a relationship between a class and an
28
+ * element, so it stays here.
29
+ *
30
+ * Anything past one hop remains out of reach by design and is a review finding,
31
+ * not a lint finding.
32
+ */
33
+
34
+ /** The same-file binding an identifier refers to, or undefined. */
35
+ function findVariable(scope, name) {
36
+ for (let current = scope; current; current = current.upper) {
37
+ const variable = current.set.get(name);
38
+ if (variable) return variable;
39
+ }
40
+ return undefined;
41
+ }
2
42
 
3
- export function isPackageOwnedPrimitive(filename) {
4
- return filename.replaceAll("\\", "/").includes(PACKAGE_PRIMITIVE_PATH);
43
+ /**
44
+ * The initializer of the same-file `const` an identifier names. Undefined for
45
+ * imports, parameters, `let`/`var`, and anything declared more than once —
46
+ * every shape whose value is not fixed by this file alone.
47
+ */
48
+ function constInitializer(identifier, context) {
49
+ if (!context?.sourceCode) return undefined;
50
+ const variable = findVariable(context.sourceCode.getScope(identifier), identifier.name);
51
+ if (!variable || variable.defs.length !== 1) return undefined;
52
+ const [definition] = variable.defs;
53
+ if (definition.type !== "Variable" || definition.parent?.kind !== "const") return undefined;
54
+ return definition.node.init ?? undefined;
5
55
  }
6
56
 
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;
57
+ /** Every string-literal value of an object literal; a lookup can select any of them. */
58
+ function objectStringValues(objectExpression) {
59
+ const values = [];
60
+ for (const property of objectExpression.properties) {
61
+ if (property.type !== "Property") continue;
62
+ if (property.value.type === "Literal" && typeof property.value.value === "string") {
63
+ values.push(property.value.value);
64
+ }
16
65
  }
17
- return undefined;
66
+ return values;
18
67
  }
19
68
 
20
- function collectStaticClassFragments(
21
- node,
22
- fragments,
23
- context,
24
- seen = new Set(),
25
- includeObjectKeys = true,
26
- ) {
27
- if (!node) return;
69
+ /** The one hop: the class strings a resolved `const` contributes, if any. */
70
+ function resolvedFragments(identifier, context, { objectsOnly = false } = {}) {
71
+ const initializer = constInitializer(identifier, context);
72
+ if (!initializer) return [];
73
+ if (initializer.type === "ObjectExpression") return objectStringValues(initializer);
74
+ if (objectsOnly) return [];
75
+ return initializer.type === "Literal" && typeof initializer.value === "string"
76
+ ? [initializer.value]
77
+ : [];
78
+ }
28
79
 
29
- if (seen.has(node)) return;
30
- seen.add(node);
80
+ /** Every string literal and template chunk in the subtree, in source order. */
81
+ function literalFragments(node, fragments, { skipPropertyKeys, context }) {
82
+ if (!node || typeof node.type !== "string") return;
31
83
 
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);
84
+ if (node.type === "Literal") {
85
+ if (typeof node.value === "string") fragments.push(node.value);
38
86
  return;
39
87
  }
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);
88
+ if (node.type === "TemplateElement") {
89
+ fragments.push(node.value.cooked ?? node.value.raw);
59
90
  return;
60
91
  }
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);
92
+ // `USAGE_TONE_CLASS[kpi.tone ?? "normal"]` selects a class string, it does
93
+ // not contain one: the names inside the lookup are keys. The hop reads the
94
+ // table instead — any of its values can be the one selected — and the
95
+ // property is never descended into.
96
+ if (node.type === "MemberExpression") {
97
+ if (node.object.type === "Identifier") {
98
+ fragments.push(...resolvedFragments(node.object, context, { objectsOnly: true }));
95
99
  }
96
100
  return;
97
101
  }
98
102
  if (node.type === "Identifier") {
99
- collectStaticClassFragments(
100
- variableInitializer(node, context),
101
- fragments,
102
- context,
103
- seen,
104
- includeObjectKeys,
105
- );
103
+ fragments.push(...resolvedFragments(node, context));
106
104
  return;
107
105
  }
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
106
 
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;
107
+ for (const [key, value] of Object.entries(node)) {
108
+ if (key === "parent" || key === "loc" || key === "range") continue;
109
+ // A cva variant map's keys name variants, not classes.
110
+ if (skipPropertyKeys && key === "key" && node.type === "Property" && !node.computed) continue;
111
+ if (Array.isArray(value)) {
112
+ for (const child of value) literalFragments(child, fragments, { skipPropertyKeys, context });
113
+ } else if (value && typeof value === "object") {
114
+ literalFragments(value, fragments, { skipPropertyKeys, context });
115
+ }
128
116
  }
129
- return undefined;
130
117
  }
131
118
 
132
- function collectStaticSpreadClassFragments(node, fragments, context, seen = new Set()) {
133
- if (!node || seen.has(node)) return;
134
- seen.add(node);
119
+ function utilities(node, { skipPropertyKeys = false, context } = {}) {
120
+ const fragments = [];
121
+ literalFragments(node, fragments, { skipPropertyKeys, context });
122
+ return fragments.flatMap((fragment) => fragment.split(/\s+/).filter(Boolean));
123
+ }
135
124
 
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
- }
125
+ /**
126
+ * Every class utility written inside one node, wherever it sits. The reader for
127
+ * a rule whose surface is not a JSX element — a `cn()`/`cva()` call assigned to
128
+ * a `className` property, or a bare class string in a props object — which is
129
+ * where this repository's composed class lists actually live.
130
+ *
131
+ * Pass `skipPropertyKeys` for a cva call, whose variant-map keys name variants
132
+ * rather than classes.
133
+ */
134
+ export function nodeClassUtilities(node, context, { skipPropertyKeys = false } = {}) {
135
+ return utilities(node, { skipPropertyKeys, context });
170
136
  }
171
137
 
138
+ /**
139
+ * The class utilities written in one `className` JSX attribute. Pass `context`
140
+ * to take the one hop; without it the reader stays literal-only.
141
+ */
172
142
  export function classUtilities(attribute, context) {
173
143
  if (attribute?.type !== "JSXAttribute" || attribute.name.name !== "className") return [];
174
- return staticClassUtilities(attribute.value, context);
144
+ return utilities(attribute.value, { context });
175
145
  }
176
146
 
177
- export function staticStringFragments(node, context) {
178
- const fragments = [];
179
- collectStaticClassFragments(node, fragments, context);
180
- return fragments;
147
+ /** The `className` a spread object carries — its own key only, not its other values. */
148
+ function spreadClassUtilities(attribute, context) {
149
+ if (attribute.argument.type !== "Identifier") return [];
150
+ const initializer = constInitializer(attribute.argument, context);
151
+ if (initializer?.type !== "ObjectExpression") return [];
152
+ for (const property of initializer.properties) {
153
+ if (property.type !== "Property" || property.computed) continue;
154
+ const name = property.key.type === "Identifier" ? property.key.name : property.key.value;
155
+ if (name !== "className" && name !== "class") continue;
156
+ if (property.value.type !== "Literal" || typeof property.value.value !== "string") continue;
157
+ return property.value.value.split(/\s+/).filter(Boolean);
158
+ }
159
+ return [];
181
160
  }
182
161
 
183
- function staticClassUtilities(node, context) {
184
- return staticStringFragments(node, context).flatMap((fragment) =>
185
- fragment.split(/\s+/).filter(Boolean),
162
+ /** The class utilities written across every attribute of one element, spreads included. */
163
+ export function openingElementClassUtilities(node, context) {
164
+ return node.attributes.flatMap((attribute) =>
165
+ attribute.type === "JSXSpreadAttribute"
166
+ ? spreadClassUtilities(attribute, context)
167
+ : classUtilities(attribute, context),
186
168
  );
187
169
  }
188
170
 
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
-
171
+ /**
172
+ * `className` attributes plus `cva()` calls — the two places a design-token
173
+ * utility is written. The token rules run over both because a cva variant map
174
+ * is where a component's class strings actually live.
175
+ */
195
176
  export function tokenUtilityVisitors(context, onUtility) {
196
177
  const cvaNames = new Set();
197
178
 
198
- function visitUtilities(node, utilities) {
199
- for (const utility of utilities) onUtility(node, utility);
200
- }
201
-
202
179
  return {
203
180
  ImportDeclaration(node) {
204
181
  if (node.source.value !== "class-variance-authority") return;
@@ -209,23 +186,32 @@ export function tokenUtilityVisitors(context, onUtility) {
209
186
  }
210
187
  },
211
188
  JSXAttribute(node) {
212
- visitUtilities(node, classUtilities(node, context));
189
+ for (const utility of classUtilities(node, context)) onUtility(node, utility);
213
190
  },
214
191
  CallExpression(node) {
215
192
  if (node.callee.type !== "Identifier" || !cvaNames.has(node.callee.name)) return;
216
- visitUtilities(node, staticValueClassUtilities(node, context));
193
+ for (const utility of utilities(node, { skipPropertyKeys: true, context })) {
194
+ onUtility(node, utility);
195
+ }
217
196
  },
218
197
  };
219
198
  }
220
199
 
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));
200
+ /*
201
+ * Package ownership as a per-rule option (#821, #822). `@nadicodeai/ui`'s own
202
+ * source is the place the primitives are BUILT, so a rule that polices what
203
+ * callers do to a primitive has nothing to say there — that is the "exempt"
204
+ * default. A rule whose invariant holds inside the package too selects "lint".
205
+ */
206
+ const PACKAGE_PRIMITIVE_PATH = "packages/ui/src/";
207
+
208
+ export const packageOwnedPrimitivesSchema = {
209
+ type: "object",
210
+ properties: { packageOwnedPrimitives: { enum: ["exempt", "lint"] } },
211
+ additionalProperties: false,
212
+ };
213
+
214
+ export function skipsPackageOwnedPrimitive(context) {
215
+ if (context.options?.[0]?.packageOwnedPrimitives === "lint") return false;
216
+ return context.filename.replaceAll("\\", "/").includes(PACKAGE_PRIMITIVE_PATH);
231
217
  }
@@ -0,0 +1,100 @@
1
+ /*
2
+ * `@nadicodeai/ui` ships standard components. An operator login, a portal
3
+ * activation screen, a customer-credential panel — those are use cases, and a
4
+ * use case in the package is a screen two consumers can never both want. The
5
+ * boundary is named in the app's vocabulary, so the rule reads names: the
6
+ * module's own file name and everything it exports.
7
+ *
8
+ * Standard tool checked (ADR 0057 tier 2): core `no-restricted-syntax` matches
9
+ * an exported identifier against a regex through esquery, but it is given one
10
+ * node at a time and never the file that node is in — and a module called
11
+ * `activation.tsx` exporting `Banner` is the case the file name is there to
12
+ * catch. `context.filename` is why this is a rule rather than a selector list.
13
+ *
14
+ * The vocabulary itself is configuration, not code: the banned words live in the
15
+ * workspace's eslint config where the package boundary is drawn, and this file
16
+ * holds only the matching. Replaces the @nadicodeai/ui no-app-feature-components
17
+ * scanner (#822), which built a TypeScript AST over the package's components to
18
+ * ask the same question.
19
+ */
20
+
21
+ /** `OperatorLoginCard` and `operator-login.tsx` both normalise to `operator-login`. */
22
+ function normalizeName(value) {
23
+ return value
24
+ .replace(/\.[jt]sx?$/, "")
25
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
26
+ .replace(/[\s_]+/g, "-")
27
+ .toLowerCase();
28
+ }
29
+
30
+ /** The declared names one `export` statement introduces. */
31
+ function exportedNames(node) {
32
+ if (!node.declaration) {
33
+ return node.specifiers.map((specifier) => specifier.exported.name ?? specifier.exported.value);
34
+ }
35
+ const declaration = node.declaration;
36
+ if (declaration.type === "VariableDeclaration") {
37
+ return declaration.declarations
38
+ .filter((declarator) => declarator.id.type === "Identifier")
39
+ .map((declarator) => declarator.id.name);
40
+ }
41
+ return declaration.id ? [declaration.id.name] : [];
42
+ }
43
+
44
+ export const noAppFeatureComponent = {
45
+ meta: {
46
+ type: "problem",
47
+ docs: {
48
+ description: "Disallow app-owned feature and use-case names in a shared component package",
49
+ },
50
+ schema: [
51
+ {
52
+ type: "object",
53
+ properties: {
54
+ names: {
55
+ type: "array",
56
+ items: {
57
+ type: "object",
58
+ properties: { feature: { type: "string" }, pattern: { type: "string" } },
59
+ required: ["feature", "pattern"],
60
+ additionalProperties: false,
61
+ },
62
+ },
63
+ },
64
+ additionalProperties: false,
65
+ },
66
+ ],
67
+ messages: {
68
+ appFeature:
69
+ "{{what}} names the {{feature}} use case. This package ships standard components; compose an app feature inside the consuming app.",
70
+ },
71
+ },
72
+ create(context) {
73
+ const names = (context.options?.[0]?.names ?? []).map(({ feature, pattern }) => ({
74
+ feature,
75
+ pattern: new RegExp(pattern),
76
+ }));
77
+ if (names.length === 0) return {};
78
+
79
+ const feature = (candidate) =>
80
+ names.find(({ pattern }) => pattern.test(normalizeName(candidate)))?.feature;
81
+
82
+ const report = (node, candidate, what) => {
83
+ const matched = feature(candidate);
84
+ if (matched)
85
+ context.report({ node, messageId: "appFeature", data: { what, feature: matched } });
86
+ };
87
+
88
+ return {
89
+ Program(node) {
90
+ const file = context.filename.replaceAll("\\", "/").split("/").at(-1) ?? "";
91
+ report(node, file, `The module name "${file}"`);
92
+ },
93
+ ExportNamedDeclaration(node) {
94
+ for (const name of exportedNames(node)) {
95
+ report(node, name, `The export "${name}"`);
96
+ }
97
+ },
98
+ };
99
+ },
100
+ };
@@ -1,9 +1,40 @@
1
+ /*
2
+ * Legacy Zod 3 APIs, in two sets that are enforced from two different places.
3
+ *
4
+ * UNANNOTATED — the four Zod itself leaves without a `@deprecated` tag.
5
+ * Verified against zod 4.4.3's declarations: `nativeEnum` and `promise` carry
6
+ * only prose, `.strict()` and `.strip()` carry only a "consider…" hint. No tag
7
+ * means no type-aware rule can ever fire on them, so they are this rule's own
8
+ * policy and are reported in every workspace.
9
+ *
10
+ * ANNOTATED — the rest, which Zod does tag `@deprecated`.
11
+ * `@typescript-eslint/no-deprecated` reports those with the library's own
12
+ * replacement text, but only where lint parses with type information. In this
13
+ * repository that is `@nadicodeai/ui` alone: switching the Portal on measured
14
+ * 10.4s → 25.8s per lint run and surfaced 15 unrelated findings (React's
15
+ * `FormEvent`, vitest's `toThrowError`), so typed lint stays ui-only (#821).
16
+ * The Portal and the website are where this repository writes most of its Zod,
17
+ * so the names stay listed here for them — a second copy of an upstream
18
+ * annotation, kept deliberately because the alternative is no coverage at all
19
+ * in two of three workspaces (#822).
20
+ *
21
+ * The split is a rule option: `upstreamAnnotated: "report"` (default) names
22
+ * them, `"defer"` leaves them to the type-aware rule. `@nadicodeai/ui` selects
23
+ * `"defer"` in the root config, so no file is reported twice.
24
+ */
1
25
  const LEGACY_FACTORIES = new Map([
2
26
  ["nativeEnum", "z.enum()"],
3
27
  ["promise", "await the value before parsing it"],
4
28
  ]);
5
29
 
6
30
  const LEGACY_METHODS = {
31
+ object: new Map([
32
+ ["strict", "z.strictObject()"],
33
+ ["strip", "z.object(), which strips unknown keys by default"],
34
+ ]),
35
+ };
36
+
37
+ const UPSTREAM_ANNOTATED_METHODS = {
7
38
  number: new Map([
8
39
  ["finite", "remove it; z.number() already rejects infinite values"],
9
40
  ["safe", ".int()"],
@@ -12,13 +43,13 @@ const LEGACY_METHODS = {
12
43
  object: new Map([
13
44
  ["merge", ".extend(other.shape)"],
14
45
  ["passthrough", "z.looseObject()"],
15
- ["strict", "z.strictObject()"],
16
- ["strip", "z.object(), which strips unknown keys by default"],
17
46
  ]),
18
- string: new Map(
19
- [
47
+ string: new Map([
48
+ ...[
20
49
  "base64",
21
50
  "base64url",
51
+ "cidrv4",
52
+ "cidrv6",
22
53
  "cuid",
23
54
  "cuid2",
24
55
  "e164",
@@ -38,14 +69,24 @@ const LEGACY_METHODS = {
38
69
  "uuidv7",
39
70
  "xid",
40
71
  ].map((name) => [name, `z.${name.startsWith("uuidv") ? "uuid" : name}()`]),
41
- ),
72
+ ...["date", "datetime", "duration", "time"].map((name) => [name, `z.iso.${name}()`]),
73
+ ]),
42
74
  };
43
75
 
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}()`);
76
+ /** The method table a run consults: the four always, the annotated names unless deferred. */
77
+ function legacyMethods(context) {
78
+ if (context.options?.[0]?.upstreamAnnotated === "defer") return LEGACY_METHODS;
79
+ const merged = {};
80
+ for (const family of new Set([
81
+ ...Object.keys(LEGACY_METHODS),
82
+ ...Object.keys(UPSTREAM_ANNOTATED_METHODS),
83
+ ])) {
84
+ merged[family] = new Map([
85
+ ...(UPSTREAM_ANNOTATED_METHODS[family] ?? []),
86
+ ...(LEGACY_METHODS[family] ?? []),
87
+ ]);
88
+ }
89
+ return merged;
49
90
  }
50
91
 
51
92
  function memberName(node) {
@@ -62,17 +103,24 @@ export const noDeprecatedZodApi = {
62
103
  meta: {
63
104
  type: "problem",
64
105
  docs: {
65
- description: "Disallow deprecated and legacy Zod APIs when a Zod 4 replacement exists",
106
+ description: "Disallow the Zod 3 APIs that Zod 4 replaced without marking them @deprecated",
66
107
  },
67
- schema: [],
108
+ schema: [
109
+ {
110
+ type: "object",
111
+ properties: { upstreamAnnotated: { enum: ["report", "defer"] } },
112
+ additionalProperties: false,
113
+ },
114
+ ],
68
115
  messages: {
69
- legacyFactory: "{{api}} is deprecated in Zod 4; use {{replacement}}.",
70
- legacyMethod: "{{api}} is deprecated or legacy in Zod 4; use {{replacement}}.",
116
+ legacyFactory: "{{api}} is legacy in Zod 4; use {{replacement}}.",
117
+ legacyMethod: "{{api}} is legacy in Zod 4; use {{replacement}}.",
71
118
  },
72
119
  },
73
120
 
74
121
  create(context) {
75
122
  const sourceCode = context.sourceCode;
123
+ const methods = legacyMethods(context);
76
124
  const zodBindings = new Set();
77
125
 
78
126
  function isZodBinding(identifier) {
@@ -142,7 +190,7 @@ export const noDeprecatedZodApi = {
142
190
 
143
191
  const factory = rootFactory(node.callee.object);
144
192
  const family = factory === "strictObject" || factory === "looseObject" ? "object" : factory;
145
- const replacement = LEGACY_METHODS[family]?.get(method);
193
+ const replacement = methods[family]?.get(method);
146
194
  if (replacement) {
147
195
  context.report({
148
196
  node: node.callee.property,