@hjmds/design-contracts 1.4.0 → 1.5.0

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "designSystemVersion": "1.4.0",
3
+ "designSystemVersion": "1.5.0",
4
4
  "packageName": "@hjmds/design-contracts",
5
5
  "components": [
6
6
  {
@@ -9,10 +9,10 @@
9
9
  "name": "Text",
10
10
  "category": "foundation",
11
11
  "platform": "shared",
12
- "contractStatus": "beta",
12
+ "contractStatus": "stable",
13
13
  "surfaces": {
14
- "web": "beta",
15
- "native": "beta"
14
+ "web": "stable",
15
+ "native": "stable"
16
16
  },
17
17
  "requirements": [
18
18
  {
@@ -144,10 +144,10 @@
144
144
  "name": "Icon",
145
145
  "category": "foundation",
146
146
  "platform": "shared",
147
- "contractStatus": "beta",
147
+ "contractStatus": "stable",
148
148
  "surfaces": {
149
- "web": "beta",
150
- "native": "beta"
149
+ "web": "stable",
150
+ "native": "stable"
151
151
  },
152
152
  "requirements": [
153
153
  {
@@ -161,7 +161,6 @@
161
161
  "scenarios": [
162
162
  "default",
163
163
  "dark",
164
- "long-copy",
165
164
  "large-text",
166
165
  "rtl",
167
166
  "reduced-motion",
@@ -173,7 +172,6 @@
173
172
  "scenarios": [
174
173
  "default",
175
174
  "dark",
176
- "long-copy",
177
175
  "large-text",
178
176
  "rtl",
179
177
  "reduced-motion",
@@ -257,7 +255,6 @@
257
255
  "scenarios": [
258
256
  "default",
259
257
  "dark",
260
- "long-copy",
261
258
  "large-text",
262
259
  "rtl",
263
260
  "reduced-motion",
@@ -269,7 +266,6 @@
269
266
  "scenarios": [
270
267
  "default",
271
268
  "dark",
272
- "long-copy",
273
269
  "large-text",
274
270
  "rtl",
275
271
  "reduced-motion",
@@ -336,10 +332,10 @@
336
332
  "name": "Stack",
337
333
  "category": "layout",
338
334
  "platform": "shared",
339
- "contractStatus": "beta",
335
+ "contractStatus": "stable",
340
336
  "surfaces": {
341
- "web": "beta",
342
- "native": "beta"
337
+ "web": "stable",
338
+ "native": "stable"
343
339
  },
344
340
  "requirements": [
345
341
  {
@@ -384,10 +380,10 @@
384
380
  "name": "Container",
385
381
  "category": "layout",
386
382
  "platform": "shared",
387
- "contractStatus": "beta",
383
+ "contractStatus": "stable",
388
384
  "surfaces": {
389
- "web": "beta",
390
- "native": "beta"
385
+ "web": "stable",
386
+ "native": "stable"
391
387
  },
392
388
  "requirements": [
393
389
  {
@@ -743,10 +739,10 @@
743
739
  "name": "IconButton",
744
740
  "category": "action",
745
741
  "platform": "shared",
746
- "contractStatus": "beta",
742
+ "contractStatus": "stable",
747
743
  "surfaces": {
748
- "web": "beta",
749
- "native": "beta"
744
+ "web": "stable",
745
+ "native": "stable"
750
746
  },
751
747
  "requirements": [
752
748
  {
@@ -760,7 +756,6 @@
760
756
  "scenarios": [
761
757
  "default",
762
758
  "dark",
763
- "long-copy",
764
759
  "large-text",
765
760
  "rtl",
766
761
  "reduced-motion",
@@ -772,7 +767,6 @@
772
767
  "scenarios": [
773
768
  "default",
774
769
  "dark",
775
- "long-copy",
776
770
  "large-text",
777
771
  "rtl",
778
772
  "reduced-motion",
@@ -3137,10 +3131,10 @@
3137
3131
  "name": "Badge",
3138
3132
  "category": "data-display",
3139
3133
  "platform": "shared",
3140
- "contractStatus": "beta",
3134
+ "contractStatus": "stable",
3141
3135
  "surfaces": {
3142
- "web": "beta",
3143
- "native": "beta"
3136
+ "web": "stable",
3137
+ "native": "stable"
3144
3138
  },
3145
3139
  "requirements": [
3146
3140
  {
@@ -3233,10 +3227,10 @@
3233
3227
  "name": "Card",
3234
3228
  "category": "data-display",
3235
3229
  "platform": "shared",
3236
- "contractStatus": "beta",
3230
+ "contractStatus": "stable",
3237
3231
  "surfaces": {
3238
- "web": "beta",
3239
- "native": "beta"
3232
+ "web": "stable",
3233
+ "native": "stable"
3240
3234
  },
3241
3235
  "requirements": [
3242
3236
  {
@@ -3895,10 +3889,10 @@
3895
3889
  "name": "Tag",
3896
3890
  "category": "data-display",
3897
3891
  "platform": "shared",
3898
- "contractStatus": "beta",
3892
+ "contractStatus": "stable",
3899
3893
  "surfaces": {
3900
- "web": "beta",
3901
- "native": "beta"
3894
+ "web": "stable",
3895
+ "native": "stable"
3902
3896
  },
3903
3897
  "requirements": [
3904
3898
  {
@@ -4028,10 +4022,10 @@
4028
4022
  "name": "Notice",
4029
4023
  "category": "feedback",
4030
4024
  "platform": "shared",
4031
- "contractStatus": "beta",
4025
+ "contractStatus": "stable",
4032
4026
  "surfaces": {
4033
- "web": "beta",
4034
- "native": "beta"
4027
+ "web": "stable",
4028
+ "native": "stable"
4035
4029
  },
4036
4030
  "requirements": [
4037
4031
  {
@@ -4076,10 +4070,10 @@
4076
4070
  "name": "Progress",
4077
4071
  "category": "feedback",
4078
4072
  "platform": "shared",
4079
- "contractStatus": "beta",
4073
+ "contractStatus": "stable",
4080
4074
  "surfaces": {
4081
- "web": "beta",
4082
- "native": "beta"
4075
+ "web": "stable",
4076
+ "native": "stable"
4083
4077
  },
4084
4078
  "requirements": [
4085
4079
  {
@@ -4124,10 +4118,10 @@
4124
4118
  "name": "Spinner",
4125
4119
  "category": "feedback",
4126
4120
  "platform": "shared",
4127
- "contractStatus": "beta",
4121
+ "contractStatus": "stable",
4128
4122
  "surfaces": {
4129
- "web": "beta",
4130
- "native": "beta"
4123
+ "web": "stable",
4124
+ "native": "stable"
4131
4125
  },
4132
4126
  "requirements": [
4133
4127
  {
@@ -4141,7 +4135,6 @@
4141
4135
  "scenarios": [
4142
4136
  "default",
4143
4137
  "dark",
4144
- "long-copy",
4145
4138
  "large-text",
4146
4139
  "rtl",
4147
4140
  "reduced-motion",
@@ -4153,7 +4146,6 @@
4153
4146
  "scenarios": [
4154
4147
  "default",
4155
4148
  "dark",
4156
- "long-copy",
4157
4149
  "large-text",
4158
4150
  "rtl",
4159
4151
  "reduced-motion",
@@ -4172,10 +4164,10 @@
4172
4164
  "name": "Skeleton",
4173
4165
  "category": "feedback",
4174
4166
  "platform": "shared",
4175
- "contractStatus": "beta",
4167
+ "contractStatus": "stable",
4176
4168
  "surfaces": {
4177
- "web": "beta",
4178
- "native": "beta"
4169
+ "web": "stable",
4170
+ "native": "stable"
4179
4171
  },
4180
4172
  "requirements": [
4181
4173
  {
@@ -4189,7 +4181,6 @@
4189
4181
  "scenarios": [
4190
4182
  "default",
4191
4183
  "dark",
4192
- "long-copy",
4193
4184
  "large-text",
4194
4185
  "rtl",
4195
4186
  "reduced-motion",
@@ -4201,7 +4192,6 @@
4201
4192
  "scenarios": [
4202
4193
  "default",
4203
4194
  "dark",
4204
- "long-copy",
4205
4195
  "large-text",
4206
4196
  "rtl",
4207
4197
  "reduced-motion",
@@ -4796,10 +4786,10 @@
4796
4786
  "name": "DesignSystemProvider",
4797
4787
  "category": "provider",
4798
4788
  "platform": "shared",
4799
- "contractStatus": "beta",
4789
+ "contractStatus": "stable",
4800
4790
  "surfaces": {
4801
- "web": "beta",
4802
- "native": "beta"
4791
+ "web": "stable",
4792
+ "native": "stable"
4803
4793
  },
4804
4794
  "requirements": [
4805
4795
  {
@@ -1,4 +1,21 @@
1
- # Stable Core 0.8
1
+ # Stable Core
2
+
3
+ ## 1.5.0 승격
4
+
5
+ 다음 13개를 stable로 올렸습니다: `Text`, `Icon`, `Stack`, `Container`, `DesignSystemProvider`,
6
+ `IconButton`, `Badge`, `Card`, `Tag`, `Notice`, `Progress`, `Spinner`, `Skeleton`.
7
+
8
+ - 근거: 1.5.0에서 renderer 시나리오 증거를 이름뿐인 검사에서 실제 검사로 바꿨습니다
9
+ (Web `test/scenario-matrix.browser.test.tsx`, Native `test/scenario-matrix.test.tsx`). 두 renderer에서
10
+ 요구 시나리오가 모두 통과하고 세 제품 이상이 쓰는 컴포넌트만 올렸습니다.
11
+ - 이 전환으로 Web의 "모든 시나리오 증거 완비"는 33개에서 16개로 줄었다가 승격 대상 보강 뒤
12
+ 24개가 됐습니다. 줄어든 것은 이전 수치가 과대 표시였기 때문입니다.
13
+ - 아이콘·로딩 표시·구분선처럼 보이는 글자 슬롯이 없는 컴포넌트는 long-copy 요구에서 뺐습니다
14
+ (`showcase.ts`의 `textlessComponentNames`).
15
+ - keyboard·platform-parity를 요구하는 컴포넌트(Checkbox, Switch, Dialog, Sheet 등)는 그 증거가
16
+ 아직 없어 beta로 남습니다. 생성된 evidence 문서의 "Missing required scenarios"가 남은 일입니다.
17
+
18
+ ## 0.8 첫 stable slice
2
19
 
3
20
  첫 renderer stable slice는 `Surface`, `Button`, `Field`, `TextArea`다. 이 네 컴포넌트는
4
21
  계약이 이미 stable이고 Web/RN renderer가 같은 public intent를 실행한다.
package/docs/tag.md CHANGED
@@ -47,11 +47,10 @@ recipe가 안정화되면 그쪽에 위임하고, 이 계약에는 넣지 않습
47
47
 
48
48
  ## 현재 maturity와 남은 증거
49
49
 
50
- Tag contract와 Web/Native surface는 모두 **beta**입니다. 두 first-party renderer의 canonical
51
- default render proof가 `tagRecipe`의 정적 text/background anatomy를 실행하고, generated
52
- manifest와 evidence registry가 이 상태를 함께 검증합니다.
53
-
54
- beta는 전체 환경 인증을 뜻하지 않습니다. dark, RTL, 200% text/Dynamic Type, screen reader와
55
- 실제 device screenshot은 아직 scenario별 실행 proof가 없으며 generated evidence의 debt로
56
- 남습니다. 야잘알의 기존 `AppBadge` 사용처를 canonical Tag renderer로 마이그레이션하고 이
57
- 환경 증거까지 연결한 뒤 stable 승격을 검토합니다.
50
+ Tag contract와 Web/Native surface는 모두 **stable**입니다(1.5.0). 두 first-party renderer의
51
+ scenario matrix가 dark(라이트 전용 색 누수 없음), 2배 글자, RTL, 모션 줄이기, 접근 이름,
52
+ 긴 문구 줄바꿈을 실제 계산 스타일로 검사하고 모두 통과했습니다. Web은 실제 브라우저에서
53
+ 배치까지, Native는 test renderer의 style 값까지 봅니다.
54
+
55
+ stable은 전체 환경 인증을 뜻하지 않습니다. screen reader 실사용과 실제 device screenshot은
56
+ 제품 기기 QA의 범위입니다.
@@ -50,3 +50,15 @@ contracts·tree-select까지 연쇄로 초과).
50
50
  형태로 보이는지(3:1). 기존 AA 검사가 못 잡던 축이다.
51
51
  - `keeps neutral surfaces and text in one low-saturation family per theme` — 중성 역할의
52
52
  채도 상한 30%. 강조 역할(`primary`·`contentBrand`·`danger`·`surfaceAccent`)은 제외한다.
53
+
54
+ ## 라이트 `textSub`를 AA로 올렸다 (2026-09-26)
55
+
56
+ 브랜드 팔레트 대비 검사([brand-boundary.md](./brand-boundary.md))를 만들면서 기본 팔레트에 같은 기준을 돌렸더니
57
+ 라이트 `textSub` `#6b7684`가 `surface`(`#f2f4f6`) 위에서 **4.19:1**로 본문 AA(4.5)에 못 미쳤다. `textSub`는
58
+ `Text tone="subtle"`의 실제 글자색이라 장식 등급으로 볼 수 없다. 같은 색상·채도에서 명도만 내린 `#65707d`
59
+ (`surface` 4.57:1, `bg` 5.04:1)로 바꿨다. 텍스트 램프 순서(`textMuted` > `textSub` > `textWeak`)는 유지된다.
60
+ 모펀은 같은 이유로 이미 `textSub`를 `#626E7D`로 보정해 쓰고 있었다.
61
+
62
+ `borderControl`은 `#6b7684`를 그대로 둔다. 비텍스트 기준(3:1)이고 `surface` 위 4.19:1로 충분하다.
63
+
64
+ **버린 대안:** 검사에서 `textSub`를 본문 기준에서 빼기. 실제 글자에 쓰이는 색을 기준에서 빼면 검사가 존재 이유를 잃는다.
package/docs/theming.md CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  HJM은 `theme`(light/dark/system) 같은 **환경**과, 그 환경이 해석된 **값**을 분리해서
4
4
  받는다. 제품 브랜드색은 값 쪽에 넣는다. 이 문서는 새 제품이 처음 부딪히는 그 경로만
5
- 설명한다. 팔레트를 어떻게 고를지는 [theme-palette.md](./theme-palette.md), 색의 의미
6
- 구분은 [identity.md](./identity.md)에 있다.
5
+ 설명한다. **무엇을 어디까지 바꿀 수 있는지와 대비 검사 규칙은 [brand-boundary.md](./brand-boundary.md)가
6
+ 단일 원본이다.** 팔레트를 어떻게 고를지는 [theme-palette.md](./theme-palette.md), 색의 의미 구분은
7
+ [identity.md](./identity.md)에 있다.
7
8
 
8
9
  ## 두 가지 사용 방식
9
10
 
@@ -52,29 +53,23 @@ function ProductProvider({ preference, systemDark, children }) {
52
53
  }
53
54
  ```
54
55
 
55
- React Native는 `HjmNativeProvider`가 같은 `value`를 받는다. 실제 사용 예는 BurnTok의
56
- `apps/web/src/components/ThemeProvider.tsx`다 — 경계선 색 두 개만 주입하고 나머지는
57
- 기본값을 쓴다.
56
+ React Native는 `HjmNativeProvider`가 같은 `value`를 받는다. `value`를 넘기면 Provider가 OS 설정 관찰을
57
+ 멈추므로, 위 예시처럼 system theme 등의 신호를 제품이 구독해 resolver에 넣는다. 실제 사용 예는 BurnTok의
58
+ `apps/web/src/components/ThemeProvider.tsx`(경계선 두 key)와 `apps/mobile/src/components/ThemeProvider.tsx`
59
+ (경계선 두 key + 표면 두 key)다.
60
+
61
+ 팔레트를 바꾸면 제품 테스트에서 대비 검사를 돌린다.
62
+
63
+ ```ts
64
+ import { checkBrandPaletteContrast } from "@hjmds/design-contracts/palette-contrast";
65
+
66
+ expect(checkBrandPaletteContrast(PRODUCT_BRAND_PALETTE)).toEqual({ light: [], dark: [] });
67
+ ```
58
68
 
59
69
  ## 덮어도 되는 것과 아닌 것
60
70
 
61
- | key | 덮기 | 이유 |
62
- | --- | --- | --- |
63
- | `primary` / `onPrimary` / `contentBrand` | 권장 | 브랜드의 자리다. 주 행동과 현재 위치를 이 색이 말한다 |
64
- | `borderControl` / `focus` | 선택 | 브랜드 채도가 높으면 포커스 대비를 맞추기 위해 함께 조정한다 |
65
- | `border` / `borderControl` | 선택 | 제품 경계선 밀도가 다를 때. BurnTok이 이 둘만 주입한다 |
66
- | `bg` / `surface` / `text*` 중성 계열 | 신중히 | 대비 검증이 붙어 있다. 바꾸면 라이트·다크 양쪽에서 4.5:1을 다시 확인한다 |
67
- | `danger` / `success` / `warning` / `info` | 비권장 | 상태색을 브랜드색으로 바꾸면 "오류"와 "브랜드"가 같은 색이 된다 |
68
- | 컴포넌트별 색 | 불가 | recipe가 semantic key만 읽는다. 컴포넌트 하나만 다른 색이 되면 그것은 제품 예외지 테마가 아니다 |
69
-
70
- ## 하지 말아야 할 세 가지
71
-
72
- 1. **CSS로 `.hjm-*` 클래스를 덮거나 `--hjm-color-*`를 인라인 style로 재정의하지 않는다.** 그 순간 업그레이드마다 깨진다. 필요한 것이
73
- semantic key로 표현되지 않으면 그것은 계약 공백이고, 우회가 아니라 이슈로 올린다.
74
- 2. **recipe 값을 읽어 인라인 스타일로 다시 싣지 않는다.** 렌더러가 이미 그 값을
75
- 칠한다. 제품이 다시 실으면 두 벌이 생기고 한쪽만 갱신된다.
76
- 3. **제공자 브랜드색을 팔레트에 넣지 않는다.** 소셜 로그인 색은 테마가 아니라 남의
77
- 자산이다 — [AuthProviderButton](./provider-button.md)이 그 자리를 갖는다.
71
+ 규칙은 [brand-boundary.md](./brand-boundary.md)에 있다. 요약하면 `brandPalette`의 17개 semantic key만 바꿀 수 있고,
72
+ 상태 강조색과 컴포넌트별 색은 바꿀 수 없으며, `.hjm-*`·`--hjm-*` CSS 재정의는 지원하는 경로가 아니다.
78
73
 
79
74
  ## 어댑터를 두는 이유
80
75
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hjmds/design-contracts",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Renderer-neutral design contracts, tokens, recipes, and behaviors shared by HJM products.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,6 +46,12 @@
46
46
  "import": "./dist/color-references.js",
47
47
  "default": "./dist/color-references.js"
48
48
  },
49
+ "./palette-contrast": {
50
+ "types": "./dist/palette-contrast.d.ts",
51
+ "react-native": "./dist/palette-contrast.js",
52
+ "import": "./dist/palette-contrast.js",
53
+ "default": "./dist/palette-contrast.js"
54
+ },
49
55
  "./responsive": {
50
56
  "types": "./dist/responsive.d.ts",
51
57
  "react-native": "./dist/responsive.js",