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
@@ -19,7 +19,7 @@
19
19
  * ⚠️ 글자수 ↔ 등급 환산은 아직 디자인 확정본이 없어 토큰 값에서 계산한 잠정치다.
20
20
  * 확정표가 오면 CHAR_RATIO 를 그 값으로 바꾼다. GRADES · CHAR_RATIO 모두 옵션으로 덮을 수 있다.
21
21
  */
22
- import { FIELD_WIDTHS } from "../scale.gen.mjs";
22
+ import { FIELD_WIDTHS } from '../scale.gen.mjs';
23
23
 
24
24
  /**
25
25
  * 폭 등급 (px). `--cmp-field-width-*` 토큰에서 scale.gen.mjs 를 거쳐 들어온다 —
@@ -50,26 +50,26 @@ export const CHAR_RATIO = { narrow: 0.55, wide: 1 };
50
50
  */
51
51
  export const COMPONENTS = {
52
52
  SInput: {
53
- lengthProp: "maxLength",
53
+ lengthProp: 'maxLength',
54
54
  font: { sm: 12, md: 14 },
55
55
  chrome: { sm: 26, md: 34 },
56
56
  korean: true,
57
- grades: ["sm", "md", "lg", "xl"],
57
+ grades: ['sm', 'md', 'lg', 'xl'],
58
58
  },
59
59
  SBarcodeInput: {
60
- lengthProp: "maxLength",
60
+ lengthProp: 'maxLength',
61
61
  font: { sm: 12, md: 14 },
62
62
  chrome: { sm: 26, md: 34 },
63
63
  korean: false, // 바코드는 영숫자다
64
- grades: ["sm", "md", "lg", "xl"],
64
+ grades: ['sm', 'md', 'lg', 'xl'],
65
65
  },
66
66
  SNumberInput: {
67
- lengthProp: "max",
67
+ lengthProp: 'max',
68
68
  font: { sm: 12, md: 14 },
69
69
  chrome: { sm: 26, md: 34 },
70
70
  chromeStepper: { sm: 66, md: 82 },
71
71
  korean: false,
72
- grades: ["xs", "sm", "md", "lg", "xl"], // xs 는 숫자 필드 전용 등급이다
72
+ grades: ['xs', 'sm', 'md', 'lg', 'xl'], // xs 는 숫자 필드 전용 등급이다
73
73
  },
74
74
  };
75
75
 
@@ -89,7 +89,7 @@ export function smallestGradeFor(spec, size, len, ratio, grades = GRADES) {
89
89
 
90
90
  /** JSX 속성 노드 */
91
91
  function attr(node, name) {
92
- return node.attributes.find(a => a.type === "JSXAttribute" && a.name?.name === name);
92
+ return node.attributes.find(a => a.type === 'JSXAttribute' && a.name?.name === name);
93
93
  }
94
94
 
95
95
  /**
@@ -103,8 +103,8 @@ function literal(node, name) {
103
103
  if (!a) return undefined;
104
104
  const v = a.value;
105
105
  if (!v) return true; // <SNumberInput useButton /> 같은 boolean shorthand 는 true 다
106
- if (v.type === "Literal") return v.value;
107
- if (v.type === "JSXExpressionContainer" && v.expression.type === "Literal") {
106
+ if (v.type === 'Literal') return v.value;
107
+ if (v.type === 'JSXExpressionContainer' && v.expression.type === 'Literal') {
108
108
  return v.expression.value;
109
109
  }
110
110
  return null;
@@ -118,22 +118,22 @@ function digitsOf(max) {
118
118
 
119
119
  export default {
120
120
  meta: {
121
- type: "problem",
121
+ type: 'problem',
122
122
  docs: {
123
- description: "필드 폭을 maxLength(스키마 상한) 기준 등급으로 고정",
123
+ description: '필드 폭을 maxLength(스키마 상한) 기준 등급으로 고정',
124
124
  },
125
125
  // px → 등급 이름 치환만 자동으로 고친다. 등급 선택 자체는 스키마를 봐야 하므로 손대지 않는다.
126
- fixable: "code",
126
+ fixable: 'code',
127
127
  schema: [
128
128
  {
129
- type: "object",
129
+ type: 'object',
130
130
  properties: {
131
131
  /** 폭 등급 px 재정의 — 디자인 확정본이 오면 여기로 덮는다 */
132
- grades: { type: "object", additionalProperties: { type: "number" } },
132
+ grades: { type: 'object', additionalProperties: { type: 'number' } },
133
133
  /** 글자폭 비율 재정의 */
134
134
  charRatio: {
135
- type: "object",
136
- properties: { narrow: { type: "number" }, wide: { type: "number" } },
135
+ type: 'object',
136
+ properties: { narrow: { type: 'number' }, wide: { type: 'number' } },
137
137
  additionalProperties: false,
138
138
  },
139
139
  },
@@ -142,21 +142,21 @@ export default {
142
142
  ],
143
143
  messages: {
144
144
  noGrade:
145
- "`{{lengthProp}}={{len}}` 로 상한이 정해진 필드인데 `width` 가 없어 행 전체를 차지합니다. `width=\"{{suggest}}\"`({{suggestPx}}px) 등급으로 고정하세요. (§1-2)",
145
+ '`{{lengthProp}}={{len}}` 로 상한이 정해진 필드인데 `width` 가 없어 행 전체를 차지합니다. `width="{{suggest}}"`({{suggestPx}}px) 등급으로 고정하세요. (§1-2)',
146
146
  offGrade:
147
- "`width={{{width}}}` 는 폭 등급이 아닙니다. {{gradeList}} 중 하나이거나, 상한이 없으면 `width=\"100%\"` 여야 합니다. (§1-2)",
147
+ '`width={{{width}}}` 는 폭 등급이 아닙니다. {{gradeList}} 중 하나이거나, 상한이 없으면 `width="100%"` 여야 합니다. (§1-2)',
148
148
  pxGrade:
149
- "`width={{{width}}}` 는 {{grade}} 등급의 px 값입니다. `width=\"{{grade}}\"` 로 등급을 주세요 — px 를 박으면 `--cmp-field-width-*` 가 바뀌어도 이 화면만 그 자리에 남습니다. (§1-2)",
149
+ '`width={{{width}}}` 는 {{grade}} 등급의 px 값입니다. `width="{{grade}}"` 로 등급을 주세요 — px 를 박으면 `--cmp-field-width-*` 가 바뀌어도 이 화면만 그 자리에 남습니다. (§1-2)',
150
150
  noBound:
151
- "`width` 로 폭을 고정했는데 `{{lengthProp}}` 이 없습니다. 폭의 근거가 스키마 상한이므로, 상한이 없으면 행 전체(`width=\"100%\"` 또는 생략)로 둡니다. (§1-2)",
151
+ '`width` 로 폭을 고정했는데 `{{lengthProp}}` 이 없습니다. 폭의 근거가 스키마 상한이므로, 상한이 없으면 행 전체(`width="100%"` 또는 생략)로 둡니다. (§1-2)',
152
152
  tooNarrow:
153
- "`{{lengthProp}}={{len}}` 이 {{grade}} 등급({{gradePx}}px)에 담기지 않습니다. `width=\"{{suggest}}\"`({{suggestPx}}px) 이상이 필요합니다. (§1-2)",
153
+ '`{{lengthProp}}={{len}}` 이 {{grade}} 등급({{gradePx}}px)에 담기지 않습니다. `width="{{suggest}}"`({{suggestPx}}px) 이상이 필요합니다. (§1-2)',
154
154
  tooWide:
155
- "`{{lengthProp}}={{len}}` 은 {{suggest}} 등급({{suggestPx}}px)으로 충분한데 {{grade}} 등급({{gradePx}}px)을 주었습니다. 폭은 상한에 맞춥니다. (§1-2)",
155
+ '`{{lengthProp}}={{len}}` 은 {{suggest}} 등급({{suggestPx}}px)으로 충분한데 {{grade}} 등급({{gradePx}}px)을 주었습니다. 폭은 상한에 맞춥니다. (§1-2)',
156
156
  overMax:
157
- "`{{lengthProp}}={{len}}` 은 최대 등급 xl({{xl}}px)로도 담기지 않습니다. 행 전체(`width=\"100%\"`)로 둡니다. (§1-2)",
157
+ '`{{lengthProp}}={{len}}` 은 최대 등급 xl({{xl}}px)로도 담기지 않습니다. 행 전체(`width="100%"`)로 둡니다. (§1-2)',
158
158
  fullWidthBounded:
159
- "`{{lengthProp}}={{len}}` 로 상한이 정해져 있으므로 `width=\"100%\"` 가 아니라 `width=\"{{suggest}}\"`({{suggestPx}}px) 등급으로 고정합니다. 행 전체는 상한이 없거나 xl 을 넘을 때만 씁니다. (§1-2)",
159
+ '`{{lengthProp}}={{len}}` 로 상한이 정해져 있으므로 `width="100%"` 가 아니라 `width="{{suggest}}"`({{suggestPx}}px) 등급으로 고정합니다. 행 전체는 상한이 없거나 xl 을 넘을 때만 씁니다. (§1-2)',
160
160
  },
161
161
  },
162
162
 
@@ -166,18 +166,19 @@ export default {
166
166
 
167
167
  return {
168
168
  JSXOpeningElement(node) {
169
- if (node.name.type !== "JSXIdentifier") return;
169
+ if (node.name.type !== 'JSXIdentifier') return;
170
170
  const base = COMPONENTS[node.name.name];
171
171
  if (!base) return;
172
172
 
173
173
  // spread 가 있으면 width·maxLength 가 그 안에 있을 수 있어 판단하지 않는다
174
- if (node.attributes.some(a => a.type === "JSXSpreadAttribute")) return;
174
+ if (node.attributes.some(a => a.type === 'JSXSpreadAttribute')) return;
175
175
 
176
- const size = literal(node, "size") === "md" ? "md" : "sm";
176
+ const size = literal(node, 'size') === 'md' ? 'md' : 'sm';
177
177
  // 스테퍼가 붙으면 글자가 쓸 수 있는 폭이 크게 줄어든다 (useButton 기본값은 false)
178
- const spec = literal(node, "useButton") === true && base.chromeStepper
179
- ? { ...base, chrome: base.chromeStepper }
180
- : base;
178
+ const spec =
179
+ literal(node, 'useButton') === true && base.chromeStepper
180
+ ? { ...base, chrome: base.chromeStepper }
181
+ : base;
181
182
 
182
183
  const capacityAt = (grade, r) => capacity(spec, size, grade, r, grades);
183
184
  const smallestFor = (len, r) => smallestGradeFor(spec, size, len, r, grades);
@@ -189,18 +190,14 @@ export default {
189
190
  const rawLen = literal(node, spec.lengthProp);
190
191
  if (rawLen === null) return; // 변수로 넘긴 상한 — 판단 불가
191
192
  const len =
192
- rawLen === undefined
193
- ? undefined
194
- : spec.lengthProp === "max"
195
- ? digitsOf(rawLen)
196
- : rawLen;
193
+ rawLen === undefined ? undefined : spec.lengthProp === 'max' ? digitsOf(rawLen) : rawLen;
197
194
  if (len !== undefined && (!Number.isFinite(len) || len <= 0)) return;
198
195
 
199
- const rawWidth = literal(node, "width");
196
+ const rawWidth = literal(node, 'width');
200
197
  if (rawWidth === null) return; // 변수로 넘긴 폭 — 판단 불가
201
198
 
202
- const reportNode = attr(node, "width") ?? attr(node, spec.lengthProp) ?? node.name;
203
- const gradeList = spec.grades.map(g => `${g}(${grades[g]})`).join(" · ");
199
+ const reportNode = attr(node, 'width') ?? attr(node, spec.lengthProp) ?? node.name;
200
+ const gradeList = spec.grades.map(g => `${g}(${grades[g]})`).join(' · ');
204
201
 
205
202
  /* ── width 가 없다 ── */
206
203
  if (rawWidth === undefined) {
@@ -209,20 +206,20 @@ export default {
209
206
  if (!suggest) return; // xl 로도 안 담긴다 → 행 전체가 맞다
210
207
  context.report({
211
208
  node: reportNode,
212
- messageId: "noGrade",
209
+ messageId: 'noGrade',
213
210
  data: { lengthProp: spec.lengthProp, len: rawLen, suggest, suggestPx: grades[suggest] },
214
211
  });
215
212
  return;
216
213
  }
217
214
 
218
215
  /* ── width="100%" ── */
219
- if (rawWidth === "100%") {
216
+ if (rawWidth === '100%') {
220
217
  if (len === undefined) return; // 상한 없음 + 행 전체 = 규칙대로다
221
218
  const suggest = smallestFor(len, wide);
222
219
  if (!suggest) return; // xl 초과 → 행 전체가 맞다
223
220
  context.report({
224
221
  node: reportNode,
225
- messageId: "fullWidthBounded",
222
+ messageId: 'fullWidthBounded',
226
223
  data: { lengthProp: spec.lengthProp, len: rawLen, suggest, suggestPx: grades[suggest] },
227
224
  });
228
225
  return;
@@ -231,12 +228,15 @@ export default {
231
228
  /* ── width 가 등급인가 ── */
232
229
  // 등급 이름이 정답이다. px 는 등급 값과 일치할 때만 받아 주고(아래 pxGrade) 등급으로 바꾸게 한다.
233
230
  const byName = spec.grades.includes(rawWidth);
234
- const px = typeof rawWidth === "number" ? rawWidth : /^(\d+)px$/.exec(String(rawWidth))?.[1];
235
- const grade = byName ? rawWidth : px != null && spec.grades.find(g => grades[g] === Number(px));
231
+ const px =
232
+ typeof rawWidth === 'number' ? rawWidth : /^(\d+)px$/.exec(String(rawWidth))?.[1];
233
+ const grade = byName
234
+ ? rawWidth
235
+ : px != null && spec.grades.find(g => grades[g] === Number(px));
236
236
  if (!grade) {
237
237
  context.report({
238
238
  node: reportNode,
239
- messageId: "offGrade",
239
+ messageId: 'offGrade',
240
240
  data: { width: String(rawWidth), gradeList },
241
241
  });
242
242
  return;
@@ -246,7 +246,7 @@ export default {
246
246
  if (len === undefined) {
247
247
  context.report({
248
248
  node: reportNode,
249
- messageId: "noBound",
249
+ messageId: 'noBound',
250
250
  data: { lengthProp: spec.lengthProp },
251
251
  });
252
252
  return;
@@ -258,14 +258,14 @@ export default {
258
258
  if (!suggest) {
259
259
  context.report({
260
260
  node: reportNode,
261
- messageId: "overMax",
261
+ messageId: 'overMax',
262
262
  data: { lengthProp: spec.lengthProp, len: rawLen, xl: grades.xl },
263
263
  });
264
264
  return;
265
265
  }
266
266
  context.report({
267
267
  node: reportNode,
268
- messageId: "tooNarrow",
268
+ messageId: 'tooNarrow',
269
269
  data: {
270
270
  lengthProp: spec.lengthProp,
271
271
  len: rawLen,
@@ -283,7 +283,7 @@ export default {
283
283
  if (enough && grades[enough] < grades[grade]) {
284
284
  context.report({
285
285
  node: reportNode,
286
- messageId: "tooWide",
286
+ messageId: 'tooWide',
287
287
  data: {
288
288
  lengthProp: spec.lengthProp,
289
289
  len: rawLen,
@@ -300,7 +300,7 @@ export default {
300
300
  if (!byName) {
301
301
  context.report({
302
302
  node: reportNode,
303
- messageId: "pxGrade",
303
+ messageId: 'pxGrade',
304
304
  data: { width: String(rawWidth), grade },
305
305
  fix: fixer => fixer.replaceText(reportNode, `width="${grade}"`),
306
306
  });
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * AGENTS.md §1-2.
9
9
  */
10
- import { collectClassTokens, stripVariants, stripModifiers } from "../lib/class-names.mjs";
10
+ import { collectClassTokens, stripVariants, stripModifiers } from '../lib/class-names.mjs';
11
11
 
12
12
  /**
13
13
  * 토큰이 존재하는 속성 접두어 — 여기에 임의 값을 쓰면 디자인 시스템을 우회하는 것이다.
@@ -15,59 +15,102 @@ import { collectClassTokens, stripVariants, stripModifiers } from "../lib/class-
15
15
  */
16
16
  const TOKEN_BACKED = [
17
17
  // 색 (text- 는 색·폰트크기 양쪽 모두 토큰이 있어 어느 쪽이든 대상)
18
- "text", "bg", "border", "outline", "ring", "shadow", "fill", "stroke",
19
- "divide", "accent", "caret", "decoration", "from", "via", "to",
18
+ 'text',
19
+ 'bg',
20
+ 'border',
21
+ 'outline',
22
+ 'ring',
23
+ 'shadow',
24
+ 'fill',
25
+ 'stroke',
26
+ 'divide',
27
+ 'accent',
28
+ 'caret',
29
+ 'decoration',
30
+ 'from',
31
+ 'via',
32
+ 'to',
20
33
  // 타이포
21
- "font", "leading", "tracking",
34
+ 'font',
35
+ 'leading',
36
+ 'tracking',
22
37
  // 간격
23
- "gap", "gap-x", "gap-y", "space-x", "space-y",
24
- "p", "px", "py", "pt", "pr", "pb", "pl", "ps", "pe",
25
- "m", "mx", "my", "mt", "mr", "mb", "ml", "ms", "me",
38
+ 'gap',
39
+ 'gap-x',
40
+ 'gap-y',
41
+ 'space-x',
42
+ 'space-y',
43
+ 'p',
44
+ 'px',
45
+ 'py',
46
+ 'pt',
47
+ 'pr',
48
+ 'pb',
49
+ 'pl',
50
+ 'ps',
51
+ 'pe',
52
+ 'm',
53
+ 'mx',
54
+ 'my',
55
+ 'mt',
56
+ 'mr',
57
+ 'mb',
58
+ 'ml',
59
+ 'ms',
60
+ 'me',
26
61
  // 모서리
27
- "rounded", "rounded-t", "rounded-r", "rounded-b", "rounded-l",
28
- "rounded-tl", "rounded-tr", "rounded-br", "rounded-bl",
62
+ 'rounded',
63
+ 'rounded-t',
64
+ 'rounded-r',
65
+ 'rounded-b',
66
+ 'rounded-l',
67
+ 'rounded-tl',
68
+ 'rounded-tr',
69
+ 'rounded-br',
70
+ 'rounded-bl',
29
71
  ];
30
72
 
31
73
  /** 대괄호 안이 토큰 참조인지 — `var(--x)` 또는 `length:var(--x)` 같은 데이터타입 접두어 허용 */
32
74
  function isTokenReference(inner) {
33
- const body = inner.includes(":") ? inner.slice(inner.indexOf(":") + 1) : inner;
75
+ const body = inner.includes(':') ? inner.slice(inner.indexOf(':') + 1) : inner;
34
76
  return /^var\(--[\w-]+\)$/.test(body.trim());
35
77
  }
36
78
 
37
79
  /** `bg-[#eee]` → `bg` 처럼 임의 값 앞의 유틸리티 접두어를 뽑는다 */
38
80
  function prefixOf(util) {
39
- const idx = util.indexOf("[");
40
- return idx <= 0 ? "" : util.slice(0, idx - 1); // 대괄호 바로 앞의 `-` 제외
81
+ const idx = util.indexOf('[');
82
+ return idx <= 0 ? '' : util.slice(0, idx - 1); // 대괄호 바로 앞의 `-` 제외
41
83
  }
42
84
 
43
85
  export default {
44
86
  meta: {
45
- type: "problem",
87
+ type: 'problem',
46
88
  docs: {
47
- description: "색·타이포·간격·모서리에 임의 값 금지 — 토큰 유틸리티 또는 var(--token) 참조 사용",
89
+ description:
90
+ '색·타이포·간격·모서리에 임의 값 금지 — 토큰 유틸리티 또는 var(--token) 참조 사용',
48
91
  },
49
92
  schema: [
50
93
  {
51
- type: "object",
94
+ type: 'object',
52
95
  properties: {
53
96
  /** 예외로 허용할 클래스 (완전 일치) */
54
- allow: { type: "array", items: { type: "string" }, uniqueItems: true },
97
+ allow: { type: 'array', items: { type: 'string' }, uniqueItems: true },
55
98
  /** 검사 대상에서 뺄 유틸리티 접두어 (예: ["shadow"]) */
56
- ignorePrefixes: { type: "array", items: { type: "string" }, uniqueItems: true },
99
+ ignorePrefixes: { type: 'array', items: { type: 'string' }, uniqueItems: true },
57
100
  },
58
101
  additionalProperties: false,
59
102
  },
60
103
  ],
61
104
  messages: {
62
105
  arbitrary:
63
- "`{{token}}` — {{prefix}} 는 디자인 토큰이 있는 속성이라 임의 값을 쓸 수 없습니다. 토큰 유틸리티를 쓰거나 var(--token) 으로 참조하세요. (AGENTS.md §1-2)",
106
+ '`{{token}}` — {{prefix}} 는 디자인 토큰이 있는 속성이라 임의 값을 쓸 수 없습니다. 토큰 유틸리티를 쓰거나 var(--token) 으로 참조하세요. (AGENTS.md §1-2)',
64
107
  },
65
108
  },
66
109
 
67
110
  create(context) {
68
111
  const allow = new Set(context.options[0]?.allow ?? []);
69
112
  const ignored = new Set(context.options[0]?.ignorePrefixes ?? []);
70
- const targets = TOKEN_BACKED.filter((p) => !ignored.has(p));
113
+ const targets = TOKEN_BACKED.filter(p => !ignored.has(p));
71
114
 
72
115
  return {
73
116
  JSXAttribute(node) {
@@ -83,7 +126,7 @@ export default {
83
126
  const prefix = prefixOf(util);
84
127
  if (!targets.includes(prefix)) continue;
85
128
 
86
- context.report({ node: valueNode, messageId: "arbitrary", data: { token, prefix } });
129
+ context.report({ node: valueNode, messageId: 'arbitrary', data: { token, prefix } });
87
130
  }
88
131
  },
89
132
  };
@@ -16,51 +16,70 @@
16
16
  *
17
17
  * AGENTS.md §2-2.
18
18
  */
19
- import { collectClassTokens, stripVariants, stripModifiers } from "../lib/class-names.mjs";
20
- import { SPACING } from "../scale.gen.mjs";
19
+ import { collectClassTokens, stripVariants, stripModifiers } from '../lib/class-names.mjs';
20
+ import { SPACING } from '../scale.gen.mjs';
21
21
 
22
22
  /** 값이 순수하게 간격 스케일에서 오는 유틸리티 접두어만 대상으로 한다 */
23
23
  const SPACING_PREFIXES = [
24
- "gap-x", "gap-y", "gap",
25
- "space-x", "space-y",
26
- "px", "py", "pt", "pr", "pb", "pl", "ps", "pe", "p",
27
- "mx", "my", "mt", "mr", "mb", "ml", "ms", "me", "m",
24
+ 'gap-x',
25
+ 'gap-y',
26
+ 'gap',
27
+ 'space-x',
28
+ 'space-y',
29
+ 'px',
30
+ 'py',
31
+ 'pt',
32
+ 'pr',
33
+ 'pb',
34
+ 'pl',
35
+ 'ps',
36
+ 'pe',
37
+ 'p',
38
+ 'mx',
39
+ 'my',
40
+ 'mt',
41
+ 'mr',
42
+ 'mb',
43
+ 'ml',
44
+ 'ms',
45
+ 'me',
46
+ 'm',
28
47
  ];
29
48
 
30
49
  const ALLOWED = new Set([0, ...SPACING]);
31
50
  const MAX = Math.max(...SPACING);
32
51
 
33
52
  /** Tailwind 기본 스케일에서 `p-<n>` 이 실제로 만드는 px (`--spacing` = 0.25rem) */
34
- const RENDERED_PX = (value) => value * 4;
53
+ const RENDERED_PX = value => value * 4;
35
54
 
36
- const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
55
+ const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
37
56
 
38
57
  /**
39
58
  * 토큰 시작 경계 — 앞은 공백이거나 문자열 따옴표(= 문자열의 시작)여야 한다.
40
59
  * `(?<![\w-])` 만 쓰면 `md:p-4 p-4` 에서 `p-4` 를 찾을 때 variant 안쪽(`md:` 뒤)이
41
60
  * 먼저 걸려 엉뚱한 클래스를 고친다.
42
61
  */
43
- const TOKEN_START = "(?<![^\\s'\"`])";
62
+ const TOKEN_START = '(?<![^\\s\'"`])';
44
63
 
45
64
  export default {
46
65
  meta: {
47
- type: "problem",
66
+ type: 'problem',
48
67
  docs: {
49
- description: "간격 유틸리티는 sd- 접두가 붙은 토큰 스케일 값만 사용",
68
+ description: '간격 유틸리티는 sd- 접두가 붙은 토큰 스케일 값만 사용',
50
69
  },
51
70
  // 스케일 밖 값(`gap-sd-13`)에는 fix 를 내지 않는다. `unsafeFix` 를 켠 팀의
52
71
  // 접두 누락 수정에만 쓰이지만, 선언은 정적이어야 하므로 여기 남는다.
53
- fixable: "code",
72
+ fixable: 'code',
54
73
  hasSuggestions: true,
55
74
  schema: [
56
75
  {
57
- type: "object",
76
+ type: 'object',
58
77
  properties: {
59
78
  /**
60
79
  * 접두 없는 간격 유틸리티(`p-16`)도 잡는다. 기본 false.
61
80
  * 기본값에서는 두 해석(`p-sd-16` = 16px / 현재 값 유지)을 제안으로만 낸다.
62
81
  */
63
- requirePrefix: { type: "boolean" },
82
+ requirePrefix: { type: 'boolean' },
64
83
  /**
65
84
  * 접두 누락을 "클래스의 숫자 = px" 로 단정하고 `p-16` → `p-sd-16` 자동 수정한다.
66
85
  *
@@ -68,27 +87,27 @@ export default {
68
87
  * 켜면 여백이 1/4 로 줄어든다. 이전 버전의 디자인 시스템 스케일을 쓰던 앱을
69
88
  * 한 번에 옮길 때만 켜고, 마이그레이션이 끝나면 다시 끈다.
70
89
  */
71
- unsafeFix: { type: "boolean" },
90
+ unsafeFix: { type: 'boolean' },
72
91
  },
73
92
  additionalProperties: false,
74
93
  },
75
94
  ],
76
95
  messages: {
77
96
  offScale:
78
- "`{{token}}` 의 {{value}} 는 간격 스케일에 없어 스타일이 적용되지 않습니다. 가까운 값: {{nearest}}. (AGENTS.md §2-2)",
97
+ '`{{token}}` 의 {{value}} 는 간격 스케일에 없어 스타일이 적용되지 않습니다. 가까운 값: {{nearest}}. (AGENTS.md §2-2)',
79
98
  // 접두 누락 — 어느 해석이 스케일에 있느냐로 갈린다.
80
99
  // `p-16` 은 지금 64px 이므로 "16px 의도" 와 "64px 유지" 가 서로 다른 결과다.
81
100
  missingPrefixBoth:
82
- "`{{token}}` 은 현재 {{actual}}px 입니다 (Tailwind 기본 스케일). {{value}}px 을 의도했다면 → `{{asLiteral}}` · {{actual}}px 을 유지하려면 → `{{asRendered}}` (AGENTS.md §2-2)",
101
+ '`{{token}}` 은 현재 {{actual}}px 입니다 (Tailwind 기본 스케일). {{value}}px 을 의도했다면 → `{{asLiteral}}` · {{actual}}px 을 유지하려면 → `{{asRendered}}` (AGENTS.md §2-2)',
83
102
  missingPrefixLiteral:
84
- "`{{token}}` 은 현재 {{actual}}px 입니다 (Tailwind 기본 스케일). {{value}}px 을 의도했다면 → `{{asLiteral}}`. {{actual}}px 유지는 간격 스케일에 값이 없습니다. {{fallback}} (AGENTS.md §2-2)",
103
+ '`{{token}}` 은 현재 {{actual}}px 입니다 (Tailwind 기본 스케일). {{value}}px 을 의도했다면 → `{{asLiteral}}`. {{actual}}px 유지는 간격 스케일에 값이 없습니다. {{fallback}} (AGENTS.md §2-2)',
85
104
  missingPrefixRendered:
86
- "`{{token}}` 은 현재 {{actual}}px 입니다 (Tailwind 기본 스케일). {{actual}}px 을 유지하려면 → `{{asRendered}}`. {{value}}px 은 간격 스케일에 없습니다. (AGENTS.md §2-2)",
105
+ '`{{token}}` 은 현재 {{actual}}px 입니다 (Tailwind 기본 스케일). {{actual}}px 을 유지하려면 → `{{asRendered}}`. {{value}}px 은 간격 스케일에 없습니다. (AGENTS.md §2-2)',
87
106
  missingPrefixNone:
88
- "`{{token}}` 은 현재 {{actual}}px 입니다 (Tailwind 기본 스케일). {{value}}px·{{actual}}px 둘 다 간격 스케일에 없습니다. {{fallback}} (AGENTS.md §2-2)",
107
+ '`{{token}}` 은 현재 {{actual}}px 입니다 (Tailwind 기본 스케일). {{value}}px·{{actual}}px 둘 다 간격 스케일에 없습니다. {{fallback}} (AGENTS.md §2-2)',
89
108
  // 제안(suggestion) 설명 — 편집기 목록에 그대로 뜬다
90
- useLiteral: "`{{to}}` 적용 — 숫자를 px 로 해석 ({{actual}}px → {{value}}px, 여백이 바뀝니다)",
91
- useRendered: "`{{to}}` 적용 — 현재 {{actual}}px 유지",
109
+ useLiteral: '`{{to}}` 적용 — 숫자를 px 로 해석 ({{actual}}px → {{value}}px, 여백이 바뀝니다)',
110
+ useRendered: '`{{to}}` 적용 — 현재 {{actual}}px 유지',
92
111
  },
93
112
  },
94
113
 
@@ -98,7 +117,7 @@ export default {
98
117
  const unsafeFix = context.options[0]?.unsafeFix ?? false;
99
118
 
100
119
  /** 토큰 문자열을 소스에서 찾아 치환하는 fixer (없으면 null) */
101
- const replaceToken = (valueNode, token, fixed) => (fixer) => {
120
+ const replaceToken = (valueNode, token, fixed) => fixer => {
102
121
  const text = sourceCode.getText(valueNode);
103
122
  const idx = text.search(new RegExp(`${TOKEN_START}${escapeRegExp(token)}(?![\\w-])`));
104
123
  if (idx === -1) return null;
@@ -107,14 +126,17 @@ export default {
107
126
  };
108
127
 
109
128
  /** 스케일을 value 와의 거리순으로 (동률이면 작은 값 먼저) */
110
- const byDistance = (value) =>
129
+ const byDistance = value =>
111
130
  [...ALLOWED].sort((a, b) => Math.abs(a - value) - Math.abs(b - value) || a - b);
112
131
 
113
132
  /** 가장 가까운 값 하나 — 거리순 배열을 다시 정렬하면 안 된다 (64 → 62 지 60 이 아니다) */
114
- const nearestOne = (value) => byDistance(value)[0];
133
+ const nearestOne = value => byDistance(value)[0];
115
134
 
116
135
  /** 스케일에서 value 에 가장 가까운 값 두 개 (표시용이라 오름차순) */
117
- const nearestOf = (value) => byDistance(value).slice(0, 2).sort((a, b) => a - b);
136
+ const nearestOf = value =>
137
+ byDistance(value)
138
+ .slice(0, 2)
139
+ .sort((a, b) => a - b);
118
140
 
119
141
  /**
120
142
  * 대응 토큰이 없을 때의 안내. 기준은 클래스에 적힌 숫자가 아니라 **실제 렌더 px** 다.
@@ -133,7 +155,7 @@ export default {
133
155
  for (const { token, node: valueNode } of collectClassTokens(node)) {
134
156
  const util = stripModifiers(stripVariants(token));
135
157
 
136
- const prefix = SPACING_PREFIXES.find((p) => util.startsWith(`${p}-`));
158
+ const prefix = SPACING_PREFIXES.find(p => util.startsWith(`${p}-`));
137
159
  if (!prefix) continue;
138
160
 
139
161
  const raw = util.slice(prefix.length + 1);
@@ -150,7 +172,7 @@ export default {
150
172
 
151
173
  const rendered = RENDERED_PX(value);
152
174
  /** `md:p-4` → `md:p-sd-16` — variant·부호 접두는 그대로 두고 뒤만 갈아낀다 */
153
- const rewrite = (px) =>
175
+ const rewrite = px =>
154
176
  token.replace(
155
177
  new RegExp(`${escapeRegExp(`${prefix}-${raw}`)}$`),
156
178
  `${prefix}-sd-${px}`,
@@ -165,22 +187,22 @@ export default {
165
187
  token,
166
188
  value: raw,
167
189
  actual: String(rendered),
168
- asLiteral: asLiteral ?? "",
169
- asRendered: asRendered ?? "",
190
+ asLiteral: asLiteral ?? '',
191
+ asRendered: asRendered ?? '',
170
192
  fallback: fallbackFor(prefix, rendered),
171
193
  };
172
194
 
173
195
  const suggest = [];
174
196
  if (asLiteral) {
175
197
  suggest.push({
176
- messageId: "useLiteral",
198
+ messageId: 'useLiteral',
177
199
  data: { to: asLiteral, actual: String(rendered), value: raw },
178
200
  fix: replaceToken(valueNode, token, asLiteral),
179
201
  });
180
202
  }
181
203
  if (asRendered) {
182
204
  suggest.push({
183
- messageId: "useRendered",
205
+ messageId: 'useRendered',
184
206
  data: { to: asRendered, actual: String(rendered) },
185
207
  fix: replaceToken(valueNode, token, asRendered),
186
208
  });
@@ -188,11 +210,11 @@ export default {
188
210
 
189
211
  const messageId = asLiteral
190
212
  ? asRendered
191
- ? "missingPrefixBoth"
192
- : "missingPrefixLiteral"
213
+ ? 'missingPrefixBoth'
214
+ : 'missingPrefixLiteral'
193
215
  : asRendered
194
- ? "missingPrefixRendered"
195
- : "missingPrefixNone";
216
+ ? 'missingPrefixRendered'
217
+ : 'missingPrefixNone';
196
218
 
197
219
  context.report({
198
220
  node: valueNode,
@@ -200,9 +222,7 @@ export default {
200
222
  data,
201
223
  suggest,
202
224
  // 마이그레이션용 옵트인 — "숫자 = px" 를 단정하므로 여백 값이 바뀐다
203
- ...(unsafeFix && asLiteral
204
- ? { fix: replaceToken(valueNode, token, asLiteral) }
205
- : {}),
225
+ ...(unsafeFix && asLiteral ? { fix: replaceToken(valueNode, token, asLiteral) } : {}),
206
226
  });
207
227
  continue;
208
228
  }
@@ -212,8 +232,8 @@ export default {
212
232
 
213
233
  context.report({
214
234
  node: valueNode,
215
- messageId: "offScale",
216
- data: { token, value: String(value), nearest: nearestOf(value).join(" / ") },
235
+ messageId: 'offScale',
236
+ data: { token, value: String(value), nearest: nearestOf(value).join(' / ') },
217
237
  });
218
238
  }
219
239
  },