@adea-ai/ui 0.78.2 → 0.79.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.
@@ -28,6 +28,7 @@ declare const plugin: {
28
28
  rules: {
29
29
  'no-raw-interactive-elements': RuleModule;
30
30
  'no-primitive-library-imports': RuleModule;
31
+ 'no-interactive-wrappers': RuleModule;
31
32
  };
32
33
  };
33
34
  export default plugin;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lint/index.ts"],"names":[],"mappings":"AAmBA,KAAK,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,IAAI,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAA;AAa/E,KAAK,UAAU,GAAG;IAChB,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS,CAAA;QACf,IAAI,EAAE;YAAE,WAAW,EAAE,MAAM,CAAA;SAAE,CAAA;QAC7B,MAAM,EAAE,IAAI,EAAE,CAAA;QACd,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KACjC,CAAA;IACD,MAAM,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC,CAAA;CACtE,CAAA;AAED,UAAU,WAAW;IACnB,OAAO,CAAC,EAAE,OAAO,EAAE,CAAA;IACnB,MAAM,EAAE,CAAC,UAAU,EAAE;QACnB,IAAI,EAAE,OAAO,CAAA;QACb,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;KAC5B,KAAK,IAAI,CAAA;CACX;AAwHD,QAAA,MAAM,MAAM;;;;;;;;CAMX,CAAA;AAED,eAAe,MAAM,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lint/index.ts"],"names":[],"mappings":"AAmBA,KAAK,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,IAAI,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAA;AAa/E,KAAK,UAAU,GAAG;IAChB,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS,CAAA;QACf,IAAI,EAAE;YAAE,WAAW,EAAE,MAAM,CAAA;SAAE,CAAA;QAC7B,MAAM,EAAE,IAAI,EAAE,CAAA;QACd,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KACjC,CAAA;IACD,MAAM,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC,CAAA;CACtE,CAAA;AAED,UAAU,WAAW;IACnB,OAAO,CAAC,EAAE,OAAO,EAAE,CAAA;IACnB,MAAM,EAAE,CAAC,UAAU,EAAE;QACnB,IAAI,EAAE,OAAO,CAAA;QACb,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;KAC5B,KAAK,IAAI,CAAA;CACX;AA2QD,QAAA,MAAM,MAAM;;;;;;;;;CAOX,CAAA;AAED,eAAe,MAAM,CAAA"}
package/dist/lint.js CHANGED
@@ -75,6 +75,87 @@ var DEFAULT_PRIMITIVE_PACKAGES = Object.freeze([
75
75
  "radix-ui",
76
76
  "@base-ui-components"
77
77
  ]);
78
+ /**
79
+ * The interactive ARIA roles an application can dress a generic element in, and
80
+ * the primitive that owns each role. `link` is deliberately absent: an `<a>` is
81
+ * natively interactive and Button itself supports `as="a"` for the link-styled
82
+ * case.
83
+ */
84
+ var ROLE_PRIMITIVES = Object.freeze({
85
+ button: {
86
+ primitive: "Button",
87
+ path: "@adea-ai/ui/components/ui/button"
88
+ },
89
+ checkbox: {
90
+ primitive: "Checkbox",
91
+ path: "@adea-ai/ui/components/ui/checkbox"
92
+ },
93
+ radio: {
94
+ primitive: "RadioGroup",
95
+ path: "@adea-ai/ui/components/ui/radio-group"
96
+ },
97
+ switch: {
98
+ primitive: "Switch",
99
+ path: "@adea-ai/ui/components/ui/switch"
100
+ },
101
+ tab: {
102
+ primitive: "Tabs",
103
+ path: "@adea-ai/ui/components/ui/tabs"
104
+ },
105
+ option: {
106
+ primitive: "Select",
107
+ path: "@adea-ai/ui/components/ui/select"
108
+ },
109
+ menuitem: {
110
+ primitive: "DropdownMenu",
111
+ path: "@adea-ai/ui/components/ui/dropdown-menu"
112
+ },
113
+ textbox: {
114
+ primitive: "Input",
115
+ path: "@adea-ai/ui/components/ui/input"
116
+ }
117
+ });
118
+ /**
119
+ * The elements whose only job is layout or text. A click handler or a tabindex on
120
+ * one of these is a control being hand-built out of a non-control.
121
+ */
122
+ var GENERIC_ELEMENTS = /* @__PURE__ */ new Set([
123
+ "div",
124
+ "span",
125
+ "li",
126
+ "ul",
127
+ "ol",
128
+ "td",
129
+ "th",
130
+ "p",
131
+ "h1",
132
+ "h2",
133
+ "h3",
134
+ "h4",
135
+ "h5",
136
+ "h6",
137
+ "section",
138
+ "article",
139
+ "aside",
140
+ "header",
141
+ "footer",
142
+ "nav",
143
+ "main",
144
+ "img",
145
+ "svg"
146
+ ]);
147
+ var CLICK_HANDLERS = /* @__PURE__ */ new Set(["onClick", "onDoubleClick"]);
148
+ /** Reads a static `role` value; a dynamic role is undecidable statically, so it is left to review. */
149
+ var roleOf = (node) => {
150
+ for (const attribute of node.attributes ?? []) {
151
+ if (attribute.type !== "JSXAttribute" || attribute.name?.name !== "role") continue;
152
+ if (attribute.value?.type === "Literal" && typeof attribute.value.value === "string") return attribute.value.value;
153
+ return null;
154
+ }
155
+ return null;
156
+ };
157
+ /** Whether the element carries the attribute, by name. */
158
+ var has = (node, name) => (node.attributes ?? []).some((attribute) => attribute.type === "JSXAttribute" && attribute.name?.name === name);
78
159
  var plugin = {
79
160
  meta: { name: "adea" },
80
161
  rules: {
@@ -121,6 +202,75 @@ var plugin = {
121
202
  }
122
203
  };
123
204
  }
205
+ },
206
+ "no-interactive-wrappers": {
207
+ meta: {
208
+ type: "problem",
209
+ docs: { description: "Generic elements dressed as interactive controls — an interactive role, a click handler or a tabindex on a layout element — re-implement a primitive by hand. Compose the real component." },
210
+ schema: [{
211
+ type: "object",
212
+ properties: {
213
+ allow: {
214
+ type: "array",
215
+ items: { type: "string" }
216
+ },
217
+ message: { type: "string" }
218
+ },
219
+ additionalProperties: false
220
+ }],
221
+ messages: {
222
+ roleWrapper: "A {{element}} with role=\"{{role}}\" hand-builds {{primitive}}: use {{primitive}} from {{path}}. The primitive brings keyboard activation, focus behaviour and ARIA wiring; the wrapper re-implements them badly.",
223
+ clickWrapper: "A click handler on {{element}} composes {{primitive}} by hand: use {{primitive}} from {{path}}. A div that reacts to clicks is a button that cannot be reached by keyboard.",
224
+ tabindexWrapper: "A tabindex on {{element}} makes a layout element focusable, which is focus management the primitives already own: compose the real component instead."
225
+ }
226
+ },
227
+ create(context) {
228
+ const options = context.options?.[0] ?? {};
229
+ const allowed = new Set(options.allow ?? []);
230
+ return { JSXOpeningElement(node) {
231
+ if (node.name?.type !== "JSXIdentifier") return;
232
+ const element = node.name.name;
233
+ if (allowed.has(element) || element in ELEMENT_PRIMITIVES) return;
234
+ const role = roleOf(node);
235
+ if (role && role in ROLE_PRIMITIVES) {
236
+ const target = ROLE_PRIMITIVES[role];
237
+ context.report({
238
+ node,
239
+ messageId: "roleWrapper",
240
+ data: {
241
+ element,
242
+ role,
243
+ primitive: target.primitive,
244
+ path: target.path
245
+ },
246
+ ...options.message ? { message: options.message } : {}
247
+ });
248
+ return;
249
+ }
250
+ if (GENERIC_ELEMENTS.has(element)) {
251
+ if ([...CLICK_HANDLERS].find((handler) => has(node, handler))) {
252
+ const target = ROLE_PRIMITIVES.button;
253
+ context.report({
254
+ node,
255
+ messageId: "clickWrapper",
256
+ data: {
257
+ element,
258
+ primitive: target.primitive,
259
+ path: target.path
260
+ },
261
+ ...options.message ? { message: options.message } : {}
262
+ });
263
+ return;
264
+ }
265
+ if (has(node, "tabIndex") || has(node, "tabindex")) context.report({
266
+ node,
267
+ messageId: "tabindexWrapper",
268
+ data: { element },
269
+ ...options.message ? { message: options.message } : {}
270
+ });
271
+ }
272
+ } };
273
+ }
124
274
  }
125
275
  }
126
276
  };
package/dist/lint.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"lint.js","names":[],"sources":["../src/lint/index.ts"],"sourcesContent":["/*\n * The design system's own oxlint plugin.\n *\n * The styling rules live in `@shadcn/lint`; what that plugin cannot know is which\n * *elements* the design system owns. This is the other half of the contract, for\n * the applications that consume the library: an interactive element this package\n * provides is not re-implemented at the application layer, and the primitive\n * libraries this package is built from are not imported around it.\n *\n * Loaded as an oxlint jsPlugin:\n *\n * // .oxlintrc.json\n * { \"jsPlugins\": [\"@adea-ai/ui/lint\"], \"rules\": { \"adea/no-raw-interactive-elements\": \"error\" } }\n *\n * Rules are errors by intent. A violation means an application composed its own\n * control out of raw markup — unreachable focus behaviour, an absent keyboard\n * story, a second implementation of a decision this package exists to make once.\n */\n\ntype Json = string | number | boolean | null | Json[] | { [key: string]: Json }\n\n/** The raw elements this package replaces, and the primitive that replaces them. */\nconst ELEMENT_PRIMITIVES: Readonly<Record<string, { primitive: string; path: string }>> =\n Object.freeze({\n button: { primitive: 'Button', path: '@adea-ai/ui/components/ui/button' },\n input: { primitive: 'Input', path: '@adea-ai/ui/components/ui/input' },\n textarea: { primitive: 'Textarea', path: '@adea-ai/ui/components/ui/textarea' },\n select: { primitive: 'Select', path: '@adea-ai/ui/components/ui/select' },\n option: { primitive: 'Select', path: '@adea-ai/ui/components/ui/select' },\n label: { primitive: 'Label', path: '@adea-ai/ui/components/ui/label' },\n })\n\ntype RuleModule = {\n meta: {\n type: 'problem'\n docs: { description: string }\n schema: Json[]\n messages: Record<string, string>\n }\n create: (context: LintContext) => Record<string, (node: any) => void>\n}\n\ninterface LintContext {\n options?: unknown[]\n report: (descriptor: {\n node: unknown\n messageId?: string\n message?: string\n data?: Record<string, Json>\n }) => void\n}\n\ninterface RawElementOptions {\n /** Element names this rule should not report (rare; prefer fixing the file). */\n allow?: string[]\n /** Replaces the default report message. */\n message?: string\n}\n\nconst noRawInteractiveElements: RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Interactive elements this package provides (button, input, textarea, select, option, label) are composed from the package, not written as raw markup.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n allow: { type: 'array', items: { type: 'string' } },\n message: { type: 'string' },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n rawElement:\n '{{element}} is provided by the design system: use {{primitive}} from {{path}}. A raw element has no keyboard story, no focus behaviour and no token styling — that is the work the primitive already did.',\n },\n },\n create(context) {\n const options = (context.options?.[0] ?? {}) as RawElementOptions\n const allowed = new Set(options.allow ?? [])\n const emit = (node: unknown, element: string) => {\n const target = ELEMENT_PRIMITIVES[element]!\n context.report({\n node,\n messageId: 'rawElement',\n data: { element, primitive: target.primitive, path: target.path },\n ...(options.message ? { message: options.message } : {}),\n })\n }\n return {\n JSXOpeningElement(node: any) {\n if (node.name?.type !== 'JSXIdentifier') return\n const element = node.name.name\n if (!(element in ELEMENT_PRIMITIVES) || allowed.has(element)) return\n emit(node, element)\n },\n }\n },\n}\n\ninterface PrimitiveImportOptions {\n /** Package prefixes this rule reports. Defaults to the primitive libraries the ui package wraps. */\n packages?: string[]\n /** Replaces the default report message. */\n message?: string\n}\n\nconst DEFAULT_PRIMITIVE_PACKAGES = Object.freeze([\n '@kobalte/core',\n '@ark-ui',\n '@zag-js',\n '@radix-ui',\n 'radix-ui',\n '@base-ui-components',\n])\n\nconst noPrimitiveLibraryImports: RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'The primitive libraries the design system is built on (Kobalte and friends) are its internal affair — compose the exported components instead of importing the primitives.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n packages: { type: 'array', items: { type: 'string' } },\n message: { type: 'string' },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n primitiveImport:\n 'Import from the design system, not from {{package}}: the primitives it wraps carry the focus, keyboard and ARIA behaviour the components exist to provide.',\n },\n },\n create(context) {\n const options = (context.options?.[0] ?? {}) as PrimitiveImportOptions\n const prefixes = options.packages ?? DEFAULT_PRIMITIVE_PACKAGES\n const offender = (source: unknown): string | null => {\n const name = String(source ?? '')\n return prefixes.find((prefix) => name === prefix || name.startsWith(`${prefix}/`)) ?? null\n }\n const report = (node: unknown, source: unknown) => {\n const pkg = offender(source)!\n context.report({\n node,\n messageId: 'primitiveImport',\n data: { package: pkg },\n ...(options.message ? { message: options.message } : {}),\n })\n }\n return {\n ImportDeclaration(node: any) {\n if (offender(node.source?.value)) report(node, node.source.value)\n },\n ImportExpression(node: any) {\n if (typeof node.source?.value === 'string' && offender(node.source.value))\n report(node, node.source.value)\n },\n }\n },\n}\n\nconst plugin = {\n meta: { name: 'adea' },\n rules: {\n 'no-raw-interactive-elements': noRawInteractiveElements,\n 'no-primitive-library-imports': noPrimitiveLibraryImports,\n },\n}\n\nexport default plugin\n"],"mappings":";;AAsBA,IAAM,qBACJ,OAAO,OAAO;CACZ,QAAQ;EAAE,WAAW;EAAU,MAAM;CAAmC;CACxE,OAAO;EAAE,WAAW;EAAS,MAAM;CAAkC;CACrE,UAAU;EAAE,WAAW;EAAY,MAAM;CAAqC;CAC9E,QAAQ;EAAE,WAAW;EAAU,MAAM;CAAmC;CACxE,QAAQ;EAAE,WAAW;EAAU,MAAM;CAAmC;CACxE,OAAO;EAAE,WAAW;EAAS,MAAM;CAAkC;AACvE,CAAC;AA6BH,IAAM,2BAAuC;CAC3C,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,wJACJ;EACA,QAAQ,CACN;GACE,MAAM;GACN,YAAY;IACV,OAAO;KAAE,MAAM;KAAS,OAAO,EAAE,MAAM,SAAS;IAAE;IAClD,SAAS,EAAE,MAAM,SAAS;GAC5B;GACA,sBAAsB;EACxB,CACF;EACA,UAAU,EACR,YACE,4MACJ;CACF;CACA,OAAO,SAAS;EACd,MAAM,UAAW,QAAQ,UAAU,MAAM,CAAC;EAC1C,MAAM,UAAU,IAAI,IAAI,QAAQ,SAAS,CAAC,CAAC;EAC3C,MAAM,QAAQ,MAAe,YAAoB;GAC/C,MAAM,SAAS,mBAAmB;GAClC,QAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KAAE;KAAS,WAAW,OAAO;KAAW,MAAM,OAAO;IAAK;IAChE,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;GACxD,CAAC;EACH;EACA,OAAO,EACL,kBAAkB,MAAW;GAC3B,IAAI,KAAK,MAAM,SAAS,iBAAiB;GACzC,MAAM,UAAU,KAAK,KAAK;GAC1B,IAAI,EAAE,WAAW,uBAAuB,QAAQ,IAAI,OAAO,GAAG;GAC9D,KAAK,MAAM,OAAO;EACpB,EACF;CACF;AACF;AASA,IAAM,6BAA6B,OAAO,OAAO;CAC/C;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAoDD,IAAM,SAAS;CACb,MAAM,EAAE,MAAM,OAAO;CACrB,OAAO;EACL,+BAA+B;EAC/B,gCAAgC;GArDlC,MAAM;IACJ,MAAM;IACN,MAAM,EACJ,aACE,6KACJ;IACA,QAAQ,CACN;KACE,MAAM;KACN,YAAY;MACV,UAAU;OAAE,MAAM;OAAS,OAAO,EAAE,MAAM,SAAS;MAAE;MACrD,SAAS,EAAE,MAAM,SAAS;KAC5B;KACA,sBAAsB;IACxB,CACF;IACA,UAAU,EACR,iBACE,6JACJ;GACF;GACA,OAAO,SAAS;IACd,MAAM,UAAW,QAAQ,UAAU,MAAM,CAAC;IAC1C,MAAM,WAAW,QAAQ,YAAY;IACrC,MAAM,YAAY,WAAmC;KACnD,MAAM,OAAO,OAAO,UAAU,EAAE;KAChC,OAAO,SAAS,MAAM,WAAW,SAAS,UAAU,KAAK,WAAW,GAAG,OAAO,EAAE,CAAC,KAAK;IACxF;IACA,MAAM,UAAU,MAAe,WAAoB;KACjD,MAAM,MAAM,SAAS,MAAM;KAC3B,QAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,SAAS,IAAI;MACrB,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;KACxD,CAAC;IACH;IACA,OAAO;KACL,kBAAkB,MAAW;MAC3B,IAAI,SAAS,KAAK,QAAQ,KAAK,GAAG,OAAO,MAAM,KAAK,OAAO,KAAK;KAClE;KACA,iBAAiB,MAAW;MAC1B,IAAI,OAAO,KAAK,QAAQ,UAAU,YAAY,SAAS,KAAK,OAAO,KAAK,GACtE,OAAO,MAAM,KAAK,OAAO,KAAK;KAClC;IACF;GACF;EAOkC;CAClC;AACF"}
1
+ {"version":3,"file":"lint.js","names":[],"sources":["../src/lint/index.ts"],"sourcesContent":["/*\n * The design system's own oxlint plugin.\n *\n * The styling rules live in `@shadcn/lint`; what that plugin cannot know is which\n * *elements* the design system owns. This is the other half of the contract, for\n * the applications that consume the library: an interactive element this package\n * provides is not re-implemented at the application layer, and the primitive\n * libraries this package is built from are not imported around it.\n *\n * Loaded as an oxlint jsPlugin:\n *\n * // .oxlintrc.json\n * { \"jsPlugins\": [\"@adea-ai/ui/lint\"], \"rules\": { \"adea/no-raw-interactive-elements\": \"error\" } }\n *\n * Rules are errors by intent. A violation means an application composed its own\n * control out of raw markup — unreachable focus behaviour, an absent keyboard\n * story, a second implementation of a decision this package exists to make once.\n */\n\ntype Json = string | number | boolean | null | Json[] | { [key: string]: Json }\n\n/** The raw elements this package replaces, and the primitive that replaces them. */\nconst ELEMENT_PRIMITIVES: Readonly<Record<string, { primitive: string; path: string }>> =\n Object.freeze({\n button: { primitive: 'Button', path: '@adea-ai/ui/components/ui/button' },\n input: { primitive: 'Input', path: '@adea-ai/ui/components/ui/input' },\n textarea: { primitive: 'Textarea', path: '@adea-ai/ui/components/ui/textarea' },\n select: { primitive: 'Select', path: '@adea-ai/ui/components/ui/select' },\n option: { primitive: 'Select', path: '@adea-ai/ui/components/ui/select' },\n label: { primitive: 'Label', path: '@adea-ai/ui/components/ui/label' },\n })\n\ntype RuleModule = {\n meta: {\n type: 'problem'\n docs: { description: string }\n schema: Json[]\n messages: Record<string, string>\n }\n create: (context: LintContext) => Record<string, (node: any) => void>\n}\n\ninterface LintContext {\n options?: unknown[]\n report: (descriptor: {\n node: unknown\n messageId?: string\n message?: string\n data?: Record<string, Json>\n }) => void\n}\n\ninterface RawElementOptions {\n /** Element names this rule should not report (rare; prefer fixing the file). */\n allow?: string[]\n /** Replaces the default report message. */\n message?: string\n}\n\nconst noRawInteractiveElements: RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Interactive elements this package provides (button, input, textarea, select, option, label) are composed from the package, not written as raw markup.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n allow: { type: 'array', items: { type: 'string' } },\n message: { type: 'string' },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n rawElement:\n '{{element}} is provided by the design system: use {{primitive}} from {{path}}. A raw element has no keyboard story, no focus behaviour and no token styling — that is the work the primitive already did.',\n },\n },\n create(context) {\n const options = (context.options?.[0] ?? {}) as RawElementOptions\n const allowed = new Set(options.allow ?? [])\n const emit = (node: unknown, element: string) => {\n const target = ELEMENT_PRIMITIVES[element]!\n context.report({\n node,\n messageId: 'rawElement',\n data: { element, primitive: target.primitive, path: target.path },\n ...(options.message ? { message: options.message } : {}),\n })\n }\n return {\n JSXOpeningElement(node: any) {\n if (node.name?.type !== 'JSXIdentifier') return\n const element = node.name.name\n if (!(element in ELEMENT_PRIMITIVES) || allowed.has(element)) return\n emit(node, element)\n },\n }\n },\n}\n\ninterface PrimitiveImportOptions {\n /** Package prefixes this rule reports. Defaults to the primitive libraries the ui package wraps. */\n packages?: string[]\n /** Replaces the default report message. */\n message?: string\n}\n\nconst DEFAULT_PRIMITIVE_PACKAGES = Object.freeze([\n '@kobalte/core',\n '@ark-ui',\n '@zag-js',\n '@radix-ui',\n 'radix-ui',\n '@base-ui-components',\n])\n\n/**\n * The interactive ARIA roles an application can dress a generic element in, and\n * the primitive that owns each role. `link` is deliberately absent: an `<a>` is\n * natively interactive and Button itself supports `as=\"a\"` for the link-styled\n * case.\n */\nconst ROLE_PRIMITIVES: Readonly<Record<string, { primitive: string; path: string }>> =\n Object.freeze({\n button: { primitive: 'Button', path: '@adea-ai/ui/components/ui/button' },\n checkbox: { primitive: 'Checkbox', path: '@adea-ai/ui/components/ui/checkbox' },\n radio: { primitive: 'RadioGroup', path: '@adea-ai/ui/components/ui/radio-group' },\n switch: { primitive: 'Switch', path: '@adea-ai/ui/components/ui/switch' },\n tab: { primitive: 'Tabs', path: '@adea-ai/ui/components/ui/tabs' },\n option: { primitive: 'Select', path: '@adea-ai/ui/components/ui/select' },\n menuitem: { primitive: 'DropdownMenu', path: '@adea-ai/ui/components/ui/dropdown-menu' },\n textbox: { primitive: 'Input', path: '@adea-ai/ui/components/ui/input' },\n })\n\n/**\n * The elements whose only job is layout or text. A click handler or a tabindex on\n * one of these is a control being hand-built out of a non-control.\n */\nconst GENERIC_ELEMENTS: ReadonlySet<string> = new Set([\n 'div',\n 'span',\n 'li',\n 'ul',\n 'ol',\n 'td',\n 'th',\n 'p',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'section',\n 'article',\n 'aside',\n 'header',\n 'footer',\n 'nav',\n 'main',\n 'img',\n 'svg',\n])\n\nconst CLICK_HANDLERS: ReadonlySet<string> = new Set(['onClick', 'onDoubleClick'])\n\ninterface InteractiveWrapperOptions {\n /** Element names this rule should not report (rare; prefer fixing the file). */\n allow?: string[]\n /** Replaces the default report message. */\n message?: string\n}\n\n/** Reads a static `role` value; a dynamic role is undecidable statically, so it is left to review. */\nconst roleOf = (node: any): string | null => {\n for (const attribute of node.attributes ?? []) {\n if (attribute.type !== 'JSXAttribute' || attribute.name?.name !== 'role') continue\n if (attribute.value?.type === 'Literal' && typeof attribute.value.value === 'string')\n return attribute.value.value\n return null\n }\n return null\n}\n\n/** Whether the element carries the attribute, by name. */\nconst has = (node: any, name: string): boolean =>\n (node.attributes ?? []).some(\n (attribute: any) => attribute.type === 'JSXAttribute' && attribute.name?.name === name\n )\n\nconst noInteractiveWrappers: RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Generic elements dressed as interactive controls — an interactive role, a click handler or a tabindex on a layout element — re-implement a primitive by hand. Compose the real component.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n allow: { type: 'array', items: { type: 'string' } },\n message: { type: 'string' },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n roleWrapper:\n 'A {{element}} with role=\"{{role}}\" hand-builds {{primitive}}: use {{primitive}} from {{path}}. The primitive brings keyboard activation, focus behaviour and ARIA wiring; the wrapper re-implements them badly.',\n clickWrapper:\n 'A click handler on {{element}} composes {{primitive}} by hand: use {{primitive}} from {{path}}. A div that reacts to clicks is a button that cannot be reached by keyboard.',\n tabindexWrapper:\n 'A tabindex on {{element}} makes a layout element focusable, which is focus management the primitives already own: compose the real component instead.',\n },\n },\n create(context) {\n const options = (context.options?.[0] ?? {}) as InteractiveWrapperOptions\n const allowed = new Set(options.allow ?? [])\n return {\n JSXOpeningElement(node: any) {\n if (node.name?.type !== 'JSXIdentifier') return\n const element = node.name.name\n if (allowed.has(element) || element in ELEMENT_PRIMITIVES) return\n\n const role = roleOf(node)\n if (role && role in ROLE_PRIMITIVES) {\n const target = ROLE_PRIMITIVES[role]!\n context.report({\n node,\n messageId: 'roleWrapper',\n data: { element, role, primitive: target.primitive, path: target.path },\n ...(options.message ? { message: options.message } : {}),\n })\n return\n }\n\n if (GENERIC_ELEMENTS.has(element)) {\n const click = [...CLICK_HANDLERS].find((handler) => has(node, handler))\n if (click) {\n const target = ROLE_PRIMITIVES.button!\n context.report({\n node,\n messageId: 'clickWrapper',\n data: { element, primitive: target.primitive, path: target.path },\n ...(options.message ? { message: options.message } : {}),\n })\n return\n }\n if (has(node, 'tabIndex') || has(node, 'tabindex')) {\n context.report({\n node,\n messageId: 'tabindexWrapper',\n data: { element },\n ...(options.message ? { message: options.message } : {}),\n })\n }\n }\n },\n }\n },\n}\n\nconst noPrimitiveLibraryImports: RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'The primitive libraries the design system is built on (Kobalte and friends) are its internal affair — compose the exported components instead of importing the primitives.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n packages: { type: 'array', items: { type: 'string' } },\n message: { type: 'string' },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n primitiveImport:\n 'Import from the design system, not from {{package}}: the primitives it wraps carry the focus, keyboard and ARIA behaviour the components exist to provide.',\n },\n },\n create(context) {\n const options = (context.options?.[0] ?? {}) as PrimitiveImportOptions\n const prefixes = options.packages ?? DEFAULT_PRIMITIVE_PACKAGES\n const offender = (source: unknown): string | null => {\n const name = String(source ?? '')\n return prefixes.find((prefix) => name === prefix || name.startsWith(`${prefix}/`)) ?? null\n }\n const report = (node: unknown, source: unknown) => {\n const pkg = offender(source)!\n context.report({\n node,\n messageId: 'primitiveImport',\n data: { package: pkg },\n ...(options.message ? { message: options.message } : {}),\n })\n }\n return {\n ImportDeclaration(node: any) {\n if (offender(node.source?.value)) report(node, node.source.value)\n },\n ImportExpression(node: any) {\n if (typeof node.source?.value === 'string' && offender(node.source.value))\n report(node, node.source.value)\n },\n }\n },\n}\n\nconst plugin = {\n meta: { name: 'adea' },\n rules: {\n 'no-raw-interactive-elements': noRawInteractiveElements,\n 'no-primitive-library-imports': noPrimitiveLibraryImports,\n 'no-interactive-wrappers': noInteractiveWrappers,\n },\n}\n\nexport default plugin\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.78.2",
3
+ "version": "0.79.0",
4
4
  "description": "The Adea design system: Kobalte-backed Solid components, semantic OKLCH tokens, and the app-shell layout primitives shared by Adea and Cortana.",
5
5
  "keywords": [
6
6
  "adea",
package/src/lint/index.ts CHANGED
@@ -118,6 +118,153 @@ const DEFAULT_PRIMITIVE_PACKAGES = Object.freeze([
118
118
  '@base-ui-components',
119
119
  ])
120
120
 
121
+ /**
122
+ * The interactive ARIA roles an application can dress a generic element in, and
123
+ * the primitive that owns each role. `link` is deliberately absent: an `<a>` is
124
+ * natively interactive and Button itself supports `as="a"` for the link-styled
125
+ * case.
126
+ */
127
+ const ROLE_PRIMITIVES: Readonly<Record<string, { primitive: string; path: string }>> =
128
+ Object.freeze({
129
+ button: { primitive: 'Button', path: '@adea-ai/ui/components/ui/button' },
130
+ checkbox: { primitive: 'Checkbox', path: '@adea-ai/ui/components/ui/checkbox' },
131
+ radio: { primitive: 'RadioGroup', path: '@adea-ai/ui/components/ui/radio-group' },
132
+ switch: { primitive: 'Switch', path: '@adea-ai/ui/components/ui/switch' },
133
+ tab: { primitive: 'Tabs', path: '@adea-ai/ui/components/ui/tabs' },
134
+ option: { primitive: 'Select', path: '@adea-ai/ui/components/ui/select' },
135
+ menuitem: { primitive: 'DropdownMenu', path: '@adea-ai/ui/components/ui/dropdown-menu' },
136
+ textbox: { primitive: 'Input', path: '@adea-ai/ui/components/ui/input' },
137
+ })
138
+
139
+ /**
140
+ * The elements whose only job is layout or text. A click handler or a tabindex on
141
+ * one of these is a control being hand-built out of a non-control.
142
+ */
143
+ const GENERIC_ELEMENTS: ReadonlySet<string> = new Set([
144
+ 'div',
145
+ 'span',
146
+ 'li',
147
+ 'ul',
148
+ 'ol',
149
+ 'td',
150
+ 'th',
151
+ 'p',
152
+ 'h1',
153
+ 'h2',
154
+ 'h3',
155
+ 'h4',
156
+ 'h5',
157
+ 'h6',
158
+ 'section',
159
+ 'article',
160
+ 'aside',
161
+ 'header',
162
+ 'footer',
163
+ 'nav',
164
+ 'main',
165
+ 'img',
166
+ 'svg',
167
+ ])
168
+
169
+ const CLICK_HANDLERS: ReadonlySet<string> = new Set(['onClick', 'onDoubleClick'])
170
+
171
+ interface InteractiveWrapperOptions {
172
+ /** Element names this rule should not report (rare; prefer fixing the file). */
173
+ allow?: string[]
174
+ /** Replaces the default report message. */
175
+ message?: string
176
+ }
177
+
178
+ /** Reads a static `role` value; a dynamic role is undecidable statically, so it is left to review. */
179
+ const roleOf = (node: any): string | null => {
180
+ for (const attribute of node.attributes ?? []) {
181
+ if (attribute.type !== 'JSXAttribute' || attribute.name?.name !== 'role') continue
182
+ if (attribute.value?.type === 'Literal' && typeof attribute.value.value === 'string')
183
+ return attribute.value.value
184
+ return null
185
+ }
186
+ return null
187
+ }
188
+
189
+ /** Whether the element carries the attribute, by name. */
190
+ const has = (node: any, name: string): boolean =>
191
+ (node.attributes ?? []).some(
192
+ (attribute: any) => attribute.type === 'JSXAttribute' && attribute.name?.name === name
193
+ )
194
+
195
+ const noInteractiveWrappers: RuleModule = {
196
+ meta: {
197
+ type: 'problem',
198
+ docs: {
199
+ description:
200
+ 'Generic elements dressed as interactive controls — an interactive role, a click handler or a tabindex on a layout element — re-implement a primitive by hand. Compose the real component.',
201
+ },
202
+ schema: [
203
+ {
204
+ type: 'object',
205
+ properties: {
206
+ allow: { type: 'array', items: { type: 'string' } },
207
+ message: { type: 'string' },
208
+ },
209
+ additionalProperties: false,
210
+ },
211
+ ],
212
+ messages: {
213
+ roleWrapper:
214
+ 'A {{element}} with role="{{role}}" hand-builds {{primitive}}: use {{primitive}} from {{path}}. The primitive brings keyboard activation, focus behaviour and ARIA wiring; the wrapper re-implements them badly.',
215
+ clickWrapper:
216
+ 'A click handler on {{element}} composes {{primitive}} by hand: use {{primitive}} from {{path}}. A div that reacts to clicks is a button that cannot be reached by keyboard.',
217
+ tabindexWrapper:
218
+ 'A tabindex on {{element}} makes a layout element focusable, which is focus management the primitives already own: compose the real component instead.',
219
+ },
220
+ },
221
+ create(context) {
222
+ const options = (context.options?.[0] ?? {}) as InteractiveWrapperOptions
223
+ const allowed = new Set(options.allow ?? [])
224
+ return {
225
+ JSXOpeningElement(node: any) {
226
+ if (node.name?.type !== 'JSXIdentifier') return
227
+ const element = node.name.name
228
+ if (allowed.has(element) || element in ELEMENT_PRIMITIVES) return
229
+
230
+ const role = roleOf(node)
231
+ if (role && role in ROLE_PRIMITIVES) {
232
+ const target = ROLE_PRIMITIVES[role]!
233
+ context.report({
234
+ node,
235
+ messageId: 'roleWrapper',
236
+ data: { element, role, primitive: target.primitive, path: target.path },
237
+ ...(options.message ? { message: options.message } : {}),
238
+ })
239
+ return
240
+ }
241
+
242
+ if (GENERIC_ELEMENTS.has(element)) {
243
+ const click = [...CLICK_HANDLERS].find((handler) => has(node, handler))
244
+ if (click) {
245
+ const target = ROLE_PRIMITIVES.button!
246
+ context.report({
247
+ node,
248
+ messageId: 'clickWrapper',
249
+ data: { element, primitive: target.primitive, path: target.path },
250
+ ...(options.message ? { message: options.message } : {}),
251
+ })
252
+ return
253
+ }
254
+ if (has(node, 'tabIndex') || has(node, 'tabindex')) {
255
+ context.report({
256
+ node,
257
+ messageId: 'tabindexWrapper',
258
+ data: { element },
259
+ ...(options.message ? { message: options.message } : {}),
260
+ })
261
+ }
262
+ }
263
+ },
264
+ }
265
+ },
266
+ }
267
+
121
268
  const noPrimitiveLibraryImports: RuleModule = {
122
269
  meta: {
123
270
  type: 'problem',
@@ -173,6 +320,7 @@ const plugin = {
173
320
  rules: {
174
321
  'no-raw-interactive-elements': noRawInteractiveElements,
175
322
  'no-primitive-library-imports': noPrimitiveLibraryImports,
323
+ 'no-interactive-wrappers': noInteractiveWrappers,
176
324
  },
177
325
  }
178
326