@nadicodeai/ui 12.1.54 → 12.1.56

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 (43) hide show
  1. package/dist/eslint/index.js +27 -69
  2. package/dist/eslint/rules/jsx-class-utility.js +2 -56
  3. package/dist/eslint/rules/no-choice-control-wrapper.js +95 -0
  4. package/dist/eslint/rules/no-comment.js +44 -0
  5. package/dist/eslint/rules/no-subject-derived-expectation.js +55 -1
  6. package/dist/eslint/strict-design-system.js +224 -0
  7. package/dist/standalone/index.d.ts +6 -0
  8. package/dist/standalone/index.js +3 -0
  9. package/dist/standalone/orb-page.d.ts +2 -0
  10. package/dist/standalone/orb-page.d.ts.map +1 -0
  11. package/dist/standalone/orb-page.js +11 -0
  12. package/dist/styles/components.css +1 -1
  13. package/package.json +7 -2
  14. package/dist/.build-kind +0 -1
  15. package/dist/eslint/rules/card-row-owner.js +0 -167
  16. package/dist/eslint/rules/confirm-destructive-action.js +0 -177
  17. package/dist/eslint/rules/drawer-owns-its-edges.js +0 -66
  18. package/dist/eslint/rules/expect-chain.js +0 -60
  19. package/dist/eslint/rules/icon-control-accessible-name.js +0 -156
  20. package/dist/eslint/rules/menu-command-owner.js +0 -45
  21. package/dist/eslint/rules/no-bare-page-navigation.js +0 -46
  22. package/dist/eslint/rules/no-elevated-edge.js +0 -141
  23. package/dist/eslint/rules/no-existence-only-assertion.js +0 -89
  24. package/dist/eslint/rules/no-invalid-token-utility.js +0 -46
  25. package/dist/eslint/rules/no-mono-title-register.js +0 -73
  26. package/dist/eslint/rules/no-native-visible-control.js +0 -92
  27. package/dist/eslint/rules/no-nested-overlay.js +0 -56
  28. package/dist/eslint/rules/no-off-grid-spacing.js +0 -115
  29. package/dist/eslint/rules/no-paired-size-utility.js +0 -66
  30. package/dist/eslint/rules/no-raw-shadow.js +0 -70
  31. package/dist/eslint/rules/no-repainted-focus.js +0 -75
  32. package/dist/eslint/rules/no-resting-settings-control.js +0 -89
  33. package/dist/eslint/rules/no-route-width-override.js +0 -135
  34. package/dist/eslint/rules/no-store-cast.js +0 -46
  35. package/dist/eslint/rules/no-token-category-bypass.js +0 -53
  36. package/dist/eslint/rules/no-unpinned-vercel-functions-import.js +0 -104
  37. package/dist/eslint/rules/one-control-height.js +0 -163
  38. package/dist/eslint/rules/overlay-body-owns-scroll.js +0 -62
  39. package/dist/eslint/rules/overlay-mounts-closed.js +0 -102
  40. package/dist/eslint/rules/prefer-radio-for-few-static-options.js +0 -77
  41. package/dist/eslint/rules/settings-section-description.js +0 -45
  42. package/dist/eslint/rules/shadcn-composition.js +0 -197
  43. package/dist/eslint/rules/table-column-wraps-by-meta.js +0 -55
@@ -1,77 +0,0 @@
1
- /*
2
- * A short fixed set of choices is shown, not hidden.
3
- *
4
- * A `Select` spends a click and a popup to reveal what it holds. That trade pays
5
- * for itself over a long or a loaded list; it does not pay for two or three
6
- * options that are known at author time, where the popup costs the reader the
7
- * one thing they came for — seeing what the alternatives are — and costs the
8
- * keyboard user an open-then-arrow instead of a single arrow.
9
- *
10
- * The count has to be STATIC to be a finding. A `SelectContent` filled from
11
- * `{items.map(...)}` holds however many rows the data has today, so its shape is
12
- * not this rule's business and any expression child stands the rule down.
13
- *
14
- * Standard tool checked first: `no-restricted-syntax` can select a
15
- * `SelectContent`, but not one whose children are exactly two or three elements
16
- * of one name and nothing dynamic. That is a property of the subtree, so it is
17
- * read here.
18
- */
19
- import { jsxComponentName } from "./jsx-class-utility.js";
20
-
21
- const CONTENT_TAG = "SelectContent";
22
- const ITEM_TAG = "SelectItem";
23
-
24
- /*
25
- * The items one content subtree renders, and whether anything in it is computed.
26
- * Groups and separators are descended through; an item's own children are not,
27
- * because a label built from a variable says nothing about how many items there
28
- * are. Attributes are out of scope for the same reason: `value={option.value}`
29
- * on a hand-written item is ordinary.
30
- */
31
- function scanContent(node, context, found) {
32
- for (const child of node.children) {
33
- if (child.type === "JSXExpressionContainer") {
34
- if (child.expression.type !== "JSXEmptyExpression") found.dynamic = true;
35
- continue;
36
- }
37
- if (child.type === "JSXFragment") {
38
- scanContent(child, context, found);
39
- continue;
40
- }
41
- if (child.type !== "JSXElement") continue;
42
- if (jsxComponentName(child.openingElement, context) === ITEM_TAG) {
43
- found.items.push(child);
44
- continue;
45
- }
46
- scanContent(child, context, found);
47
- }
48
- return found;
49
- }
50
-
51
- export const preferRadioForFewStaticOptions = {
52
- meta: {
53
- type: "suggestion",
54
- docs: {
55
- description: "Prefer RadioGroup over Select for two or three statically written options",
56
- },
57
- schema: [],
58
- messages: {
59
- fewStaticOptions:
60
- "This Select holds {{count}} options fixed at author time, so a reader has to open it to learn what the alternatives are. RadioGroup shows all {{count}} at once and takes one arrow key instead of an open and an arrow. Use RadioGroup with a RadioGroupItem per option.",
61
- },
62
- },
63
- create(context) {
64
- return {
65
- JSXElement(node) {
66
- if (jsxComponentName(node.openingElement, context) !== CONTENT_TAG) return;
67
- const { items, dynamic } = scanContent(node, context, { items: [], dynamic: false });
68
- if (dynamic || items.length < 2 || items.length > 3) return;
69
- context.report({
70
- node: node.openingElement,
71
- messageId: "fewStaticOptions",
72
- data: { count: String(items.length) },
73
- });
74
- },
75
- };
76
- },
77
- };
@@ -1,45 +0,0 @@
1
- /*
2
- * Every settings section says what it changes.
3
- *
4
- * DESIGN.md ("Cards and sections") gives a settings section a one-line
5
- * description under its title, the line that tells a person what changing the
6
- * fields below will do before they change them. The failure this catches: the
7
- * Portal's Profile photo and Personal details sections, whose titles named the
8
- * content and nothing said what the change reaches.
9
- *
10
- * What is read is a `SettingsSection` element and whether it passes a
11
- * `description` attribute. A spread may carry one, so an element with a spread
12
- * attribute is not reported.
13
- *
14
- * Standard tool checked first: `no-restricted-syntax` can select
15
- * `JSXOpeningElement[name.name="SettingsSection"]:not(:has(JSXAttribute[name.name="description"]))`,
16
- * but not through a renamed import, which `jsxComponentName` resolves.
17
- */
18
- import { jsxComponentName } from "./jsx-class-utility.js";
19
-
20
- export const settingsSectionDescription = {
21
- meta: {
22
- type: "problem",
23
- docs: {
24
- description: "Require a one-line description on every SettingsSection",
25
- },
26
- schema: [],
27
- messages: {
28
- missingDescription:
29
- 'This SettingsSection has no description. Say in one line what changing it does, under its title (DESIGN.md "Cards and sections", rule/settings-section-described).',
30
- },
31
- },
32
- create(context) {
33
- return {
34
- JSXOpeningElement(node) {
35
- if (jsxComponentName(node, context) !== "SettingsSection") return;
36
- const described = node.attributes.some(
37
- (attribute) =>
38
- attribute.type === "JSXSpreadAttribute" ||
39
- (attribute.name?.type === "JSXIdentifier" && attribute.name.name === "description"),
40
- );
41
- if (!described) context.report({ node, messageId: "missingDescription" });
42
- },
43
- };
44
- },
45
- };
@@ -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
- };