sellmate-design-system-react 9.0.0-beta.3 → 9.0.0-beta.30

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 (164) hide show
  1. package/AGENTS.md +599 -94
  2. package/README.md +111 -9
  3. package/dist/components/SBadge/README.md +24 -0
  4. package/dist/components/SBadge/SBadge.d.ts +1 -1
  5. package/dist/components/SBarcodeInput/README.md +9 -1
  6. package/dist/components/SBarcodeInput/SBarcodeInput.d.ts +7 -1
  7. package/dist/components/SButton/README.md +36 -0
  8. package/dist/components/SCalendar/README.md +13 -0
  9. package/dist/components/SCallout/README.md +15 -0
  10. package/dist/components/SCard/README.md +16 -0
  11. package/dist/components/SCard/SCard.d.ts +17 -2
  12. package/dist/components/SCheckbox/README.md +8 -0
  13. package/dist/components/SChipFilter/README.md +326 -13
  14. package/dist/components/SChipFilter/SChipFilter.d.ts +137 -58
  15. package/dist/components/SChipFilter/index.d.ts +1 -1
  16. package/dist/components/SChipInput/README.md +15 -1
  17. package/dist/components/SChipInput/SChipInput.d.ts +7 -1
  18. package/dist/components/SChipInput/index.d.ts +1 -1
  19. package/dist/components/SCircleProgress/README.md +8 -0
  20. package/dist/components/SConfirmModal/README.md +17 -1
  21. package/dist/components/SConfirmModal/SConfirmModal.d.ts +2 -2
  22. package/dist/components/SConfirmModal/index.d.ts +1 -1
  23. package/dist/components/SDatePicker/README.md +57 -2
  24. package/dist/components/SDatePicker/SDatePicker.d.ts +57 -4
  25. package/dist/components/SDatePicker/index.d.ts +1 -1
  26. package/dist/components/SDateRangePicker/README.md +17 -2
  27. package/dist/components/SDateRangePicker/SDateRangePicker.d.ts +16 -3
  28. package/dist/components/SDivider/README.md +5 -0
  29. package/dist/components/SDivider/SDivider.d.ts +11 -0
  30. package/dist/components/SDraggableItem/README.md +37 -0
  31. package/dist/components/SDraggableItem/SDraggableItem.d.ts +2 -0
  32. package/dist/components/SDraggableItem/index.d.ts +1 -1
  33. package/dist/components/SDraggableList/README.md +30 -0
  34. package/dist/components/SDraggableList/SDraggableList.d.ts +11 -0
  35. package/dist/components/SDraggableList/index.d.ts +1 -1
  36. package/dist/components/SDrawer/README.md +8 -0
  37. package/dist/components/SDropdownButton/README.md +19 -0
  38. package/dist/components/SEditor/EditorBody.d.ts +36 -0
  39. package/dist/components/SEditor/EditorToolbar.d.ts +93 -0
  40. package/dist/components/SEditor/README.md +203 -0
  41. package/dist/components/SEditor/SEditor.d.ts +114 -0
  42. package/dist/components/SEditor/editor-icons.d.ts +63 -0
  43. package/dist/components/SEditor/editor.config.d.ts +119 -0
  44. package/dist/components/SEditor/index.d.ts +8 -0
  45. package/dist/components/SEditor/tiptap-api.d.ts +29 -0
  46. package/dist/components/SEditor/use-is-mobile.d.ts +14 -0
  47. package/dist/components/SExpansionItem/README.md +36 -0
  48. package/dist/components/SExpansionItem/index.d.ts +1 -1
  49. package/dist/components/SField/README.md +27 -2
  50. package/dist/components/SField/SField.d.ts +22 -4
  51. package/dist/components/SFilePicker/README.md +15 -1
  52. package/dist/components/SFilePicker/SFilePicker.d.ts +7 -1
  53. package/dist/components/SFooter/README.md +25 -0
  54. package/dist/components/SFooter/SFooter.d.ts +4 -3
  55. package/dist/components/SForm/README.md +11 -0
  56. package/dist/components/SGhostButton/README.md +20 -4
  57. package/dist/components/SGnb/README.md +51 -0
  58. package/dist/components/SGnb/SGnb.d.ts +22 -0
  59. package/dist/components/SGnb/gnb.config.d.ts +7 -0
  60. package/dist/components/SGnb/index.d.ts +1 -1
  61. package/dist/components/SGnbSystem/README.md +121 -0
  62. package/dist/components/SGnbSystem/SGnbSystem.d.ts +93 -0
  63. package/dist/components/SGnbSystem/gnbSystem.config.d.ts +89 -0
  64. package/dist/components/SGnbSystem/index.d.ts +2 -0
  65. package/dist/components/SGuide/README.md +15 -0
  66. package/dist/components/SIcon/README.md +8 -0
  67. package/dist/components/SIcon/SIcon.d.ts +1 -1
  68. package/dist/components/SIcon/icons.gen.d.ts +2 -0
  69. package/dist/components/SImage/README.md +14 -0
  70. package/dist/components/SInput/README.md +3 -1
  71. package/dist/components/SInput/SInput.d.ts +7 -1
  72. package/dist/components/SKeyValueTable/README.md +87 -0
  73. package/dist/components/SKeyValueTable/SKeyValueTable.d.ts +14 -3
  74. package/dist/components/SKeyValueTable/index.d.ts +1 -1
  75. package/dist/components/SLayout/README.md +16 -0
  76. package/dist/components/SLayout/index.d.ts +1 -1
  77. package/dist/components/SLinearProgress/README.md +15 -0
  78. package/dist/components/SLinearProgress/SLinearProgress.d.ts +3 -0
  79. package/dist/components/SLinearProgress/index.d.ts +1 -1
  80. package/dist/components/SList/README.md +5 -1
  81. package/dist/components/SList/SList.d.ts +0 -2
  82. package/dist/components/SListItem/README.md +41 -0
  83. package/dist/components/SListItem/index.d.ts +1 -1
  84. package/dist/components/SLoadingModal/README.md +9 -1
  85. package/dist/components/SLoadingModal/SLoadingModal.d.ts +2 -2
  86. package/dist/components/SLoadingModal/index.d.ts +1 -1
  87. package/dist/components/SNumberInput/README.md +9 -1
  88. package/dist/components/SNumberInput/SNumberInput.d.ts +7 -1
  89. package/dist/components/SPage/README.md +41 -1
  90. package/dist/components/SPage/SPage.d.ts +24 -2
  91. package/dist/components/SPage/index.d.ts +1 -1
  92. package/dist/components/SPage/page.config.d.ts +8 -0
  93. package/dist/components/SPopover/README.md +15 -0
  94. package/dist/components/SPopup/README.md +19 -0
  95. package/dist/components/SPortal/README.md +14 -0
  96. package/dist/components/SRadio/README.md +20 -0
  97. package/dist/components/SRadioButton/README.md +18 -0
  98. package/dist/components/SRadioButton/SRadioButton.d.ts +1 -1
  99. package/dist/components/SRadioCard/README.md +78 -0
  100. package/dist/components/SRadioCard/SRadioCard.d.ts +29 -0
  101. package/dist/components/SRadioCard/SRadioCardGroup.d.ts +36 -0
  102. package/dist/components/SRadioCard/index.d.ts +2 -0
  103. package/dist/components/SScrollArea/README.md +14 -0
  104. package/dist/components/SSearchInput/README.md +63 -0
  105. package/dist/components/SSearchInput/SSearchInput.d.ts +52 -0
  106. package/dist/components/SSearchInput/index.d.ts +1 -0
  107. package/dist/components/SSectionHeaderCard/README.md +44 -20
  108. package/dist/components/SSectionHeaderCard/SSectionHeaderCard.d.ts +31 -14
  109. package/dist/components/SSectionHeaderCard/index.d.ts +1 -1
  110. package/dist/components/SSelect/README.md +24 -4
  111. package/dist/components/SSelect/SSelect.d.ts +14 -2
  112. package/dist/components/SSplitter/README.md +15 -0
  113. package/dist/components/SStepper/README.md +26 -0
  114. package/dist/components/SSwitch/README.md +13 -0
  115. package/dist/components/SSystemActionButton/README.md +134 -0
  116. package/dist/components/SSystemActionButton/SSystemActionButton.d.ts +79 -0
  117. package/dist/components/SSystemActionButton/index.d.ts +2 -0
  118. package/dist/components/SSystemActionButton/systemActionButton.config.d.ts +87 -0
  119. package/dist/components/STable/README.md +87 -1
  120. package/dist/components/STable/STable.d.ts +141 -11
  121. package/dist/components/STable/index.d.ts +1 -1
  122. package/dist/components/STabs/README.md +12 -1
  123. package/dist/components/STabs/STabs.d.ts +2 -4
  124. package/dist/components/STabs/index.d.ts +1 -1
  125. package/dist/components/STabs/tabs.config.d.ts +3 -4
  126. package/dist/components/STag/README.md +52 -0
  127. package/dist/components/STag/STag.d.ts +3 -1
  128. package/dist/components/STextLink/README.md +17 -0
  129. package/dist/components/STextLink/STextLink.d.ts +2 -0
  130. package/dist/components/STextarea/README.md +1 -1
  131. package/dist/components/STextarea/STextarea.d.ts +7 -1
  132. package/dist/components/STimePicker/README.md +15 -1
  133. package/dist/components/STimePicker/STimePicker.d.ts +7 -1
  134. package/dist/components/STimePicker/timepicker.config.d.ts +7 -0
  135. package/dist/components/STimeRangePicker/README.md +27 -1
  136. package/dist/components/STimeRangePicker/STimeRangePicker.d.ts +7 -1
  137. package/dist/components/SToast/README.md +24 -0
  138. package/dist/components/SToggle/README.md +8 -0
  139. package/dist/components/SToggle/index.d.ts +1 -1
  140. package/dist/components/STooltip/README.md +22 -0
  141. package/dist/components/STree/README.md +41 -0
  142. package/dist/components/STree/STree.d.ts +8 -0
  143. package/dist/components/STree/index.d.ts +1 -1
  144. package/dist/index.cjs +5483 -1150
  145. package/dist/index.cjs.map +1 -1
  146. package/dist/index.d.ts +6 -0
  147. package/dist/index.js +5458 -1149
  148. package/dist/index.js.map +1 -1
  149. package/dist/lib/field-width.d.ts +31 -0
  150. package/dist/lib/floating-height.d.ts +15 -0
  151. package/dist/lib/story-docs.d.ts +19 -3
  152. package/dist/lib/truncated-value-tooltip.d.ts +18 -0
  153. package/dist/llms-full.txt +2813 -193
  154. package/dist/llms.txt +604 -96
  155. package/dist/styles.css +1249 -135
  156. package/dist/theme.css +142 -16
  157. package/eslint/index.mjs +14 -0
  158. package/eslint/lib/table-column.mjs +26 -0
  159. package/eslint/rules/divider-vertical-height.mjs +137 -0
  160. package/eslint/rules/field-width-grade.d.mts +41 -0
  161. package/eslint/rules/field-width-grade.mjs +311 -0
  162. package/eslint/rules/table-column-width.mjs +110 -0
  163. package/eslint/scale.gen.mjs +5 -2
  164. package/package.json +11 -1
@@ -0,0 +1,311 @@
1
+ /**
2
+ * 필드 폭을 `maxLength`(= 스키마 상한) 기준으로 고정했는지 검사한다.
3
+ *
4
+ * 확정 규칙 (design-feedback-final.md §1-2):
5
+ * 필드 너비는 maxLength 로 정한다.
6
+ * - 상한이 등급 안에 들어오면 → 그 등급
7
+ * - 상한이 등급 상한(xl)을 넘거나 상한이 없으면 → 행 전체 + 100%
8
+ * - 셀렉트는 예외 (maxLength 개념이 없어 목록 최장값 기준) → 대상 아님
9
+ *
10
+ * 디자인팀이 요구한 "검수: maxLength ↔ 폭 등급 적합성 필수 확인" 을 자동화한 것이다.
11
+ * 사람이 하는 검수는 스키마가 나중에 확정되는 화면에서 빠지기 쉽다 —
12
+ * 시안 단계의 잠정 등급이 그대로 남아도 화면은 멀쩡해 보이기 때문이다.
13
+ *
14
+ * 대상이 아닌 것과 그 이유:
15
+ * STextarea 여러 줄로 접히므로 maxLength 가 폭을 정하지 않는다
16
+ * SSelect 계열 maxLength 개념이 없다 (§1-2 예외 — 목록 최장값 기준)
17
+ * SDatePicker 계열 컴포넌트가 자체 상한을 갖는다 (B-14)
18
+ *
19
+ * ⚠️ 글자수 ↔ 등급 환산은 아직 디자인 확정본이 없어 토큰 값에서 계산한 잠정치다.
20
+ * 확정표가 오면 CHAR_RATIO 를 그 값으로 바꾼다. GRADES · CHAR_RATIO 모두 옵션으로 덮을 수 있다.
21
+ */
22
+ import { FIELD_WIDTHS } from "../scale.gen.mjs";
23
+
24
+ /**
25
+ * 폭 등급 (px). `--cmp-field-width-*` 토큰에서 scale.gen.mjs 를 거쳐 들어온다 —
26
+ * 컴포넌트(`SFieldWidth`)와 이 규칙이 같은 소스를 본다. 여기에 숫자를 적지 않는다.
27
+ */
28
+ export const GRADES = FIELD_WIDTHS;
29
+
30
+ /**
31
+ * 글자 하나가 차지하는 폭 (font-size 대비 비율).
32
+ * narrow — 영문·숫자. "이 등급에 안 담긴다" 를 판정할 때 쓴다(가장 좁게 잡아도 넘치면 확실히 위반)
33
+ * wide — 한글(전각). "한 등급 작아도 된다" 를 판정할 때 쓴다(가장 넓게 잡아도 남으면 확실히 과하다)
34
+ * 두 판정 사이에는 아무것도 보고하지 않는 구간이 남는다 — 오탐보다 미탐을 택한 것이다.
35
+ */
36
+ export const CHAR_RATIO = { narrow: 0.55, wide: 1 };
37
+
38
+ /**
39
+ * 컴포넌트별 기하. chrome 은 글자가 쓸 수 없는 폭(좌우 패딩 · 테두리 · 스테퍼)의 합이고
40
+ * 전부 theme.css 토큰 값에서 계산했다.
41
+ *
42
+ * SInput/SBarcodeInput paddingX(12·16) × 2 + border 1 × 2
43
+ * SNumberInput 스테퍼가 없으면 패딩이 두 배가 된다(SNumberInput.tsx:388) —
44
+ * 없을 때 paddingX(6·8) × 2 × 2 + border,
45
+ * 있을 때 paddingX × 2 + stepper(20·24) × 2 + gap(= paddingX) × 2 + border
46
+ *
47
+ * lengthProp — 숫자 필드의 스키마 상한은 `maxLength` 가 아니라 `max` 다.
48
+ * korean — 한글이 들어갈 수 있는가. false 면 두 판정 모두 narrow 로 계산한다.
49
+ * chromeStepper — `useButton` 일 때의 chrome. 기본값이 false 라 chrome 쪽이 기본이다.
50
+ */
51
+ export const COMPONENTS = {
52
+ SInput: {
53
+ lengthProp: "maxLength",
54
+ font: { sm: 12, md: 14 },
55
+ chrome: { sm: 26, md: 34 },
56
+ korean: true,
57
+ grades: ["sm", "md", "lg", "xl"],
58
+ },
59
+ SBarcodeInput: {
60
+ lengthProp: "maxLength",
61
+ font: { sm: 12, md: 14 },
62
+ chrome: { sm: 26, md: 34 },
63
+ korean: false, // 바코드는 영숫자다
64
+ grades: ["sm", "md", "lg", "xl"],
65
+ },
66
+ SNumberInput: {
67
+ lengthProp: "max",
68
+ font: { sm: 12, md: 14 },
69
+ chrome: { sm: 26, md: 34 },
70
+ chromeStepper: { sm: 66, md: 82 },
71
+ korean: false,
72
+ grades: ["xs", "sm", "md", "lg", "xl"], // xs 는 숫자 필드 전용 등급이다
73
+ },
74
+ };
75
+
76
+ /**
77
+ * 등급이 담을 수 있는 글자수. playground 의 실측 페이지가 같은 식을 쓰도록 내보낸다
78
+ * (숫자를 두 곳에 적으면 한쪽만 고쳐져 검수가 무의미해진다).
79
+ */
80
+ export function capacity(spec, size, grade, ratio, grades = GRADES) {
81
+ const usable = Math.max(0, grades[grade] - spec.chrome[size]);
82
+ return Math.floor(usable / (spec.font[size] * ratio));
83
+ }
84
+
85
+ /** 그 비율로 len 글자를 담을 수 있는 가장 작은 등급 (없으면 undefined) */
86
+ export function smallestGradeFor(spec, size, len, ratio, grades = GRADES) {
87
+ return spec.grades.find(g => capacity(spec, size, g, ratio, grades) >= len);
88
+ }
89
+
90
+ /** JSX 속성 노드 */
91
+ function attr(node, name) {
92
+ return node.attributes.find(a => a.type === "JSXAttribute" && a.name?.name === name);
93
+ }
94
+
95
+ /**
96
+ * 속성에서 리터럴 값을 꺼낸다.
97
+ * 없음 → undefined
98
+ * 리터럴 → 값
99
+ * 그 밖(변수) → null (판단 불가 — 조용히 넘긴다)
100
+ */
101
+ function literal(node, name) {
102
+ const a = attr(node, name);
103
+ if (!a) return undefined;
104
+ const v = a.value;
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") {
108
+ return v.expression.value;
109
+ }
110
+ return null;
111
+ }
112
+
113
+ /** `max={1000}` → 자릿수 4. 천단위 콤마도 폭을 먹으므로 함께 센다 */
114
+ function digitsOf(max) {
115
+ const digits = String(Math.abs(Math.trunc(max))).length;
116
+ return digits + Math.floor((digits - 1) / 3);
117
+ }
118
+
119
+ export default {
120
+ meta: {
121
+ type: "problem",
122
+ docs: {
123
+ description: "필드 폭을 maxLength(스키마 상한) 기준 등급으로 고정",
124
+ },
125
+ // px → 등급 이름 치환만 자동으로 고친다. 등급 선택 자체는 스키마를 봐야 하므로 손대지 않는다.
126
+ fixable: "code",
127
+ schema: [
128
+ {
129
+ type: "object",
130
+ properties: {
131
+ /** 폭 등급 px 재정의 — 디자인 확정본이 오면 여기로 덮는다 */
132
+ grades: { type: "object", additionalProperties: { type: "number" } },
133
+ /** 글자폭 비율 재정의 */
134
+ charRatio: {
135
+ type: "object",
136
+ properties: { narrow: { type: "number" }, wide: { type: "number" } },
137
+ additionalProperties: false,
138
+ },
139
+ },
140
+ additionalProperties: false,
141
+ },
142
+ ],
143
+ messages: {
144
+ noGrade:
145
+ "`{{lengthProp}}={{len}}` 로 상한이 정해진 필드인데 `width` 가 없어 행 전체를 차지합니다. `width=\"{{suggest}}\"`({{suggestPx}}px) 등급으로 고정하세요. (§1-2)",
146
+ offGrade:
147
+ "`width={{{width}}}` 는 폭 등급이 아닙니다. {{gradeList}} 중 하나이거나, 상한이 없으면 `width=\"100%\"` 여야 합니다. (§1-2)",
148
+ pxGrade:
149
+ "`width={{{width}}}` 는 {{grade}} 등급의 px 값입니다. `width=\"{{grade}}\"` 로 등급을 주세요 — px 를 박으면 `--cmp-field-width-*` 가 바뀌어도 이 화면만 그 자리에 남습니다. (§1-2)",
150
+ noBound:
151
+ "`width` 로 폭을 고정했는데 `{{lengthProp}}` 이 없습니다. 폭의 근거가 스키마 상한이므로, 상한이 없으면 행 전체(`width=\"100%\"` 또는 생략)로 둡니다. (§1-2)",
152
+ tooNarrow:
153
+ "`{{lengthProp}}={{len}}` 이 {{grade}} 등급({{gradePx}}px)에 담기지 않습니다. `width=\"{{suggest}}\"`({{suggestPx}}px) 이상이 필요합니다. (§1-2)",
154
+ tooWide:
155
+ "`{{lengthProp}}={{len}}` 은 {{suggest}} 등급({{suggestPx}}px)으로 충분한데 {{grade}} 등급({{gradePx}}px)을 주었습니다. 폭은 상한에 맞춥니다. (§1-2)",
156
+ overMax:
157
+ "`{{lengthProp}}={{len}}` 은 최대 등급 xl({{xl}}px)로도 담기지 않습니다. 행 전체(`width=\"100%\"`)로 둡니다. (§1-2)",
158
+ fullWidthBounded:
159
+ "`{{lengthProp}}={{len}}` 로 상한이 정해져 있으므로 `width=\"100%\"` 가 아니라 `width=\"{{suggest}}\"`({{suggestPx}}px) 등급으로 고정합니다. 행 전체는 상한이 없거나 xl 을 넘을 때만 씁니다. (§1-2)",
160
+ },
161
+ },
162
+
163
+ create(context) {
164
+ const grades = { ...GRADES, ...(context.options[0]?.grades ?? {}) };
165
+ const ratio = { ...CHAR_RATIO, ...(context.options[0]?.charRatio ?? {}) };
166
+
167
+ return {
168
+ JSXOpeningElement(node) {
169
+ if (node.name.type !== "JSXIdentifier") return;
170
+ const base = COMPONENTS[node.name.name];
171
+ if (!base) return;
172
+
173
+ // spread 가 있으면 width·maxLength 가 그 안에 있을 수 있어 판단하지 않는다
174
+ if (node.attributes.some(a => a.type === "JSXSpreadAttribute")) return;
175
+
176
+ const size = literal(node, "size") === "md" ? "md" : "sm";
177
+ // 스테퍼가 붙으면 글자가 쓸 수 있는 폭이 크게 줄어든다 (useButton 기본값은 false)
178
+ const spec = literal(node, "useButton") === true && base.chromeStepper
179
+ ? { ...base, chrome: base.chromeStepper }
180
+ : base;
181
+
182
+ const capacityAt = (grade, r) => capacity(spec, size, grade, r, grades);
183
+ const smallestFor = (len, r) => smallestGradeFor(spec, size, len, r, grades);
184
+
185
+ const narrow = ratio.narrow;
186
+ // 한글이 들어갈 수 없는 필드는 두 판정 모두 좁은 글자폭으로 계산한다
187
+ const wide = spec.korean ? ratio.wide : ratio.narrow;
188
+
189
+ const rawLen = literal(node, spec.lengthProp);
190
+ if (rawLen === null) return; // 변수로 넘긴 상한 — 판단 불가
191
+ const len =
192
+ rawLen === undefined
193
+ ? undefined
194
+ : spec.lengthProp === "max"
195
+ ? digitsOf(rawLen)
196
+ : rawLen;
197
+ if (len !== undefined && (!Number.isFinite(len) || len <= 0)) return;
198
+
199
+ const rawWidth = literal(node, "width");
200
+ if (rawWidth === null) return; // 변수로 넘긴 폭 — 판단 불가
201
+
202
+ const reportNode = attr(node, "width") ?? attr(node, spec.lengthProp) ?? node.name;
203
+ const gradeList = spec.grades.map(g => `${g}(${grades[g]})`).join(" · ");
204
+
205
+ /* ── width 가 없다 ── */
206
+ if (rawWidth === undefined) {
207
+ if (len === undefined) return; // 둘 다 없음 = 상한 없는 필드 → 행 전체가 맞다
208
+ const suggest = smallestFor(len, wide);
209
+ if (!suggest) return; // xl 로도 안 담긴다 → 행 전체가 맞다
210
+ context.report({
211
+ node: reportNode,
212
+ messageId: "noGrade",
213
+ data: { lengthProp: spec.lengthProp, len: rawLen, suggest, suggestPx: grades[suggest] },
214
+ });
215
+ return;
216
+ }
217
+
218
+ /* ── width="100%" ── */
219
+ if (rawWidth === "100%") {
220
+ if (len === undefined) return; // 상한 없음 + 행 전체 = 규칙대로다
221
+ const suggest = smallestFor(len, wide);
222
+ if (!suggest) return; // xl 초과 → 행 전체가 맞다
223
+ context.report({
224
+ node: reportNode,
225
+ messageId: "fullWidthBounded",
226
+ data: { lengthProp: spec.lengthProp, len: rawLen, suggest, suggestPx: grades[suggest] },
227
+ });
228
+ return;
229
+ }
230
+
231
+ /* ── width 가 등급인가 ── */
232
+ // 등급 이름이 정답이다. px 는 등급 값과 일치할 때만 받아 주고(아래 pxGrade) 등급으로 바꾸게 한다.
233
+ 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));
236
+ if (!grade) {
237
+ context.report({
238
+ node: reportNode,
239
+ messageId: "offGrade",
240
+ data: { width: String(rawWidth), gradeList },
241
+ });
242
+ return;
243
+ }
244
+
245
+ /* ── 등급은 맞다. 상한과 맞는가 ── */
246
+ if (len === undefined) {
247
+ context.report({
248
+ node: reportNode,
249
+ messageId: "noBound",
250
+ data: { lengthProp: spec.lengthProp },
251
+ });
252
+ return;
253
+ }
254
+
255
+ // 영문·숫자로 채워도 넘치면 등급이 확실히 좁다
256
+ if (capacityAt(grade, narrow) < len) {
257
+ const suggest = smallestFor(len, narrow);
258
+ if (!suggest) {
259
+ context.report({
260
+ node: reportNode,
261
+ messageId: "overMax",
262
+ data: { lengthProp: spec.lengthProp, len: rawLen, xl: grades.xl },
263
+ });
264
+ return;
265
+ }
266
+ context.report({
267
+ node: reportNode,
268
+ messageId: "tooNarrow",
269
+ data: {
270
+ lengthProp: spec.lengthProp,
271
+ len: rawLen,
272
+ grade,
273
+ gradePx: grades[grade],
274
+ suggest,
275
+ suggestPx: grades[suggest],
276
+ },
277
+ });
278
+ return;
279
+ }
280
+
281
+ // 한글로 채워도 더 작은 등급에 들어가면 등급이 확실히 과하다
282
+ const enough = smallestFor(len, wide);
283
+ if (enough && grades[enough] < grades[grade]) {
284
+ context.report({
285
+ node: reportNode,
286
+ messageId: "tooWide",
287
+ data: {
288
+ lengthProp: spec.lengthProp,
289
+ len: rawLen,
290
+ grade,
291
+ gradePx: grades[grade],
292
+ suggest: enough,
293
+ suggestPx: grades[enough],
294
+ },
295
+ });
296
+ return;
297
+ }
298
+
299
+ // 등급도 상한도 맞다. 남은 것은 표기 — px 는 토큰을 따라가지 않으므로 등급 이름으로 바꾼다.
300
+ if (!byName) {
301
+ context.report({
302
+ node: reportNode,
303
+ messageId: "pxGrade",
304
+ data: { width: String(rawWidth), grade },
305
+ fix: fixer => fixer.replaceText(reportNode, `width="${grade}"`),
306
+ });
307
+ }
308
+ },
309
+ };
310
+ },
311
+ };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `STableColumn` 의 폭 선언을 검사한다.
3
+ *
4
+ * 테이블은 `table-fixed` 라 컬럼 폭이 `<colgroup>` 의 `<col width>` 로 들어간다.
5
+ * 그래서 폭에 넣을 수 있는 값은 px 뿐이고, 그 밖의 값은 **화면에서 조용히 어긋난다** —
6
+ * `'30%'` 는 `parseInt` 에 30(px)으로 읽히고, `clamp()` 같은 함수형 값은 NaN 이 되어
7
+ * `<col>` 폭이 통째로 무효가 된 뒤 auto 로 떨어진다. 둘 다 에러 없이 화면만 틀어진다.
8
+ *
9
+ * 그리고 폭을 아예 주지 않으면 기본 120px 이 조용히 들어간다. "짧은 열이라 그대로 둔 것"과
10
+ * "판단을 빠뜨린 것"이 구분되지 않으므로, 120px 이 맞더라도 `width: 120` 을 적는 편이 낫다.
11
+ *
12
+ * `autoWidth` 는 남은 폭을 흡수하는 스페이서 열 하나에 쓰는 것이라, 값을 그리는 열
13
+ * (`render` · `format` 이 붙은 열)에 주면 폭이 다른 열에 좌우돼 화면마다 달라진다.
14
+ *
15
+ * 대상은 `name`(문자열 리터럴) · `label` 에 더해 `field` 또는 `autoWidth` 를 가진 객체 리터럴이다.
16
+ * `STableColumn` 타입이 값을 그리는 열에는 `field` 를, 스페이서 열에는 `autoWidth` 를 요구하므로
17
+ * 검사 범위는 그대로이면서, 같은 모양(`name` + `label`)인 검색 필터 필드가 걸리지 않는다.
18
+ *
19
+ * AGENTS.md §3-4.
20
+ */
21
+
22
+ import { prop, asColumnShape } from "../lib/table-column.mjs";
23
+
24
+ /** px 로 읽히는 폭 값인가 — 숫자 리터럴 또는 `'120px'` */
25
+ function pxWidth(valueNode) {
26
+ if (!valueNode) return undefined;
27
+ if (valueNode.type === "Literal" && typeof valueNode.value === "number") return valueNode.value;
28
+ if (valueNode.type === "Literal" && typeof valueNode.value === "string") {
29
+ const m = /^\s*(\d+)px\s*$/.exec(valueNode.value);
30
+ return m ? Number(m[1]) : undefined;
31
+ }
32
+ return undefined;
33
+ }
34
+
35
+ export default {
36
+ meta: {
37
+ type: "problem",
38
+ docs: {
39
+ description: "테이블 컬럼 폭은 px 로만 주고, autoWidth 는 스페이서 열에만 쓴다",
40
+ },
41
+ schema: [
42
+ {
43
+ type: "object",
44
+ properties: {
45
+ /** 검사에서 뺄 컬럼 name */
46
+ allow: { type: "array", items: { type: "string" }, uniqueItems: true },
47
+ },
48
+ additionalProperties: false,
49
+ },
50
+ ],
51
+ messages: {
52
+ noWidth:
53
+ "컬럼 `{{name}}` 에 `width` 도 `autoWidth` 도 없어 기본 120px 이 조용히 들어갑니다. 120px 이 맞더라도 `width: 120` 으로 적어 판단한 값임을 남기세요. (AGENTS.md §3-4)",
54
+ badWidth:
55
+ "컬럼 `{{name}}` 의 `width: {{width}}` 는 px 이 아닙니다. 컬럼 폭은 `<col width>` 로 들어가 % · clamp() · min() 은 계산되지 않고 조용히 깨집니다 — `width: 120` 또는 `'120px'` 로 주세요. (AGENTS.md §3-4)",
56
+ autoWidthWithContent:
57
+ "컬럼 `{{name}}` 은 `{{contentProp}}` 로 값을 그리는데 `autoWidth` 입니다. `autoWidth` 는 남은 폭을 흡수하는 스페이서 열 하나에만 씁니다 — 값이 들어가는 열은 폭이 다른 열에 좌우돼 화면마다 달라집니다. px 로 폭을 명시하세요. (AGENTS.md §3-4)",
58
+ },
59
+ },
60
+
61
+ create(context) {
62
+ const allow = new Set(context.options[0]?.allow ?? []);
63
+
64
+ return {
65
+ ObjectExpression(node) {
66
+ const column = asColumnShape(node);
67
+ if (!column) return;
68
+ const { columnName } = column;
69
+ if (allow.has(columnName)) return;
70
+
71
+ const widthProp = prop(node, "width");
72
+ const autoWidthProp = prop(node, "autoWidth");
73
+ // autoWidth 를 변수로 넘기면 켜졌는지 알 수 없다 — 판단하지 않는다
74
+ const autoWidthValue = autoWidthProp?.value;
75
+ if (autoWidthProp && autoWidthValue?.type !== "Literal") return;
76
+ const autoWidth = autoWidthValue?.value === true;
77
+
78
+ if (autoWidth) {
79
+ const contentProp = prop(node, "render") ?? prop(node, "format");
80
+ if (contentProp) {
81
+ context.report({
82
+ node: autoWidthProp,
83
+ messageId: "autoWidthWithContent",
84
+ data: {
85
+ name: columnName,
86
+ contentProp: contentProp.key.name ?? contentProp.key.value,
87
+ },
88
+ });
89
+ }
90
+ return;
91
+ }
92
+
93
+ if (!widthProp) {
94
+ context.report({ node: column.nameProp, messageId: "noWidth", data: { name: columnName } });
95
+ return;
96
+ }
97
+
98
+ // 변수·계산식으로 넘긴 폭은 정적으로 알 수 없다
99
+ if (widthProp.value.type !== "Literal") return;
100
+ if (pxWidth(widthProp.value) === undefined) {
101
+ context.report({
102
+ node: widthProp,
103
+ messageId: "badWidth",
104
+ data: { name: columnName, width: context.sourceCode.getText(widthProp.value) },
105
+ });
106
+ }
107
+ },
108
+ };
109
+ },
110
+ };
@@ -10,7 +10,10 @@ export const TEXT_SIZES = [10,11,12,13,14,16,18,20,24,28,32];
10
10
  export const RADIUS = ["2","4","6","8","full","control-sm","control-md","feedback-xs-square","feedback-sm-square","feedback-pill","field-sm","field-md","navigation","item-bordered"];
11
11
 
12
12
  /** 타이포그래피 프리셋 클래스 */
13
- export const TYPO_PRESETS = ["typo-body-lg-bold","typo-body-lg-default","typo-body-lg-medium","typo-body-md-bold","typo-body-md-default","typo-body-md-medium","typo-body-sm-bold","typo-body-sm-default","typo-body-sm-medium","typo-body-xs-default","typo-control-lg","typo-control-md-default","typo-control-md-underline","typo-control-sm-default","typo-control-sm-underline","typo-control-xs-bold","typo-control-xs-default","typo-control-xs-medium","typo-control-xs-underline","typo-feedback-md-bold","typo-feedback-md-medium","typo-feedback-md-underline","typo-feedback-sm-bold","typo-feedback-sm-medium","typo-feedback-sm-regular","typo-feedback-sm-underline","typo-feedback-xs","typo-field-lg-bold","typo-field-lg-default","typo-field-lg-medium","typo-field-md-bold","typo-field-md-default","typo-field-md-medium","typo-field-sm-bold","typo-field-sm-default","typo-field-sm-medium","typo-field-xs-default","typo-heading-lg","typo-heading-md","typo-heading-sm","typo-heading-xs","typo-heading-xxs","typo-item-md-default","typo-item-md-selected","typo-item-sm-default","typo-item-sm-selected","typo-navigation-lg-medium","typo-navigation-lg-semibold","typo-navigation-md-bold","typo-navigation-sm-bold","typo-navigation-sm-default","typo-navigation-sm-medium","typo-navigation-xs-bold","typo-navigation-xs-medium","typo-table-accent","typo-table-body","typo-table-header"];
13
+ export const TYPO_PRESETS = ["typo-body-lg-bold","typo-body-lg-default","typo-body-lg-medium","typo-body-md-bold","typo-body-md-default","typo-body-md-medium","typo-body-md-semibold","typo-body-sm-bold","typo-body-sm-default","typo-body-sm-medium","typo-body-sm-semibold","typo-body-xs-default","typo-control-lg","typo-control-md-default","typo-control-md-underline","typo-control-sm-default","typo-control-sm-underline","typo-control-xs-bold","typo-control-xs-default","typo-control-xs-medium","typo-control-xs-underline","typo-feedback-md-bold","typo-feedback-md-medium","typo-feedback-md-underline","typo-feedback-sm-bold","typo-feedback-sm-medium","typo-feedback-sm-regular","typo-feedback-sm-underline","typo-feedback-xs","typo-field-lg-bold","typo-field-lg-default","typo-field-lg-medium","typo-field-md-bold","typo-field-md-default","typo-field-md-medium","typo-field-sm-bold","typo-field-sm-default","typo-field-sm-medium","typo-field-xs-default","typo-heading-lg","typo-heading-md","typo-heading-sm","typo-heading-xs","typo-heading-xxs","typo-item-md-default","typo-item-md-selected","typo-item-sm-default","typo-item-sm-selected","typo-navigation-lg-medium","typo-navigation-lg-semibold","typo-navigation-md-bold","typo-navigation-sm-bold","typo-navigation-sm-default","typo-navigation-sm-medium","typo-navigation-xs-bold","typo-navigation-xs-medium","typo-table-accent","typo-table-body","typo-table-header"];
14
14
 
15
15
  /** 색상 유틸리티 키 (text-*, bg-*, border-* 공통) */
16
- export const COLORS = ["bg-accent","bg-accentBright","bg-accentFaint","bg-accentLight","bg-brand","bg-caution","bg-danger","bg-dangerLight","bg-deep","bg-frame","bg-neutralBright","bg-neutralLight","bg-progress","bg-screen","bg-subtle","bg-success","bg-successLight","bg-tableHeader","bg-warning","black","blue-strong","blue-subtle","border-accent","border-accentLight","border-bold","border-danger","border-default","border-strong","border-success","border-warning","brilliantblue-05","brilliantblue-10","brilliantblue-20","brilliantblue-25","brilliantblue-40","brilliantblue-50","brilliantblue-60","brilliantblue-70","brilliantblue-75","brilliantblue-80","brilliantblue-85","brilliantblue-90","brilliantblue-95","brilliantblue-99","control-bg-inverseHover","danger-15","danger-20","danger-30","danger-45","danger-60","danger-70","danger-75","danger-80","danger-85","danger-90","danger-95","danger-99","danger-strong","danger-subtle","darkblue-strong","darkblue-subtle","disabled-bg","disabled-border","disabled-icon","disabled-text","divider-default","fg-accent","fg-accentLight","fg-brand","fg-danger","fg-dangerLight","fg-deep","fg-inverse","fg-primary","fg-quaternary","fg-secondary","fg-subtleCool","fg-subtleWarm","fg-success","fg-successLight","fg-tertiary","fg-warning","field-bg-default","field-bg-hover","field-bg-readOnly","field-border-danger","field-border-default","field-border-focus","field-border-hover","field-border-success","field-icon-default","field-text-default","field-text-placeholder","field-text-readOnly","green-15","green-25","green-45","green-55","green-65","green-70","green-75","green-80","green-85","green-90","green-95","green-99","green-strong","green-subtle","grey-05","grey-10","grey-20","grey-25","grey-30","grey-35","grey-45","grey-50","grey-55","grey-60","grey-65","grey-70","grey-80","grey-85","grey-90","grey-95","grey-strong","grey-subtle","indigo-strong","indigo-subtle","lightblue-strong","lightblue-subtle","link-accent","navigation-gnb-bg-dark","oceanblue-15","oceanblue-25","oceanblue-50","oceanblue-60","oceanblue-65","oceanblue-70","oceanblue-75","oceanblue-80","oceanblue-85","oceanblue-90","oceanblue-95","oceanblue-99","olive-05","olive-10","olive-15","olive-20","olive-30","olive-45","olive-55","olive-65","olive-70","olive-80","olive-90","olive-95","orange-10","orange-20","orange-35","orange-45","orange-55","orange-60","orange-65","orange-75","orange-85","orange-90","orange-95","orange-99","orange-strong","orange-subtle","primary-05","primary-10","primary-20","primary-25","primary-40","primary-50","primary-60","primary-70","primary-75","primary-80","primary-85","primary-90","primary-95","primary-99","red-15","red-20","red-30","red-45","red-60","red-70","red-75","red-80","red-85","red-90","red-95","red-99","red-strong","red-subtle","steelblue-10","steelblue-25","steelblue-45","steelblue-60","steelblue-65","steelblue-70","steelblue-75","steelblue-80","steelblue-85","steelblue-90","steelblue-95","steelblue-99","success-15","success-25","success-45","success-55","success-65","success-70","success-75","success-80","success-85","success-90","success-95","success-99","success-strong","success-subtle","warning-10","warning-20","warning-35","warning-45","warning-55","warning-60","warning-65","warning-75","warning-85","warning-90","warning-95","warning-99","warning-strong","warning-subtle","white","yellow-10","yellow-20","yellow-25","yellow-30","yellow-40","yellow-45","yellow-50","yellow-60","yellow-70","yellow-80","yellow-90","yellow-95","yellow-moderate","yellow-strong","yellow-subtle"];
16
+ export const COLORS = ["bg-accent","bg-accentBright","bg-accentFaint","bg-accentLight","bg-brand","bg-caution","bg-danger","bg-dangerLight","bg-deep","bg-frame","bg-neutralBright","bg-neutralLight","bg-progress","bg-screen","bg-subtle","bg-success","bg-successLight","bg-tableHeader","bg-warning","black","blue-strong","blue-subtle","border-accent","border-accentLight","border-bold","border-danger","border-default","border-strong","border-success","border-warning","brilliantblue-05","brilliantblue-10","brilliantblue-20","brilliantblue-25","brilliantblue-40","brilliantblue-50","brilliantblue-60","brilliantblue-70","brilliantblue-75","brilliantblue-80","brilliantblue-85","brilliantblue-90","brilliantblue-95","brilliantblue-99","control-bg-inverseHover","danger-15","danger-20","danger-30","danger-45","danger-60","danger-70","danger-75","danger-80","danger-85","danger-90","danger-95","danger-99","danger-strong","danger-subtle","darkblue-strong","darkblue-subtle","disabled-bg","disabled-border","disabled-icon","disabled-text","divider-default","fg-accent","fg-accentLight","fg-brand","fg-danger","fg-dangerLight","fg-deep","fg-inverse","fg-primary","fg-quaternary","fg-secondary","fg-subtleCool","fg-subtleWarm","fg-success","fg-successLight","fg-tertiary","fg-warning","field-bg-default","field-bg-hover","field-bg-readOnly","field-border-danger","field-border-default","field-border-focus","field-border-hover","field-border-success","field-icon-default","field-text-default","field-text-placeholder","field-text-readOnly","green-15","green-25","green-45","green-55","green-65","green-70","green-75","green-80","green-85","green-90","green-95","green-99","green-strong","green-subtle","grey-05","grey-10","grey-20","grey-25","grey-30","grey-35","grey-45","grey-50","grey-55","grey-60","grey-65","grey-70","grey-80","grey-85","grey-90","grey-95","grey-strong","grey-subtle","indigo-strong","indigo-subtle","lightblue-strong","lightblue-subtle","link-accent","navigation-gnb-bg-dark","navigation-gnb-bg-system","oceanblue-15","oceanblue-25","oceanblue-50","oceanblue-60","oceanblue-65","oceanblue-70","oceanblue-75","oceanblue-80","oceanblue-85","oceanblue-90","oceanblue-95","oceanblue-99","olive-05","olive-10","olive-15","olive-20","olive-30","olive-45","olive-55","olive-65","olive-70","olive-80","olive-90","olive-95","orange-10","orange-20","orange-35","orange-45","orange-55","orange-60","orange-65","orange-75","orange-85","orange-90","orange-95","orange-99","orange-strong","orange-subtle","primary-05","primary-10","primary-20","primary-25","primary-40","primary-50","primary-60","primary-70","primary-75","primary-80","primary-85","primary-90","primary-95","primary-99","red-15","red-20","red-30","red-45","red-60","red-70","red-75","red-80","red-85","red-90","red-95","red-99","red-strong","red-subtle","steelblue-10","steelblue-25","steelblue-45","steelblue-60","steelblue-65","steelblue-70","steelblue-75","steelblue-80","steelblue-85","steelblue-90","steelblue-95","steelblue-99","success-15","success-25","success-45","success-55","success-65","success-70","success-75","success-80","success-85","success-90","success-95","success-99","success-strong","success-subtle","warning-10","warning-20","warning-35","warning-45","warning-55","warning-60","warning-65","warning-75","warning-85","warning-90","warning-95","warning-99","warning-strong","warning-subtle","white","yellow-10","yellow-20","yellow-25","yellow-30","yellow-40","yellow-45","yellow-50","yellow-60","yellow-70","yellow-80","yellow-90","yellow-95","yellow-moderate","yellow-strong","yellow-subtle"];
17
+
18
+ /** 필드 폭 등급 (px). SFieldWidth 의 'xs'|'sm'|'md'|'lg'|'xl' 이 풀리는 값이다 */
19
+ export const FIELD_WIDTHS = {"xs":80,"sm":120,"md":160,"lg":240,"xl":480};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sellmate-design-system-react",
3
- "version": "9.0.0-beta.3",
3
+ "version": "9.0.0-beta.30",
4
4
  "description": "Sellmate Design System — React (TypeScript + Tailwind v4) port",
5
5
  "license": "UNLICENSED",
6
6
  "keywords": [
@@ -88,6 +88,16 @@
88
88
  "@radix-ui/react-popover": "^1.1.18",
89
89
  "@radix-ui/react-switch": "^1.3.2",
90
90
  "@radix-ui/react-tooltip": "^1.2.11",
91
+ "@tiptap/core": "^3.30.2",
92
+ "@tiptap/extension-image": "^3.30.2",
93
+ "@tiptap/extension-list": "^3.30.2",
94
+ "@tiptap/extension-table": "^3.30.2",
95
+ "@tiptap/extension-text-align": "^3.30.2",
96
+ "@tiptap/extension-text-style": "^3.30.2",
97
+ "@tiptap/extension-typography": "^3.30.2",
98
+ "@tiptap/extensions": "^3.30.2",
99
+ "@tiptap/react": "^3.30.2",
100
+ "@tiptap/starter-kit": "^3.30.2",
91
101
  "class-variance-authority": "^0.7.1",
92
102
  "clsx": "^2.1.1",
93
103
  "tailwind-merge": "^3.0.2"