@nadicodeai/ui 1.0.2 → 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.
Files changed (133) hide show
  1. package/AGENTS.md +13 -13
  2. package/README.md +1 -1
  3. package/dist/agents.d.ts +8 -8
  4. package/dist/components/accordion.js +1 -1
  5. package/dist/components/action-band.js +1 -1
  6. package/dist/components/agent-card.d.ts +1 -1
  7. package/dist/components/agent-visual.d.ts +1 -1
  8. package/dist/components/alert-dialog.d.ts +1 -1
  9. package/dist/components/automation-section.d.ts +2 -2
  10. package/dist/components/avatar.d.ts.map +1 -1
  11. package/dist/components/avatar.js +3 -1
  12. package/dist/components/badge.js +1 -1
  13. package/dist/components/brand-icons/apple.d.ts +1 -1
  14. package/dist/components/brand-icons/linux.d.ts +1 -1
  15. package/dist/components/brand-icons/windows.d.ts +1 -1
  16. package/dist/components/brand-icons.d.ts +4 -4
  17. package/dist/components/button-group.d.ts +1 -1
  18. package/dist/components/button.js +1 -1
  19. package/dist/components/calendar.d.ts +1 -1
  20. package/dist/components/calendar.js +1 -1
  21. package/dist/components/card.d.ts.map +1 -1
  22. package/dist/components/card.js +2 -4
  23. package/dist/components/carousel.d.ts +1 -1
  24. package/dist/components/carousel.d.ts.map +1 -1
  25. package/dist/components/chart-breakdown-table.d.ts +2 -2
  26. package/dist/components/chart-bullet-bar.js +5 -5
  27. package/dist/components/chart-date-range-control.d.ts +1 -1
  28. package/dist/components/chart-kpi-stat.d.ts +1 -1
  29. package/dist/components/chart-time-series.d.ts +1 -1
  30. package/dist/components/chart-time-series.d.ts.map +1 -1
  31. package/dist/components/chart-time-series.js +10 -9
  32. package/dist/components/chart.d.ts.map +1 -1
  33. package/dist/components/chart.js +13 -3
  34. package/dist/components/checkbox.js +1 -1
  35. package/dist/components/combobox.js +2 -2
  36. package/dist/components/command.d.ts +1 -1
  37. package/dist/components/command.d.ts.map +1 -1
  38. package/dist/components/command.js +2 -2
  39. package/dist/components/context-menu.js +1 -1
  40. package/dist/components/dashboard-shell.d.ts +1 -1
  41. package/dist/components/disclosure-row.d.ts.map +1 -1
  42. package/dist/components/disclosure-row.js +1 -3
  43. package/dist/components/dropdown-menu.js +1 -1
  44. package/dist/components/faq-item.d.ts +1 -1
  45. package/dist/components/faq-item.d.ts.map +1 -1
  46. package/dist/components/faq-item.js +2 -2
  47. package/dist/components/feedback-state.d.ts +1 -1
  48. package/dist/components/field.d.ts +1 -1
  49. package/dist/components/footer.d.ts.map +1 -1
  50. package/dist/components/footer.js +1 -1
  51. package/dist/components/form.d.ts +1 -1
  52. package/dist/components/glass-control.js +1 -1
  53. package/dist/components/hero.js +1 -1
  54. package/dist/components/input-group.d.ts +1 -1
  55. package/dist/components/input-group.js +3 -3
  56. package/dist/components/input-otp.d.ts.map +1 -1
  57. package/dist/components/input-otp.js +6 -3
  58. package/dist/components/input.js +1 -1
  59. package/dist/components/integration-flow.d.ts.map +1 -1
  60. package/dist/components/integration-flow.js +5 -5
  61. package/dist/components/item.d.ts +1 -1
  62. package/dist/components/item.js +1 -1
  63. package/dist/components/label-system.js +1 -1
  64. package/dist/components/live-demo.js +2 -2
  65. package/dist/components/menubar.d.ts +1 -1
  66. package/dist/components/menubar.js +1 -1
  67. package/dist/components/native-select.js +1 -1
  68. package/dist/components/nav-bar.d.ts.map +1 -1
  69. package/dist/components/nav-bar.js +1 -1
  70. package/dist/components/navigation-menu.d.ts.map +1 -1
  71. package/dist/components/navigation-menu.js +5 -5
  72. package/dist/components/pagination.d.ts +1 -1
  73. package/dist/components/plugin-slot-boundary.d.ts.map +1 -1
  74. package/dist/components/plugin-slot-boundary.js +1 -1
  75. package/dist/components/popover.js +1 -1
  76. package/dist/components/radio-group.js +1 -1
  77. package/dist/components/resizable.d.ts.map +1 -1
  78. package/dist/components/resizable.js +4 -2
  79. package/dist/components/scroll-area.js +1 -1
  80. package/dist/components/seam-grid.d.ts +1 -1
  81. package/dist/components/select.js +4 -4
  82. package/dist/components/settings-section.d.ts +1 -1
  83. package/dist/components/sidebar.d.ts +4 -4
  84. package/dist/components/sidebar.d.ts.map +1 -1
  85. package/dist/components/sidebar.js +6 -7
  86. package/dist/components/slider.js +1 -1
  87. package/dist/components/status-dot.js +2 -2
  88. package/dist/components/switch.js +1 -1
  89. package/dist/components/tabs.js +1 -1
  90. package/dist/components/textarea.js +1 -1
  91. package/dist/components/texture-strip.d.ts.map +1 -1
  92. package/dist/components/texture-strip.js +3 -0
  93. package/dist/components/theme-mode-switcher.d.ts +2 -2
  94. package/dist/components/toggle-group.d.ts +1 -1
  95. package/dist/components/toggle.js +1 -1
  96. package/dist/eslint/index.js +23 -17
  97. package/dist/eslint/rules/confirm-destructive-action.js +177 -0
  98. package/dist/eslint/rules/jsx-class-utility.js +162 -176
  99. package/dist/eslint/rules/no-app-feature-component.js +100 -0
  100. package/dist/eslint/rules/no-deprecated-zod-api.js +63 -15
  101. package/dist/eslint/rules/no-elevated-edge.js +144 -0
  102. package/dist/eslint/rules/no-mono-title-register.js +73 -0
  103. package/dist/eslint/rules/no-native-visible-control.js +6 -3
  104. package/dist/eslint/rules/no-package-component-redefinition.js +175 -0
  105. package/dist/eslint/rules/no-paired-size-utility.js +66 -0
  106. package/dist/eslint/rules/no-repainted-focus.js +80 -0
  107. package/dist/eslint/rules/no-resting-settings-control.js +87 -0
  108. package/dist/eslint/rules/no-shadcn-appearance-override.js +30 -56
  109. package/dist/eslint/rules/no-unpinned-vercel-functions-import.js +25 -23
  110. package/dist/eslint/rules/server-action-discipline.js +182 -0
  111. package/dist/eslint/rules/shadcn-composition.js +131 -0
  112. package/dist/hooks/index.d.ts +3 -3
  113. package/dist/index.d.ts +132 -132
  114. package/dist/internal/agent-visual/poster.d.ts +1 -1
  115. package/dist/internal/agent-visual/renderer.d.ts +1 -1
  116. package/dist/internal/assistant-ui/attachment.js +1 -1
  117. package/dist/internal/assistant-ui/follow-up-suggestions.d.ts.map +1 -1
  118. package/dist/internal/assistant-ui/follow-up-suggestions.js +5 -1
  119. package/dist/internal/assistant-ui/reasoning.d.ts +1 -1
  120. package/dist/internal/assistant-ui/tool-fallback.d.ts +1 -1
  121. package/dist/internal/assistant-ui/tool-group.d.ts +1 -1
  122. package/dist/internal/assistant-ui/tooltip-icon-button.d.ts +1 -1
  123. package/dist/styles/globals.css +37 -12
  124. package/docs/consuming-cross-repo.md +2 -2
  125. package/docs/contract.md +30 -20
  126. package/docs/migration/base-ui-migration.md +2 -2
  127. package/package.json +7 -6
  128. package/dist/eslint/rules/no-design-system-src-import.js +0 -11
  129. package/dist/eslint/rules/no-raw-color-utility.js +0 -109
  130. package/dist/eslint/rules/no-raw-focus-ring-width.js +0 -30
  131. package/dist/eslint/rules/no-raw-logo-import.js +0 -41
  132. package/dist/eslint/rules/no-ui-src-import.js +0 -10
  133. package/dist/eslint/rules/src-import-boundary.js +0 -207
@@ -1,7 +1,22 @@
1
+ /*
2
+ * The @nadicodeai/ui package contract (`packages/ui/docs/contract.md`):
3
+ * "Consumers compose structure. Appearance — colour, elevation, radius,
4
+ * opacity, the primitive's own border — belongs to the package." A caller that
5
+ * repaints an imported primitive forks its appearance into the call site, where
6
+ * the next token change does not reach it.
7
+ *
8
+ * The theme's palette restriction (design-system, #821) removed the raw
9
+ * Tailwind palette, so `bg-red-500` is no longer a class anywhere. What it did
10
+ * NOT remove is repainting with the SEMANTIC roles — `bg-background`,
11
+ * `text-foreground`, `rounded-xl`, `shadow-lg`, `opacity-50`, `border-0` — all
12
+ * of which still compile and still fork the primitive. Those are what survives
13
+ * here; the raw-palette names are gone from the sets below with the palette.
14
+ */
1
15
  import {
2
16
  classUtilities,
3
- isPackageOwnedPrimitive,
4
17
  openingElementClassUtilities,
18
+ packageOwnedPrimitivesSchema,
19
+ skipsPackageOwnedPrimitive,
5
20
  } from "./jsx-class-utility.js";
6
21
 
7
22
  function withoutVariants(utility) {
@@ -18,55 +33,6 @@ function isTextLayoutUtility(utility) {
18
33
  );
19
34
  }
20
35
 
21
- const COLOR_NAMES = new Set([
22
- "black",
23
- "white",
24
- "slate",
25
- "gray",
26
- "zinc",
27
- "neutral",
28
- "stone",
29
- "red",
30
- "orange",
31
- "amber",
32
- "yellow",
33
- "lime",
34
- "green",
35
- "emerald",
36
- "teal",
37
- "cyan",
38
- "sky",
39
- "blue",
40
- "indigo",
41
- "violet",
42
- "purple",
43
- "fuchsia",
44
- "pink",
45
- "rose",
46
- "nc",
47
- "background",
48
- "foreground",
49
- "card",
50
- "popover",
51
- "primary",
52
- "secondary",
53
- "muted",
54
- "accent",
55
- "destructive",
56
- "border",
57
- "input",
58
- "ring",
59
- "focus",
60
- ]);
61
-
62
- function hasColorValue(utility, prefix) {
63
- if (!utility.startsWith(prefix)) {
64
- return false;
65
- }
66
- const value = utility.slice(prefix.length).split("/")[0];
67
- return value.startsWith("[") || COLOR_NAMES.has(value.split("-")[0]);
68
- }
69
-
70
36
  function isBorderMechanic(utility) {
71
37
  return /^(?:border(?:-(?:[xy]|[trblse]{1,2}|[0-9]+|px|dashed|dotted|double|hidden|none))?)$/.test(
72
38
  utility,
@@ -83,18 +49,24 @@ function isRingOrOutlineMechanic(utility) {
83
49
  );
84
50
  }
85
51
 
52
+ /*
53
+ * A border/ring/outline utility is either one of the enumerated MECHANICS
54
+ * (width, side, style, inset/offset — structure the caller owns) or it paints.
55
+ * There is no third case left to test for: the palette restriction means an
56
+ * unrecognised `border-…` value is not a class, and
57
+ * `better-tailwindcss/no-unknown-classes` reports it as one. That is what
58
+ * retired the hand-written colour-name list this rule used to carry.
59
+ */
86
60
  function isAppearanceUtility(utility) {
87
61
  const base = withoutVariants(utility);
88
62
  return (
89
63
  /^appearance-/.test(base) ||
90
64
  /^bg-/.test(base) ||
91
65
  isBorderRemoval(base) ||
92
- (base.startsWith("border") && !isBorderMechanic(base) && hasColorValue(base, "border-")) ||
66
+ (base.startsWith("border") && !isBorderMechanic(base)) ||
93
67
  /^shadow(?:-|$)/.test(base) ||
94
68
  /^rounded(?:-|$)/.test(base) ||
95
- ((base.startsWith("ring") || base.startsWith("outline")) &&
96
- !isRingOrOutlineMechanic(base) &&
97
- (hasColorValue(base, "ring-") || hasColorValue(base, "outline-"))) ||
69
+ ((base.startsWith("ring") || base.startsWith("outline")) && !isRingOrOutlineMechanic(base)) ||
98
70
  /^opacity-/.test(base) ||
99
71
  (/^text-/.test(base) && !isTypographySize(base) && !isTextLayoutUtility(base))
100
72
  );
@@ -123,14 +95,16 @@ export const noShadcnAppearanceOverride = {
123
95
  description:
124
96
  "Disallow app callers repainting imported shadcn primitives; compose their structural layout only",
125
97
  },
126
- schema: [],
98
+ // Default "exempt": inside @nadicodeai/ui the primitives ARE the appearance,
99
+ // so every utility here would be a false positive.
100
+ schema: [packageOwnedPrimitivesSchema],
127
101
  messages: {
128
102
  appearanceOverride:
129
103
  "Do not repaint an imported @nadicodeai/ui primitive with {{utility}}; use its semantic variant or package primitive.",
130
104
  },
131
105
  },
132
106
  create(context) {
133
- if (isPackageOwnedPrimitive(context.filename)) {
107
+ if (skipsPackageOwnedPrimitive(context)) {
134
108
  return {};
135
109
  }
136
110
 
@@ -1,11 +1,33 @@
1
+ // The standard tool was checked first: core `no-restricted-imports` takes
2
+ // `paths[].importNames` / `allowImportNames`, so a per-symbol allowlist is
3
+ // within its vocabulary. What it cannot do is bind that allowlist to a file.
4
+ // The ban would live in the shared options and each allowed module would need a
5
+ // file-scoped override — and flat config replaces rule options instead of
6
+ // merging them, so every one of those overrides must restate the whole shared
7
+ // object (the package-source patterns, the raw-logo patterns, the portal's kit
8
+ // barrel) to keep them alive. That is one copy of the ban list per allowed
9
+ // module, where a single stale copy silently reopens a boundary; the same
10
+ // last-wins failure `tests/module-boundary-imports.test.ts` exists to catch. A
11
+ // plugin rule keeps the module-to-symbol map in one place and carries the
12
+ // incident and the remediation in its message.
13
+ //
14
+ // Static `import` declarations only. The rule's provenance is a real production
15
+ // crash — `ipAddress()` duck-typed its input and threw on the device-authorize
16
+ // route (docs/solutions/portal-device-authorize-vercel-headers-crash.md) — and
17
+ // that crash arrived through a plain named import, which is also the only form
18
+ // that can prove which symbols it binds. The rule used to also flag any string
19
+ // literal or template literal spelling the package name anywhere in a file,
20
+ // reaching dynamic `import()`, `require`, and re-exports; nothing in this
21
+ // repository writes those forms, and the catch-all fired on any quoted mention
22
+ // of the name.
1
23
  const PACKAGE_NAME = "@vercel/functions";
2
24
 
3
25
  // Third-party helpers stay behind owned, single-purpose capability seams. Pin
4
26
  // the full workspace-relative suffix so a same-named file in another workspace
5
27
  // cannot acquire a platform dependency by shadowing an allowed module.
6
28
  const ALLOWED_IMPORTS = new Map([
7
- ["portal/src/lib/server/db.ts", new Set(["attachDatabasePool"])],
8
- ["portal/src/lib/server/deferred-work.ts", new Set(["waitUntil"])],
29
+ ["apps/portal/src/lib/server/db.ts", new Set(["attachDatabasePool"])],
30
+ ["apps/portal/src/lib/server/deferred-work.ts", new Set(["waitUntil"])],
9
31
  ]);
10
32
 
11
33
  function matchesPackage(value) {
@@ -38,7 +60,7 @@ export const noUnpinnedVercelFunctionsImport = {
38
60
  meta: {
39
61
  type: "problem",
40
62
  docs: {
41
- description: "Confine @vercel/functions imports to pinned edge modules and symbols",
63
+ description: "Confine static @vercel/functions imports to pinned edge modules and symbols",
42
64
  },
43
65
  messages: {
44
66
  banned:
@@ -75,26 +97,6 @@ export const noUnpinnedVercelFunctionsImport = {
75
97
  report(node, node.source.value);
76
98
  }
77
99
  },
78
- // Everything except a static import is judged by the quoted reference
79
- // itself, whatever expression carries it: dynamic import(), require in
80
- // any callee shape (bare, module.require, createRequire), export ...
81
- // from, or a bare string that smuggles the specifier to a loader. Only
82
- // static named imports can prove which symbols they bind, so only they
83
- // get the allowlist path above.
84
- Literal(node) {
85
- if (!matchesPackage(node.value)) {
86
- return;
87
- }
88
- if (node.parent.type === "ImportDeclaration") {
89
- return;
90
- }
91
- report(node, node.value);
92
- },
93
- TemplateLiteral(node) {
94
- if (node.expressions.length === 0 && matchesPackage(node.quasis[0]?.value.cooked)) {
95
- report(node, node.quasis[0].value.cooked);
96
- }
97
- },
98
100
  };
99
101
  },
100
102
  };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * A Next.js server action is a POST endpoint with a function's syntax: anything
3
+ * on the internet can call it with any arguments. So an exported server action
4
+ * must resolve its viewer before it acts, and must parse whatever the client
5
+ * sent before it trusts it.
6
+ *
7
+ * Standard tools checked first. `@next/eslint-plugin-next` is already on
8
+ * through `eslint-config-next`, and none of its rules read a server action's
9
+ * body — the set is script, head, font and image hygiene. `typescript-eslint`
10
+ * can type the argument but cannot require that a call happened. The tier-1
11
+ * option — a `defineAction` wrapper that makes an unvalidated or unauthorized
12
+ * action untypeable — was tried and rejected for this change: Next binds the
13
+ * exported function itself, so adopting it rewrites all 62 exported actions in
14
+ * the same diff that retires the guard.
15
+ *
16
+ * The rule reads one function at a time — no whole-tree scan, no cross-file
17
+ * dataflow, no exception ledger. A deliberately public action carries an inline
18
+ * `eslint-disable-next-line` with its reason, which is reviewed in the diff that
19
+ * adds it rather than in a list somewhere else.
20
+ */
21
+
22
+ /** Every helper that resolves the viewer and refuses an unauthorized one. */
23
+ const AUTH_CALLS = new Set([
24
+ "customerActionContext",
25
+ "getPortalSession",
26
+ "installationActionContext",
27
+ "partnerActionContext",
28
+ "requirePlatformAdminViewer",
29
+ "requirePlatformOrPartnerViewer",
30
+ "requirePortalAdmin",
31
+ "requirePortalViewer",
32
+ "requireViewerForPage",
33
+ "resolveDeviceApprover",
34
+ "resolvePortalAccessState",
35
+ ]);
36
+
37
+ /**
38
+ * A schema's `parse`/`safeParse`. `JSON.parse` turns a string into an object
39
+ * and asserts nothing about its shape, so it is not validation and is excluded
40
+ * by name.
41
+ */
42
+ const VALIDATION_CALLS = new Set(["parse", "safeParse"]);
43
+
44
+ function hasUseServer(body) {
45
+ return (body ?? []).some(
46
+ (statement) =>
47
+ statement.type === "ExpressionStatement" &&
48
+ statement.expression.type === "Literal" &&
49
+ statement.expression.value === "use server",
50
+ );
51
+ }
52
+
53
+ function calleeName(node) {
54
+ if (node.callee.type === "Identifier") return node.callee.name;
55
+ if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier") {
56
+ if (node.callee.object.type === "Identifier" && node.callee.object.name === "JSON") {
57
+ return undefined;
58
+ }
59
+ return node.callee.property.name;
60
+ }
61
+ return undefined;
62
+ }
63
+
64
+ /**
65
+ * How a module-level function reaches the outside, which is what decides
66
+ * whether Next binds it as a POST endpoint. All five spellings bind the same
67
+ * way, so the rule reads all five: `export async function a`, `export default
68
+ * async function a`, `export const a = async () => {}`, `export const a =
69
+ * async function () {}`, and a bare declaration later named in `export { a }`.
70
+ * Returns the identifier to report on, or undefined when the function is not a
71
+ * module-level export.
72
+ */
73
+ function exportedIdentifier(node) {
74
+ const parent = node.parent;
75
+ if (!parent) return undefined;
76
+ // export [default] async function a() {}
77
+ if (parent.type === "ExportNamedDeclaration" || parent.type === "ExportDefaultDeclaration") {
78
+ return node.id ?? node;
79
+ }
80
+ // export const a = async () => {} / async function () {}
81
+ if (parent.type === "VariableDeclarator" && parent.id.type === "Identifier") {
82
+ const declaration = parent.parent;
83
+ if (declaration?.parent?.type === "ExportNamedDeclaration") return parent.id;
84
+ // A bare `const a = async () => {}` that a later `export { a }` names.
85
+ if (declaration?.parent?.type === "Program") return parent.id;
86
+ return undefined;
87
+ }
88
+ // A bare `async function a() {}` that a later `export { a }` names.
89
+ if (parent.type === "Program") return node.id ?? undefined;
90
+ return undefined;
91
+ }
92
+
93
+ /** @type {import("eslint").Rule.RuleModule} */
94
+ export const serverActionDiscipline = {
95
+ meta: {
96
+ type: "problem",
97
+ docs: {
98
+ description: "An exported server action resolves its viewer and parses its client input.",
99
+ },
100
+ schema: [],
101
+ messages: {
102
+ missingAuth:
103
+ "Server action '{{name}}' calls no viewer check. Resolve the viewer first (requirePortalViewer, customerActionContext, partnerActionContext, …), or disable this rule inline with the reason it is public.",
104
+ missingValidation:
105
+ "Server action '{{name}}' takes client input and never parses it. Run the Zod schema's parse/safeParse before trusting the argument.",
106
+ },
107
+ },
108
+ create(context) {
109
+ const moduleIsServer = hasUseServer(context.sourceCode.ast.body);
110
+ /**
111
+ * One frame per function, holding only the calls written in that function's
112
+ * own body. A call parked inside a nested callback is not a call the action
113
+ * itself makes, so it does not count for the action.
114
+ * @type {Array<Set<string>>}
115
+ */
116
+ const callStack = [];
117
+ /**
118
+ * Candidates are judged at `Program:exit` rather than when each function
119
+ * closes, because `export { a }` can name a declaration written above it.
120
+ * @type {Array<{ identifier: object, name: string, calls: Set<string>, hasParams: boolean, inlineExported: boolean }>}
121
+ */
122
+ const candidates = [];
123
+ /** Names an `export { … }` specifier list makes public. @type {Set<string>} */
124
+ const specifierExports = new Set();
125
+
126
+ function enter() {
127
+ callStack.push(new Set());
128
+ }
129
+
130
+ function exit(node) {
131
+ const calls = callStack.pop() ?? new Set();
132
+ if (!node.async) return;
133
+ if (!(moduleIsServer || hasUseServer(node.body?.body))) return;
134
+ const identifier = exportedIdentifier(node);
135
+ if (!identifier) return;
136
+ const parent = node.parent;
137
+ const inlineExported =
138
+ parent?.type === "ExportNamedDeclaration" ||
139
+ parent?.type === "ExportDefaultDeclaration" ||
140
+ parent?.parent?.parent?.type === "ExportNamedDeclaration";
141
+ candidates.push({
142
+ identifier,
143
+ name: identifier.name ?? "default",
144
+ calls,
145
+ hasParams: node.params.length > 0,
146
+ inlineExported,
147
+ });
148
+ }
149
+
150
+ return {
151
+ FunctionDeclaration: enter,
152
+ "FunctionDeclaration:exit": exit,
153
+ FunctionExpression: enter,
154
+ "FunctionExpression:exit": exit,
155
+ ArrowFunctionExpression: enter,
156
+ "ArrowFunctionExpression:exit": exit,
157
+ ExportNamedDeclaration(node) {
158
+ for (const specifier of node.specifiers ?? []) {
159
+ const local = specifier.local;
160
+ if (local?.type === "Identifier") specifierExports.add(local.name);
161
+ }
162
+ },
163
+ CallExpression(node) {
164
+ const name = calleeName(node);
165
+ if (!name) return;
166
+ callStack.at(-1)?.add(name);
167
+ },
168
+ "Program:exit"() {
169
+ for (const candidate of candidates) {
170
+ if (!candidate.inlineExported && !specifierExports.has(candidate.name)) continue;
171
+ const { identifier, name, calls, hasParams } = candidate;
172
+ if (![...AUTH_CALLS].some((call) => calls.has(call))) {
173
+ context.report({ node: identifier, messageId: "missingAuth", data: { name } });
174
+ }
175
+ if (hasParams && ![...VALIDATION_CALLS].some((call) => calls.has(call))) {
176
+ context.report({ node: identifier, messageId: "missingValidation", data: { name } });
177
+ }
178
+ }
179
+ },
180
+ };
181
+ },
182
+ };
@@ -0,0 +1,131 @@
1
+ /*
2
+ * The four composition conventions the shadcn primitives 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
+ *
14
+ * Standard tool checked (ADR 0057 tier 2): `jsx-a11y` covers roles and ARIA on
15
+ * intrinsic elements and says nothing about these composed components;
16
+ * `no-restricted-syntax` selects a node by its own shape, and every one of
17
+ * these is a statement about an ancestor or a descendant. Replaces the JSX half
18
+ * of the retired @nadicodeai/ui composition-conventions scanner (#822); stock
19
+ * upstream shapes carry an inline disable with the reason at the site.
20
+ */
21
+ import { packageOwnedPrimitivesSchema, skipsPackageOwnedPrimitive } from "./jsx-class-utility.js";
22
+
23
+ const OVERLAY_TITLES = {
24
+ AlertDialogContent: "AlertDialogTitle",
25
+ DialogContent: "DialogTitle",
26
+ DrawerContent: "DrawerTitle",
27
+ SheetContent: "SheetTitle",
28
+ };
29
+
30
+ function elementName(node) {
31
+ if (node?.type === "JSXElement") return elementName(node.openingElement);
32
+ if (node?.type !== "JSXOpeningElement") return undefined;
33
+ if (node.name.type === "JSXIdentifier") return node.name.name;
34
+ if (node.name.type === "JSXMemberExpression") return node.name.property.name;
35
+ return undefined;
36
+ }
37
+
38
+ function hasAttribute(node, attributeName) {
39
+ return node.attributes.some(
40
+ (attribute) =>
41
+ attribute.type === "JSXAttribute" &&
42
+ attribute.name.type === "JSXIdentifier" &&
43
+ attribute.name.name === attributeName,
44
+ );
45
+ }
46
+
47
+ /**
48
+ * True when `tagName` is rendered anywhere below this element — through a
49
+ * conditional or a map as much as directly, so the whole subtree is walked
50
+ * rather than the JSX children alone.
51
+ */
52
+ function hasDescendant(node, tagName, root = node) {
53
+ if (!node || typeof node.type !== "string") return false;
54
+ if (node !== root && node.type === "JSXElement" && elementName(node) === tagName) return true;
55
+ for (const [key, value] of Object.entries(node)) {
56
+ if (key === "parent") continue;
57
+ if (Array.isArray(value)) {
58
+ if (value.some((child) => hasDescendant(child, tagName, root))) return true;
59
+ } else if (value && typeof value === "object" && hasDescendant(value, tagName, root)) {
60
+ return true;
61
+ }
62
+ }
63
+ return false;
64
+ }
65
+
66
+ function ancestorNames(node, sourceCode) {
67
+ return sourceCode
68
+ .getAncestors(node)
69
+ .filter((ancestor) => ancestor.type === "JSXElement")
70
+ .map((ancestor) => elementName(ancestor))
71
+ .filter(Boolean);
72
+ }
73
+
74
+ function isIconTag(tagName) {
75
+ return tagName === "Icon" || tagName.endsWith("Icon");
76
+ }
77
+
78
+ export const shadcnComposition = {
79
+ meta: {
80
+ type: "problem",
81
+ docs: {
82
+ description: "Require the shadcn primitives' composition conventions where they are composed",
83
+ },
84
+ schema: [packageOwnedPrimitivesSchema],
85
+ messages: {
86
+ overlayTitle:
87
+ '{{tag}} must include {{title}}; use className="sr-only" when the title is visually hidden.',
88
+ selectGroup: "SelectItem must be rendered inside SelectGroup.",
89
+ avatarFallback: "AvatarImage requires an AvatarFallback in the same Avatar.",
90
+ buttonIcon: "An icon inside Button must carry data-icon.",
91
+ },
92
+ },
93
+ create(context) {
94
+ if (skipsPackageOwnedPrimitive(context)) {
95
+ return {};
96
+ }
97
+
98
+ return {
99
+ JSXOpeningElement(node) {
100
+ const tag = elementName(node);
101
+ if (!tag) return;
102
+ const element = node.parent;
103
+
104
+ const title = OVERLAY_TITLES[tag];
105
+ if (title && !hasDescendant(element, title)) {
106
+ context.report({ node, messageId: "overlayTitle", data: { tag, title } });
107
+ }
108
+
109
+ if (tag === "Avatar" && hasDescendant(element, "AvatarImage")) {
110
+ if (!hasDescendant(element, "AvatarFallback")) {
111
+ context.report({ node, messageId: "avatarFallback" });
112
+ }
113
+ return;
114
+ }
115
+
116
+ if (tag !== "SelectItem" && tag !== "AvatarImage" && !isIconTag(tag)) return;
117
+
118
+ const ancestors = ancestorNames(node, context.sourceCode);
119
+ if (tag === "SelectItem" && !ancestors.includes("SelectGroup")) {
120
+ context.report({ node, messageId: "selectGroup" });
121
+ }
122
+ if (tag === "AvatarImage" && !ancestors.includes("Avatar")) {
123
+ context.report({ node, messageId: "avatarFallback" });
124
+ }
125
+ if (isIconTag(tag) && ancestors.includes("Button") && !hasAttribute(node, "data-icon")) {
126
+ context.report({ node, messageId: "buttonIcon" });
127
+ }
128
+ },
129
+ };
130
+ },
131
+ };
@@ -1,4 +1,4 @@
1
- export { useBelowBreakpoint } from "./use-below-breakpoint";
2
- export { useConfirmAction } from "./use-confirm-action";
3
- export * from "./use-mobile";
1
+ export { useBelowBreakpoint } from "./use-below-breakpoint.js";
2
+ export { useConfirmAction } from "./use-confirm-action.js";
3
+ export * from "./use-mobile.js";
4
4
  //# sourceMappingURL=index.d.ts.map