@nadicodeai/ui 12.1.54 → 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.
Files changed (38) 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/styles/components.css +1 -1
  8. package/package.json +2 -2
  9. package/dist/.build-kind +0 -1
  10. package/dist/eslint/rules/card-row-owner.js +0 -167
  11. package/dist/eslint/rules/confirm-destructive-action.js +0 -177
  12. package/dist/eslint/rules/drawer-owns-its-edges.js +0 -66
  13. package/dist/eslint/rules/expect-chain.js +0 -60
  14. package/dist/eslint/rules/icon-control-accessible-name.js +0 -156
  15. package/dist/eslint/rules/menu-command-owner.js +0 -45
  16. package/dist/eslint/rules/no-bare-page-navigation.js +0 -46
  17. package/dist/eslint/rules/no-elevated-edge.js +0 -141
  18. package/dist/eslint/rules/no-existence-only-assertion.js +0 -89
  19. package/dist/eslint/rules/no-invalid-token-utility.js +0 -46
  20. package/dist/eslint/rules/no-mono-title-register.js +0 -73
  21. package/dist/eslint/rules/no-native-visible-control.js +0 -92
  22. package/dist/eslint/rules/no-nested-overlay.js +0 -56
  23. package/dist/eslint/rules/no-off-grid-spacing.js +0 -115
  24. package/dist/eslint/rules/no-paired-size-utility.js +0 -66
  25. package/dist/eslint/rules/no-raw-shadow.js +0 -70
  26. package/dist/eslint/rules/no-repainted-focus.js +0 -75
  27. package/dist/eslint/rules/no-resting-settings-control.js +0 -89
  28. package/dist/eslint/rules/no-route-width-override.js +0 -135
  29. package/dist/eslint/rules/no-store-cast.js +0 -46
  30. package/dist/eslint/rules/no-token-category-bypass.js +0 -53
  31. package/dist/eslint/rules/no-unpinned-vercel-functions-import.js +0 -104
  32. package/dist/eslint/rules/one-control-height.js +0 -163
  33. package/dist/eslint/rules/overlay-body-owns-scroll.js +0 -62
  34. package/dist/eslint/rules/overlay-mounts-closed.js +0 -102
  35. package/dist/eslint/rules/prefer-radio-for-few-static-options.js +0 -77
  36. package/dist/eslint/rules/settings-section-description.js +0 -45
  37. package/dist/eslint/rules/shadcn-composition.js +0 -197
  38. package/dist/eslint/rules/table-column-wraps-by-meta.js +0 -55
@@ -1,115 +0,0 @@
1
- /*
2
- * The spacing ladder is closed. DESIGN.md, "Layout > Spacing": "One ladder
3
- * carries every gap, pad, and stack step. A register picks the rungs it needs;
4
- * it never interpolates."
5
- *
6
- * A pixel value typed into a spacing utility — `p-[13px]`, `gap-[7px]` — is that
7
- * interpolation. It reads as deliberate and is usually an eyeballed nudge, it
8
- * cannot move when the ladder does, and every one of them puts a second rhythm
9
- * beside the one the rest of the surface is built on, which is what makes a
10
- * layout look almost aligned.
11
- *
12
- * Only a bare pixel literal is a finding. `p-[var(--nc-cell-padding)]`,
13
- * `px-[clamp(24px,5vw,64px)]` and `top-[calc(var(--titlebar-height)+0.5rem)]` are
14
- * relationships to something the page owns rather than a measurement invented at
15
- * the call site, so they are outside this rule. So is a value that already lands
16
- * on a rung: it is the ladder, spelled the long way.
17
- *
18
- * Standard tool checked first: `better-tailwindcss`'s `no-restricted-classes`
19
- * matches a class against a pattern and could refuse every arbitrary spacing
20
- * value, but its message is the pattern's, one text for every hit. The fix here
21
- * is a different token for every value, and naming the nearest rung is the whole
22
- * point of the message, so the arithmetic lives in a rule.
23
- */
24
- import { tokenUtilityVisitors } from "./jsx-class-utility.js";
25
-
26
- /*
27
- * The ladder as the theme emits it: `--spacing-nadicode-*`, which Tailwind
28
- * spells `p-nadicode-md`, `gap-nadicode-lg` and so on. These are the rungs a
29
- * step may take and the ones a message offers.
30
- */
31
- const LADDER = [
32
- [0, "0"],
33
- [4, "nadicode-xxs"],
34
- [8, "nadicode-xs"],
35
- [12, "nadicode-sm"],
36
- [16, "nadicode-md"],
37
- [24, "nadicode-lg"],
38
- [32, "nadicode-xl"],
39
- [40, "nadicode-2xl"],
40
- [48, "nadicode-3xl"],
41
- [64, "nadicode-4xl"],
42
- [96, "nadicode-5xl"],
43
- [128, "nadicode-6xl"],
44
- ];
45
-
46
- /*
47
- * Control geometry from the same section: the seam weight, the focus outline and
48
- * its offset, a control's inline padding, and the coarse-pointer target. Each
49
- * owns one role rather than the general rhythm, so a value that lands on one is
50
- * legal and none of them is ever offered as the nearest rung for a gap.
51
- */
52
- const CONTROL_GEOMETRY = new Set([1, 2, 10, 44]);
53
-
54
- const SPACING_PREFIX =
55
- "(?:p|px|py|pt|pr|pb|pl|ps|pe|m|mx|my|mt|mr|mb|ml|ms|me|gap|gap-x|gap-y|space-x|space-y|inset|inset-x|inset-y|top|right|bottom|left|start|end)";
56
- const OFF_GRID = new RegExp(`^-?(${SPACING_PREFIX})-\\[(-?\\d+(?:\\.\\d+)?)px\\]$`);
57
-
58
- /** `md:hover:p-[13px]` → `p-[13px]`, with a colon inside brackets left alone. */
59
- function withoutVariants(token) {
60
- let bracketDepth = 0;
61
- let start = 0;
62
- for (let index = 0; index < token.length; index += 1) {
63
- if (token[index] === "[") bracketDepth += 1;
64
- if (token[index] === "]") bracketDepth = Math.max(0, bracketDepth - 1);
65
- if (token[index] === ":" && bracketDepth === 0) start = index + 1;
66
- }
67
- return token.slice(start);
68
- }
69
-
70
- /** The closest rung, and the larger of the two when a value sits between them. */
71
- function nearestRung(pixels) {
72
- return LADDER.reduce((closest, rung) =>
73
- Math.abs(rung[0] - pixels) <= Math.abs(closest[0] - pixels) ? rung : closest,
74
- );
75
- }
76
-
77
- /*
78
- * A step below zero is written both ways — `-mt-[3px]` puts the sign on the
79
- * utility, `bottom-[-5px]` puts it in the value — and the replacement carries it
80
- * on the utility either way, because a token holds a magnitude.
81
- */
82
- function offGridFinding(utility) {
83
- const unqualified = withoutVariants(utility);
84
- const match = OFF_GRID.exec(unqualified);
85
- if (!match) return undefined;
86
- const [, prefix, value] = match;
87
- const pixels = Math.abs(Number(value));
88
- if (CONTROL_GEOMETRY.has(pixels)) return undefined;
89
- const [rung, token] = nearestRung(pixels);
90
- if (rung === pixels) return undefined;
91
- const sign = unqualified.startsWith("-") || value.startsWith("-") ? "-" : "";
92
- return { value: String(pixels), rung: String(rung), replacement: `${sign}${prefix}-${token}` };
93
- }
94
-
95
- export const noOffGridSpacing = {
96
- meta: {
97
- type: "problem",
98
- docs: {
99
- description:
100
- "Disallow a pixel value typed into a spacing utility off the design-system ladder",
101
- },
102
- schema: [],
103
- messages: {
104
- offGridSpacing:
105
- '"{{utility}}" puts a {{value}}px step beside a layout built on the design-system spacing ladder, so the rhythm reads as almost aligned and the value cannot follow the tokens. The nearest rung is {{rung}}px: use {{replacement}}.',
106
- },
107
- },
108
- create(context) {
109
- return tokenUtilityVisitors(context, (node, utility) => {
110
- const finding = offGridFinding(utility);
111
- if (!finding) return;
112
- context.report({ node, messageId: "offGridSpacing", data: { utility, ...finding } });
113
- });
114
- },
115
- };
@@ -1,66 +0,0 @@
1
- /*
2
- * A square is written once: `size-4`, never `h-4 w-4`.
3
- *
4
- * Standard tool checked first: `better-tailwindcss`'s
5
- * `no-restricted-classes` judges one class at a time, so it can ban `h-4`
6
- * outright but cannot see that a `w-4` is beside it — and the pair is the whole
7
- * point, since `h-4 w-full` is fine. The relationship lives here, on the shared
8
- * className reader.
9
- *
10
- * Replaces the `paired-size` half of the retired @nadicodeai/ui
11
- * composition-conventions scanner (#822). Stock shadcn shapes that cannot be
12
- * written any other way carry an inline disable with the reason at the site.
13
- */
14
- import {
15
- openingElementClassUtilities,
16
- packageOwnedPrimitivesSchema,
17
- skipsPackageOwnedPrimitive,
18
- } from "./jsx-class-utility.js";
19
-
20
- /** `md:h-4` → `{ variants: "md:", axis: "h", value: "4" }`, or undefined. */
21
- function sizeUtility(utility) {
22
- const separator = utility.lastIndexOf(":");
23
- const variants = utility.slice(0, separator + 1);
24
- const base = utility.slice(separator + 1);
25
- const match = /^([hw])-(.+)$/.exec(base);
26
- return match ? { variants, axis: match[1], value: match[2] } : undefined;
27
- }
28
-
29
- export const noPairedSizeUtility = {
30
- meta: {
31
- type: "problem",
32
- docs: {
33
- description: "Disallow paired h-* and w-* utilities of equal value; use size-*",
34
- },
35
- schema: [packageOwnedPrimitivesSchema],
36
- messages: {
37
- pairedSize: "Write the square once: use {{variants}}size-{{value}} instead of a h/w pair.",
38
- },
39
- },
40
- create(context) {
41
- if (skipsPackageOwnedPrimitive(context)) {
42
- return {};
43
- }
44
-
45
- return {
46
- JSXOpeningElement(node) {
47
- const seen = new Map();
48
- for (const utility of openingElementClassUtilities(node, context)) {
49
- const parsed = sizeUtility(utility);
50
- if (!parsed) continue;
51
- const key = `${parsed.variants}${parsed.value}`;
52
- const axes = seen.get(key) ?? new Set();
53
- axes.add(parsed.axis);
54
- seen.set(key, axes);
55
- if (axes.size === 2) {
56
- context.report({
57
- node,
58
- messageId: "pairedSize",
59
- data: { variants: parsed.variants, value: parsed.value },
60
- });
61
- }
62
- }
63
- },
64
- };
65
- },
66
- };
@@ -1,70 +0,0 @@
1
- /*
2
- * Depth comes off the ladder, not out of Tailwind's box.
3
- *
4
- * DESIGN.md, "Elevation & Depth": the four rungs are the four owned utilities,
5
- * each authored per mode and carrying its own ring as the recipe's final layer.
6
- * A stock `shadow-md` is a single light-mode drop shadow: it disappears against
7
- * the true-black dark workspace, it leaves the surface without the ring its
8
- * neighbours have, and it does not move when the tokens do. Two surfaces meant
9
- * to sit at the same physical level then read as different objects.
10
- *
11
- * `shadow-none` stays legal — removing a shadow an inherited shape brought with
12
- * it is the correct thing to write, and it is what `packages/ui` already writes.
13
- *
14
- * The two packages that BUILD the ladder are outside the rule: the design system
15
- * authors the rung values, and `@nadicodeai/ui` composes the utilities onto its
16
- * primitives. Everywhere else a shadow is a caller inventing depth.
17
- *
18
- * Standard tool checked first: `better-tailwindcss/no-unknown-classes` cannot
19
- * see these, because the generated theme clears only the `--color-*` namespace
20
- * and leaves Tailwind's own `--shadow-*` in place, so `shadow-lg` is a real
21
- * class that really paints. `no-restricted-classes` in the Portal and the
22
- * package configs carries `shadow-tooltip|menu|dialog|sheet` — the NAMED shadows
23
- * the ladder replaced — and nothing about the size scale; `no-invalid-token-utility`
24
- * judges `shadow-nc-*` against the generated colour tokens only; and the
25
- * colour-boundary patterns fire on `-nc-` or on an arbitrary colour value. A
26
- * probe file carrying `shadow-lg shadow-md shadow-[0_2px_4px_black]` linted
27
- * clean in `apps/portal` before this rule existed. Holding it as a repo-wide
28
- * restricted class would need the same entry restated in five configs, since a
29
- * later `no-restricted-classes` entry replaces the earlier one rather than
30
- * adding to it, so it is one rule instead.
31
- */
32
- import { tokenUtilityVisitors } from "./jsx-class-utility.js";
33
-
34
- const RAW_SHADOW = /^shadow(?:-(?:2xs|xs|sm|md|lg|xl|2xl|inner|initial|\[.*\]))?$/;
35
- const PACKAGE_LADDER_PATHS = ["packages/ui/src/", "packages/design-system/"];
36
-
37
- function withoutVariants(token) {
38
- let bracketDepth = 0;
39
- let start = 0;
40
- for (let index = 0; index < token.length; index += 1) {
41
- if (token[index] === "[") bracketDepth += 1;
42
- if (token[index] === "]") bracketDepth = Math.max(0, bracketDepth - 1);
43
- if (token[index] === ":" && bracketDepth === 0) start = index + 1;
44
- }
45
- return token.slice(start);
46
- }
47
-
48
- export const noRawShadow = {
49
- meta: {
50
- type: "problem",
51
- docs: {
52
- description:
53
- "Disallow Tailwind's stock shadow utilities outside the packages that own the material ladder",
54
- },
55
- schema: [],
56
- messages: {
57
- rawShadow:
58
- '"{{utility}}" paints a light-mode drop shadow with no ring, so the surface loses its edge against the dark workspace and cannot follow the tokens. Choose the physical level instead: nc-elevation-resting, nc-elevation-raised, nc-elevation-floating, or nc-elevation-modal.',
59
- },
60
- },
61
- create(context) {
62
- const filename = context.filename.replaceAll("\\", "/");
63
- if (PACKAGE_LADDER_PATHS.some((path) => filename.includes(path))) return {};
64
-
65
- return tokenUtilityVisitors(context, (node, utility) => {
66
- if (!RAW_SHADOW.test(withoutVariants(utility))) return;
67
- context.report({ node, messageId: "rawShadow", data: { utility } });
68
- });
69
- },
70
- };
@@ -1,75 +0,0 @@
1
- import { classUtilities } from "./jsx-class-utility.js";
2
-
3
- /**
4
- * Keyboard focus has one owner: the globals.css base layer draws the offset
5
- * ink outline on every :focus-visible element (DESIGN.md "Focus ring"), and an
6
- * invalid control takes the same geometry in the destructive hue. A component
7
- * that paints focus with a box-shadow ring, a raw outline width, or its own
8
- * hue forks that treatment. Standard tool checked first: no
9
- * upstream Tailwind rule models "focus variant may only suppress or restate
10
- * the canonical outline".
11
- *
12
- * A focus-variant utility may only suppress the outline (outline-none,
13
- * outline-hidden), restate the canonical treatment on a proxy target through
14
- * the one shared `nc-focus-outline` utility plus a ring-role color (as
15
- * input-group does when the group carries the indicator for its inner
16
- * control), or move non-paint properties (z-index, colors of the element
17
- * itself, …). Restating the style, width, and offset as separate utilities is
18
- * a fork of that owner, so the raw outline width belongs to nobody.
19
- */
20
- const RING_BASE = /^-?ring(?:-|$)/;
21
- const ALLOWED_OUTLINE_BASE = new Set(["nc-focus-outline", "outline-none", "outline-hidden"]);
22
- const ALLOWED_OUTLINE_COLOR = /^outline-(?:ring|sidebar-ring)$/;
23
- const OUTLINE_OFFSET = /^-?outline-offset-/;
24
-
25
- function baseOf(utility) {
26
- let depth = 0;
27
- let base = utility;
28
- for (let i = 0; i < utility.length; i += 1) {
29
- const ch = utility[i];
30
- if (ch === "[" || ch === "(") depth += 1;
31
- else if (ch === "]" || ch === ")") depth -= 1;
32
- else if (ch === ":" && depth === 0) base = utility.slice(i + 1);
33
- }
34
- return base;
35
- }
36
-
37
- function repaintedFocus(utility) {
38
- if (!utility.includes("focus")) return undefined;
39
- const base = baseOf(utility);
40
- if (base === utility) return undefined;
41
- if (RING_BASE.test(base)) return "boxShadowFocus";
42
- if (base.startsWith("[outline")) return "customFocusOutline";
43
- if (ALLOWED_OUTLINE_BASE.has(base)) return undefined;
44
- if (!base.startsWith("outline")) return undefined;
45
- if (ALLOWED_OUTLINE_COLOR.test(base) || OUTLINE_OFFSET.test(base)) return undefined;
46
- return "customFocusOutline";
47
- }
48
-
49
- export const noRepaintedFocus = {
50
- meta: {
51
- type: "problem",
52
- docs: {
53
- description:
54
- "Keep keyboard focus on the base-layer outline: no box-shadow ring, raw width, or component hue on a focus variant",
55
- },
56
- schema: [],
57
- messages: {
58
- boxShadowFocus:
59
- "Focus never paints a box-shadow: the base layer draws the offset outline. Drop {{utility}}.",
60
- customFocusOutline:
61
- "{{utility}} repaints focus with its own width or hue. The base layer owns the focus outline; a proxy target takes nc-focus-outline plus a ring-role color.",
62
- },
63
- },
64
- create(context) {
65
- return {
66
- JSXAttribute(node) {
67
- for (const utility of classUtilities(node)) {
68
- const messageId = repaintedFocus(utility);
69
- if (messageId === undefined) continue;
70
- context.report({ node, messageId, data: { utility } });
71
- }
72
- },
73
- };
74
- },
75
- };
@@ -1,89 +0,0 @@
1
- /*
2
- * The settings law, proved on the surfaces that declare they follow it.
3
- *
4
- * A file that renders `SettingsFactsSection` has said its settings are a
5
- * reading surface: current values stated as facts, one affordance opening the
6
- * editor that changes them. The regression this catches is a form control
7
- * rendered beside those facts rather than inside the editor they summon.
8
- *
9
- * "Inside the editor" means inside a `SettingsSection`, an `ActionDialogForm`'s
10
- * `fields` prop, or a `Dialog`/`Drawer` subtree for a surface that owns its own. Being a
11
- * CHILD of `ActionDialogForm` is the opposite: children are the resting surface
12
- * the dialog is opened from, which is exactly where a control must not be.
13
- *
14
- * The scope is the declaring file rather than every page, so a registry
15
- * toolbar's search or a mobile nav select is never a violation — a guard whose
16
- * allow-list carries live violations documents debt instead of preventing it.
17
- *
18
- * Standard tool checked first: `no-restricted-syntax` selects one
19
- * node by shape and cannot ask what else the file renders, nor whether an
20
- * ancestor summoned the subtree. Both are relationships, so the rule lives
21
- * here. Replaces `restingSettingsControlViolations` in the retired portal
22
- * page-grammar analyzers (#821).
23
- */
24
- import { jsxComponentName } from "./jsx-class-utility.js";
25
-
26
- // The controls a person types or picks into. Lowercase `input` is deliberately
27
- // absent: a hidden field carries context a summoned editor already decided, and
28
- // it is not a control anyone rests their eyes on.
29
- const RESTING_CONTROL_TAGS = new Set([
30
- "Checkbox",
31
- "Input",
32
- "InputOTP",
33
- "NativeSelect",
34
- "Select",
35
- "Switch",
36
- "Textarea",
37
- ]);
38
-
39
- const FACTS_SECTION_TAG = "SettingsFactsSection";
40
-
41
- function summons(node, context) {
42
- if (node.type === "JSXAttribute") {
43
- return (
44
- node.name.name === "fields" && jsxComponentName(node.parent, context) === "ActionDialogForm"
45
- );
46
- }
47
- if (node.type !== "JSXElement") return false;
48
- const name = jsxComponentName(node.openingElement, context);
49
- return ["SettingsSection", "Dialog", "DialogContent", "Drawer", "DrawerContent"].includes(name);
50
- }
51
-
52
- export const noRestingSettingsControl = {
53
- meta: {
54
- type: "problem",
55
- docs: {
56
- description:
57
- "Disallow a form control resting beside the facts it should be summoned over, on a settings-facts surface",
58
- },
59
- schema: [],
60
- messages: {
61
- restingControl:
62
- "<{{tag}}> rests beside its facts: use SettingsSection for a form, or put it in the summoned editor (ActionDialogForm `fields`, Dialog, or Drawer).",
63
- },
64
- },
65
- create(context) {
66
- let declaresFacts = false;
67
- const resting = [];
68
-
69
- return {
70
- JSXOpeningElement(node) {
71
- const name = jsxComponentName(node, context);
72
- if (name === FACTS_SECTION_TAG) {
73
- declaresFacts = true;
74
- return;
75
- }
76
- if (!name || !RESTING_CONTROL_TAGS.has(name)) return;
77
- if (context.sourceCode.getAncestors(node).some((ancestor) => summons(ancestor, context)))
78
- return;
79
- resting.push({ node, name });
80
- },
81
- "Program:exit"() {
82
- if (!declaresFacts) return;
83
- for (const { node, name } of resting) {
84
- context.report({ node, messageId: "restingControl", data: { tag: name } });
85
- }
86
- },
87
- };
88
- },
89
- };
@@ -1,135 +0,0 @@
1
- /*
2
- * A Portal route does not decide how wide it is.
3
- *
4
- * The signed-in chrome owns the content column: `(dashboard)/layout.tsx` renders
5
- * `PortalShell`, and a reading-width surface takes `ReadingColumn` from
6
- * `@nadicodeai/ui`. A route that caps its own width with `max-w-*` or centres
7
- * itself with `mx-auto` makes a second owner for one measurement, which is how
8
- * neighbouring pages end up at 42rem and 48rem with nothing to say which is
9
- * right, and how a shell change stops reaching half the routes.
10
- *
11
- * What is read is the OUTERMOST element the route renders — the root of every
12
- * expression its default export returns, descended through fragments and through
13
- * the conditionals a route writes around a fallback. A width inside the page is
14
- * a different statement: a skeleton that matches the field it stands in for, or
15
- * a form column, constrains its own element, not the route, and those are
16
- * ordinary. The rule's subject is the route's own frame.
17
- *
18
- * `max-w-full` and `max-w-none` are kept legal for the same reason: they remove
19
- * a cap rather than set one, so neither can be the second owner of a width.
20
- *
21
- * Standard tool checked first: `better-tailwindcss/no-restricted-classes`
22
- * matches a class wherever it is written and cannot ask whether the element
23
- * carrying it is the one the route returns. That relationship is the rule, so it
24
- * is read here.
25
- */
26
- import { openingElementClassUtilities } from "./jsx-class-utility.js";
27
-
28
- const CENTERING = "mx-auto";
29
- const MAX_WIDTH = /^-?max-w-(.+)$/;
30
- const UNCAPPED = new Set(["full", "none"]);
31
-
32
- function withoutVariants(token) {
33
- let bracketDepth = 0;
34
- let start = 0;
35
- for (let index = 0; index < token.length; index += 1) {
36
- if (token[index] === "[") bracketDepth += 1;
37
- if (token[index] === "]") bracketDepth = Math.max(0, bracketDepth - 1);
38
- if (token[index] === ":" && bracketDepth === 0) start = index + 1;
39
- }
40
- return token.slice(start);
41
- }
42
-
43
- function isWidthOverride(utility) {
44
- const unqualified = withoutVariants(utility);
45
- if (unqualified === CENTERING) return true;
46
- const capped = MAX_WIDTH.exec(unqualified);
47
- return capped ? !UNCAPPED.has(capped[1]) : false;
48
- }
49
-
50
- /** The elements one returned expression renders at its outermost level. */
51
- function rootElements(node, found = []) {
52
- if (!node) return found;
53
- switch (node.type) {
54
- case "JSXElement":
55
- found.push(node);
56
- return found;
57
- case "JSXFragment":
58
- for (const child of node.children) rootElements(child, found);
59
- return found;
60
- case "JSXExpressionContainer":
61
- return rootElements(node.expression, found);
62
- case "ConditionalExpression":
63
- rootElements(node.consequent, found);
64
- return rootElements(node.alternate, found);
65
- case "LogicalExpression":
66
- return rootElements(node.right, found);
67
- default:
68
- return found;
69
- }
70
- }
71
-
72
- const FUNCTION_TYPES = new Set([
73
- "ArrowFunctionExpression",
74
- "FunctionDeclaration",
75
- "FunctionExpression",
76
- ]);
77
-
78
- /*
79
- * Every expression the route function returns, its own returns only: a helper
80
- * declared inside it renders something else, and its root is not the route's.
81
- */
82
- function returnedExpressions(node, found = []) {
83
- if (!node || typeof node.type !== "string") return found;
84
- if (node.type === "ReturnStatement") {
85
- if (node.argument) found.push(node.argument);
86
- return found;
87
- }
88
- if (FUNCTION_TYPES.has(node.type)) return found;
89
- for (const [key, value] of Object.entries(node)) {
90
- if (key === "parent" || key === "loc" || key === "range") continue;
91
- if (Array.isArray(value)) {
92
- for (const child of value) returnedExpressions(child, found);
93
- } else if (value && typeof value === "object") {
94
- returnedExpressions(value, found);
95
- }
96
- }
97
- return found;
98
- }
99
-
100
- function routeRoots(routeFunction) {
101
- const body = routeFunction.body;
102
- if (body.type !== "BlockStatement") return rootElements(body);
103
- return returnedExpressions(body).flatMap((expression) => rootElements(expression));
104
- }
105
-
106
- export const noRouteWidthOverride = {
107
- meta: {
108
- type: "problem",
109
- docs: {
110
- description: "Disallow a Portal route file setting the width of the frame it returns",
111
- },
112
- schema: [],
113
- messages: {
114
- routeWidthOverride:
115
- '"{{utility}}" makes this route a second owner of the content width, so neighbouring pages drift apart and a shell change stops reaching this one. Page width belongs to the shell column; a reading-width surface takes ReadingColumn from @nadicodeai/ui.',
116
- },
117
- },
118
- create(context) {
119
- return {
120
- ExportDefaultDeclaration(node) {
121
- if (!FUNCTION_TYPES.has(node.declaration.type)) return;
122
- for (const root of routeRoots(node.declaration)) {
123
- for (const utility of openingElementClassUtilities(root.openingElement, context)) {
124
- if (!isWidthOverride(utility)) continue;
125
- context.report({
126
- node: root.openingElement,
127
- messageId: "routeWidthOverride",
128
- data: { utility },
129
- });
130
- }
131
- }
132
- },
133
- };
134
- },
135
- };
@@ -1,46 +0,0 @@
1
- /*
2
- * A test hands the subject the store methods it calls, as an object typed by
3
- * the subject's own parameter. `as PortalStore` (or `as unknown as
4
- * PortalStore`) turns a partial object into the whole 227-method store: the
5
- * compiler then checks nothing about the methods the fake does implement, and a
6
- * subject that grows a call reaches `undefined` at run time instead of failing
7
- * to compile. Narrow the subject's parameter to the capability it uses
8
- * (`Pick<SomePersistence, "a" | "b">`), then build the fake as that type.
9
- */
10
-
11
- export const noStoreCast = {
12
- meta: {
13
- type: "problem",
14
- docs: {
15
- description:
16
- "Disallow casting a partial object to the whole store in a test; type the subject by the store methods it calls and build the fake as that type",
17
- },
18
- schema: [
19
- {
20
- type: "object",
21
- properties: { types: { type: "array", items: { type: "string" } } },
22
- additionalProperties: false,
23
- },
24
- ],
25
- messages: {
26
- storeCast:
27
- '`as {{name}}` hides which store methods this fake implements, so the compiler checks none of them. Type the subject\'s store parameter as the methods it calls (`Pick<SomePersistence, "a" | "b">`) and build the fake as `Parameters<typeof subject>[0]`.',
28
- },
29
- },
30
-
31
- create(context) {
32
- const names = new Set(context.options[0]?.types ?? ["PortalStore"]);
33
- return {
34
- TSAsExpression(node) {
35
- const type = node.typeAnnotation;
36
- if (
37
- type.type === "TSTypeReference" &&
38
- type.typeName.type === "Identifier" &&
39
- names.has(type.typeName.name)
40
- ) {
41
- context.report({ node, messageId: "storeCast", data: { name: type.typeName.name } });
42
- }
43
- },
44
- };
45
- },
46
- };
@@ -1,53 +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 borderRadius = require("@nadicodeai/design-system/tailwind").theme.extend.borderRadius;
7
- const radiusNameByValue = new Map(
8
- Object.entries(borderRadius).map(([name, value]) => [value, name]),
9
- );
10
- const ARBITRARY_RADIUS = /^(rounded(?:-(?:t|r|b|l|s|e|ss|se|ee|es|tl|tr|br|bl))?)-\[(.+)\]$/;
11
- const CANONICAL_RADIUS_VARIABLE = /^var\(--(?:nc-rounded|radius)-([a-z0-9-]+)\)$/;
12
-
13
- function canonicalRadiusReplacement(utility) {
14
- const variantBoundary = utility.lastIndexOf(":") + 1;
15
- const variants = utility.slice(0, variantBoundary);
16
- const unqualified = utility.slice(variantBoundary);
17
- const match = ARBITRARY_RADIUS.exec(unqualified);
18
- if (!match) return undefined;
19
-
20
- const [, prefix, rawValue] = match;
21
- const value = rawValue.trim();
22
- const variable = CANONICAL_RADIUS_VARIABLE.exec(value);
23
- const token = variable?.[1] ?? radiusNameByValue.get(value);
24
- if (!token || !(token in borderRadius)) return undefined;
25
-
26
- return `${variants}${prefix}-${token}`;
27
- }
28
-
29
- export const noTokenCategoryBypass = {
30
- meta: {
31
- type: "problem",
32
- docs: {
33
- description:
34
- "Disallow arbitrary radius values when the generated design system provides an exact utility",
35
- },
36
- schema: [],
37
- messages: {
38
- canonicalTokenUtility:
39
- "Replace {{utility}} with the generated design-system utility {{replacement}}.",
40
- },
41
- },
42
- create(context) {
43
- return tokenUtilityVisitors(context, (node, utility) => {
44
- const replacement = canonicalRadiusReplacement(utility);
45
- if (!replacement) return;
46
- context.report({
47
- node,
48
- messageId: "canonicalTokenUtility",
49
- data: { replacement, utility },
50
- });
51
- });
52
- },
53
- };