@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.
- package/dist/lint/index.d.ts +1 -0
- package/dist/lint/index.d.ts.map +1 -1
- package/dist/lint.js +150 -0
- package/dist/lint.js.map +1 -1
- package/package.json +1 -1
- package/src/lint/index.ts +148 -0
package/dist/lint/index.d.ts
CHANGED
package/dist/lint/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
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
|
|