@af-mobile/eslint-plugin 2.0.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.
@@ -0,0 +1,33 @@
1
+ // L3-3 aiflow/wc-part-naming(warn)
2
+ // 检测:part="xxx" 属性名非 kebab-case
3
+ const KEBAB_RE = /^[a-z]+(-[a-z0-9]+)*$/;
4
+
5
+ export default {
6
+ meta: {
7
+ type: 'suggestion',
8
+ docs: { description: 'part 属性名必须 kebab-case' },
9
+ schema: [],
10
+ messages: {
11
+ naming: "Part name '{{name}}' should be kebab-case (e.g. 'dialog-content')",
12
+ },
13
+ },
14
+ create(context) {
15
+ const filename = context.filename || context.getFilename();
16
+ if (!/src[\\/](?:charts[\\/])?components[\\/].*\.js$/.test(filename)) return {};
17
+
18
+ function checkString(str, node) {
19
+ const re = /part\s*=\s*"([^"]*)"/g;
20
+ let m;
21
+ while ((m = re.exec(str))) {
22
+ const partName = m[1].trim();
23
+ if (partName && !KEBAB_RE.test(partName)) {
24
+ context.report({ node, messageId: 'naming', data: { name: partName } });
25
+ }
26
+ }
27
+ }
28
+ return {
29
+ Literal(node) { if (typeof node.value === 'string') checkString(node.value, node); },
30
+ TemplateElement(node) { if (node.value?.raw) checkString(node.value.raw, node); },
31
+ };
32
+ },
33
+ };
@@ -0,0 +1,72 @@
1
+ // L3-2 aiflow/wc-shadow-use-token(error)
2
+ // 检测:Shadow 组件 CSS 字符串中颜色/间距/字号/圆角硬编码(非 var(--*))
3
+ // 例外:backdrop 遮罩半透明黑
4
+ import postcss from 'postcss';
5
+
6
+ // 硬编码模式:颜色(#xxx / rgb / rgba / 命名色)、间距(Npx / Nrem)、字号、圆角
7
+ const COLOR_RE = /(#([0-9a-f]{3,8})\b|rgb\(|rgba\(|\b(red|blue|green|white|black|gray|yellow|orange|purple|pink|brown|cyan)\b)/i;
8
+ const SIZE_RE = /\b\d+(px|rem)\b/;
9
+
10
+ // 需要检查的 CSS 属性
11
+ const CHECKED_PROPS = new Set([
12
+ 'color', 'background', 'background-color', 'border-color', 'border',
13
+ 'padding', 'margin', 'font-size', 'border-radius', 'width', 'height',
14
+ 'top', 'right', 'bottom', 'left', 'gap', 'box-shadow',
15
+ ]);
16
+
17
+ // 例外属性值(放行)
18
+ const EXCEPTIONS = /backdrop|mask/i;
19
+
20
+ export default {
21
+ meta: {
22
+ type: 'problem',
23
+ docs: { description: 'Shadow 组件 CSS 必须使用 token 变量' },
24
+ schema: [],
25
+ messages: {
26
+ hardcoded: "'{{prop}}: {{value}}' in Shadow CSS must use var(--*). Use token variables for cross-theme visual consistency",
27
+ },
28
+ },
29
+ create(context) {
30
+ const filename = context.filename || context.getFilename();
31
+ if (!/src[\\/](?:charts[\\/])?components[\\/].*\.js$/.test(filename)) return {};
32
+
33
+ const sourceCode = context.sourceCode || context.getSourceCode();
34
+ const source = sourceCode.getText();
35
+ // 仅检查 Shadow 组件(含 useShadow = true)
36
+ if (!/useShadow\s*=\s*true/.test(source)) return {};
37
+
38
+ // 提取 JS 中所有模板字符串内容(CSS 常量)
39
+ function checkCss(cssText, node) {
40
+ if (!cssText.includes(':')) return;
41
+ try {
42
+ const root = postcss.parse(cssText);
43
+ root.walkDecls(decl => {
44
+ if (!CHECKED_PROPS.has(decl.prop)) return;
45
+ // 放行 var(--*) 值
46
+ if (/^var\(/.test(decl.value.trim())) return;
47
+ // 放行 backdrop 例外:选择器含 ::backdrop(遮罩半透明黑,L1 无 mask token,见 L4 §3.4 L3-2)
48
+ const selector = decl.parent?.selector || '';
49
+ if (EXCEPTIONS.test(selector) || EXCEPTIONS.test(decl.prop) || EXCEPTIONS.test(decl.value)) return;
50
+ // 检测硬编码
51
+ if (COLOR_RE.test(decl.value) || SIZE_RE.test(decl.value)) {
52
+ context.report({
53
+ node,
54
+ loc: {
55
+ start: { line: decl.source.start.line, column: decl.source.start.column - 1 },
56
+ end: { line: decl.source.end.line, column: decl.source.end.column },
57
+ },
58
+ messageId: 'hardcoded',
59
+ data: { prop: decl.prop, value: decl.value },
60
+ });
61
+ }
62
+ });
63
+ } catch { /* 非 CSS 内容跳过 */ }
64
+ }
65
+
66
+ return {
67
+ TemplateElement(node) {
68
+ if (node.value?.raw) checkCss(node.value.raw, node);
69
+ },
70
+ };
71
+ },
72
+ };
@@ -0,0 +1,87 @@
1
+ {
2
+ "af-dialog": {
3
+ "role": "dialog",
4
+ "ariaLabel": true,
5
+ "description": "模态对话框需要 role=dialog 和 aria-label"
6
+ },
7
+ "af-toast": {
8
+ "ariaLive": true,
9
+ "role": "status",
10
+ "description": "轻提示需要 role=status 和 aria-live=polite"
11
+ },
12
+ "af-tabs": {
13
+ "role": "tablist",
14
+ "description": "标签页容器需要 role=tablist"
15
+ },
16
+ "af-action-sheet": {
17
+ "role": "dialog",
18
+ "ariaLabel": true,
19
+ "description": "底部弹窗需要 role=dialog 和 aria-label"
20
+ },
21
+ "af-list": {
22
+ "role": "list",
23
+ "description": "列表容器需要 role=list"
24
+ },
25
+ "af-swiper": {
26
+ "role": "region",
27
+ "ariaLabel": true,
28
+ "description": "轮播图需要 role=region 和 aria-label"
29
+ },
30
+ "af-dropdown": {
31
+ "role": "listbox",
32
+ "description": "下拉菜单触发器是 combobox,列表用 role=listbox(WAI-ARIA combobox 模式)"
33
+ },
34
+ "af-backtop": {
35
+ "ariaLabel": true,
36
+ "description": "返回顶部按钮需要 aria-label"
37
+ },
38
+ "af-picker": {
39
+ "role": "listbox",
40
+ "description": "选择器列需要 role=listbox"
41
+ },
42
+ "af-img": {
43
+ "description": "图片组件无强制 ARIA 要求"
44
+ },
45
+ "af-switch": {
46
+ "role": "switch",
47
+ "ariaChecked": true,
48
+ "description": "开关需要 role=switch 和 aria-checked"
49
+ },
50
+ "af-search-bar": {
51
+ "description": "搜索栏无强制容器 ARIA;清除按钮需 aria-label(由组件内置)"
52
+ },
53
+ "af-skeleton-page": {
54
+ "role": "status",
55
+ "ariaLive": true,
56
+ "description": "骨架屏需要 role=status 和 aria-live=polite"
57
+ },
58
+ "af-upload": {
59
+ "ariaLabel": true,
60
+ "description": "上传组件的触发按钮需 aria-label;预览网格用 role=list"
61
+ },
62
+ "af-chart-line": {
63
+ "role": "img",
64
+ "ariaLabel": true,
65
+ "description": "折线图 svg 需要 role=img 和 aria-label 摘要(全量数据由 sr-table 提供)"
66
+ },
67
+ "af-chart-bar": {
68
+ "role": "img",
69
+ "ariaLabel": true,
70
+ "description": "柱状图 svg 需要 role=img 和 aria-label 摘要(全量数据由 sr-table 提供)"
71
+ },
72
+ "af-chart-pie": {
73
+ "role": "img",
74
+ "ariaLabel": true,
75
+ "description": "饼图 svg 需要 role=img 和 aria-label 摘要(全量数据由 sr-table 提供)"
76
+ },
77
+ "af-chart-radar": {
78
+ "role": "img",
79
+ "ariaLabel": true,
80
+ "description": "雷达图 svg 需要 role=img 和 aria-label 摘要(全量数据由 sr-table 提供)"
81
+ },
82
+ "af-chart-funnel": {
83
+ "role": "img",
84
+ "ariaLabel": true,
85
+ "description": "漏斗图 svg 需要 role=img 和 aria-label 摘要(全量数据由 sr-table 提供)"
86
+ }
87
+ }
@@ -0,0 +1,55 @@
1
+ // AIFlow UI —— ESLint 规则共享工具
2
+ import { readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { resolve, dirname } from 'node:path';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const whitelistPath = resolve(__dirname, 'whitelist-v1.json');
8
+ const whitelist = JSON.parse(readFileSync(whitelistPath, 'utf8'));
9
+
10
+ // 所有合法 class 集合(recipe + atomic)
11
+ export const ALL_CLASSES = new Set([
12
+ ...whitelist.classes.recipe,
13
+ ...whitelist.classes.atomic,
14
+ ]);
15
+
16
+ // 所有合法组件 tagName 集合
17
+ export const ALL_COMPONENTS = new Set(whitelist.components);
18
+
19
+ // 从字符串中提取所有 class="xxx" 的 class 列表
20
+ export function extractClassLists(str) {
21
+ const results = [];
22
+ const re = /class\s*=\s*"([^"]*)"/g;
23
+ let m;
24
+ while ((m = re.exec(str))) {
25
+ const classes = m[1].split(/\s+/).filter(Boolean);
26
+ if (classes.length) results.push({ classes, offset: m.index, raw: m[0] });
27
+ }
28
+ return results;
29
+ }
30
+
31
+ // 从字符串中提取所有 class='xxx'(单引号)的 class 列表
32
+ export function extractClassListsSingle(str) {
33
+ const results = [];
34
+ const re = /class\s*=\s*'([^']*)'/g;
35
+ let m;
36
+ while ((m = re.exec(str))) {
37
+ const classes = m[1].split(/\s+/).filter(Boolean);
38
+ if (classes.length) results.push({ classes, offset: m.index, raw: m[0] });
39
+ }
40
+ return results;
41
+ }
42
+
43
+ // 合并提取双引号和单引号的 class
44
+ export function extractAllClassLists(str) {
45
+ return [...extractClassLists(str), ...extractClassListsSingle(str)];
46
+ }
47
+
48
+ // 从字符串中提取所有自定义元素标签 <af-xxx>
49
+ export function extractCustomElements(str) {
50
+ const results = [];
51
+ const re = /<([a-z]+-[a-z-]+)/g;
52
+ let m;
53
+ while ((m = re.exec(str))) results.push(m[1]);
54
+ return results;
55
+ }
@@ -0,0 +1,278 @@
1
+ {
2
+ "version": "v1",
3
+ "aiflowVersion": "1.0.0",
4
+ "classes": {
5
+ "recipe": [
6
+ "actions",
7
+ "avatar",
8
+ "badge",
9
+ "body",
10
+ "btn",
11
+ "btn-block",
12
+ "btn-danger",
13
+ "btn-ghost",
14
+ "btn-lg",
15
+ "btn-sm",
16
+ "btn-success",
17
+ "caption",
18
+ "card",
19
+ "cell",
20
+ "center",
21
+ "checkbox",
22
+ "checkbox-sm",
23
+ "checkout-bar",
24
+ "collapse",
25
+ "collapse-content",
26
+ "collapse-summary",
27
+ "divider",
28
+ "empty",
29
+ "form-err",
30
+ "form-row",
31
+ "form-row-h",
32
+ "hero",
33
+ "input",
34
+ "input-bar",
35
+ "input-err",
36
+ "label",
37
+ "list",
38
+ "list-item",
39
+ "list-item-compact",
40
+ "meta",
41
+ "navbar",
42
+ "navbar-fixed",
43
+ "notice",
44
+ "notice-scroll",
45
+ "notice-text",
46
+ "page",
47
+ "price",
48
+ "price-del",
49
+ "progress",
50
+ "progress-danger",
51
+ "progress-lg",
52
+ "progress-sm",
53
+ "progress-success",
54
+ "radio",
55
+ "radio-sm",
56
+ "rate",
57
+ "rate-lg",
58
+ "rate-readonly",
59
+ "rate-sm",
60
+ "rate-star",
61
+ "search-bar-clear",
62
+ "search-bar-icon",
63
+ "search-bar-wrap",
64
+ "search-input",
65
+ "segmented",
66
+ "segmented-block",
67
+ "segmented-item",
68
+ "sheet",
69
+ "skeleton",
70
+ "skeleton-block",
71
+ "skeleton-block-h-md",
72
+ "skeleton-block-h-sm",
73
+ "skeleton-circle",
74
+ "skeleton-line",
75
+ "skeleton-page",
76
+ "skeleton-w-40",
77
+ "skeleton-w-60",
78
+ "skeleton-w-80",
79
+ "spinner",
80
+ "spinner-lg",
81
+ "spinner-sm",
82
+ "stats-grid",
83
+ "step",
84
+ "step-active",
85
+ "step-circle",
86
+ "step-done",
87
+ "step-label",
88
+ "steps",
89
+ "subtitle",
90
+ "switch",
91
+ "switch-loading",
92
+ "switch-on",
93
+ "switch-sm",
94
+ "switch-thumb",
95
+ "tab-item",
96
+ "tabbar",
97
+ "tabbar-fixed",
98
+ "tag",
99
+ "tag-danger",
100
+ "tag-ok",
101
+ "tag-warn",
102
+ "textarea",
103
+ "thumb",
104
+ "title",
105
+ "toast",
106
+ "upload-grid",
107
+ "upload-trigger"
108
+ ],
109
+ "atomic": [
110
+ "aic",
111
+ "bg-brand",
112
+ "bg-muted",
113
+ "f",
114
+ "fc",
115
+ "flex-1",
116
+ "g-0",
117
+ "g-1",
118
+ "g-2",
119
+ "g-3",
120
+ "g-4",
121
+ "jcc",
122
+ "jce",
123
+ "jcsb",
124
+ "m-0",
125
+ "m-1",
126
+ "m-2",
127
+ "m-3",
128
+ "m-4",
129
+ "p-0",
130
+ "p-1",
131
+ "p-10",
132
+ "p-2",
133
+ "p-3",
134
+ "p-4",
135
+ "p-5",
136
+ "p-6",
137
+ "p-8",
138
+ "r-0",
139
+ "r-f",
140
+ "r-l",
141
+ "r-m",
142
+ "r-s",
143
+ "shadow-lg",
144
+ "shadow-md",
145
+ "shadow-sm",
146
+ "t-b",
147
+ "t-center",
148
+ "t-left",
149
+ "t-lg",
150
+ "t-m",
151
+ "t-md",
152
+ "t-right",
153
+ "t-sm",
154
+ "t-xl",
155
+ "t-xs",
156
+ "text-brand",
157
+ "text-danger",
158
+ "text-muted",
159
+ "text-success",
160
+ "w-full",
161
+ "ws-nowrap"
162
+ ]
163
+ },
164
+ "components": [
165
+ "af-action-sheet",
166
+ "af-backtop",
167
+ "af-badge",
168
+ "af-calendar",
169
+ "af-cascade-picker",
170
+ "af-chart-bar",
171
+ "af-chart-funnel",
172
+ "af-chart-line",
173
+ "af-chart-pie",
174
+ "af-chart-radar",
175
+ "af-countdown",
176
+ "af-dialog",
177
+ "af-dropdown",
178
+ "af-field",
179
+ "af-img",
180
+ "af-list",
181
+ "af-navbar",
182
+ "af-notice-bar",
183
+ "af-picker",
184
+ "af-progress",
185
+ "af-pull-refresh",
186
+ "af-rate",
187
+ "af-search-bar",
188
+ "af-skeleton-page",
189
+ "af-stepper",
190
+ "af-steps",
191
+ "af-swipe-cell",
192
+ "af-swiper",
193
+ "af-switch",
194
+ "af-tabbar",
195
+ "af-tabs",
196
+ "af-toast",
197
+ "af-upload"
198
+ ],
199
+ "tokens": [
200
+ "--c-bg",
201
+ "--c-border",
202
+ "--c-brand",
203
+ "--c-card",
204
+ "--c-danger",
205
+ "--c-muted",
206
+ "--c-muted-bg",
207
+ "--c-onbrand",
208
+ "--c-success",
209
+ "--c-text",
210
+ "--c-warn",
211
+ "--dur-base",
212
+ "--dur-fast",
213
+ "--dur-slow",
214
+ "--ease-in-out",
215
+ "--ease-out",
216
+ "--fw-bold",
217
+ "--fw-medium",
218
+ "--fw-normal",
219
+ "--lh-normal",
220
+ "--lh-tight",
221
+ "--palette-bg",
222
+ "--palette-border",
223
+ "--palette-brand",
224
+ "--palette-card",
225
+ "--palette-color-scheme",
226
+ "--palette-danger",
227
+ "--palette-muted",
228
+ "--palette-muted-bg",
229
+ "--palette-onbrand",
230
+ "--palette-shadow-lg",
231
+ "--palette-shadow-md",
232
+ "--palette-shadow-sm",
233
+ "--palette-success",
234
+ "--palette-text",
235
+ "--palette-warn",
236
+ "--r-f",
237
+ "--r-l",
238
+ "--r-m",
239
+ "--r-s",
240
+ "--s-1",
241
+ "--s-2",
242
+ "--s-3",
243
+ "--s-4",
244
+ "--s-5",
245
+ "--s-6",
246
+ "--shadow-lg",
247
+ "--shadow-md",
248
+ "--shadow-sm",
249
+ "--t-lg",
250
+ "--t-md",
251
+ "--t-sm",
252
+ "--t-xl",
253
+ "--t-xs",
254
+ "--z-base",
255
+ "--z-dropdown",
256
+ "--z-modal",
257
+ "--z-sticky"
258
+ ],
259
+ "forbiddenInlineStyle": [
260
+ "color",
261
+ "background",
262
+ "background-color",
263
+ "background-image",
264
+ "padding",
265
+ "padding-top",
266
+ "padding-right",
267
+ "padding-bottom",
268
+ "padding-left",
269
+ "margin",
270
+ "margin-top",
271
+ "margin-right",
272
+ "margin-bottom",
273
+ "margin-left",
274
+ "font-size",
275
+ "border-radius",
276
+ "box-shadow"
277
+ ]
278
+ }