sellmate-design-system-react 9.0.0-beta.75 → 9.0.0-beta.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/AGENTS.md +35 -0
  2. package/README.md +5 -5
  3. package/bin/sellmate-ds.mjs +91 -59
  4. package/dist/components/SCircleProgress/README.md +6 -3
  5. package/dist/components/SCircleProgress/SCircleProgress.d.ts +11 -3
  6. package/dist/components/SLinearProgress/README.md +2 -2
  7. package/dist/components/SLinearProgress/SLinearProgress.d.ts +8 -2
  8. package/dist/components/SSelect/README.md +18 -2
  9. package/dist/components/SSelect/SSelect.d.ts +28 -5
  10. package/dist/components/SSelect/index.d.ts +1 -1
  11. package/dist/components/STable/README.md +29 -20
  12. package/dist/components/STable/STable.d.ts +47 -31
  13. package/dist/components/STable/index.d.ts +1 -1
  14. package/dist/index.cjs +107 -50
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.js +107 -50
  17. package/dist/index.js.map +1 -1
  18. package/dist/llms-full.txt +90 -28
  19. package/dist/llms.txt +37 -1
  20. package/dist/styles.css +16 -0
  21. package/dist/theme.css +3 -0
  22. package/eslint/index.mjs +42 -42
  23. package/eslint/lib/class-names.mjs +23 -25
  24. package/eslint/lib/table-column.mjs +11 -11
  25. package/eslint/rules/component-group-gap.mjs +32 -30
  26. package/eslint/rules/divider-vertical-height.mjs +22 -22
  27. package/eslint/rules/field-width-grade.mjs +50 -50
  28. package/eslint/rules/no-arbitrary-class.mjs +63 -20
  29. package/eslint/rules/no-off-scale-spacing.mjs +61 -41
  30. package/eslint/rules/no-raw-html-control.mjs +38 -36
  31. package/eslint/rules/prefer-typo-preset.mjs +23 -17
  32. package/eslint/rules/require-locale-number.mjs +14 -19
  33. package/eslint/rules/table-column-width.mjs +21 -17
  34. package/eslint/rules/table-numeric-align.mjs +14 -14
  35. package/eslint/scale.gen.mjs +1 -1
  36. package/package.json +3 -3
@@ -10,14 +10,12 @@
10
10
  * 동적으로 조립되는 부분(변수·삼항의 식별자 등)은 정적 분석 대상이 아니므로 건너뛴다.
11
11
  */
12
12
 
13
- const CLASS_ATTRS = new Set(["className", "class"]);
14
- const CLASS_HELPERS = new Set(["cn", "clsx", "classNames", "classnames", "cva", "twMerge"]);
13
+ const CLASS_ATTRS = new Set(['className', 'class']);
14
+ const CLASS_HELPERS = new Set(['cn', 'clsx', 'classNames', 'classnames', 'cva', 'twMerge']);
15
15
 
16
16
  /** 문자열을 공백 기준 클래스 토큰으로 나눈다 (variant 접두어는 유지) */
17
17
  export function tokenize(value) {
18
- return String(value)
19
- .split(/\s+/)
20
- .filter(Boolean);
18
+ return String(value).split(/\s+/).filter(Boolean);
21
19
  }
22
20
 
23
21
  /**
@@ -38,16 +36,16 @@ function indexOfTopLevelColon(s) {
38
36
  let depth = 0;
39
37
  for (let i = 0; i < s.length; i++) {
40
38
  const c = s[i];
41
- if (c === "[") depth++;
42
- else if (c === "]") depth--;
43
- else if (c === ":" && depth === 0) return i;
39
+ if (c === '[') depth++;
40
+ else if (c === ']') depth--;
41
+ else if (c === ':' && depth === 0) return i;
44
42
  }
45
43
  return -1;
46
44
  }
47
45
 
48
46
  /** `!gap-12` / `-mt-8` 같은 접두 기호 제거 */
49
47
  export function stripModifiers(token) {
50
- return token.replace(/^!/, "").replace(/^-/, "");
48
+ return token.replace(/^!/, '').replace(/^-/, '');
51
49
  }
52
50
 
53
51
  /**
@@ -55,7 +53,7 @@ export function stripModifiers(token) {
55
53
  * @returns {Array<{ token: string, node: import('estree').Node }>}
56
54
  */
57
55
  export function collectClassTokens(attrNode) {
58
- if (attrNode.type !== "JSXAttribute") return [];
56
+ if (attrNode.type !== 'JSXAttribute') return [];
59
57
  const name = attrNode.name?.name;
60
58
  if (!CLASS_ATTRS.has(name)) return [];
61
59
 
@@ -64,46 +62,46 @@ export function collectClassTokens(attrNode) {
64
62
  for (const token of tokenize(raw)) out.push({ token, node });
65
63
  };
66
64
 
67
- const visit = (node) => {
65
+ const visit = node => {
68
66
  if (!node) return;
69
67
  switch (node.type) {
70
- case "Literal":
71
- if (typeof node.value === "string") push(node.value, node);
68
+ case 'Literal':
69
+ if (typeof node.value === 'string') push(node.value, node);
72
70
  break;
73
- case "TemplateLiteral":
71
+ case 'TemplateLiteral':
74
72
  // 정적 부분만 본다. 보간(${}) 안쪽은 아래 표현식 방문으로 따로 처리.
75
- for (const q of node.quasis) push(q.value.cooked ?? "", node);
73
+ for (const q of node.quasis) push(q.value.cooked ?? '', node);
76
74
  for (const e of node.expressions) visit(e);
77
75
  break;
78
- case "JSXExpressionContainer":
76
+ case 'JSXExpressionContainer':
79
77
  visit(node.expression);
80
78
  break;
81
- case "CallExpression": {
79
+ case 'CallExpression': {
82
80
  const callee = node.callee;
83
- const fnName = callee?.type === "Identifier" ? callee.name : callee?.property?.name;
81
+ const fnName = callee?.type === 'Identifier' ? callee.name : callee?.property?.name;
84
82
  if (fnName && CLASS_HELPERS.has(fnName)) {
85
83
  for (const arg of node.arguments) visit(arg);
86
84
  }
87
85
  break;
88
86
  }
89
- case "ConditionalExpression":
87
+ case 'ConditionalExpression':
90
88
  visit(node.consequent);
91
89
  visit(node.alternate);
92
90
  break;
93
- case "LogicalExpression":
91
+ case 'LogicalExpression':
94
92
  visit(node.left);
95
93
  visit(node.right);
96
94
  break;
97
- case "ArrayExpression":
95
+ case 'ArrayExpression':
98
96
  for (const el of node.elements) visit(el);
99
97
  break;
100
- case "ObjectExpression":
98
+ case 'ObjectExpression':
101
99
  // clsx({ 'gap-12': cond }) 형태 — 키가 클래스다
102
100
  for (const prop of node.properties) {
103
- if (prop.type === "Property" && prop.key) {
104
- if (prop.key.type === "Literal" && typeof prop.key.value === "string") {
101
+ if (prop.type === 'Property' && prop.key) {
102
+ if (prop.key.type === 'Literal' && typeof prop.key.value === 'string') {
105
103
  push(prop.key.value, prop.key);
106
- } else if (prop.key.type === "Identifier" && !prop.computed) {
104
+ } else if (prop.key.type === 'Identifier' && !prop.computed) {
107
105
  push(prop.key.name, prop.key);
108
106
  }
109
107
  }
@@ -58,17 +58,17 @@ export function isNonQuantityLabel(label) {
58
58
  /** 객체 리터럴에서 프로퍼티 노드를 이름으로 찾는다 */
59
59
  export function prop(objectNode, name) {
60
60
  return objectNode.properties.find(
61
- (p) =>
62
- p.type === "Property" &&
61
+ p =>
62
+ p.type === 'Property' &&
63
63
  !p.computed &&
64
- ((p.key.type === "Identifier" && p.key.name === name) ||
65
- (p.key.type === "Literal" && p.key.value === name)),
64
+ ((p.key.type === 'Identifier' && p.key.name === name) ||
65
+ (p.key.type === 'Literal' && p.key.value === name)),
66
66
  );
67
67
  }
68
68
 
69
69
  /** 문자열 리터럴 값 (아니면 undefined) */
70
70
  export function literalString(node) {
71
- return node?.value?.type === "Literal" && typeof node.value.value === "string"
71
+ return node?.value?.type === 'Literal' && typeof node.value.value === 'string'
72
72
  ? node.value.value
73
73
  : undefined;
74
74
  }
@@ -89,10 +89,10 @@ export function literalString(node) {
89
89
  * @returns {{ nameProp, labelProp, columnName: string | undefined } | null}
90
90
  */
91
91
  export function asColumnShape(node) {
92
- const nameProp = prop(node, "name");
93
- const labelProp = prop(node, "label");
92
+ const nameProp = prop(node, 'name');
93
+ const labelProp = prop(node, 'label');
94
94
  if (!nameProp || !labelProp) return null;
95
- if (!prop(node, "field") && !prop(node, "autoWidth")) return null;
95
+ if (!prop(node, 'field') && !prop(node, 'autoWidth')) return null;
96
96
  // name 이 문자열 리터럴이 아니면 컬럼 정의로 단정하지 않는다 (일반 객체 오탐 방지)
97
97
  const columnName = literalString(nameProp);
98
98
  if (columnName === undefined) return null;
@@ -104,9 +104,9 @@ export function asColumnShape(node) {
104
104
  * @returns {{ nameProp, labelProp, label: string } | null}
105
105
  */
106
106
  export function asColumn(node) {
107
- const nameProp = prop(node, "name");
108
- const labelProp = prop(node, "label");
109
- if (!nameProp || !labelProp || !prop(node, "field")) return null;
107
+ const nameProp = prop(node, 'name');
108
+ const labelProp = prop(node, 'label');
109
+ if (!nameProp || !labelProp || !prop(node, 'field')) return null;
110
110
 
111
111
  const label = literalString(labelProp);
112
112
  if (label === undefined) return null; // 동적 label 은 판단하지 않는다
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * AGENTS.md §2-2.
13
13
  */
14
- import { collectClassTokens, stripVariants, stripModifiers } from "../lib/class-names.mjs";
14
+ import { collectClassTokens, stripVariants, stripModifiers } from '../lib/class-names.mjs';
15
15
 
16
16
  /**
17
17
  * 컴포넌트별 기대 간격 (px).
@@ -33,8 +33,8 @@ const GROUP_GAP = {
33
33
 
34
34
  /** 이 컴포넌트들은 전용 그룹 컴포넌트가 따로 있다 */
35
35
  const HAS_GROUP_COMPONENT = {
36
- SRadio: "SRadioGroup (direction 으로 간격을 맞춥니다)",
37
- SRadioButton: "SRadioGroup — 그룹 간격이 -1px(테두리 겹침)이라 직접 만들지 않습니다",
36
+ SRadio: 'SRadioGroup (direction 으로 간격을 맞춥니다)',
37
+ SRadioButton: 'SRadioGroup — 그룹 간격이 -1px(테두리 겹침)이라 직접 만들지 않습니다',
38
38
  };
39
39
 
40
40
  /** gap 유틸리티에서 px 값을 뽑는다. 토큰 var() 참조면 null(검사 제외) */
@@ -43,7 +43,7 @@ function readGap(tokens) {
43
43
  const util = stripModifiers(stripVariants(token));
44
44
  const m = util.match(/^gap-(.+)$/);
45
45
  if (!m) continue;
46
- if (m[1].startsWith("[")) return { skip: true }; // gap-[var(--...)] 등
46
+ if (m[1].startsWith('[')) return { skip: true }; // gap-[var(--...)] 등
47
47
  // 간격 스케일은 `gap-sd-8` 형태다. 접두 없는 `gap-8` 은 Tailwind 기본 스케일이라
48
48
  // 값이 다르므로(32px) no-off-scale-spacing 이 따로 잡는다 — 여기선 다루지 않는다.
49
49
  const scaled = /^sd-(\d+)$/.exec(m[1]);
@@ -55,21 +55,19 @@ function readGap(tokens) {
55
55
  /** JSX 자식 중 컴포넌트 엘리먼트만 (공백 텍스트 제외) */
56
56
  function elementChildren(node) {
57
57
  return (node.children ?? []).filter(
58
- (ch) =>
59
- ch.type === "JSXElement" &&
60
- ch.openingElement.name.type === "JSXIdentifier",
58
+ ch => ch.type === 'JSXElement' && ch.openingElement.name.type === 'JSXIdentifier',
61
59
  );
62
60
  }
63
61
 
64
- const nameOf = (el) => el.openingElement.name.name;
62
+ const nameOf = el => el.openingElement.name.name;
65
63
 
66
64
  /** 자식들의 size prop 중 가장 먼저 나오는 리터럴 값 */
67
65
  function sizeOf(children) {
68
66
  for (const ch of children) {
69
67
  const attr = ch.openingElement.attributes.find(
70
- (a) => a.type === "JSXAttribute" && a.name?.name === "size",
68
+ a => a.type === 'JSXAttribute' && a.name?.name === 'size',
71
69
  );
72
- if (attr?.value?.type === "Literal" && typeof attr.value.value === "string") {
70
+ if (attr?.value?.type === 'Literal' && typeof attr.value.value === 'string') {
73
71
  return attr.value.value;
74
72
  }
75
73
  }
@@ -78,33 +76,33 @@ function sizeOf(children) {
78
76
 
79
77
  /** spec(number | {default, bySize}) 을 size 로 해석 */
80
78
  function resolve(spec, size) {
81
- if (typeof spec === "number") return spec;
79
+ if (typeof spec === 'number') return spec;
82
80
  if (!spec) return undefined;
83
81
  return (size && spec.bySize?.[size]) ?? spec.default;
84
82
  }
85
83
 
86
84
  export default {
87
85
  meta: {
88
- type: "problem",
86
+ type: 'problem',
89
87
  docs: {
90
- description: "같은 컴포넌트를 나열할 때 컴포넌트별 그룹 간격 사용",
88
+ description: '같은 컴포넌트를 나열할 때 컴포넌트별 그룹 간격 사용',
91
89
  },
92
90
  schema: [
93
91
  {
94
- type: "object",
92
+ type: 'object',
95
93
  properties: {
96
- ignore: { type: "array", items: { type: "string" }, uniqueItems: true },
94
+ ignore: { type: 'array', items: { type: 'string' }, uniqueItems: true },
97
95
  },
98
96
  additionalProperties: false,
99
97
  },
100
98
  ],
101
99
  messages: {
102
100
  wrongGap:
103
- "{{component}} 를 {{dirLabel}} 배열할 때 간격은 `gap-sd-{{expected}}` 입니다{{actualLabel}}. (AGENTS.md §2-2)",
101
+ '{{component}} 를 {{dirLabel}} 배열할 때 간격은 `gap-sd-{{expected}}` 입니다{{actualLabel}}. (AGENTS.md §2-2)',
104
102
  useGroupComponent:
105
- "{{component}} 를 직접 flex 로 감싸지 마세요 — {{suggestion}}. (AGENTS.md §2-2)",
103
+ '{{component}} 를 직접 flex 로 감싸지 마세요 — {{suggestion}}. (AGENTS.md §2-2)',
106
104
  },
107
- fixable: "code",
105
+ fixable: 'code',
108
106
  },
109
107
 
110
108
  create(context) {
@@ -113,7 +111,8 @@ export default {
113
111
  return {
114
112
  JSXElement(node) {
115
113
  const classAttr = node.openingElement.attributes.find(
116
- (a) => a.type === "JSXAttribute" && (a.name?.name === "className" || a.name?.name === "class"),
114
+ a =>
115
+ a.type === 'JSXAttribute' && (a.name?.name === 'className' || a.name?.name === 'class'),
117
116
  );
118
117
  if (!classAttr) return;
119
118
 
@@ -121,23 +120,24 @@ export default {
121
120
  const utils = tokens.map(({ token }) => stripModifiers(stripVariants(token)));
122
121
 
123
122
  // flex 컨테이너가 아니면 대상이 아니다
124
- if (!utils.includes("flex") && !utils.includes("inline-flex")) return;
123
+ if (!utils.includes('flex') && !utils.includes('inline-flex')) return;
125
124
  // 양끝 분리는 gap 규칙 대상이 아니다 (하단 버튼 배치 등)
126
- if (utils.some((u) => u.startsWith("justify-between") || u.startsWith("justify-around"))) return;
125
+ if (utils.some(u => u.startsWith('justify-between') || u.startsWith('justify-around')))
126
+ return;
127
127
 
128
128
  const children = elementChildren(node);
129
129
  if (children.length < 2) return;
130
130
 
131
131
  // 같은 컴포넌트로만 이루어진 그룹만 판단한다
132
132
  const first = nameOf(children[0]);
133
- if (!children.every((ch) => nameOf(ch) === first)) return;
133
+ if (!children.every(ch => nameOf(ch) === first)) return;
134
134
  if (ignore.has(first)) return;
135
135
 
136
136
  // 전용 그룹 컴포넌트가 있으면 그쪽을 권한다
137
137
  if (HAS_GROUP_COMPONENT[first]) {
138
138
  context.report({
139
139
  node: node.openingElement,
140
- messageId: "useGroupComponent",
140
+ messageId: 'useGroupComponent',
141
141
  data: { component: first, suggestion: HAS_GROUP_COMPONENT[first] },
142
142
  });
143
143
  return;
@@ -146,7 +146,7 @@ export default {
146
146
  const spec = GROUP_GAP[first];
147
147
  if (!spec) return;
148
148
 
149
- const vertical = utils.includes("flex-col") || utils.includes("flex-col-reverse");
149
+ const vertical = utils.includes('flex-col') || utils.includes('flex-col-reverse');
150
150
  const dirSpec = spec.both ?? (vertical ? spec.vertical : spec.horizontal);
151
151
  const expected = resolve(dirSpec, sizeOf(children));
152
152
  if (expected == null) return;
@@ -157,24 +157,26 @@ export default {
157
157
 
158
158
  context.report({
159
159
  node: node.openingElement,
160
- messageId: "wrongGap",
160
+ messageId: 'wrongGap',
161
161
  data: {
162
162
  component: first,
163
- dirLabel: spec.both ? "" : vertical ? "세로로" : "가로로",
163
+ dirLabel: spec.both ? '' : vertical ? '세로로' : '가로로',
164
164
  expected: String(expected),
165
- actualLabel: gap.value == null ? " (지정되지 않았습니다)" : ` — 현재 gap-sd-${gap.value}`,
165
+ actualLabel:
166
+ gap.value == null ? ' (지정되지 않았습니다)' : ` — 현재 gap-sd-${gap.value}`,
166
167
  },
167
168
  fix(fixer) {
168
169
  // 기존 gap 유틸리티가 리터럴 문자열일 때만 안전하게 교체한다
169
170
  if (gap.value == null) return null;
170
171
  const literal = classAttr.value;
171
172
  const raw =
172
- literal?.type === "Literal"
173
+ literal?.type === 'Literal'
173
174
  ? literal
174
- : literal?.type === "JSXExpressionContainer" && literal.expression.type === "Literal"
175
+ : literal?.type === 'JSXExpressionContainer' &&
176
+ literal.expression.type === 'Literal'
175
177
  ? literal.expression
176
178
  : null;
177
- if (!raw || typeof raw.value !== "string") return null;
179
+ if (!raw || typeof raw.value !== 'string') return null;
178
180
  const next = raw.value.replace(
179
181
  new RegExp(`(^|\\s)gap-sd-${gap.value}(?=\\s|$)`),
180
182
  `$1gap-sd-${expected}`,
@@ -16,18 +16,18 @@
16
16
  *
17
17
  * AGENTS.md §3-6.
18
18
  */
19
- import { collectClassTokens, stripVariants, stripModifiers } from "../lib/class-names.mjs";
19
+ import { collectClassTokens, stripVariants, stripModifiers } from '../lib/class-names.mjs';
20
20
 
21
21
  /** 높이를 정하는 유틸리티인가 — `h-auto` 만 예외(기본값과 같아 stretch 가 살아 있다) */
22
22
  function heightUtil(util) {
23
23
  const m = /^h-(.+)$/.exec(util);
24
- if (!m || m[1] === "auto") return null;
24
+ if (!m || m[1] === 'auto') return null;
25
25
  return m[1];
26
26
  }
27
27
 
28
28
  /** 교차축 정렬을 이미 직접 지정했는가 */
29
29
  function alignsSelf(util) {
30
- return /^self-/.test(util) || util === "my-auto" || util === "mt-auto" || util === "mb-auto";
30
+ return /^self-/.test(util) || util === 'my-auto' || util === 'mt-auto' || util === 'mb-auto';
31
31
  }
32
32
 
33
33
  /**
@@ -41,45 +41,45 @@ function toHeightProp(value) {
41
41
  if (scaled) return `{${scaled[1]}}`;
42
42
  const arbitrary = /^\[(.+)\]$/.exec(value);
43
43
  if (!arbitrary) return null;
44
- const inner = arbitrary[1].replace(/_/g, " ");
44
+ const inner = arbitrary[1].replace(/_/g, ' ');
45
45
  const px = /^(\d+(?:\.\d+)?)px$/.exec(inner);
46
46
  if (px) return `{${px[1]}}`;
47
47
  if (inner.includes('"')) return null;
48
48
  return `"${inner}"`;
49
49
  }
50
50
 
51
- const isClassAttr = (a) =>
52
- a.type === "JSXAttribute" && (a.name?.name === "className" || a.name?.name === "class");
51
+ const isClassAttr = a =>
52
+ a.type === 'JSXAttribute' && (a.name?.name === 'className' || a.name?.name === 'class');
53
53
 
54
54
  export default {
55
55
  meta: {
56
- type: "problem",
56
+ type: 'problem',
57
57
  docs: {
58
- description: "세로 SDivider 의 높이는 className 이 아니라 height prop 으로 지정",
58
+ description: '세로 SDivider 의 높이는 className 이 아니라 height prop 으로 지정',
59
59
  },
60
60
  schema: [],
61
61
  messages: {
62
62
  useHeightProp:
63
- "세로 SDivider 의 높이는 `height` prop 으로 주세요{{suggestion}} — `className` 으로 주면 `align-self: stretch` 가 무효가 되어 선이 줄 맨 위에 붙습니다. (AGENTS.md §3-6)",
63
+ '세로 SDivider 의 높이는 `height` prop 으로 주세요{{suggestion}} — `className` 으로 주면 `align-self: stretch` 가 무효가 되어 선이 줄 맨 위에 붙습니다. (AGENTS.md §3-6)',
64
64
  },
65
- fixable: "code",
65
+ fixable: 'code',
66
66
  },
67
67
 
68
68
  create(context) {
69
69
  return {
70
70
  JSXOpeningElement(node) {
71
- if (node.name.type !== "JSXIdentifier" || node.name.name !== "SDivider") return;
71
+ if (node.name.type !== 'JSXIdentifier' || node.name.name !== 'SDivider') return;
72
72
 
73
73
  const attrs = node.attributes;
74
- const named = (n) => attrs.find((a) => a.type === "JSXAttribute" && a.name?.name === n);
74
+ const named = n => attrs.find(a => a.type === 'JSXAttribute' && a.name?.name === n);
75
75
 
76
76
  // 세로 분할선만 대상. `vertical={expr}` 처럼 값이 정적이지 않으면 판단하지 않는다.
77
- const vertical = named("vertical");
77
+ const vertical = named('vertical');
78
78
  if (!vertical) return;
79
79
  if (vertical.value !== null && vertical.value?.expression?.value !== true) return;
80
80
 
81
81
  // 이미 height prop 을 쓰고 있으면 컴포넌트가 정렬까지 맡는다
82
- if (named("height")) return;
82
+ if (named('height')) return;
83
83
 
84
84
  const classAttr = attrs.find(isClassAttr);
85
85
  if (!classAttr) return;
@@ -98,29 +98,29 @@ export default {
98
98
 
99
99
  context.report({
100
100
  node,
101
- messageId: "useHeightProp",
102
- data: { suggestion: propValue ? ` (\`height=${propValue}\`)` : "" },
101
+ messageId: 'useHeightProp',
102
+ data: { suggestion: propValue ? ` (\`height=${propValue}\`)` : '' },
103
103
  fix(fixer) {
104
104
  if (!propValue) return null;
105
105
  // className 이 통짜 문자열일 때만 안전하게 손댄다 (cn()·템플릿 리터럴은 제외)
106
106
  const raw =
107
- classAttr.value?.type === "Literal"
107
+ classAttr.value?.type === 'Literal'
108
108
  ? classAttr.value
109
- : classAttr.value?.type === "JSXExpressionContainer" &&
110
- classAttr.value.expression.type === "Literal"
109
+ : classAttr.value?.type === 'JSXExpressionContainer' &&
110
+ classAttr.value.expression.type === 'Literal'
111
111
  ? classAttr.value.expression
112
112
  : null;
113
- if (!raw || typeof raw.value !== "string") return null;
113
+ if (!raw || typeof raw.value !== 'string') return null;
114
114
 
115
115
  const rest = raw.value
116
116
  .split(/\s+/)
117
117
  .filter(Boolean)
118
- .filter((t) => t !== `h-${height}`);
118
+ .filter(t => t !== `h-${height}`);
119
119
  if (rest.length === raw.value.split(/\s+/).filter(Boolean).length) return null;
120
120
 
121
121
  const fixes = [fixer.insertTextAfter(vertical, ` height=${propValue}`)];
122
122
  if (rest.length > 0) {
123
- fixes.push(fixer.replaceText(raw, `"${rest.join(" ")}"`));
123
+ fixes.push(fixer.replaceText(raw, `"${rest.join(' ')}"`));
124
124
  } else {
125
125
  // 속성만 지우면 공백이 두 칸 남는다 — 앞 공백까지 함께 지운다
126
126
  const src = context.sourceCode ?? context.getSourceCode();