@af-mobile/eslint-plugin 2.0.4 → 2.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.
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // af-mobile UI —— eslint-plugin-af-mobile 入口(ESLint 9 flat config 兼容)
2
- // 20 条规则:L1(2) + L2(7) + L3(6) + L3.5(5)
2
+ // 24 条规则:L1(2) + L2(8) + L3(6) + L3.5(5) + k(3)
3
3
  // (definePage 消费端 7 条规则已随 definePage 全局单例移除)
4
4
  import noTokenModification from './rules/no-token-modification.js';
5
5
  import noInlineStyle from './rules/no-inline-style.js';
@@ -10,6 +10,7 @@ import noArbitraryValue from './rules/no-arbitrary-value.js';
10
10
  import noTailwindSyntax from './rules/no-tailwind-syntax.js';
11
11
  import preferComponent from './rules/prefer-component.js';
12
12
  import atomicDuplicate from './rules/atomic-duplicate.js';
13
+ import noEmojiIcon from './rules/no-emoji-icon.js';
13
14
  import noRegisterAll from './rules/no-register-all.js';
14
15
  import wcLightNoStyle from './rules/wc-light-no-style.js';
15
16
  import wcShadowUseToken from './rules/wc-shadow-use-token.js';
@@ -21,15 +22,18 @@ import wcBlockNoInternalRef from './rules/wc-block-no-internal-ref.js';
21
22
  import wcBlockPropsCount from './rules/wc-block-props-count.js';
22
23
  import wcBlockStates from './rules/wc-block-states.js';
23
24
  import wcBlockVariantEnum from './rules/wc-block-variant-enum.js';
25
+ import kNoBareAnd from './rules/k-no-bare-and.js';
26
+ import kNoObjectInterpolation from './rules/k-no-object-interpolation.js';
27
+ import kForRequireKey from './rules/k-for-require-key.js';
24
28
 
25
29
  const plugin = {
26
- meta: { name: 'eslint-plugin-af-mobile', version: '2.0.4' },
30
+ meta: { name: 'eslint-plugin-af-mobile', version: '2.1.0' },
27
31
  rules: {
28
32
  // L1(2 条,全部 error;tokens-css-locked 由 CODEOWNERS + 分支保护,非 ESLint 规则)
29
33
  'no-token-modification': noTokenModification,
30
34
  'no-inline-style': noInlineStyle,
31
35
  // L1-3 tokens-css-locked 是 CODEOWNERS + 分支保护,非 ESLint 规则
32
- // L2(7 条:4 error + 3 warn)
36
+ // L2(8 条:4 error + 4 warn)
33
37
  'token-whitelist': tokenWhitelist,
34
38
  'no-recipe-break': noRecipeBreak,
35
39
  'no-variant-conflict': noVariantConflict,
@@ -37,6 +41,7 @@ const plugin = {
37
41
  'no-tailwind-syntax': noTailwindSyntax,
38
42
  'prefer-component': preferComponent,
39
43
  'atomic-duplicate': atomicDuplicate,
44
+ 'no-emoji-icon': noEmojiIcon,
40
45
  // L3(6 条:4 error + 2 warn)
41
46
  'wc-light-no-style': wcLightNoStyle,
42
47
  'wc-shadow-use-token': wcShadowUseToken,
@@ -50,6 +55,11 @@ const plugin = {
50
55
  'wc-block-props-count': wcBlockPropsCount,
51
56
  'wc-block-states': wcBlockStates,
52
57
  'wc-block-variant-enum': wcBlockVariantEnum,
58
+ // k 层(3 条:D-001=B 应用层推广的配套约束;仅对从 @af-mobile/ui/k 导入的 html/For 生效,
59
+ // 主包同名 API 语义不同不受约束,规则内经 collectKImports 判定)
60
+ 'k-no-bare-and': kNoBareAnd,
61
+ 'k-no-object-interpolation': kNoObjectInterpolation,
62
+ 'k-for-require-key': kForRequireKey,
53
63
  },
54
64
  configs: {},
55
65
  };
@@ -73,6 +83,7 @@ Object.assign(plugin.configs, {
73
83
  'af-mobile/no-variant-conflict': 'warn',
74
84
  'af-mobile/prefer-component': 'warn',
75
85
  'af-mobile/atomic-duplicate': 'warn',
86
+ 'af-mobile/no-emoji-icon': 'warn',
76
87
  // L3 error
77
88
  'af-mobile/wc-light-no-style': 'error',
78
89
  'af-mobile/wc-shadow-use-token': 'error',
@@ -88,6 +99,11 @@ Object.assign(plugin.configs, {
88
99
  // L3.5 error(按需引入铁律:registerAll 全量注册 = 全局引入,一律禁止)
89
100
  'af-mobile/no-register-all': 'error',
90
101
  'af-mobile/wc-block-variant-enum': 'warn',
102
+ // k 层(k 渲染/应用层:裸 && 渲染 "false"、对象插值 "[object Object]" 是静默 bug → error;
103
+ // For 缺 key 对对象列表破坏 keyed 复用但原始值列表合法 → warn)
104
+ 'af-mobile/k-no-bare-and': 'error',
105
+ 'af-mobile/k-no-object-interpolation': 'error',
106
+ 'af-mobile/k-for-require-key': 'warn',
91
107
  },
92
108
  },
93
109
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@af-mobile/eslint-plugin",
3
- "version": "2.0.4",
4
- "description": "af-mobile UI ESLint plugin — 27 rules (L1/L2/L3/L3.5) enforcing design-system + Web Component constraints",
3
+ "version": "2.1.0",
4
+ "description": "af-mobile UI ESLint plugin — 24 rules (L1/L2/L3/L3.5/k) enforcing design-system + Web Component + k-layer constraints",
5
5
  "type": "module",
6
6
  "main": "./index.js",
7
7
  "module": "./index.js",
@@ -2,13 +2,21 @@
2
2
  // 检测:同一 class 属性内出现两个同属性原子(如 p-4 p-2),保留最后一个
3
3
  import { extractAllClassLists } from '../utils/helpers.js';
4
4
 
5
- // 属性前缀映射:p-* → padding, m-* → margin, t-* font-size, r-* → border-radius
5
+ // 字重类与字号类共享 t- 前缀但作用于不同属性(font-weight vs font-size),必须分桶判定:
6
+ // t-xl t-b 是"大字号+粗体"的合法排版组合,不是字号重复
7
+ // (v1.6.1 修复:旧前缀正则曾把两者混为 font-size,autofix 直接删掉字号类,破坏视觉强调)
8
+ const FONT_SIZE = new Set(['t-display', 't-xs', 't-sm', 't-md', 't-lg', 't-xl']);
9
+ const FONT_WEIGHT = new Set(['t-b', 't-m', 't-semibold']);
10
+
11
+ // 属性桶:p-* → padding, m-* → margin, t-* → font-size, r-* → border-radius, 字重类 → font-weight
6
12
  function getPropPrefix(cls) {
13
+ if (FONT_WEIGHT.has(cls)) return 'fw';
14
+ if (FONT_SIZE.has(cls)) return 't';
7
15
  const m = cls.match(/^([pmtr])-(.+)$/);
8
16
  return m ? m[1] : null;
9
17
  }
10
18
 
11
- const PROP_LABELS = { p: 'padding', m: 'margin', t: 'font-size', r: 'border-radius' };
19
+ const PROP_LABELS = { p: 'padding', m: 'margin', t: 'font-size', r: 'border-radius', fw: 'font-weight' };
12
20
 
13
21
  export default {
14
22
  meta: {
@@ -0,0 +1,33 @@
1
+ // k-3 af-mobile/k-for-require-key(warn)
2
+ // For 省略 key 时以项本身为键:原始值列表语义正确;对象列表以引用为键——
3
+ // 数据源每次 set 新数组即整行重建,keyed 复用失效(src/k/README.md 词表卡 key 边界)
4
+ // 修正:显式传 key: 'id'(稳定字段名);纯原始值列表可显式关闭本规则
5
+ // 仅对从 k 入口(@af-mobile/ui/k)导入的 For 生效
6
+ import { collectKImports } from '../utils/helpers.js';
7
+
8
+ export default {
9
+ meta: {
10
+ type: 'suggestion',
11
+ docs: { description: 'k For 建议显式传 key(对象项省略 key 以引用为键,引用变则整行重建)' },
12
+ schema: [],
13
+ messages: {
14
+ forKeyMissing: 'For is missing "key" — object items are keyed by reference when omitted, so every source change rebuilds all rows; pass key: "id" (primitive lists may disable this rule)',
15
+ },
16
+ },
17
+ create(context) {
18
+ const kNames = collectKImports(context.sourceCode.ast.body);
19
+ if (kNames.For.size === 0) return {};
20
+ return {
21
+ CallExpression(node) {
22
+ if (node.callee.type !== 'Identifier' || !kNames.For.has(node.callee.name)) return;
23
+ const arg = node.arguments[0];
24
+ if (!arg || arg.type !== 'ObjectExpression') return;
25
+ const hasKey = arg.properties.some((p) => p.type === 'Property' && (
26
+ (p.key.type === 'Identifier' && p.key.name === 'key')
27
+ || (p.key.type === 'Literal' && p.key.value === 'key')
28
+ ));
29
+ if (!hasKey) context.report({ node: arg, messageId: 'forKeyMissing' });
30
+ },
31
+ };
32
+ },
33
+ };
@@ -0,0 +1,31 @@
1
+ // k-1 af-mobile/k-no-bare-and(error)
2
+ // k 的 html`` 子位渲染 String(val):裸 && 在假值时把字面量 "false" 渲染成文本节点
3
+ // (JSX `cond && <X/>` 的幻觉写法;k 无"假值不渲染"语义)
4
+ // 修正:三元 `${cond ? x : null}`(null → 空串)或 Show({ when, kids })
5
+ // 仅对从 k 入口(@af-mobile/ui/k)导入的 html 生效;主包 html``(字符串拼接)不受约束
6
+ import { collectKImports } from '../utils/helpers.js';
7
+
8
+ export default {
9
+ meta: {
10
+ type: 'problem',
11
+ docs: { description: 'k html`` 子位禁止裸 && 表达式(假值渲染 "false" 文本)' },
12
+ schema: [],
13
+ messages: {
14
+ bareAnd: 'Bare && in k html`` renders literal text "false" when falsy — use a ternary `${cond ? x : null}` or Show({ when, kids })',
15
+ },
16
+ },
17
+ create(context) {
18
+ const kNames = collectKImports(context.sourceCode.ast.body);
19
+ if (kNames.html.size === 0) return {};
20
+ return {
21
+ TaggedTemplateExpression(node) {
22
+ if (node.tag.type !== 'Identifier' || !kNames.html.has(node.tag.name)) return;
23
+ for (const expr of node.quasi.expressions) {
24
+ if (expr.type === 'LogicalExpression' && expr.operator === '&&') {
25
+ context.report({ node: expr, messageId: 'bareAnd' });
26
+ }
27
+ }
28
+ },
29
+ };
30
+ },
31
+ };
@@ -0,0 +1,31 @@
1
+ // k-2 af-mobile/k-no-object-interpolation(error)
2
+ // k 的 html`` 插值经 String(val):对象字面量渲染成 "[object Object]" 文本
3
+ // 典型来源:主包 html`` 的可信 HTML 语法 `${{ raw: '<b>x</b>' }}` 在 k 不存在(JSX/主包双重幻觉)
4
+ // 修正:插 signal/getter、字符串或 DOM 节点;数组合法(bindKids 展开为多节点)不报
5
+ // 仅对从 k 入口(@af-mobile/ui/k)导入的 html 生效;主包 html`` 的 { raw } 语法不受约束
6
+ import { collectKImports } from '../utils/helpers.js';
7
+
8
+ export default {
9
+ meta: {
10
+ type: 'problem',
11
+ docs: { description: 'k html`` 插值禁止对象字面量(渲染 "[object Object]";k 无 { raw } 语法)' },
12
+ schema: [],
13
+ messages: {
14
+ objectInterpolation: 'Object literal in k html`` renders "[object Object]" — k has no { raw } syntax; interpolate a signal/getter, string, or DOM node instead (arrays are legal, they expand to multiple nodes)',
15
+ },
16
+ },
17
+ create(context) {
18
+ const kNames = collectKImports(context.sourceCode.ast.body);
19
+ if (kNames.html.size === 0) return {};
20
+ return {
21
+ TaggedTemplateExpression(node) {
22
+ if (node.tag.type !== 'Identifier' || !kNames.html.has(node.tag.name)) return;
23
+ for (const expr of node.quasi.expressions) {
24
+ if (expr.type === 'ObjectExpression') {
25
+ context.report({ node: expr, messageId: 'objectInterpolation' });
26
+ }
27
+ }
28
+ },
29
+ };
30
+ },
31
+ };
@@ -0,0 +1,69 @@
1
+ // L2-8 af-mobile/no-emoji-icon(warn)
2
+ // 检测 emoji 当图标——"廉价感"第一视觉来源(v1.6.1 规则化,配套 prompt 图标规范)
3
+ // 检测点(低误报,只查图标语境):
4
+ // 1. icon 属性值:tabs/tabbar 配置对象 { label: 'x', icon: '📋' }(旧 few-shot 示例的反模式)
5
+ // 2. HTML 字符串中 tab-item 元素 / data-role="icon" 元素的文本内容含 emoji
6
+ // 修正:用 24px stroke SVG(prompt 图标 path 库);af-tabbar 的 icon 仅支持文本字符,需要图标时省略 icon 字段
7
+ const EMOJI_RE = /\p{Extended_Pictographic}/u;
8
+
9
+ function firstEmoji(str) {
10
+ const m = str.match(EMOJI_RE);
11
+ return m ? m[0] : null;
12
+ }
13
+
14
+ // HTML 字符串中的图标语境:tab-item 元素 / data-role="icon" 元素(取开闭标签之间的纯文本段)
15
+ const ICON_CONTEXT_RES = [
16
+ /<[\w-]+[^>]*\bclass="[^"]*\btab-item\b[^"]*"[^>]*>([^<]*)</g,
17
+ /<[\w-]+[^>]*\bdata-role="icon"[^>]*>([^<]*)</g,
18
+ ];
19
+
20
+ function checkHtmlString(str, node, report) {
21
+ for (const re of ICON_CONTEXT_RES) {
22
+ re.lastIndex = 0;
23
+ let m;
24
+ while ((m = re.exec(str))) {
25
+ const emoji = firstEmoji(m[1]);
26
+ if (emoji) report(node, emoji);
27
+ }
28
+ }
29
+ }
30
+
31
+ export default {
32
+ meta: {
33
+ type: 'suggestion',
34
+ docs: { description: '禁止 emoji 当图标(用 24px stroke SVG)' },
35
+ schema: [],
36
+ messages: {
37
+ emojiIcon: "Emoji {{emoji}} used as icon — use a 24px stroke SVG instead; af-tabbar's icon prop only supports text characters",
38
+ },
39
+ },
40
+ create(context) {
41
+ const report = (node, emoji) => context.report({ node, messageId: 'emojiIcon', data: { emoji } });
42
+ return {
43
+ // { icon: '📋' } 配置对象(af-tabbar / af-tabs 的 tabs 数组)
44
+ Property(node) {
45
+ const keyName = node.key.type === 'Identifier'
46
+ ? node.key.name
47
+ : (node.key.type === 'Literal' ? String(node.key.value) : null);
48
+ if (keyName !== 'icon') return;
49
+ const v = node.value;
50
+ if (v.type === 'Literal' && typeof v.value === 'string') {
51
+ const emoji = firstEmoji(v.value);
52
+ if (emoji) report(v, emoji);
53
+ } else if (v.type === 'TemplateLiteral' && v.expressions.length === 0) {
54
+ const emoji = firstEmoji(v.quasis[0].value.raw);
55
+ if (emoji) report(v, emoji);
56
+ }
57
+ },
58
+ // HTML 字符串(含 template literal)中的图标语境元素
59
+ Literal(node) {
60
+ if (typeof node.value !== 'string') return;
61
+ checkHtmlString(node.value, node, report);
62
+ },
63
+ TemplateElement(node) {
64
+ if (!node.value || !node.value.raw) return;
65
+ checkHtmlString(node.value.raw, node, report);
66
+ },
67
+ };
68
+ },
69
+ };
@@ -66,13 +66,15 @@ export default {
66
66
  additionalProperties: false,
67
67
  }],
68
68
  messages: {
69
- forbidden: "Inline style '{{value}}' is forbidden. Use class instead",
69
+ // v1.6.1:报错带具体违规属性名(旧报错只给整段 style,AI 修正时只能整段删掉重猜,加剧样式削平)
70
+ forbidden: "Inline style property '{{prop}}' is forbidden in '{{value}}'. Use class instead",
70
71
  },
71
72
  },
72
73
 
73
74
  create(context) {
74
75
  const options = context.options[0] || {};
75
76
  const extraAllow = new Set(options.allowProperties || []);
77
+ const nodeText = (node) => (context.sourceCode || context.getSourceCode()).getText(node);
76
78
 
77
79
  return {
78
80
  // 检测字符串字面量(含 template literal)
@@ -81,7 +83,7 @@ export default {
81
83
  const violations = checkStyleString(node.value);
82
84
  for (const v of violations) {
83
85
  if (extraAllow.has(v.prop)) continue;
84
- context.report({ node, messageId: 'forbidden', data: { value: v.value } });
86
+ context.report({ node, messageId: 'forbidden', data: { prop: v.prop, value: v.value } });
85
87
  }
86
88
  },
87
89
  TemplateElement(node) {
@@ -89,7 +91,7 @@ export default {
89
91
  const violations = checkStyleString(node.value.raw);
90
92
  for (const v of violations) {
91
93
  if (extraAllow.has(v.prop)) continue;
92
- context.report({ node, messageId: 'forbidden', data: { value: v.value } });
94
+ context.report({ node, messageId: 'forbidden', data: { prop: v.prop, value: v.value } });
93
95
  }
94
96
  },
95
97
  // 检测 el.style.setProperty('color', ...) / el.style.xxx = '...' 绕过
@@ -106,7 +108,7 @@ export default {
106
108
  if (propArg.value.startsWith('--')) return;
107
109
  const prop = propArg.value.toLowerCase();
108
110
  if (FORBIDDEN.has(prop) && !ALLOWED_LAYOUT.has(prop) && !extraAllow.has(prop)) {
109
- context.report({ node, messageId: 'forbidden', data: { value: `${prop}` } });
111
+ context.report({ node, messageId: 'forbidden', data: { prop, value: nodeText(node) } });
110
112
  }
111
113
  },
112
114
  AssignmentExpression(node) {
@@ -119,7 +121,7 @@ export default {
119
121
  // kebab-case 转换:backgroundColor → background-color
120
122
  const kebab = propName.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
121
123
  if (FORBIDDEN.has(kebab) && !ALLOWED_LAYOUT.has(kebab) && !extraAllow.has(kebab)) {
122
- context.report({ node, messageId: 'forbidden', data: { value: kebab } });
124
+ context.report({ node, messageId: 'forbidden', data: { prop: kebab, value: nodeText(node) } });
123
125
  }
124
126
  },
125
127
  };
@@ -6,16 +6,27 @@ import { extractAllClassLists } from '../utils/helpers.js';
6
6
  const CONFLICT_GROUPS = [
7
7
  ['btn-sm', 'btn-lg'],
8
8
  ['tag-ok', 'tag-warn', 'tag-danger'],
9
+ // 描边式语义色标签(v1.6.1 新增,同 tag 语义色互斥规则)
10
+ ['tag-plain-ok', 'tag-plain-warn', 'tag-plain-danger'],
9
11
  // 圆角类
10
12
  ['r-s', 'r-m', 'r-l', 'r-f'],
11
13
  // padding 类
12
14
  ['p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-5', 'p-6', 'p-7', 'p-8', 'p-10'],
15
+ // 定向 padding 类(v1.6.1 新增,同为 padding-left/right 或 padding-top/bottom)
16
+ ['px-1', 'px-2', 'px-3', 'px-4'],
17
+ ['py-1', 'py-2', 'py-3', 'py-4'],
13
18
  // display 类(同为 display 属性)
14
19
  ['f', 'fi', 'fc'],
20
+ // 网格类(v1.6.1 新增,同为 display:grid,grid-2/3 含列定义)
21
+ ['grid', 'grid-2', 'grid-3'],
22
+ // 比例类(v1.6.1 新增,同为 aspect-ratio)
23
+ ['aspect-1', 'aspect-4-3', 'aspect-16-9'],
15
24
  // 行高类
16
25
  ['lh-tight', 'lh-normal'],
17
- // 背景类
18
- ['bg-brand', 'bg-card', 'bg-muted'],
26
+ // 背景类(v1.6.1 新增渐变/彩色底并入同组:同为 background 属性)
27
+ ['bg-brand', 'bg-card', 'bg-muted', 'bg-brand-soft', 'bg-danger', 'bg-success', 'bg-grad-brand', 'bg-grad-brand-soft'],
28
+ // 边框类(v1.6.1 新增,同为 border)
29
+ ['border', 'border-brand'],
19
30
  ];
20
31
 
21
32
  export default {
package/utils/helpers.js CHANGED
@@ -82,3 +82,25 @@ export function suggestNearest(name, candidates) {
82
82
  }
83
83
  return best ? ` Did you mean '${best}'?` : '';
84
84
  }
85
+
86
+ // k 层导入源:发布名 @af-mobile/ui/k,或仓库内相对路径 k/index.js / k/flow.js
87
+ const K_SOURCE_RE = /^@af-mobile\/ui\/k$|(?:^|[\\/])k[\\/](?:index|flow)\.[cm]?js$/;
88
+
89
+ // 收集从 k 入口导入的绑定名(含别名):{ html: Set<localName>, For: Set<localName> }
90
+ // k 层规则(k-no-bare-and 等)只对从 k 入口导入的 html/For 生效——
91
+ // 主包 html``(返回字符串)语义不同,不受 k 规则约束
92
+ export function collectKImports(programBody) {
93
+ const names = { html: new Set(), For: new Set() };
94
+ for (const stmt of programBody) {
95
+ if (stmt.type !== 'ImportDeclaration') continue;
96
+ if (!K_SOURCE_RE.test(stmt.source.value)) continue;
97
+ for (const spec of stmt.specifiers) {
98
+ if (spec.type !== 'ImportSpecifier') continue;
99
+ const imported = spec.imported.type === 'Identifier'
100
+ ? spec.imported.name
101
+ : String(spec.imported.value);
102
+ if (names[imported]) names[imported].add(spec.local.name);
103
+ }
104
+ }
105
+ return names;
106
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "v1",
3
- "af-mobileVersion": "1.5.2",
3
+ "af-mobileVersion": "1.7.0",
4
4
  "classes": {
5
5
  "recipe": [
6
6
  "actions",
@@ -19,10 +19,13 @@
19
19
  "btn-success",
20
20
  "caption",
21
21
  "card",
22
+ "card-media",
23
+ "card-shadow",
22
24
  "cell",
23
25
  "center",
24
26
  "checkbox",
25
27
  "checkbox-sm",
28
+ "chips",
26
29
  "clp",
27
30
  "clp-ct",
28
31
  "clp-sum",
@@ -30,12 +33,17 @@
30
33
  "cob-fx",
31
34
  "display",
32
35
  "divider",
36
+ "dot",
37
+ "dot-on",
38
+ "dots",
33
39
  "empty",
34
40
  "eyebrow",
35
41
  "form-err",
36
42
  "form-row",
37
43
  "form-row-h",
38
44
  "hero",
45
+ "hero-grad",
46
+ "icon-badge",
39
47
  "input",
40
48
  "input-bar",
41
49
  "input-bar-fx",
@@ -45,6 +53,7 @@
45
53
  "list-item",
46
54
  "list-item-cp",
47
55
  "meta",
56
+ "nav-transparent",
48
57
  "navbar",
49
58
  "navbar-fixed",
50
59
  "notice",
@@ -54,6 +63,7 @@
54
63
  "page-col",
55
64
  "price",
56
65
  "price-del",
66
+ "price-lg",
57
67
  "progress",
58
68
  "progress-danger",
59
69
  "progress-lg",
@@ -77,6 +87,7 @@
77
87
  "section-tt",
78
88
  "seg",
79
89
  "seg-blk",
90
+ "seg-brand",
80
91
  "seg-it",
81
92
  "sheet",
82
93
  "sk",
@@ -92,6 +103,7 @@
92
103
  "spinner",
93
104
  "spinner-lg",
94
105
  "spinner-sm",
106
+ "stat-num",
95
107
  "stats-grid",
96
108
  "step",
97
109
  "step-active",
@@ -114,6 +126,9 @@
114
126
  "tag-md",
115
127
  "tag-ok",
116
128
  "tag-plain",
129
+ "tag-plain-danger",
130
+ "tag-plain-ok",
131
+ "tag-plain-warn",
117
132
  "tag-warn",
118
133
  "textarea",
119
134
  "thumb",
@@ -127,9 +142,19 @@
127
142
  ],
128
143
  "atomic": [
129
144
  "aic",
145
+ "aspect-1",
146
+ "aspect-16-9",
147
+ "aspect-4-3",
130
148
  "bg-brand",
149
+ "bg-brand-soft",
131
150
  "bg-card",
151
+ "bg-danger",
152
+ "bg-grad-brand",
153
+ "bg-grad-brand-soft",
132
154
  "bg-muted",
155
+ "bg-success",
156
+ "border",
157
+ "border-brand",
133
158
  "ellipsis",
134
159
  "ellipsis-2",
135
160
  "f",
@@ -141,6 +166,9 @@
141
166
  "g-2",
142
167
  "g-3",
143
168
  "g-4",
169
+ "grid",
170
+ "grid-2",
171
+ "grid-3",
144
172
  "hl-b",
145
173
  "hl-t",
146
174
  "jcc",
@@ -155,6 +183,7 @@
155
183
  "m-3",
156
184
  "m-4",
157
185
  "m-5",
186
+ "min-w-0",
158
187
  "navbar",
159
188
  "p-0",
160
189
  "p-1",
@@ -166,6 +195,14 @@
166
195
  "p-6",
167
196
  "p-7",
168
197
  "p-8",
198
+ "px-1",
199
+ "px-2",
200
+ "px-3",
201
+ "px-4",
202
+ "py-1",
203
+ "py-2",
204
+ "py-3",
205
+ "py-4",
169
206
  "r-0",
170
207
  "r-f",
171
208
  "r-l",
@@ -189,9 +226,12 @@
189
226
  "t-xs",
190
227
  "text-brand",
191
228
  "text-danger",
229
+ "text-gray-7",
192
230
  "text-muted",
231
+ "text-onbrand",
193
232
  "text-success",
194
233
  "w-full",
234
+ "wrap",
195
235
  "ws-nowrap"
196
236
  ]
197
237
  },