@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,156 +0,0 @@
1
- /*
2
- * A control whose whole content is an icon still has to say what it does.
3
- *
4
- * An icon is a picture, and a screen reader announces "button" and nothing else
5
- * for one. Sighted readers recover the meaning from the glyph; a screen-reader
6
- * user, and anyone addressing the control by name through voice control, gets an
7
- * unnamed button. The name comes from `aria-label`, from `aria-labelledby`, or
8
- * from text inside the control that is hidden visually rather than from the
9
- * accessibility tree — the `sr-only` span this repository already writes beside
10
- * its close buttons. That last spelling needs no separate test here: hidden text
11
- * is text, so a control carrying one is not icon-only in the first place.
12
- *
13
- * `title` is NOT one of them. It does name the control for most screen readers,
14
- * but it also mints a browser tooltip, and the copy contract (the Tooltip
15
- * contract in `skills/design/product-design/references/copy.md`) says the title
16
- * attribute is not this product's help system. Accepting it here would make the
17
- * rule push authors toward the one tooltip the design system refuses.
18
- *
19
- * Two shapes are icon-only, and both are read off the element rather than out of
20
- * a list: a `Button` on one of the icon size variants (`icon`, `icon-xs`,
21
- * `icon-sm`, `icon-lg`), and any `Button` or `button` whose children render
22
- * elements and no text at all.
23
- *
24
- * Two shapes are deliberately out of scope. A `Button` written as the value of a
25
- * `render` prop is a STYLE carrier: the primitive that receives it owns the
26
- * rendered element's children and its label, as `<DropdownMenuTrigger
27
- * aria-label={label} render={<Button size="icon-sm" />}>` does in
28
- * `packages/ui/src/components/action-menu.tsx`, so the name is not this element's
29
- * to carry. An element with a `{...props}` spread may be handed a label by its
30
- * caller, and this rule does not guess what a spread holds.
31
- *
32
- * Standard tool checked first: the root config runs jsx-a11y's `alt-text`,
33
- * `aria-props`, `aria-proptypes`, `aria-unsupported-elements`,
34
- * `role-has-required-aria-props` and `role-supports-aria-props`, and Next's
35
- * core-web-vitals config adds no more. Every one of them judges an attribute
36
- * that is already present; none asks whether a control has a name at all.
37
- * `control-has-associated-label` is the upstream rule that would, and it is
38
- * enabled nowhere, looks for a label element rather than an accessible name, and
39
- * knows nothing about a size variant that means "icon".
40
- */
41
- import { jsxComponentName } from "./jsx-class-utility.js";
42
-
43
- const CONTROL_TAGS = new Set(["Button", "button"]);
44
- const ICON_SIZES = new Set(["icon", "icon-xs", "icon-sm", "icon-lg"]);
45
- const NAME_ATTRIBUTES = new Set(["aria-label", "aria-labelledby"]);
46
-
47
- function attributeName(attribute) {
48
- return attribute.type === "JSXAttribute" && attribute.name.type === "JSXIdentifier"
49
- ? attribute.name.name
50
- : undefined;
51
- }
52
-
53
- /** The literal string an attribute carries, or undefined for anything computed. */
54
- function literalAttributeValue(openingElement, name) {
55
- for (const attribute of openingElement.attributes) {
56
- if (attributeName(attribute) !== name) continue;
57
- const value = attribute.value;
58
- return value?.type === "Literal" && typeof value.value === "string" ? value.value : undefined;
59
- }
60
- return undefined;
61
- }
62
-
63
- /** True when this element stands in for another primitive's rendered element. */
64
- function isRenderProp(node, sourceCode) {
65
- return sourceCode.getAncestors(node).some((ancestor) => attributeName(ancestor) === "render");
66
- }
67
-
68
- /*
69
- * Whether an expression can only ever evaluate to JSX or to nothing. `{label}`
70
- * and `{day.getDate()}` are text; `{copied ? <Check /> : <Copy />}` is not.
71
- */
72
- function producesOnlyElements(expression) {
73
- switch (expression.type) {
74
- case "JSXEmptyExpression":
75
- case "JSXElement":
76
- case "JSXFragment":
77
- return true;
78
- case "ConditionalExpression":
79
- return (
80
- producesOnlyElements(expression.consequent) && producesOnlyElements(expression.alternate)
81
- );
82
- case "LogicalExpression":
83
- return producesOnlyElements(expression.right);
84
- case "Literal":
85
- return expression.value === null || expression.value === false;
86
- case "Identifier":
87
- return expression.name === "undefined";
88
- default:
89
- return false;
90
- }
91
- }
92
-
93
- /** Whether a subtree renders any text, visible or visually hidden. */
94
- function rendersText(node) {
95
- for (const child of node.children ?? []) {
96
- if (child.type === "JSXText") {
97
- if (child.value.trim() !== "") return true;
98
- continue;
99
- }
100
- if (child.type === "JSXElement" || child.type === "JSXFragment") {
101
- if (rendersText(child)) return true;
102
- continue;
103
- }
104
- if (child.type === "JSXExpressionContainer" && !producesOnlyElements(child.expression)) {
105
- return true;
106
- }
107
- }
108
- return false;
109
- }
110
-
111
- /** Whether a subtree renders at least one element of its own. */
112
- function rendersElement(node) {
113
- return (node.children ?? []).some(
114
- (child) =>
115
- child.type === "JSXElement" ||
116
- child.type === "JSXFragment" ||
117
- (child.type === "JSXExpressionContainer" &&
118
- child.expression.type !== "JSXEmptyExpression" &&
119
- producesOnlyElements(child.expression)),
120
- );
121
- }
122
-
123
- export const iconControlAccessibleName = {
124
- meta: {
125
- type: "problem",
126
- docs: {
127
- description: "Require an accessible name on a control whose only content is an icon",
128
- },
129
- schema: [],
130
- messages: {
131
- unnamedIconControl:
132
- 'A screen reader announces "{{tag}}" and nothing more for this control: its content is an icon, and an icon carries no name. Add aria-label with the action it performs, point aria-labelledby at the text that names it, or put that text in a child with the sr-only class.',
133
- },
134
- },
135
- create(context) {
136
- return {
137
- JSXOpeningElement(node) {
138
- const tag = jsxComponentName(node, context);
139
- if (!tag || !CONTROL_TAGS.has(tag)) return;
140
- if (node.attributes.some((attribute) => attribute.type === "JSXSpreadAttribute")) return;
141
- if (
142
- node.attributes.some((attribute) => NAME_ATTRIBUTES.has(attributeName(attribute) ?? ""))
143
- )
144
- return;
145
- if (isRenderProp(node, context.sourceCode)) return;
146
-
147
- const element = node.parent;
148
- const iconSized = ICON_SIZES.has(literalAttributeValue(node, "size") ?? "");
149
- if (!iconSized && !rendersElement(element)) return;
150
- if (rendersText(element)) return;
151
-
152
- context.report({ node, messageId: "unnamedIconControl", data: { tag } });
153
- },
154
- };
155
- },
156
- };
@@ -1,45 +0,0 @@
1
- import { jsxComponentName } from "./jsx-class-utility.js";
2
-
3
- export const menuCommandOwner = {
4
- meta: {
5
- type: "problem",
6
- docs: { description: "Keep command labels and icons in the shared menu composition." },
7
- schema: [],
8
- messages: {
9
- command:
10
- "Use DropdownMenuCommand with its required icon; it owns readable labels and trailing icon layout. DropdownMenuItem is only for an iconOnly control here.",
11
- trigger:
12
- "Use ActionMenu for three-dot commands so placement, naming, and popup behavior have one owner.",
13
- },
14
- },
15
- create(context) {
16
- return {
17
- JSXOpeningElement(node) {
18
- const name = jsxComponentName(node, context);
19
- if (
20
- name === "DropdownMenuItem" &&
21
- !node.attributes.some(
22
- (attribute) =>
23
- attribute.type === "JSXAttribute" &&
24
- attribute.name.name === "iconOnly" &&
25
- (attribute.value === null ||
26
- (attribute.value.type === "JSXExpressionContainer" &&
27
- attribute.value.expression.value === true)),
28
- )
29
- )
30
- context.report({ node, messageId: "command" });
31
- if (
32
- [
33
- "Ellipsis",
34
- "EllipsisIcon",
35
- "EllipsisVertical",
36
- "MoreHorizontal",
37
- "MoreVertical",
38
- ].includes(name)
39
- ) {
40
- context.report({ node, messageId: "trigger" });
41
- }
42
- },
43
- };
44
- },
45
- };
@@ -1,46 +0,0 @@
1
- /*
2
- * The Portal's browser suite navigates through `tests/support/hydration.ts`.
3
- * `page.goto` and `page.reload` resolve while React is still hydrating, and the
4
- * interaction a spec dispatches next lands on server markup the shell is about
5
- * to replace: #2173 lost six open menus that way, #2776 clicked "Create team"
6
- * on a page that had not taken over yet, and #2837 read a prefetch as a
7
- * refresh. `back` and `forward` are outside the rule, because they restore a
8
- * tree the client already owns rather than loading a document.
9
- */
10
-
11
- const NAVIGATIONS = new Map([
12
- ["goto", "bareGoto"],
13
- ["reload", "bareReload"],
14
- ]);
15
-
16
- function propertyName(member) {
17
- if (member.computed) return member.property.type === "Literal" ? member.property.value : null;
18
- return member.property.type === "Identifier" ? member.property.name : null;
19
- }
20
-
21
- export const noBarePageNavigation = {
22
- meta: {
23
- type: "problem",
24
- docs: {
25
- description:
26
- "Disallow a bare page.goto or page.reload in the Portal's browser suite; navigate through the helper that waits for the shell to hydrate",
27
- },
28
- schema: [],
29
- messages: {
30
- bareGoto:
31
- "`goto()` resolves before React hydrates the page, so the next click or keypress lands on server markup the shell is about to replace (#2173, #2776, #2837). Open the page with `gotoPortal` from `tests/support/hydration`, which waits for the shell's `data-hydrated`, or with `gotoOutsideShell` for an address the dashboard shell never renders, such as login, signup or an app's consent page.",
32
- bareReload:
33
- "`reload()` resolves before React hydrates the page, so the next click or keypress lands on server markup the shell is about to replace (#2776). Reload with `reloadPortal` from `tests/support/hydration`, which waits for the shell's `data-hydrated`.",
34
- },
35
- },
36
-
37
- create(context) {
38
- return {
39
- CallExpression(node) {
40
- if (node.callee.type !== "MemberExpression") return;
41
- const messageId = NAVIGATIONS.get(propertyName(node.callee));
42
- if (messageId) context.report({ node, messageId });
43
- },
44
- };
45
- },
46
- };
@@ -1,141 +0,0 @@
1
- /*
2
- * An elevated surface draws its own edge. `nc-elevation-*` paints the rung's
3
- * whole recipe, whose last layer is its own optical ring (DESIGN.md,
4
- * "core.elevation"), so a border or a second ring on the same target paints a
5
- * second edge over it: the seam this repository keeps reintroducing when a
6
- * stock shadcn shape is pasted next to an elevation.
7
- *
8
- * Standard tool checked first: `better-tailwindcss`'s
9
- * `no-restricted-classes` judges one class at a time. `border` and `ring-1` are
10
- * legal on their own — they are wrong only beside an elevation, and only when
11
- * both land on the same target, so `*:data-[slot=input]:border-input` under an
12
- * elevated parent stays legal. That relationship is what this rule reads, on
13
- * the shared className reader.
14
- *
15
- * Replaces the @nadicodeai/ui material-surface-contract scanner (#822), which
16
- * built its own TypeScript AST over the package's sources to ask the same
17
- * question. It takes no `packageOwnedPrimitives` option: the material ladder
18
- * holds wherever an elevation is written, inside the package and out.
19
- */
20
- import { nodeClassUtilities } from "./jsx-class-utility.js";
21
-
22
- const ELEVATION = /^nc-elevation-(?:resting|raised|floating|modal)$/;
23
-
24
- /*
25
- * The callees that compose a class list out of several strings. A class written
26
- * in one argument and an elevation in another are one surface, so the call —
27
- * not the string — is the unit this rule reviews.
28
- */
29
- const COMPOSERS = new Set(["cn", "clsx", "cva"]);
30
-
31
- /** `md:hover:border` → `{ modifiers: ["md", "hover"], utility: "border" }`. */
32
- function tailwindToken(token) {
33
- const segments = [];
34
- let bracketDepth = 0;
35
- let start = 0;
36
- for (let index = 0; index < token.length; index += 1) {
37
- const character = token[index];
38
- if (character === "[") bracketDepth += 1;
39
- if (character === "]") bracketDepth = Math.max(0, bracketDepth - 1);
40
- // A colon inside brackets belongs to an arbitrary value, not to a variant.
41
- if (character === ":" && bracketDepth === 0) {
42
- segments.push(token.slice(start, index));
43
- start = index + 1;
44
- }
45
- }
46
- segments.push(token.slice(start));
47
- return { modifiers: segments.slice(0, -1), utility: segments.at(-1) ?? token };
48
- }
49
-
50
- /**
51
- * What a utility paints on: the element itself, or the descendants a variant
52
- * selects. Two utilities collide only when this matches — an elevation on the
53
- * root and a border on `*:data-[slot=input]` are different surfaces.
54
- */
55
- function targetKey(modifiers) {
56
- const descendants = modifiers.filter(
57
- (modifier) => modifier === "*" || modifier === "**" || /^\[&(?:_|>|\s)/.test(modifier),
58
- );
59
- return descendants.length === 0 ? "self" : descendants.join(":");
60
- }
61
-
62
- /** A width of zero is the absence of an edge, which is what an elevation wants. */
63
- function isZeroWidthUtility(utility) {
64
- return utility === "border-0" || utility === "ring-0";
65
- }
66
-
67
- function edgeRule(utility) {
68
- if (/^border(?:$|-)/.test(utility) && !isZeroWidthUtility(utility)) return "borderWithElevation";
69
- if (/^ring(?:$|-)/.test(utility) && !isZeroWidthUtility(utility)) return "ringWithElevation";
70
- return undefined;
71
- }
72
-
73
- /** One finding per offending edge, however many elevations it sits beside. */
74
- function edgeViolations(classes) {
75
- const tokens = classes.map((token) => ({ token, ...tailwindToken(token) }));
76
- const elevations = tokens.filter(({ utility }) => ELEVATION.test(utility));
77
- const violations = new Map();
78
- for (const elevation of elevations) {
79
- const elevationTarget = targetKey(elevation.modifiers);
80
- for (const candidate of tokens) {
81
- if (targetKey(candidate.modifiers) !== elevationTarget) continue;
82
- const messageId = edgeRule(candidate.utility);
83
- if (messageId && !violations.has(candidate.token)) {
84
- violations.set(candidate.token, { messageId, shadow: elevation.token });
85
- }
86
- }
87
- }
88
- return [...violations].map(([token, finding]) => ({ token, ...finding }));
89
- }
90
-
91
- function isComposerCall(node) {
92
- return (
93
- node?.type === "CallExpression" &&
94
- node.callee.type === "Identifier" &&
95
- COMPOSERS.has(node.callee.name)
96
- );
97
- }
98
-
99
- function insideComposerCall(node) {
100
- for (let current = node.parent; current; current = current.parent) {
101
- if (isComposerCall(current)) return true;
102
- }
103
- return false;
104
- }
105
-
106
- export const noElevatedEdge = {
107
- meta: {
108
- type: "problem",
109
- docs: {
110
- description:
111
- "Disallow a border or a second ring on the same target as an nc-elevation-* utility",
112
- },
113
- schema: [],
114
- messages: {
115
- borderWithElevation:
116
- '"{{token}}" draws a hard border on the same target as {{shadow}}. An elevated surface carries its own optical ring: drop the border, or drop the elevation.',
117
- ringWithElevation:
118
- '"{{token}}" draws a second ring on the same target as {{shadow}}. An elevated surface carries its own optical ring already.',
119
- },
120
- },
121
- create(context) {
122
- const report = (node, classes) => {
123
- for (const { token, messageId, shadow } of edgeViolations(classes)) {
124
- context.report({ node, messageId, data: { token, shadow } });
125
- }
126
- };
127
-
128
- return {
129
- CallExpression(node) {
130
- // The outermost composer owns the surface: a nested one's strings are
131
- // already in its list, so reviewing both would report twice.
132
- if (!isComposerCall(node) || insideComposerCall(node)) return;
133
- report(node, nodeClassUtilities(node, context, { skipPropertyKeys: true }));
134
- },
135
- Literal(node) {
136
- if (typeof node.value !== "string" || insideComposerCall(node)) return;
137
- report(node, node.value.split(/\s+/).filter(Boolean));
138
- },
139
- };
140
- },
141
- };
@@ -1,89 +0,0 @@
1
- /*
2
- * An assertion that something exists is not an assertion about behavior.
3
- *
4
- * Two shapes in this suite pass for nearly any implementation.
5
- *
6
- * A `&&` or `||` subject returns one of its operands, not a boolean, so the
7
- * short-circuit alone can satisfy the matcher: the left operand decides the
8
- * result and the right one is never evaluated.
9
- *
10
- * An optional call yields `undefined` when the method is gone. That is exactly
11
- * what `toBeFalsy()` and `toBeUndefined()` accept, which is how
12
- * `expect(options.clientPrivileges?.()).toBeFalsy()` in
13
- * `apps/portal/src/lib/server/nadia-oauth-provider.test.ts` kept asserting that
14
- * OAuth grants no configuration privileges after the privilege hooks had been
15
- * deleted from the provider.
16
- *
17
- * What is deliberately left alone: `toBeDefined()` / `toBeTruthy()` on any
18
- * other subject. On a plain identifier or member read it is the type-narrowing
19
- * step before reading a property off it. On a Testing Library `getBy*` query it
20
- * is redundant, since the query already throws, but it is the suite's idiom for
21
- * "this element renders" and replacing it mechanically produces
22
- * `expect(getByText("X").textContent).toBe("X")`, which is a tautology of its
23
- * own: 214 such sites were rewritten that way once and reverted. `toBeNull()`
24
- * and `toBeUndefined()` on any other subject are a real claim of absence.
25
- *
26
- * Standard tools checked first: `vitest/expect-expect` refuses a test with no
27
- * assertion at all and `vitest/valid-expect` checks the call's arity; neither
28
- * asks what the asserted expression can produce, which is the whole question
29
- * here.
30
- */
31
- import { expectAssertion } from "./expect-chain.js";
32
-
33
- const SHORT_CIRCUIT_MATCHERS = new Set(["toBeDefined", "toBeTruthy", "toBeFalsy", "toBeUndefined"]);
34
- const MISSING_VALUE_MATCHERS = new Set(["toBeFalsy", "toBeUndefined"]);
35
-
36
- function isOptionalCall(node) {
37
- return node?.type === "CallExpression" && node.optional === true;
38
- }
39
-
40
- export const noExistenceOnlyAssertion = {
41
- meta: {
42
- type: "problem",
43
- docs: {
44
- description:
45
- "Disallow expectations that only assert a value exists, which pass for almost any implementation",
46
- },
47
- schema: [],
48
- messages: {
49
- shortCircuitSubject:
50
- "`{{operator}}` returns one of its operands rather than a boolean, so `{{assertion}}` passes on the short-circuit alone, with the right operand never evaluated. Assert each operand separately, or assert the value the expression stands for.",
51
- optionalCallAbsence:
52
- "`{{assertion}}` on an optional call passes when the call never happens: a deleted or renamed method makes `?.()` yield `undefined`, which is both falsy and undefined. Assert the method is present, then assert what the call returns.",
53
- },
54
- },
55
-
56
- create(context) {
57
- function assertionText(matcher, modifiers) {
58
- return [...modifiers, `${matcher}()`].join(".");
59
- }
60
-
61
- return {
62
- CallExpression(node) {
63
- const assertion = expectAssertion(node);
64
- if (!assertion) return;
65
- const { matcher, modifiers, subject } = assertion;
66
- if (!subject) return;
67
-
68
- if (modifiers.length > 0) return;
69
-
70
- if (subject.type === "LogicalExpression" && SHORT_CIRCUIT_MATCHERS.has(matcher)) {
71
- context.report({
72
- node,
73
- messageId: "shortCircuitSubject",
74
- data: { operator: subject.operator, assertion: assertionText(matcher, modifiers) },
75
- });
76
- return;
77
- }
78
-
79
- if (isOptionalCall(subject) && MISSING_VALUE_MATCHERS.has(matcher)) {
80
- context.report({
81
- node,
82
- messageId: "optionalCallAbsence",
83
- data: { assertion: assertionText(matcher, modifiers) },
84
- });
85
- }
86
- },
87
- };
88
- },
89
- };
@@ -1,46 +0,0 @@
1
- import { createRequire } from "node:module";
2
-
3
- import { tokenUtilityVisitors } from "./jsx-class-utility.js";
4
-
5
- const require = createRequire(import.meta.url);
6
- const generatedTheme = require("@nadicodeai/design-system/tailwind").theme.extend;
7
- const typographyUtilities = new Set(
8
- Object.keys(generatedTheme.fontSize).map((name) => `nc-type-${name}`),
9
- );
10
- const colorTokens = new Set(Object.keys(generatedTheme.colors));
11
- const COLOR_UTILITY =
12
- /^(?:bg|text|border|outline|ring|fill|stroke|decoration|accent|caret|divide|from|via|to|shadow)-nc-([^/]+)(?:\/.*)?$/;
13
-
14
- function withoutVariants(utility) {
15
- return utility.slice(utility.lastIndexOf(":") + 1);
16
- }
17
-
18
- function isInvalidGeneratedUtility(utility) {
19
- const unqualified = withoutVariants(utility);
20
- if (unqualified.startsWith("nc-type-")) {
21
- return !typographyUtilities.has(unqualified);
22
- }
23
-
24
- const color = COLOR_UTILITY.exec(unqualified);
25
- return color ? !colorTokens.has(color[1]) : false;
26
- }
27
-
28
- export const noInvalidTokenUtility = {
29
- meta: {
30
- type: "problem",
31
- docs: {
32
- description: "Disallow token utilities that are absent from generated design-system output",
33
- },
34
- schema: [],
35
- messages: {
36
- invalidTokenUtility:
37
- "{{utility}} is not generated by the installed nadicode design system. Use a utility from @nadicodeai/design-system/tailwind.",
38
- },
39
- },
40
- create(context) {
41
- return tokenUtilityVisitors(context, (node, utility) => {
42
- if (!isInvalidGeneratedUtility(utility)) return;
43
- context.report({ node, messageId: "invalidTokenUtility", data: { utility } });
44
- });
45
- },
46
- };
@@ -1,73 +0,0 @@
1
- /*
2
- * Design law 9: a title is prose, so it never renders in the mono-caps
3
- * register. The rule is mechanical because the register is — `font-mono`
4
- * together with `uppercase` on a slot that carries a title — and everything
5
- * judgment-bearing stays out of it: an eyebrow, a fact label, an identifier or
6
- * a marketing label may all be mono, and none of them is a title slot.
7
- *
8
- * Standard tool checked first: `better-tailwindcss`'s
9
- * `no-restricted-classes` judges one class at a time and never against the
10
- * element it lands on, so it can ban `font-mono` everywhere or nowhere. The
11
- * register is a PAIR of classes on a TITLE element, which is a relationship —
12
- * so it lives here, on the shared className reader.
13
- *
14
- * Replaces `monoTitleRegisterViolations` in the retired portal page-grammar
15
- * analyzers (#821).
16
- */
17
- import { openingElementClassUtilities } from "./jsx-class-utility.js";
18
-
19
- // The kit's title elements plus the hand-written headings.
20
- const TITLE_TAGS = new Set(["CardTitle", "h1", "h2", "h3", "h4"]);
21
- const TITLE_SLOT = /-title$/;
22
-
23
- function withoutVariants(utility) {
24
- return utility.slice(utility.lastIndexOf(":") + 1);
25
- }
26
-
27
- function slotName(node) {
28
- for (const attribute of node.attributes) {
29
- if (attribute.type !== "JSXAttribute" || attribute.name.name !== "data-slot") continue;
30
- const value = attribute.value;
31
- if (value?.type === "Literal" && typeof value.value === "string") return value.value;
32
- if (
33
- value?.type === "JSXExpressionContainer" &&
34
- value.expression.type === "Literal" &&
35
- typeof value.expression.value === "string"
36
- ) {
37
- return value.expression.value;
38
- }
39
- }
40
- return undefined;
41
- }
42
-
43
- function isTitleElement(node) {
44
- if (node.name.type !== "JSXIdentifier") return false;
45
- if (TITLE_TAGS.has(node.name.name)) return true;
46
- const slot = slotName(node);
47
- return Boolean(slot && TITLE_SLOT.test(slot));
48
- }
49
-
50
- export const noMonoTitleRegister = {
51
- meta: {
52
- type: "problem",
53
- docs: {
54
- description: "Disallow the retired mono-caps register on a title slot (design law 9)",
55
- },
56
- schema: [],
57
- messages: {
58
- monoTitle:
59
- "A title never renders in the mono-caps register: drop font-mono and uppercase (design law 9).",
60
- },
61
- },
62
- create(context) {
63
- return {
64
- JSXOpeningElement(node) {
65
- if (!isTitleElement(node)) return;
66
- const utilities = openingElementClassUtilities(node, context).map(withoutVariants);
67
- if (utilities.includes("font-mono") && utilities.includes("uppercase")) {
68
- context.report({ node, messageId: "monoTitle" });
69
- }
70
- },
71
- };
72
- },
73
- };