sh-ui-cli 0.58.8 → 0.59.1

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.
@@ -2,6 +2,35 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$description": "sh-ui 릴리즈 노트 단일 소스. docs(React)와 showcase(Flutter)가 함께 읽는다. 새 릴리즈마다 맨 앞에 추가.",
4
4
  "versions": [
5
+ {
6
+ "version": "0.59.1",
7
+ "date": "2026-05-06",
8
+ "title": "접근성 라운드 1 — text-wrap: balance + 모바일 터치 타겟",
9
+ "type": "patch",
10
+ "highlights": [
11
+ "**`Card.Title` / `Dialog.Title` / `Popover.Title` 에 `text-wrap: balance` 추가** — 헤딩 텍스트가 두 줄 이상으로 wrap 될 때 마지막 줄에 단어 1~2개만 남는 orphan 문제 해소. 단어가 두 줄에 균등 분포. 한국어처럼 단어 길이 다양한 언어에서 효과 큼. 한 줄에 들어가면 no-op.",
12
+ "**모바일 터치 타겟 자동 보정 (WCAG 2.5.5)** — tokens.css 에 `@media (hover: none) and (pointer: coarse)` 블록 추가. 손가락 입력 환경에서 `--control-sm` (32px) / `--control-md` (40px) 가 자동으로 44px 로 확장. 모든 Button/Input 등 control-* 토큰을 사용하는 컴포넌트가 자동 적용. 데스크탑 (마우스/트랙패드) 은 영향 없음.",
13
+ "**`--control-lg`** (48px) 는 이미 44px 초과라 변경 불필요.",
14
+ "**4 variant + 듀얼 카피본 + pactrack 동기화** — Tailwind/plain/module/vanilla-extract + apps/docs + cli/templates."
15
+ ],
16
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.59.1"
17
+ },
18
+ {
19
+ "version": "0.59.0",
20
+ "date": "2026-05-06",
21
+ "title": "토큰 정책 전환 — px → rem (접근성 / 유저 글꼴 확대 반응)",
22
+ "type": "minor",
23
+ "highlights": [
24
+ "**dimension 토큰을 PX → REM 으로 일괄 전환** — `--space-2: 0.5rem`, `--text-sm: 0.875rem`, `--control-md: 2.5rem` 등. 1rem = 16px 가정. 사용자가 브라우저 글꼴 키울 때 컴포넌트가 비례 반응 (WCAG 1.4.4 준수). 사용자 root font-size 안 건드리면 시각적 no-op.",
25
+ "**PX 유지 예외** — `--border-width: 1px/2px` (1px 미만 정밀도), shadow offset (`--shadow-sm: 0 1px 2px ...`), breakpoint (`640px/768px/...`, MQ 호환성). 거의 1~2 token 한정.",
26
+ "**`packages/tokens/src/primitives.json` 마이그레이션** — spacing/fontSize/controlHeight 의 \"4px\" 등 dimension 값을 rem 으로 변경. build.mjs 의 dimensionToDart 가 \"0\" 도 처리.",
27
+ "**`inject.js` 의 spacing/typography/controls emit 함수에 toRem 헬퍼 추가** — 1rem=16px 변환 + trailing zero 제거 (0.50→0.5, 1.00→1).",
28
+ "**Dart (Flutter) 측은 PX 유지** — Flutter 는 logical pixel 단위라 dimension 그대로. CSS 만 rem 화.",
29
+ "**CLAUDE.md 규칙 v2** — \"토큰 정의부 PX 허용\" → \"토큰 정의부 REM 사용 (border/shadow 만 PX 예외)\". 향후 신규 토큰도 이 정책 따라야 함.",
30
+ "**기존 사용자 마이그레이션 가이드** — 기존 프로젝트의 정적 tokens.css 는 그대로 동작 (PX). REM 의 이점을 받으려면 `sh-ui add tokens` 재설치 또는 수동 마이그레이션 필요. patch release 가 아닌 minor 라 대응 시간 확보."
31
+ ],
32
+ "url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.59.0"
33
+ },
5
34
  {
6
35
  "version": "0.58.8",
7
36
  "date": "2026-05-06",
@@ -38,7 +38,7 @@ export const CardTitle = React.forwardRef<HTMLDivElement, DivProps>(
38
38
  <div
39
39
  ref={ref}
40
40
  className={cn(
41
- "text-[length:var(--text-base)] font-semibold leading-tight tracking-tight",
41
+ "text-[length:var(--text-base)] font-semibold leading-tight tracking-tight text-balance",
42
42
  className,
43
43
  )}
44
44
  {...props}
@@ -28,6 +28,7 @@
28
28
  font-weight: var(--weight-semibold);
29
29
  line-height: 1.25;
30
30
  letter-spacing: -0.01em;
31
+ text-wrap: balance;
31
32
  }
32
33
 
33
34
  .sh-ui-card__description {
@@ -28,6 +28,7 @@
28
28
  font-weight: var(--weight-semibold);
29
29
  line-height: 1.25;
30
30
  letter-spacing: -0.01em;
31
+ text-wrap: balance;
31
32
  }
32
33
 
33
34
  .description {
@@ -69,7 +69,7 @@ export const DialogTitle = React.forwardRef<
69
69
  <BaseDialog.Title
70
70
  ref={ref}
71
71
  className={cn(
72
- "m-0 mb-[var(--space-1)] font-semibold text-[length:var(--text-lg)] leading-snug",
72
+ "m-0 mb-[var(--space-1)] font-semibold text-[length:var(--text-lg)] leading-snug text-balance",
73
73
  className,
74
74
  )}
75
75
  {...props}
@@ -62,6 +62,7 @@
62
62
  font-weight: var(--weight-semibold);
63
63
  font-size: var(--text-lg);
64
64
  line-height: 1.4;
65
+ text-wrap: balance;
65
66
  }
66
67
 
67
68
  /* ── Description ── */
@@ -62,6 +62,7 @@
62
62
  font-weight: var(--weight-semibold);
63
63
  font-size: var(--text-lg);
64
64
  line-height: 1.4;
65
+ text-wrap: balance;
65
66
  }
66
67
 
67
68
  /* ── Description ── */
@@ -55,7 +55,7 @@ export const PopoverTitle = React.forwardRef<
55
55
  return (
56
56
  <BasePopover.Title
57
57
  ref={ref}
58
- className={cn("m-0 mb-[var(--space-1)] font-semibold text-[0.9375rem]", className)}
58
+ className={cn("m-0 mb-[var(--space-1)] font-semibold text-[0.9375rem] text-balance", className)}
59
59
  {...props}
60
60
  />
61
61
  );
@@ -46,6 +46,7 @@
46
46
  margin: 0 0 var(--space-1);
47
47
  font-weight: var(--weight-semibold);
48
48
  font-size: 0.9375rem;
49
+ text-wrap: balance;
49
50
  }
50
51
 
51
52
  .sh-ui-popover__description {
@@ -46,6 +46,7 @@
46
46
  margin: 0 0 var(--space-1);
47
47
  font-weight: var(--weight-semibold);
48
48
  font-size: 0.9375rem;
49
+ text-wrap: balance;
49
50
  }
50
51
 
51
52
  .popover__description {
@@ -194,8 +194,9 @@ function cssColorToDart(css) {
194
194
  throw new Error(`지원하지 않는 CSS 색상: ${css}`);
195
195
  }
196
196
 
197
- /** "0.5rem" → 8.0, "16px" → 16.0 */
197
+ /** "0.5rem" → 8.0, "16px" → 16.0, "0" → 0.0 */
198
198
  function dimensionToDart(value) {
199
+ if (value === "0" || value === 0) return "0.0";
199
200
  const rem = /^(-?\d*\.?\d+)rem$/.exec(value);
200
201
  if (rem) return (parseFloat(rem[1]) * 16).toFixed(1);
201
202
  const px = /^(-?\d*\.?\d+)px$/.exec(value);
@@ -53,17 +53,17 @@
53
53
  },
54
54
 
55
55
  "spacing": {
56
- "0": { "$value": "0px", "$type": "dimension" },
57
- "1": { "$value": "4px", "$type": "dimension" },
58
- "2": { "$value": "8px", "$type": "dimension" },
59
- "3": { "$value": "12px", "$type": "dimension" },
60
- "4": { "$value": "16px", "$type": "dimension" },
61
- "5": { "$value": "20px", "$type": "dimension" },
62
- "6": { "$value": "24px", "$type": "dimension" },
63
- "8": { "$value": "32px", "$type": "dimension" },
64
- "10": { "$value": "40px", "$type": "dimension" },
65
- "12": { "$value": "48px", "$type": "dimension" },
66
- "16": { "$value": "64px", "$type": "dimension" }
56
+ "0": { "$value": "0", "$type": "dimension" },
57
+ "1": { "$value": "0.25rem", "$type": "dimension" },
58
+ "2": { "$value": "0.5rem", "$type": "dimension" },
59
+ "3": { "$value": "0.75rem", "$type": "dimension" },
60
+ "4": { "$value": "1rem", "$type": "dimension" },
61
+ "5": { "$value": "1.25rem", "$type": "dimension" },
62
+ "6": { "$value": "1.5rem", "$type": "dimension" },
63
+ "8": { "$value": "2rem", "$type": "dimension" },
64
+ "10": { "$value": "2.5rem", "$type": "dimension" },
65
+ "12": { "$value": "3rem", "$type": "dimension" },
66
+ "16": { "$value": "4rem", "$type": "dimension" }
67
67
  },
68
68
 
69
69
  "radius": {
@@ -76,14 +76,14 @@
76
76
  },
77
77
 
78
78
  "fontSize": {
79
- "xs": { "$value": "12px", "$type": "dimension" },
80
- "sm": { "$value": "14px", "$type": "dimension" },
81
- "base":{ "$value": "16px", "$type": "dimension" },
82
- "lg": { "$value": "18px", "$type": "dimension" },
83
- "xl": { "$value": "20px", "$type": "dimension" },
84
- "2xl": { "$value": "24px", "$type": "dimension" },
85
- "3xl": { "$value": "30px", "$type": "dimension" },
86
- "4xl": { "$value": "36px", "$type": "dimension" }
79
+ "xs": { "$value": "0.75rem", "$type": "dimension" },
80
+ "sm": { "$value": "0.875rem", "$type": "dimension" },
81
+ "base":{ "$value": "1rem", "$type": "dimension" },
82
+ "lg": { "$value": "1.125rem", "$type": "dimension" },
83
+ "xl": { "$value": "1.25rem", "$type": "dimension" },
84
+ "2xl": { "$value": "1.5rem", "$type": "dimension" },
85
+ "3xl": { "$value": "1.875rem", "$type": "dimension" },
86
+ "4xl": { "$value": "2.25rem", "$type": "dimension" }
87
87
  },
88
88
 
89
89
  "fontWeight": {
@@ -112,9 +112,9 @@
112
112
  },
113
113
 
114
114
  "controlHeight": {
115
- "sm": { "$value": "32px", "$type": "dimension" },
116
- "md": { "$value": "40px", "$type": "dimension" },
117
- "lg": { "$value": "48px", "$type": "dimension" }
115
+ "sm": { "$value": "2rem", "$type": "dimension" },
116
+ "md": { "$value": "2.5rem", "$type": "dimension" },
117
+ "lg": { "$value": "3rem", "$type": "dimension" }
118
118
  },
119
119
 
120
120
  "borderWidth": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh-ui-cli",
3
- "version": "0.58.8",
3
+ "version": "0.59.1",
4
4
  "description": "sh-ui CLI — 프로젝트 스캐폴드(create) + 컴포넌트 추가(add/list/remove) + IDE-내 AI용 MCP 서버",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -126,9 +126,17 @@ export const buildDartRadiusBlock = (theme) => {
126
126
 
127
127
  const SPACING_KEYS = ['0', '1', '2', '3', '4', '5', '6', '8', '10', '12', '16'];
128
128
 
129
+ // 1rem = 16px 가정. 0 은 unit 없이 emit (CSS 권장).
130
+ // trailing zero 제거 (0.50 → 0.5, 1.00 → 1).
131
+ const toRem = (px) => {
132
+ if (px === 0) return '0';
133
+ return `${parseFloat((px / 16).toFixed(4))}rem`;
134
+ };
135
+
129
136
  export const buildCssSpacingBlock = (spacing) =>
130
- SPACING_KEYS.map((k) => ` --space-${k}: ${spacing[k]}px;`).join('\n');
137
+ SPACING_KEYS.map((k) => ` --space-${k}: ${toRem(spacing[k])};`).join('\n');
131
138
 
139
+ // Flutter 는 logical pixel 사용. Dart 측은 PX 그대로 유지.
132
140
  export const buildDartSpacingBlock = (spacing) =>
133
141
  SPACING_KEYS.map((k) => ` s${k}: ${spacing[k].toFixed(1)},`).join('\n');
134
142
 
@@ -137,7 +145,7 @@ export const buildDartSpacingBlock = (spacing) =>
137
145
  const TYPOGRAPHY_KEYS = ['xs', 'sm', 'base', 'lg', 'xl', '2xl', '3xl', '4xl'];
138
146
 
139
147
  export const buildCssTypographyBlock = (typography) =>
140
- TYPOGRAPHY_KEYS.map((k) => ` --text-${k}: ${typography[k]}px;`).join('\n');
148
+ TYPOGRAPHY_KEYS.map((k) => ` --text-${k}: ${toRem(typography[k])};`).join('\n');
141
149
 
142
150
  // Dart 클래스는 'xl2'/'xl3'/'xl4' 명명 규칙 사용 (숫자 prefix 회피).
143
151
  const DART_TEXT_FIELD_NAMES = {
@@ -165,7 +173,7 @@ export const buildDartWeightsBlock = (weights) =>
165
173
  const CONTROL_KEYS = ['sm', 'md', 'lg'];
166
174
 
167
175
  export const buildCssControlsBlock = (controls) =>
168
- CONTROL_KEYS.map((k) => ` --control-${k}: ${controls[k]}px;`).join('\n');
176
+ CONTROL_KEYS.map((k) => ` --control-${k}: ${toRem(controls[k])};`).join('\n');
169
177
 
170
178
  export const buildDartControlsBlock = (controls) =>
171
179
  CONTROL_KEYS.map((k) => ` ${k}: ${controls[k].toFixed(1)},`).join('\n');
@@ -66,27 +66,27 @@
66
66
  --radius: 0.5rem;
67
67
  /* sh-ui:theme-radius-end */
68
68
  /* sh-ui:theme-space-start */
69
- --space-0: 0px;
70
- --space-1: 4px;
71
- --space-2: 8px;
72
- --space-3: 12px;
73
- --space-4: 16px;
74
- --space-5: 20px;
75
- --space-6: 24px;
76
- --space-8: 32px;
77
- --space-10: 40px;
78
- --space-12: 48px;
79
- --space-16: 64px;
69
+ --space-0: 0;
70
+ --space-1: 0.25rem;
71
+ --space-2: 0.5rem;
72
+ --space-3: 0.75rem;
73
+ --space-4: 1rem;
74
+ --space-5: 1.25rem;
75
+ --space-6: 1.5rem;
76
+ --space-8: 2rem;
77
+ --space-10: 2.5rem;
78
+ --space-12: 3rem;
79
+ --space-16: 4rem;
80
80
  /* sh-ui:theme-space-end */
81
81
  /* sh-ui:theme-text-start */
82
- --text-xs: 12px;
83
- --text-sm: 14px;
84
- --text-base: 16px;
85
- --text-lg: 18px;
86
- --text-xl: 20px;
87
- --text-2xl: 24px;
88
- --text-3xl: 30px;
89
- --text-4xl: 36px;
82
+ --text-xs: 0.75rem;
83
+ --text-sm: 0.875rem;
84
+ --text-base: 1rem;
85
+ --text-lg: 1.125rem;
86
+ --text-xl: 1.25rem;
87
+ --text-2xl: 1.5rem;
88
+ --text-3xl: 1.875rem;
89
+ --text-4xl: 2.25rem;
90
90
  /* sh-ui:theme-text-end */
91
91
  /* sh-ui:theme-weight-start */
92
92
  --weight-regular: 400;
@@ -110,9 +110,9 @@
110
110
  --ease-emphasized: cubic-bezier(0.2, 0, 0, 1);
111
111
  /* sh-ui:theme-ease-end */
112
112
  /* sh-ui:theme-control-start */
113
- --control-sm: 32px;
114
- --control-md: 40px;
115
- --control-lg: 48px;
113
+ --control-sm: 2rem;
114
+ --control-md: 2.5rem;
115
+ --control-lg: 3rem;
116
116
  /* sh-ui:theme-control-end */
117
117
  /* sh-ui:theme-border-width-start */
118
118
  --border-width: 1px;
@@ -137,3 +137,11 @@
137
137
  --bp-lg: 1024px;
138
138
  --bp-xl: 1280px;
139
139
  }
140
+
141
+ /* WCAG 2.5.5 — 터치 타겟 최소 44×44px. 마우스/스타일러스 대신 손가락 입력 시 control 높이를 보정. */
142
+ @media (hover: none) and (pointer: coarse) {
143
+ :root {
144
+ --control-sm: 2.75rem;
145
+ --control-md: 2.75rem;
146
+ }
147
+ }
@@ -66,27 +66,27 @@
66
66
  --radius: 0.5rem;
67
67
  /* sh-ui:theme-radius-end */
68
68
  /* sh-ui:theme-space-start */
69
- --space-0: 0px;
70
- --space-1: 4px;
71
- --space-2: 8px;
72
- --space-3: 12px;
73
- --space-4: 16px;
74
- --space-5: 20px;
75
- --space-6: 24px;
76
- --space-8: 32px;
77
- --space-10: 40px;
78
- --space-12: 48px;
79
- --space-16: 64px;
69
+ --space-0: 0;
70
+ --space-1: 0.25rem;
71
+ --space-2: 0.5rem;
72
+ --space-3: 0.75rem;
73
+ --space-4: 1rem;
74
+ --space-5: 1.25rem;
75
+ --space-6: 1.5rem;
76
+ --space-8: 2rem;
77
+ --space-10: 2.5rem;
78
+ --space-12: 3rem;
79
+ --space-16: 4rem;
80
80
  /* sh-ui:theme-space-end */
81
81
  /* sh-ui:theme-text-start */
82
- --text-xs: 12px;
83
- --text-sm: 14px;
84
- --text-base: 16px;
85
- --text-lg: 18px;
86
- --text-xl: 20px;
87
- --text-2xl: 24px;
88
- --text-3xl: 30px;
89
- --text-4xl: 36px;
82
+ --text-xs: 0.75rem;
83
+ --text-sm: 0.875rem;
84
+ --text-base: 1rem;
85
+ --text-lg: 1.125rem;
86
+ --text-xl: 1.25rem;
87
+ --text-2xl: 1.5rem;
88
+ --text-3xl: 1.875rem;
89
+ --text-4xl: 2.25rem;
90
90
  /* sh-ui:theme-text-end */
91
91
  /* sh-ui:theme-weight-start */
92
92
  --weight-regular: 400;
@@ -110,9 +110,9 @@
110
110
  --ease-emphasized: cubic-bezier(0.2, 0, 0, 1);
111
111
  /* sh-ui:theme-ease-end */
112
112
  /* sh-ui:theme-control-start */
113
- --control-sm: 32px;
114
- --control-md: 40px;
115
- --control-lg: 48px;
113
+ --control-sm: 2rem;
114
+ --control-md: 2.5rem;
115
+ --control-lg: 3rem;
116
116
  /* sh-ui:theme-control-end */
117
117
  /* sh-ui:theme-border-width-start */
118
118
  --border-width: 1px;
@@ -137,3 +137,11 @@
137
137
  --bp-lg: 1024px;
138
138
  --bp-xl: 1280px;
139
139
  }
140
+
141
+ /* WCAG 2.5.5 — 터치 타겟 최소 44×44px. 마우스/스타일러스 대신 손가락 입력 시 control 높이를 보정. */
142
+ @media (hover: none) and (pointer: coarse) {
143
+ :root {
144
+ --control-sm: 2.75rem;
145
+ --control-md: 2.75rem;
146
+ }
147
+ }
@@ -66,27 +66,27 @@
66
66
  --radius: 0.5rem;
67
67
  /* sh-ui:theme-radius-end */
68
68
  /* sh-ui:theme-space-start */
69
- --space-0: 0px;
70
- --space-1: 4px;
71
- --space-2: 8px;
72
- --space-3: 12px;
73
- --space-4: 16px;
74
- --space-5: 20px;
75
- --space-6: 24px;
76
- --space-8: 32px;
77
- --space-10: 40px;
78
- --space-12: 48px;
79
- --space-16: 64px;
69
+ --space-0: 0;
70
+ --space-1: 0.25rem;
71
+ --space-2: 0.5rem;
72
+ --space-3: 0.75rem;
73
+ --space-4: 1rem;
74
+ --space-5: 1.25rem;
75
+ --space-6: 1.5rem;
76
+ --space-8: 2rem;
77
+ --space-10: 2.5rem;
78
+ --space-12: 3rem;
79
+ --space-16: 4rem;
80
80
  /* sh-ui:theme-space-end */
81
81
  /* sh-ui:theme-text-start */
82
- --text-xs: 12px;
83
- --text-sm: 14px;
84
- --text-base: 16px;
85
- --text-lg: 18px;
86
- --text-xl: 20px;
87
- --text-2xl: 24px;
88
- --text-3xl: 30px;
89
- --text-4xl: 36px;
82
+ --text-xs: 0.75rem;
83
+ --text-sm: 0.875rem;
84
+ --text-base: 1rem;
85
+ --text-lg: 1.125rem;
86
+ --text-xl: 1.25rem;
87
+ --text-2xl: 1.5rem;
88
+ --text-3xl: 1.875rem;
89
+ --text-4xl: 2.25rem;
90
90
  /* sh-ui:theme-text-end */
91
91
  /* sh-ui:theme-weight-start */
92
92
  --weight-regular: 400;
@@ -110,9 +110,9 @@
110
110
  --ease-emphasized: cubic-bezier(0.2, 0, 0, 1);
111
111
  /* sh-ui:theme-ease-end */
112
112
  /* sh-ui:theme-control-start */
113
- --control-sm: 32px;
114
- --control-md: 40px;
115
- --control-lg: 48px;
113
+ --control-sm: 2rem;
114
+ --control-md: 2.5rem;
115
+ --control-lg: 3rem;
116
116
  /* sh-ui:theme-control-end */
117
117
  /* sh-ui:theme-border-width-start */
118
118
  --border-width: 1px;
@@ -137,3 +137,11 @@
137
137
  --bp-lg: 1024px;
138
138
  --bp-xl: 1280px;
139
139
  }
140
+
141
+ /* WCAG 2.5.5 — 터치 타겟 최소 44×44px. 마우스/스타일러스 대신 손가락 입력 시 control 높이를 보정. */
142
+ @media (hover: none) and (pointer: coarse) {
143
+ :root {
144
+ --control-sm: 2.75rem;
145
+ --control-md: 2.75rem;
146
+ }
147
+ }