@nadicodeai/ui 1.0.1 → 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.
- package/AGENTS.md +13 -13
- package/README.md +1 -1
- package/dist/agents.d.ts +8 -8
- package/dist/components/accordion.js +1 -1
- package/dist/components/action-band.js +1 -1
- package/dist/components/agent-card.d.ts +1 -1
- package/dist/components/agent-visual.d.ts +1 -1
- package/dist/components/alert-dialog.d.ts +1 -1
- package/dist/components/automation-section.d.ts +2 -2
- package/dist/components/avatar.d.ts.map +1 -1
- package/dist/components/avatar.js +3 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/brand-icons/apple.d.ts +1 -1
- package/dist/components/brand-icons/linux.d.ts +1 -1
- package/dist/components/brand-icons/windows.d.ts +1 -1
- package/dist/components/brand-icons.d.ts +4 -4
- package/dist/components/button-group.d.ts +1 -1
- package/dist/components/button.js +1 -1
- package/dist/components/calendar.d.ts +1 -1
- package/dist/components/calendar.js +1 -1
- package/dist/components/card.d.ts.map +1 -1
- package/dist/components/card.js +2 -4
- package/dist/components/carousel.d.ts +1 -1
- package/dist/components/carousel.d.ts.map +1 -1
- package/dist/components/chart-breakdown-table.d.ts +2 -2
- package/dist/components/chart-bullet-bar.js +5 -5
- package/dist/components/chart-date-range-control.d.ts +1 -1
- package/dist/components/chart-kpi-stat.d.ts +1 -1
- package/dist/components/chart-time-series.d.ts +1 -1
- package/dist/components/chart-time-series.d.ts.map +1 -1
- package/dist/components/chart-time-series.js +10 -9
- package/dist/components/chart.d.ts.map +1 -1
- package/dist/components/chart.js +13 -3
- package/dist/components/checkbox.js +1 -1
- package/dist/components/combobox.js +2 -2
- package/dist/components/command.d.ts +1 -1
- package/dist/components/command.d.ts.map +1 -1
- package/dist/components/command.js +2 -2
- package/dist/components/context-menu.js +1 -1
- package/dist/components/dashboard-shell.d.ts +1 -1
- package/dist/components/disclosure-row.d.ts.map +1 -1
- package/dist/components/disclosure-row.js +1 -3
- package/dist/components/dropdown-menu.js +1 -1
- package/dist/components/faq-item.d.ts +1 -1
- package/dist/components/faq-item.d.ts.map +1 -1
- package/dist/components/faq-item.js +2 -2
- package/dist/components/feedback-state.d.ts +1 -1
- package/dist/components/field.d.ts +1 -1
- package/dist/components/footer.d.ts.map +1 -1
- package/dist/components/footer.js +1 -1
- package/dist/components/form.d.ts +1 -1
- package/dist/components/glass-control.js +1 -1
- package/dist/components/hero.js +1 -1
- package/dist/components/input-group.d.ts +1 -1
- package/dist/components/input-group.js +3 -3
- package/dist/components/input-otp.d.ts.map +1 -1
- package/dist/components/input-otp.js +6 -3
- package/dist/components/input.js +1 -1
- package/dist/components/integration-flow.d.ts.map +1 -1
- package/dist/components/integration-flow.js +5 -5
- package/dist/components/item.d.ts +1 -1
- package/dist/components/item.js +1 -1
- package/dist/components/label-system.js +1 -1
- package/dist/components/live-demo.js +2 -2
- package/dist/components/menubar.d.ts +1 -1
- package/dist/components/menubar.js +1 -1
- package/dist/components/native-select.js +1 -1
- package/dist/components/nav-bar.d.ts.map +1 -1
- package/dist/components/nav-bar.js +1 -1
- package/dist/components/navigation-menu.d.ts.map +1 -1
- package/dist/components/navigation-menu.js +5 -5
- package/dist/components/pagination.d.ts +1 -1
- package/dist/components/plugin-slot-boundary.d.ts.map +1 -1
- package/dist/components/plugin-slot-boundary.js +1 -1
- package/dist/components/popover.js +1 -1
- package/dist/components/radio-group.js +1 -1
- package/dist/components/resizable.d.ts.map +1 -1
- package/dist/components/resizable.js +4 -2
- package/dist/components/scroll-area.js +1 -1
- package/dist/components/seam-grid.d.ts +1 -1
- package/dist/components/select.js +4 -4
- package/dist/components/settings-section.d.ts +1 -1
- package/dist/components/sidebar.d.ts +4 -4
- package/dist/components/sidebar.d.ts.map +1 -1
- package/dist/components/sidebar.js +6 -7
- package/dist/components/slider.js +1 -1
- package/dist/components/status-dot.js +2 -2
- package/dist/components/switch.js +1 -1
- package/dist/components/table.d.ts.map +1 -1
- package/dist/components/table.js +3 -3
- package/dist/components/tabs.js +1 -1
- package/dist/components/textarea.js +1 -1
- package/dist/components/texture-strip.d.ts.map +1 -1
- package/dist/components/texture-strip.js +3 -0
- package/dist/components/theme-mode-switcher.d.ts +2 -2
- package/dist/components/toggle-group.d.ts +1 -1
- package/dist/components/toggle.js +1 -1
- package/dist/eslint/index.js +23 -17
- package/dist/eslint/rules/confirm-destructive-action.js +177 -0
- package/dist/eslint/rules/jsx-class-utility.js +162 -176
- package/dist/eslint/rules/no-app-feature-component.js +100 -0
- package/dist/eslint/rules/no-deprecated-zod-api.js +63 -15
- package/dist/eslint/rules/no-elevated-edge.js +144 -0
- package/dist/eslint/rules/no-mono-title-register.js +73 -0
- package/dist/eslint/rules/no-native-visible-control.js +6 -3
- package/dist/eslint/rules/no-package-component-redefinition.js +175 -0
- package/dist/eslint/rules/no-paired-size-utility.js +66 -0
- package/dist/eslint/rules/no-repainted-focus.js +80 -0
- package/dist/eslint/rules/no-resting-settings-control.js +87 -0
- package/dist/eslint/rules/no-shadcn-appearance-override.js +30 -56
- package/dist/eslint/rules/no-unpinned-vercel-functions-import.js +25 -23
- package/dist/eslint/rules/server-action-discipline.js +182 -0
- package/dist/eslint/rules/shadcn-composition.js +131 -0
- package/dist/hooks/index.d.ts +3 -3
- package/dist/index.d.ts +132 -132
- package/dist/internal/agent-visual/poster.d.ts +1 -1
- package/dist/internal/agent-visual/renderer.d.ts +1 -1
- package/dist/internal/assistant-ui/attachment.js +1 -1
- package/dist/internal/assistant-ui/follow-up-suggestions.d.ts.map +1 -1
- package/dist/internal/assistant-ui/follow-up-suggestions.js +5 -1
- package/dist/internal/assistant-ui/reasoning.d.ts +1 -1
- package/dist/internal/assistant-ui/tool-fallback.d.ts +1 -1
- package/dist/internal/assistant-ui/tool-group.d.ts +1 -1
- package/dist/internal/assistant-ui/tooltip-icon-button.d.ts +1 -1
- package/dist/styles/globals.css +37 -12
- package/docs/consuming-cross-repo.md +2 -2
- package/docs/contract.md +38 -20
- package/docs/migration/base-ui-migration.md +2 -2
- package/package.json +7 -6
- package/dist/eslint/rules/no-design-system-src-import.js +0 -11
- package/dist/eslint/rules/no-raw-color-utility.js +0 -109
- package/dist/eslint/rules/no-raw-focus-ring-width.js +0 -30
- package/dist/eslint/rules/no-raw-logo-import.js +0 -41
- package/dist/eslint/rules/no-ui-src-import.js +0 -10
- package/dist/eslint/rules/src-import-boundary.js +0 -207
|
@@ -1,204 +1,181 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
66
|
+
return values;
|
|
18
67
|
}
|
|
19
68
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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"
|
|
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 === "
|
|
41
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
144
|
+
return utilities(attribute.value, { context });
|
|
175
145
|
}
|
|
176
146
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
+
for (const utility of utilities(node, { skipPropertyKeys: true, context })) {
|
|
194
|
+
onUtility(node, utility);
|
|
195
|
+
}
|
|
217
196
|
},
|
|
218
197
|
};
|
|
219
198
|
}
|
|
220
199
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
|
70
|
-
legacyMethod: "{{api}} is
|
|
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 =
|
|
193
|
+
const replacement = methods[family]?.get(method);
|
|
146
194
|
if (replacement) {
|
|
147
195
|
context.report({
|
|
148
196
|
node: node.callee.property,
|