sellmate-design-system-react 9.0.0-beta.2 → 9.0.0-beta.21

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 (132) hide show
  1. package/AGENTS.md +488 -90
  2. package/README.md +101 -0
  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 +288 -5
  14. package/dist/components/SChipFilter/SChipFilter.d.ts +95 -40
  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/SCircleProgress/README.md +8 -0
  19. package/dist/components/SConfirmModal/README.md +16 -0
  20. package/dist/components/SDatePicker/README.md +60 -4
  21. package/dist/components/SDatePicker/SDatePicker.d.ts +61 -5
  22. package/dist/components/SDatePicker/index.d.ts +1 -1
  23. package/dist/components/SDateRangePicker/README.md +17 -2
  24. package/dist/components/SDateRangePicker/SDateRangePicker.d.ts +16 -3
  25. package/dist/components/SDivider/README.md +4 -0
  26. package/dist/components/SDraggableItem/README.md +37 -0
  27. package/dist/components/SDraggableItem/SDraggableItem.d.ts +2 -0
  28. package/dist/components/SDraggableList/README.md +29 -0
  29. package/dist/components/SDraggableList/SDraggableList.d.ts +10 -0
  30. package/dist/components/SDraggableList/index.d.ts +1 -1
  31. package/dist/components/SDrawer/README.md +8 -0
  32. package/dist/components/SDropdownButton/README.md +19 -0
  33. package/dist/components/SEditor/EditorBody.d.ts +40 -0
  34. package/dist/components/SEditor/EditorToolbar.d.ts +87 -0
  35. package/dist/components/SEditor/README.md +230 -0
  36. package/dist/components/SEditor/SEditor.d.ts +124 -0
  37. package/dist/components/SEditor/editor-icons.d.ts +59 -0
  38. package/dist/components/SEditor/editor.config.d.ts +85 -0
  39. package/dist/components/SEditor/index.d.ts +2 -0
  40. package/dist/components/SEditor/tiptap-api.d.ts +29 -0
  41. package/dist/components/SEditor/use-is-mobile.d.ts +14 -0
  42. package/dist/components/SExpansionItem/README.md +36 -0
  43. package/dist/components/SField/README.md +27 -2
  44. package/dist/components/SField/SField.d.ts +22 -4
  45. package/dist/components/SFilePicker/README.md +15 -1
  46. package/dist/components/SFilePicker/SFilePicker.d.ts +7 -1
  47. package/dist/components/SFooter/README.md +25 -0
  48. package/dist/components/SFooter/SFooter.d.ts +4 -3
  49. package/dist/components/SForm/README.md +11 -0
  50. package/dist/components/SGhostButton/README.md +20 -2
  51. package/dist/components/SGnb/README.md +45 -0
  52. package/dist/components/SGnb/gnb.config.d.ts +7 -0
  53. package/dist/components/SGuide/README.md +15 -0
  54. package/dist/components/SIcon/README.md +4 -0
  55. package/dist/components/SIcon/SIcon.d.ts +1 -1
  56. package/dist/components/SIcon/icons.gen.d.ts +2 -0
  57. package/dist/components/SImage/README.md +14 -0
  58. package/dist/components/SInput/README.md +3 -1
  59. package/dist/components/SInput/SInput.d.ts +7 -1
  60. package/dist/components/SKeyValueTable/README.md +87 -0
  61. package/dist/components/SKeyValueTable/SKeyValueTable.d.ts +14 -3
  62. package/dist/components/SLayout/README.md +16 -0
  63. package/dist/components/SLinearProgress/README.md +8 -0
  64. package/dist/components/SList/README.md +5 -1
  65. package/dist/components/SList/SList.d.ts +0 -2
  66. package/dist/components/SListItem/README.md +41 -0
  67. package/dist/components/SLoadingModal/README.md +8 -0
  68. package/dist/components/SNumberInput/README.md +9 -1
  69. package/dist/components/SNumberInput/SNumberInput.d.ts +7 -1
  70. package/dist/components/SPage/README.md +41 -1
  71. package/dist/components/SPage/SPage.d.ts +24 -2
  72. package/dist/components/SPage/index.d.ts +1 -1
  73. package/dist/components/SPage/page.config.d.ts +8 -0
  74. package/dist/components/SPopover/README.md +15 -0
  75. package/dist/components/SPopup/README.md +19 -0
  76. package/dist/components/SPortal/README.md +14 -0
  77. package/dist/components/SRadio/README.md +20 -0
  78. package/dist/components/SRadioButton/README.md +18 -0
  79. package/dist/components/SScrollArea/README.md +14 -0
  80. package/dist/components/SSearchInput/README.md +61 -0
  81. package/dist/components/SSearchInput/SSearchInput.d.ts +52 -0
  82. package/dist/components/SSearchInput/index.d.ts +1 -0
  83. package/dist/components/SSectionHeaderCard/README.md +44 -20
  84. package/dist/components/SSectionHeaderCard/SSectionHeaderCard.d.ts +31 -14
  85. package/dist/components/SSectionHeaderCard/index.d.ts +1 -1
  86. package/dist/components/SSelect/README.md +23 -3
  87. package/dist/components/SSelect/SSelect.d.ts +13 -1
  88. package/dist/components/SSplitter/README.md +15 -0
  89. package/dist/components/SStepper/README.md +26 -0
  90. package/dist/components/SSwitch/README.md +13 -0
  91. package/dist/components/STable/README.md +72 -1
  92. package/dist/components/STable/STable.d.ts +129 -11
  93. package/dist/components/STable/index.d.ts +1 -1
  94. package/dist/components/STabs/README.md +12 -1
  95. package/dist/components/STabs/STabs.d.ts +2 -4
  96. package/dist/components/STabs/index.d.ts +1 -1
  97. package/dist/components/STabs/tabs.config.d.ts +3 -4
  98. package/dist/components/STag/README.md +47 -0
  99. package/dist/components/STextLink/README.md +17 -0
  100. package/dist/components/STextLink/STextLink.d.ts +2 -0
  101. package/dist/components/STextarea/README.md +1 -1
  102. package/dist/components/STextarea/STextarea.d.ts +7 -1
  103. package/dist/components/STimePicker/README.md +15 -1
  104. package/dist/components/STimePicker/STimePicker.d.ts +7 -1
  105. package/dist/components/STimePicker/timepicker.config.d.ts +7 -0
  106. package/dist/components/STimeRangePicker/README.md +27 -1
  107. package/dist/components/STimeRangePicker/STimeRangePicker.d.ts +7 -1
  108. package/dist/components/SToast/README.md +24 -0
  109. package/dist/components/SToggle/README.md +8 -0
  110. package/dist/components/STooltip/README.md +22 -0
  111. package/dist/components/STree/README.md +41 -0
  112. package/dist/components/STree/STree.d.ts +8 -0
  113. package/dist/components/STree/index.d.ts +1 -1
  114. package/dist/index.cjs +4266 -750
  115. package/dist/index.cjs.map +1 -1
  116. package/dist/index.d.ts +3 -0
  117. package/dist/index.js +4249 -750
  118. package/dist/index.js.map +1 -1
  119. package/dist/lib/field-width.d.ts +31 -0
  120. package/dist/lib/story-docs.d.ts +19 -3
  121. package/dist/lib/truncated-value-tooltip.d.ts +18 -0
  122. package/dist/llms-full.txt +2337 -183
  123. package/dist/llms.txt +490 -92
  124. package/dist/styles.css +684 -41
  125. package/dist/theme.css +22 -6
  126. package/eslint/index.mjs +10 -0
  127. package/eslint/lib/table-column.mjs +26 -0
  128. package/eslint/rules/field-width-grade.d.mts +41 -0
  129. package/eslint/rules/field-width-grade.mjs +311 -0
  130. package/eslint/rules/table-column-width.mjs +110 -0
  131. package/eslint/scale.gen.mjs +3 -0
  132. package/package.json +13 -1
package/dist/theme.css CHANGED
@@ -746,6 +746,11 @@
746
746
  --cmp-field-errorMessage-color: #E30000;
747
747
  --cmp-field-contents-gap: 4px;
748
748
  --cmp-field-message-paddingX: 8px;
749
+ --cmp-field-width-xs: 80px;
750
+ --cmp-field-width-sm: 120px;
751
+ --cmp-field-width-md: 160px;
752
+ --cmp-field-width-lg: 240px;
753
+ --cmp-field-width-xl: 480px;
749
754
  --cmp-textinput-sm-height: 28px;
750
755
  --cmp-textinput-sm-paddingX: 12px;
751
756
  --cmp-textinput-sm-paddingY: 4px;
@@ -893,6 +898,20 @@
893
898
  --cmp-datepicker-calendar-time-gap: 12px;
894
899
  --cmp-datepicker-calendar-time-section-gap: 12px;
895
900
  --cmp-datepicker-calendar-time-unit-gap: 2px;
901
+ --cmp-datepicker-mode-bg: #FFFFFF;
902
+ --cmp-datepicker-mode-radius: 4px;
903
+ --cmp-datepicker-mode-year-width: 120px;
904
+ --cmp-datepicker-mode-yearMonth-column-width: 64px;
905
+ --cmp-datepicker-mode-yearMonth-gap: 4px;
906
+ --cmp-datepicker-mode-item-paddingY: 4px;
907
+ --cmp-datepicker-mode-item-paddingX: 12px;
908
+ --cmp-datepicker-mode-item-gap: 8px;
909
+ --cmp-datepicker-mode-item-bg-default: #FFFFFF;
910
+ --cmp-datepicker-mode-item-bg-hover: #0075FF;
911
+ --cmp-datepicker-mode-item-content-default: #222222;
912
+ --cmp-datepicker-mode-item-content-hover: #FFFFFF;
913
+ --cmp-datepicker-mode-item-content-disabled: #888888;
914
+ --cmp-datepicker-mode-item-content-selected: #0075FF;
896
915
  --cmp-chip-height: 24px;
897
916
  --cmp-chip-paddingX: 8px;
898
917
  --cmp-chip-gap: 4px;
@@ -1224,10 +1243,8 @@
1224
1243
  --cmp-stepper-vertical-title-paddingTop: 12px;
1225
1244
  --cmp-stepper-vertical-title-paddingBottom: 6px;
1226
1245
  --cmp-stepper-vertical-title-text: #004290;
1227
- --cmp-tab-main-lg-height: 44px;
1228
- --cmp-tab-main-lg-paddingX: 32px;
1229
- --cmp-tab-main-md-height: 36px;
1230
- --cmp-tab-main-md-paddingX: 24px;
1246
+ --cmp-tab-main-height: 36px;
1247
+ --cmp-tab-main-paddingX: 24px;
1231
1248
  --cmp-tab-main-gap: 8px;
1232
1249
  --cmp-tab-main-container-gap: 4px;
1233
1250
  --cmp-tab-main-radius: 6px;
@@ -1383,9 +1400,8 @@
1383
1400
  --cmp-tree-item-rowGap: 8px;
1384
1401
  --cmp-tree-item-contentGap: 12px;
1385
1402
  --cmp-tree-item-text-default: #222222;
1386
- --cmp-tree-item-text-selected: #0075FF;
1387
1403
  --cmp-tree-item-arrow-icon: 16px;
1388
- --cmp-tree-connector-line-color: #AAAAAA;
1404
+ --cmp-tree-connector-line-color: #E1E1E1;
1389
1405
  --cmp-pageHeader-paddingX: 16px;
1390
1406
  --cmp-pageHeader-gap: 12px;
1391
1407
  --cmp-pageHeader-title-paddingLeft: 4px;
package/eslint/index.mjs CHANGED
@@ -11,6 +11,8 @@ import preferTypoPreset from "./rules/prefer-typo-preset.mjs";
11
11
  import tableNumericAlign from "./rules/table-numeric-align.mjs";
12
12
  import requireLocaleNumber from "./rules/require-locale-number.mjs";
13
13
  import componentGroupGap from "./rules/component-group-gap.mjs";
14
+ import fieldWidthGrade from "./rules/field-width-grade.mjs";
15
+ import tableColumnWidth from "./rules/table-column-width.mjs";
14
16
 
15
17
  const PLUGIN_NAME = "sellmate";
16
18
 
@@ -24,6 +26,8 @@ const plugin = {
24
26
  "table-numeric-align": tableNumericAlign,
25
27
  "require-locale-number": requireLocaleNumber,
26
28
  "component-group-gap": componentGroupGap,
29
+ "field-width-grade": fieldWidthGrade,
30
+ "table-column-width": tableColumnWidth,
27
31
  },
28
32
  };
29
33
 
@@ -54,6 +58,10 @@ const recommended = [
54
58
  [`${PLUGIN_NAME}/component-group-gap`]: "warn",
55
59
  [`${PLUGIN_NAME}/table-numeric-align`]: "warn",
56
60
  [`${PLUGIN_NAME}/require-locale-number`]: "warn",
61
+ // 폭 등급 환산이 아직 잠정치라 권고에 둔다 (디자인 확정표가 오면 error 승격 검토)
62
+ [`${PLUGIN_NAME}/field-width-grade`]: "warn",
63
+ // 컬럼 폭 — 폭 미지정(기본 120px)·스페이서 오용은 권고, % · clamp() 는 조용히 깨지므로 error
64
+ [`${PLUGIN_NAME}/table-column-width`]: "warn",
57
65
 
58
66
  // 앱 고유 치수(w-[280px])·일회성 값에는 임의 값이 정당하다. 필요한 팀만 켠다.
59
67
  [`${PLUGIN_NAME}/no-arbitrary-class`]: "off",
@@ -79,6 +87,8 @@ const strict = [
79
87
  [`${PLUGIN_NAME}/component-group-gap`]: "error",
80
88
  [`${PLUGIN_NAME}/table-numeric-align`]: "error",
81
89
  [`${PLUGIN_NAME}/require-locale-number`]: "error",
90
+ [`${PLUGIN_NAME}/field-width-grade`]: "error",
91
+ [`${PLUGIN_NAME}/table-column-width`]: "error",
82
92
  },
83
93
  },
84
94
  ];
@@ -73,6 +73,32 @@ export function literalString(node) {
73
73
  : undefined;
74
74
  }
75
75
 
76
+ /**
77
+ * STableColumn 자리인지 — `asColumn` 보다 넓게, 값을 그리지 않는 스페이서 열까지 본다.
78
+ *
79
+ * `name` · `label` 만으로는 판정하지 않는다. 소비 앱의 검색 필터 필드·상세 모달 키-값 필드가
80
+ * 정확히 같은 모양(`{ name, label, type, ... }`)이라, 그것만 보면 잡히는 것의 대부분이 표와
81
+ * 무관한 객체가 된다 — 실측에서 265건 중 261건이 오탐이었다.
82
+ *
83
+ * 그래서 `STableColumn` 타입의 판별 유니온을 그대로 옮긴다 — 값을 그리는 열은 `field` 를,
84
+ * 값을 그리지 않는 스페이서 열은 `autoWidth` 를 반드시 갖는다. 둘 다 없으면 컬럼이 아니다.
85
+ * 폭 규칙이 봐야 하는 스페이서 열은 `autoWidth` 쪽으로 그대로 들어온다.
86
+ *
87
+ * 놓치는 것: `{ ...base, name, label }` 처럼 `field` 가 스프레드로 들어오는 컬럼. 정적으로는
88
+ * 검색 필터 필드와 구분되지 않아 보고하지 않는다 (`asColumn` 을 쓰는 다른 두 규칙과 같은 한계).
89
+ * @returns {{ nameProp, labelProp, columnName: string | undefined } | null}
90
+ */
91
+ export function asColumnShape(node) {
92
+ const nameProp = prop(node, "name");
93
+ const labelProp = prop(node, "label");
94
+ if (!nameProp || !labelProp) return null;
95
+ if (!prop(node, "field") && !prop(node, "autoWidth")) return null;
96
+ // name 이 문자열 리터럴이 아니면 컬럼 정의로 단정하지 않는다 (일반 객체 오탐 방지)
97
+ const columnName = literalString(nameProp);
98
+ if (columnName === undefined) return null;
99
+ return { nameProp, labelProp, columnName };
100
+ }
101
+
76
102
  /**
77
103
  * STableColumn 형태인지 — name·label·field 셋을 모두 가진 객체만 컬럼으로 본다.
78
104
  * @returns {{ nameProp, labelProp, label: string } | null}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * playground 의 실측 페이지(FieldWidthGradePage)가 규칙과 같은 계산을 쓰기 위한 타입 선언.
3
+ * 규칙 본체는 소비 앱이 그대로 실행하는 plain ESM 이라 .mjs 로 두고, 타입만 여기에 붙인다.
4
+ */
5
+
6
+ export type FieldGrade = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
7
+ export type FieldSize = 'sm' | 'md';
8
+
9
+ export interface FieldComponentSpec {
10
+ /** 스키마 상한을 담는 prop — 숫자 필드는 maxLength 가 아니라 max 다 */
11
+ lengthProp: 'maxLength' | 'max';
12
+ font: Record<FieldSize, number>;
13
+ /** 글자가 쓸 수 없는 폭의 합 (좌우 패딩 · 테두리 · 스테퍼) */
14
+ chrome: Record<FieldSize, number>;
15
+ /** 한글이 들어갈 수 있는가 */
16
+ korean: boolean;
17
+ grades: FieldGrade[];
18
+ }
19
+
20
+ export declare const GRADES: Record<FieldGrade, number>;
21
+ export declare const CHAR_RATIO: { narrow: number; wide: number };
22
+ export declare const COMPONENTS: Record<string, FieldComponentSpec>;
23
+
24
+ export declare function capacity(
25
+ spec: FieldComponentSpec,
26
+ size: FieldSize,
27
+ grade: FieldGrade,
28
+ ratio: number,
29
+ grades?: Record<FieldGrade, number>,
30
+ ): number;
31
+
32
+ export declare function smallestGradeFor(
33
+ spec: FieldComponentSpec,
34
+ size: FieldSize,
35
+ len: number,
36
+ ratio: number,
37
+ grades?: Record<FieldGrade, number>,
38
+ ): FieldGrade | undefined;
39
+
40
+ declare const rule: unknown;
41
+ export default rule;
@@ -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
+ };
@@ -14,3 +14,6 @@ export const TYPO_PRESETS = ["typo-body-lg-bold","typo-body-lg-default","typo-bo
14
14
 
15
15
  /** 색상 유틸리티 키 (text-*, bg-*, border-* 공통) */
16
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"];
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.2",
3
+ "version": "9.0.0-beta.21",
4
4
  "description": "Sellmate Design System — React (TypeScript + Tailwind v4) port",
5
5
  "license": "UNLICENSED",
6
6
  "keywords": [
@@ -88,6 +88,18 @@
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-highlight": "^3.30.2",
93
+ "@tiptap/extension-image": "^3.30.2",
94
+ "@tiptap/extension-list": "^3.30.2",
95
+ "@tiptap/extension-subscript": "^3.30.2",
96
+ "@tiptap/extension-superscript": "^3.30.2",
97
+ "@tiptap/extension-text-align": "^3.30.2",
98
+ "@tiptap/extension-text-style": "^3.30.2",
99
+ "@tiptap/extension-typography": "^3.30.2",
100
+ "@tiptap/extensions": "^3.30.2",
101
+ "@tiptap/react": "^3.30.2",
102
+ "@tiptap/starter-kit": "^3.30.2",
91
103
  "class-variance-authority": "^0.7.1",
92
104
  "clsx": "^2.1.1",
93
105
  "tailwind-merge": "^3.0.2"