@nadicodeai/ui 12.1.53 → 12.1.55
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/dist/eslint/index.js +27 -69
- package/dist/eslint/rules/jsx-class-utility.js +2 -56
- package/dist/eslint/rules/no-choice-control-wrapper.js +95 -0
- package/dist/eslint/rules/no-comment.js +44 -0
- package/dist/eslint/rules/no-subject-derived-expectation.js +55 -1
- package/dist/eslint/strict-design-system.js +224 -0
- package/dist/styles/components.css +1 -1
- package/package.json +3 -3
- package/dist/.build-kind +0 -1
- package/dist/eslint/rules/card-row-owner.js +0 -167
- package/dist/eslint/rules/confirm-destructive-action.js +0 -177
- package/dist/eslint/rules/drawer-owns-its-edges.js +0 -66
- package/dist/eslint/rules/expect-chain.js +0 -60
- package/dist/eslint/rules/icon-control-accessible-name.js +0 -156
- package/dist/eslint/rules/menu-command-owner.js +0 -45
- package/dist/eslint/rules/no-bare-page-navigation.js +0 -46
- package/dist/eslint/rules/no-elevated-edge.js +0 -141
- package/dist/eslint/rules/no-existence-only-assertion.js +0 -89
- package/dist/eslint/rules/no-invalid-token-utility.js +0 -46
- package/dist/eslint/rules/no-mono-title-register.js +0 -73
- package/dist/eslint/rules/no-native-visible-control.js +0 -92
- package/dist/eslint/rules/no-nested-overlay.js +0 -56
- package/dist/eslint/rules/no-off-grid-spacing.js +0 -115
- package/dist/eslint/rules/no-paired-size-utility.js +0 -66
- package/dist/eslint/rules/no-raw-shadow.js +0 -70
- package/dist/eslint/rules/no-repainted-focus.js +0 -75
- package/dist/eslint/rules/no-resting-settings-control.js +0 -89
- package/dist/eslint/rules/no-route-width-override.js +0 -135
- package/dist/eslint/rules/no-store-cast.js +0 -46
- package/dist/eslint/rules/no-token-category-bypass.js +0 -53
- package/dist/eslint/rules/no-unpinned-vercel-functions-import.js +0 -104
- package/dist/eslint/rules/one-control-height.js +0 -163
- package/dist/eslint/rules/overlay-body-owns-scroll.js +0 -62
- package/dist/eslint/rules/overlay-mounts-closed.js +0 -102
- package/dist/eslint/rules/prefer-radio-for-few-static-options.js +0 -77
- package/dist/eslint/rules/settings-section-description.js +0 -45
- package/dist/eslint/rules/shadcn-composition.js +0 -197
- package/dist/eslint/rules/table-column-wraps-by-meta.js +0 -55
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The composition conventions the shared components are built to, checked
|
|
3
|
-
* where they are composed:
|
|
4
|
-
*
|
|
5
|
-
* - an overlay carries its title, so a screen reader announces what opened
|
|
6
|
-
* (`className="sr-only"` when the title is not drawn);
|
|
7
|
-
* - a `SelectItem` sits inside a `SelectGroup`, which is what gives the listbox
|
|
8
|
-
* its accessible grouping;
|
|
9
|
-
* - an `AvatarImage` has an `AvatarFallback` beside it, or the avatar is blank
|
|
10
|
-
* until the image loads;
|
|
11
|
-
* - a lucide icon inside a `Button` carries `data-icon`, the hook the button's
|
|
12
|
-
* own spacing rules key on;
|
|
13
|
-
* - a settings submit action uses SettingsSection's shared footer, so each
|
|
14
|
-
* caller cannot rebuild its own divider, padding, and action position;
|
|
15
|
-
* - the label and description beside a control in a `Field` sit in
|
|
16
|
-
* `FieldContent`, the slot a horizontal field keys on to align its checkbox
|
|
17
|
-
* with the first line of text rather than the middle of the block.
|
|
18
|
-
*
|
|
19
|
-
* Standard tool checked first: `jsx-a11y` covers roles and ARIA on
|
|
20
|
-
* intrinsic elements and says nothing about these composed components;
|
|
21
|
-
* `no-restricted-syntax` selects a node by its own shape, and every one of
|
|
22
|
-
* these is a statement about an ancestor or a descendant. Replaces the JSX half
|
|
23
|
-
* of the retired @nadicodeai/ui composition-conventions scanner (#822); stock
|
|
24
|
-
* upstream shapes carry an inline disable with the reason at the site.
|
|
25
|
-
*/
|
|
26
|
-
import {
|
|
27
|
-
jsxComponentName,
|
|
28
|
-
packageOwnedPrimitivesSchema,
|
|
29
|
-
skipsPackageOwnedPrimitive,
|
|
30
|
-
} from "./jsx-class-utility.js";
|
|
31
|
-
|
|
32
|
-
const OVERLAY_TITLES = {
|
|
33
|
-
AlertDialogContent: "AlertDialogTitle",
|
|
34
|
-
DialogContent: "DialogTitle",
|
|
35
|
-
DrawerContent: "DrawerTitle",
|
|
36
|
-
SheetContent: "SheetTitle",
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
function elementName(node) {
|
|
40
|
-
if (node?.type === "JSXElement") return elementName(node.openingElement);
|
|
41
|
-
if (node?.type !== "JSXOpeningElement") return undefined;
|
|
42
|
-
if (node.name.type === "JSXIdentifier") return node.name.name;
|
|
43
|
-
if (node.name.type === "JSXMemberExpression") return node.name.property.name;
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function hasAttribute(node, attributeName) {
|
|
48
|
-
return node.attributes.some(
|
|
49
|
-
(attribute) =>
|
|
50
|
-
attribute.type === "JSXAttribute" &&
|
|
51
|
-
attribute.name.type === "JSXIdentifier" &&
|
|
52
|
-
attribute.name.name === attributeName,
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* True when `tagName` is rendered anywhere below this element — through a
|
|
58
|
-
* conditional or a map as much as directly, so the whole subtree is walked
|
|
59
|
-
* rather than the JSX children alone.
|
|
60
|
-
*/
|
|
61
|
-
function hasDescendant(node, tagName, root = node) {
|
|
62
|
-
if (!node || typeof node.type !== "string") return false;
|
|
63
|
-
if (node !== root && node.type === "JSXElement" && elementName(node) === tagName) return true;
|
|
64
|
-
for (const [key, value] of Object.entries(node)) {
|
|
65
|
-
if (key === "parent") continue;
|
|
66
|
-
if (Array.isArray(value)) {
|
|
67
|
-
if (value.some((child) => hasDescendant(child, tagName, root))) return true;
|
|
68
|
-
} else if (value && typeof value === "object" && hasDescendant(value, tagName, root)) {
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function ancestorNames(node, sourceCode) {
|
|
76
|
-
return sourceCode
|
|
77
|
-
.getAncestors(node)
|
|
78
|
-
.filter((ancestor) => ancestor.type === "JSXElement")
|
|
79
|
-
.map((ancestor) => elementName(ancestor))
|
|
80
|
-
.filter(Boolean);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** A FieldLabel or FieldDescription grouped in a plain element directly inside a Field. */
|
|
84
|
-
function fieldTextOutsideContent(node) {
|
|
85
|
-
const tag = elementName(node);
|
|
86
|
-
if (tag !== "FieldLabel" && tag !== "FieldDescription") return false;
|
|
87
|
-
const group = node.parent?.parent;
|
|
88
|
-
if (group?.type !== "JSXElement") return false;
|
|
89
|
-
const groupName = elementName(group);
|
|
90
|
-
if (!groupName || groupName[0] !== groupName[0].toLowerCase()) return false;
|
|
91
|
-
const field = group.parent;
|
|
92
|
-
return field?.type === "JSXElement" && elementName(field) === "Field";
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function isIconTag(tagName) {
|
|
96
|
-
return tagName === "Icon" || tagName.endsWith("Icon");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function misplacedSettingsSubmit(node, context) {
|
|
100
|
-
if (jsxComponentName(node, context) !== "Button") return false;
|
|
101
|
-
const attribute = node.attributes.find(
|
|
102
|
-
(entry) => entry.type === "JSXAttribute" && entry.name.name === "type",
|
|
103
|
-
);
|
|
104
|
-
const value =
|
|
105
|
-
attribute?.value?.type === "JSXExpressionContainer"
|
|
106
|
-
? attribute.value.expression
|
|
107
|
-
: attribute?.value;
|
|
108
|
-
const type =
|
|
109
|
-
value?.type === "TemplateLiteral" && value.expressions.length === 0
|
|
110
|
-
? value.quasis[0]?.value.cooked
|
|
111
|
-
: value?.value;
|
|
112
|
-
if (type !== "submit") return false;
|
|
113
|
-
const ancestors = context.sourceCode.getAncestors(node);
|
|
114
|
-
const section = ancestors.findLast(
|
|
115
|
-
(ancestor) =>
|
|
116
|
-
ancestor.type === "JSXElement" &&
|
|
117
|
-
jsxComponentName(ancestor.openingElement, context) === "SettingsSection",
|
|
118
|
-
);
|
|
119
|
-
if (!section) return false;
|
|
120
|
-
return !ancestors.slice(ancestors.indexOf(section) + 1).some((ancestor) => {
|
|
121
|
-
if (ancestor.type === "JSXAttribute")
|
|
122
|
-
return ancestor.name.name === "footer" && ancestor.parent === section.openingElement;
|
|
123
|
-
return (
|
|
124
|
-
ancestor.type === "JSXElement" &&
|
|
125
|
-
["DialogContent", "DrawerContent", "AlertDialogContent"].includes(
|
|
126
|
-
jsxComponentName(ancestor.openingElement, context),
|
|
127
|
-
)
|
|
128
|
-
);
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export const shadcnComposition = {
|
|
133
|
-
meta: {
|
|
134
|
-
type: "problem",
|
|
135
|
-
docs: {
|
|
136
|
-
description: "Require the shadcn primitives' composition conventions where they are composed",
|
|
137
|
-
},
|
|
138
|
-
schema: [packageOwnedPrimitivesSchema],
|
|
139
|
-
messages: {
|
|
140
|
-
overlayTitle:
|
|
141
|
-
'{{tag}} must include {{title}}; use className="sr-only" when the title is visually hidden.',
|
|
142
|
-
selectGroup: "SelectItem must be rendered inside SelectGroup.",
|
|
143
|
-
avatarFallback: "AvatarImage requires an AvatarFallback in the same Avatar.",
|
|
144
|
-
buttonIcon: "An icon inside Button must carry data-icon.",
|
|
145
|
-
settingsFooter:
|
|
146
|
-
"Put a SettingsSection submit action in its footer prop; the shared component owns the divider, spacing, and action position.",
|
|
147
|
-
fieldContent:
|
|
148
|
-
"Group a Field's label and description in FieldContent, not a plain element; a horizontal Field aligns its control to the text through that slot.",
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
create(context) {
|
|
152
|
-
if (skipsPackageOwnedPrimitive(context)) {
|
|
153
|
-
return {};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
JSXOpeningElement(node) {
|
|
158
|
-
const tag = elementName(node);
|
|
159
|
-
if (!tag) return;
|
|
160
|
-
const element = node.parent;
|
|
161
|
-
|
|
162
|
-
if (misplacedSettingsSubmit(node, context)) {
|
|
163
|
-
context.report({ node, messageId: "settingsFooter" });
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (fieldTextOutsideContent(node)) {
|
|
167
|
-
context.report({ node, messageId: "fieldContent" });
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const title = OVERLAY_TITLES[tag];
|
|
171
|
-
if (title && !hasDescendant(element, title)) {
|
|
172
|
-
context.report({ node, messageId: "overlayTitle", data: { tag, title } });
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (tag === "Avatar" && hasDescendant(element, "AvatarImage")) {
|
|
176
|
-
if (!hasDescendant(element, "AvatarFallback")) {
|
|
177
|
-
context.report({ node, messageId: "avatarFallback" });
|
|
178
|
-
}
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (tag !== "SelectItem" && tag !== "AvatarImage" && !isIconTag(tag)) return;
|
|
183
|
-
|
|
184
|
-
const ancestors = ancestorNames(node, context.sourceCode);
|
|
185
|
-
if (tag === "SelectItem" && !ancestors.includes("SelectGroup")) {
|
|
186
|
-
context.report({ node, messageId: "selectGroup" });
|
|
187
|
-
}
|
|
188
|
-
if (tag === "AvatarImage" && !ancestors.includes("Avatar")) {
|
|
189
|
-
context.report({ node, messageId: "avatarFallback" });
|
|
190
|
-
}
|
|
191
|
-
if (isIconTag(tag) && ancestors.includes("Button") && !hasAttribute(node, "data-icon")) {
|
|
192
|
-
context.report({ node, messageId: "buttonIcon" });
|
|
193
|
-
}
|
|
194
|
-
},
|
|
195
|
-
};
|
|
196
|
-
},
|
|
197
|
-
};
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* A table column wraps by its definition, never by its cells. DataTable holds a
|
|
3
|
-
* fact on one line and gives a column marked `meta: { prose: true }` a floor
|
|
4
|
-
* and wrapping between words (DESIGN.md, "Tables and missing values"). A cell
|
|
5
|
-
* that sets its own wrapping gets neither the floor nor the identity's share
|
|
6
|
-
* of the width, and the table squeezes it until each word takes a line.
|
|
7
|
-
*
|
|
8
|
-
* Standard tool checked first: `better-tailwindcss`'s `no-restricted-classes`
|
|
9
|
-
* judges one class at a time, and `whitespace-normal` is right in a button or a
|
|
10
|
-
* card. What is wrong is where it is written: inside a column's `cell`, which
|
|
11
|
-
* this rule reads on the shared className reader.
|
|
12
|
-
*/
|
|
13
|
-
import { openingElementClassUtilities } from "./jsx-class-utility.js";
|
|
14
|
-
|
|
15
|
-
const WRAPPING = /^(?:.*:)?whitespace-(?:normal|pre-line|pre-wrap)$/;
|
|
16
|
-
|
|
17
|
-
/** Inside the `cell` renderer of a column definition object. */
|
|
18
|
-
function insideColumnCell(node) {
|
|
19
|
-
for (let current = node.parent; current; current = current.parent) {
|
|
20
|
-
if (
|
|
21
|
-
current.type === "Property" &&
|
|
22
|
-
!current.computed &&
|
|
23
|
-
current.key.type === "Identifier" &&
|
|
24
|
-
current.key.name === "cell"
|
|
25
|
-
) {
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export const tableColumnWrapsByMeta = {
|
|
33
|
-
meta: {
|
|
34
|
-
type: "problem",
|
|
35
|
-
docs: {
|
|
36
|
-
description: "Disallow a table cell setting its own wrapping; mark the column prose",
|
|
37
|
-
},
|
|
38
|
-
schema: [],
|
|
39
|
-
messages: {
|
|
40
|
-
cellWraps:
|
|
41
|
-
'"{{token}}" wraps one cell of a table column. Mark the column `meta: { prose: true }` so DataTable gives it a floor and wraps it between words.',
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
create(context) {
|
|
45
|
-
return {
|
|
46
|
-
JSXOpeningElement(node) {
|
|
47
|
-
if (!insideColumnCell(node)) return;
|
|
48
|
-
for (const token of openingElementClassUtilities(node, context)) {
|
|
49
|
-
if (WRAPPING.test(token))
|
|
50
|
-
context.report({ node, messageId: "cellWraps", data: { token } });
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
},
|
|
55
|
-
};
|