sellmate-design-system-react 9.0.0-beta.74 → 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.
- package/AGENTS.md +37 -2
- package/README.md +5 -5
- package/bin/sellmate-ds.mjs +91 -59
- package/dist/components/SCircleProgress/README.md +6 -3
- package/dist/components/SCircleProgress/SCircleProgress.d.ts +11 -3
- package/dist/components/SLauncherListBox/README.md +1 -1
- package/dist/components/SLauncherListBox/launcherListBox.config.d.ts +2 -2
- package/dist/components/SLinearProgress/README.md +2 -2
- package/dist/components/SLinearProgress/SLinearProgress.d.ts +8 -2
- package/dist/components/SLogo/README.md +1 -0
- package/dist/components/SLogo/logo.config.d.ts +1 -1
- package/dist/components/SSelect/README.md +18 -2
- package/dist/components/SSelect/SSelect.d.ts +28 -5
- package/dist/components/SSelect/index.d.ts +1 -1
- package/dist/components/STable/README.md +29 -20
- package/dist/components/STable/STable.d.ts +47 -31
- package/dist/components/STable/index.d.ts +1 -1
- package/dist/index.cjs +167 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +167 -50
- package/dist/index.js.map +1 -1
- package/dist/llms-full.txt +94 -31
- package/dist/llms.txt +39 -3
- package/dist/styles.css +16 -0
- package/dist/theme.css +3 -0
- package/eslint/index.mjs +42 -42
- package/eslint/lib/class-names.mjs +23 -25
- package/eslint/lib/table-column.mjs +11 -11
- package/eslint/rules/component-group-gap.mjs +32 -30
- package/eslint/rules/divider-vertical-height.mjs +22 -22
- package/eslint/rules/field-width-grade.mjs +50 -50
- package/eslint/rules/no-arbitrary-class.mjs +63 -20
- package/eslint/rules/no-off-scale-spacing.mjs +61 -41
- package/eslint/rules/no-raw-html-control.mjs +38 -36
- package/eslint/rules/prefer-typo-preset.mjs +23 -17
- package/eslint/rules/require-locale-number.mjs +14 -19
- package/eslint/rules/table-column-width.mjs +21 -17
- package/eslint/rules/table-numeric-align.mjs +14 -14
- package/eslint/scale.gen.mjs +1 -1
- 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
|
|
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:
|
|
53
|
+
lengthProp: 'maxLength',
|
|
54
54
|
font: { sm: 12, md: 14 },
|
|
55
55
|
chrome: { sm: 26, md: 34 },
|
|
56
56
|
korean: true,
|
|
57
|
-
grades: [
|
|
57
|
+
grades: ['sm', 'md', 'lg', 'xl'],
|
|
58
58
|
},
|
|
59
59
|
SBarcodeInput: {
|
|
60
|
-
lengthProp:
|
|
60
|
+
lengthProp: 'maxLength',
|
|
61
61
|
font: { sm: 12, md: 14 },
|
|
62
62
|
chrome: { sm: 26, md: 34 },
|
|
63
63
|
korean: false, // 바코드는 영숫자다
|
|
64
|
-
grades: [
|
|
64
|
+
grades: ['sm', 'md', 'lg', 'xl'],
|
|
65
65
|
},
|
|
66
66
|
SNumberInput: {
|
|
67
|
-
lengthProp:
|
|
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: [
|
|
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 ===
|
|
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 ===
|
|
107
|
-
if (v.type ===
|
|
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:
|
|
121
|
+
type: 'problem',
|
|
122
122
|
docs: {
|
|
123
|
-
description:
|
|
123
|
+
description: '필드 폭을 maxLength(스키마 상한) 기준 등급으로 고정',
|
|
124
124
|
},
|
|
125
125
|
// px → 등급 이름 치환만 자동으로 고친다. 등급 선택 자체는 스키마를 봐야 하므로 손대지 않는다.
|
|
126
|
-
fixable:
|
|
126
|
+
fixable: 'code',
|
|
127
127
|
schema: [
|
|
128
128
|
{
|
|
129
|
-
type:
|
|
129
|
+
type: 'object',
|
|
130
130
|
properties: {
|
|
131
131
|
/** 폭 등급 px 재정의 — 디자인 확정본이 오면 여기로 덮는다 */
|
|
132
|
-
grades: { type:
|
|
132
|
+
grades: { type: 'object', additionalProperties: { type: 'number' } },
|
|
133
133
|
/** 글자폭 비율 재정의 */
|
|
134
134
|
charRatio: {
|
|
135
|
-
type:
|
|
136
|
-
properties: { narrow: { type:
|
|
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
|
-
|
|
145
|
+
'`{{lengthProp}}={{len}}` 로 상한이 정해진 필드인데 `width` 가 없어 행 전체를 차지합니다. `width="{{suggest}}"`({{suggestPx}}px) 등급으로 고정하세요. (§1-2)',
|
|
146
146
|
offGrade:
|
|
147
|
-
|
|
147
|
+
'`width={{{width}}}` 는 폭 등급이 아닙니다. {{gradeList}} 중 하나이거나, 상한이 없으면 `width="100%"` 여야 합니다. (§1-2)',
|
|
148
148
|
pxGrade:
|
|
149
|
-
|
|
149
|
+
'`width={{{width}}}` 는 {{grade}} 등급의 px 값입니다. `width="{{grade}}"` 로 등급을 주세요 — px 를 박으면 `--cmp-field-width-*` 가 바뀌어도 이 화면만 그 자리에 남습니다. (§1-2)',
|
|
150
150
|
noBound:
|
|
151
|
-
|
|
151
|
+
'`width` 로 폭을 고정했는데 `{{lengthProp}}` 이 없습니다. 폭의 근거가 스키마 상한이므로, 상한이 없으면 행 전체(`width="100%"` 또는 생략)로 둡니다. (§1-2)',
|
|
152
152
|
tooNarrow:
|
|
153
|
-
|
|
153
|
+
'`{{lengthProp}}={{len}}` 이 {{grade}} 등급({{gradePx}}px)에 담기지 않습니다. `width="{{suggest}}"`({{suggestPx}}px) 이상이 필요합니다. (§1-2)',
|
|
154
154
|
tooWide:
|
|
155
|
-
|
|
155
|
+
'`{{lengthProp}}={{len}}` 은 {{suggest}} 등급({{suggestPx}}px)으로 충분한데 {{grade}} 등급({{gradePx}}px)을 주었습니다. 폭은 상한에 맞춥니다. (§1-2)',
|
|
156
156
|
overMax:
|
|
157
|
-
|
|
157
|
+
'`{{lengthProp}}={{len}}` 은 최대 등급 xl({{xl}}px)로도 담기지 않습니다. 행 전체(`width="100%"`)로 둡니다. (§1-2)',
|
|
158
158
|
fullWidthBounded:
|
|
159
|
-
|
|
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 !==
|
|
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 ===
|
|
174
|
+
if (node.attributes.some(a => a.type === 'JSXSpreadAttribute')) return;
|
|
175
175
|
|
|
176
|
-
const size = literal(node,
|
|
176
|
+
const size = literal(node, 'size') === 'md' ? 'md' : 'sm';
|
|
177
177
|
// 스테퍼가 붙으면 글자가 쓸 수 있는 폭이 크게 줄어든다 (useButton 기본값은 false)
|
|
178
|
-
const spec =
|
|
179
|
-
|
|
180
|
-
|
|
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,
|
|
196
|
+
const rawWidth = literal(node, 'width');
|
|
200
197
|
if (rawWidth === null) return; // 변수로 넘긴 폭 — 판단 불가
|
|
201
198
|
|
|
202
|
-
const reportNode = attr(node,
|
|
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:
|
|
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 ===
|
|
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:
|
|
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 =
|
|
235
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
34
|
+
'font',
|
|
35
|
+
'leading',
|
|
36
|
+
'tracking',
|
|
22
37
|
// 간격
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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(
|
|
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 ?
|
|
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:
|
|
87
|
+
type: 'problem',
|
|
46
88
|
docs: {
|
|
47
|
-
description:
|
|
89
|
+
description:
|
|
90
|
+
'색·타이포·간격·모서리에 임의 값 금지 — 토큰 유틸리티 또는 var(--token) 참조 사용',
|
|
48
91
|
},
|
|
49
92
|
schema: [
|
|
50
93
|
{
|
|
51
|
-
type:
|
|
94
|
+
type: 'object',
|
|
52
95
|
properties: {
|
|
53
96
|
/** 예외로 허용할 클래스 (완전 일치) */
|
|
54
|
-
allow: { type:
|
|
97
|
+
allow: { type: 'array', items: { type: 'string' }, uniqueItems: true },
|
|
55
98
|
/** 검사 대상에서 뺄 유틸리티 접두어 (예: ["shadow"]) */
|
|
56
|
-
ignorePrefixes: { type:
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
|
20
|
-
import { SPACING } from
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 =
|
|
53
|
+
const RENDERED_PX = value => value * 4;
|
|
35
54
|
|
|
36
|
-
const escapeRegExp =
|
|
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 =
|
|
62
|
+
const TOKEN_START = '(?<![^\\s\'"`])';
|
|
44
63
|
|
|
45
64
|
export default {
|
|
46
65
|
meta: {
|
|
47
|
-
type:
|
|
66
|
+
type: 'problem',
|
|
48
67
|
docs: {
|
|
49
|
-
description:
|
|
68
|
+
description: '간격 유틸리티는 sd- 접두가 붙은 토큰 스케일 값만 사용',
|
|
50
69
|
},
|
|
51
70
|
// 스케일 밖 값(`gap-sd-13`)에는 fix 를 내지 않는다. `unsafeFix` 를 켠 팀의
|
|
52
71
|
// 접두 누락 수정에만 쓰이지만, 선언은 정적이어야 하므로 여기 남는다.
|
|
53
|
-
fixable:
|
|
72
|
+
fixable: 'code',
|
|
54
73
|
hasSuggestions: true,
|
|
55
74
|
schema: [
|
|
56
75
|
{
|
|
57
|
-
type:
|
|
76
|
+
type: 'object',
|
|
58
77
|
properties: {
|
|
59
78
|
/**
|
|
60
79
|
* 접두 없는 간격 유틸리티(`p-16`)도 잡는다. 기본 false.
|
|
61
80
|
* 기본값에서는 두 해석(`p-sd-16` = 16px / 현재 값 유지)을 제안으로만 낸다.
|
|
62
81
|
*/
|
|
63
|
-
requirePrefix: { type:
|
|
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:
|
|
90
|
+
unsafeFix: { type: 'boolean' },
|
|
72
91
|
},
|
|
73
92
|
additionalProperties: false,
|
|
74
93
|
},
|
|
75
94
|
],
|
|
76
95
|
messages: {
|
|
77
96
|
offScale:
|
|
78
|
-
|
|
97
|
+
'`{{token}}` 의 {{value}} 는 간격 스케일에 없어 스타일이 적용되지 않습니다. 가까운 값: {{nearest}}. (AGENTS.md §2-2)',
|
|
79
98
|
// 접두 누락 — 어느 해석이 스케일에 있느냐로 갈린다.
|
|
80
99
|
// `p-16` 은 지금 64px 이므로 "16px 의도" 와 "64px 유지" 가 서로 다른 결과다.
|
|
81
100
|
missingPrefixBoth:
|
|
82
|
-
|
|
101
|
+
'`{{token}}` 은 현재 {{actual}}px 입니다 (Tailwind 기본 스케일). {{value}}px 을 의도했다면 → `{{asLiteral}}` · {{actual}}px 을 유지하려면 → `{{asRendered}}` (AGENTS.md §2-2)',
|
|
83
102
|
missingPrefixLiteral:
|
|
84
|
-
|
|
103
|
+
'`{{token}}` 은 현재 {{actual}}px 입니다 (Tailwind 기본 스케일). {{value}}px 을 의도했다면 → `{{asLiteral}}`. {{actual}}px 유지는 간격 스케일에 값이 없습니다. {{fallback}} (AGENTS.md §2-2)',
|
|
85
104
|
missingPrefixRendered:
|
|
86
|
-
|
|
105
|
+
'`{{token}}` 은 현재 {{actual}}px 입니다 (Tailwind 기본 스케일). {{actual}}px 을 유지하려면 → `{{asRendered}}`. {{value}}px 은 간격 스케일에 없습니다. (AGENTS.md §2-2)',
|
|
87
106
|
missingPrefixNone:
|
|
88
|
-
|
|
107
|
+
'`{{token}}` 은 현재 {{actual}}px 입니다 (Tailwind 기본 스케일). {{value}}px·{{actual}}px 둘 다 간격 스케일에 없습니다. {{fallback}} (AGENTS.md §2-2)',
|
|
89
108
|
// 제안(suggestion) 설명 — 편집기 목록에 그대로 뜬다
|
|
90
|
-
useLiteral:
|
|
91
|
-
useRendered:
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
133
|
+
const nearestOne = value => byDistance(value)[0];
|
|
115
134
|
|
|
116
135
|
/** 스케일에서 value 에 가장 가까운 값 두 개 (표시용이라 오름차순) */
|
|
117
|
-
const nearestOf =
|
|
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(
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
-
?
|
|
192
|
-
:
|
|
213
|
+
? 'missingPrefixBoth'
|
|
214
|
+
: 'missingPrefixLiteral'
|
|
193
215
|
: asRendered
|
|
194
|
-
?
|
|
195
|
-
:
|
|
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:
|
|
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
|
},
|