@hjmds/design-contracts 1.8.0 → 1.10.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.
Files changed (60) hide show
  1. package/dist/affix.d.ts +8 -0
  2. package/dist/affix.d.ts.map +1 -0
  3. package/dist/affix.js +7 -0
  4. package/dist/affix.js.map +1 -0
  5. package/dist/catalog.d.ts +66 -94
  6. package/dist/catalog.d.ts.map +1 -1
  7. package/dist/catalog.js +11 -11
  8. package/dist/catalog.js.map +1 -1
  9. package/dist/color-picker.d.ts +11 -0
  10. package/dist/color-picker.d.ts.map +1 -0
  11. package/dist/color-picker.js +20 -0
  12. package/dist/color-picker.js.map +1 -0
  13. package/dist/component-definitions.d.ts +0 -5
  14. package/dist/component-definitions.d.ts.map +1 -1
  15. package/dist/component-definitions.js +0 -5
  16. package/dist/component-definitions.js.map +1 -1
  17. package/dist/component-recipes.d.ts +38 -4
  18. package/dist/component-recipes.d.ts.map +1 -1
  19. package/dist/component-recipes.js +21 -5
  20. package/dist/component-recipes.js.map +1 -1
  21. package/dist/component-references.d.ts +10 -10
  22. package/dist/component-references.js +5 -5
  23. package/dist/component-references.js.map +1 -1
  24. package/dist/interaction-adapters.d.ts +53 -0
  25. package/dist/interaction-adapters.d.ts.map +1 -0
  26. package/dist/interaction-adapters.js +45 -0
  27. package/dist/interaction-adapters.js.map +1 -0
  28. package/dist/showcase.d.ts.map +1 -1
  29. package/dist/showcase.js +3 -0
  30. package/dist/showcase.js.map +1 -1
  31. package/dist/toast-liquid.d.ts +1 -1
  32. package/dist/toast-liquid.d.ts.map +1 -1
  33. package/dist/toast-liquid.js +4 -1
  34. package/dist/toast-liquid.js.map +1 -1
  35. package/dist/version.d.ts +1 -1
  36. package/dist/version.d.ts.map +1 -1
  37. package/dist/version.js +1 -1
  38. package/dist/version.js.map +1 -1
  39. package/dist/watermark.d.ts +15 -0
  40. package/dist/watermark.d.ts.map +1 -0
  41. package/dist/watermark.js +11 -0
  42. package/dist/watermark.js.map +1 -0
  43. package/docs/affix.md +27 -63
  44. package/docs/ant-design-coverage.md +8 -12
  45. package/docs/cascader.md +5 -0
  46. package/docs/catalog-decision-status.md +5 -0
  47. package/docs/color-picker.md +30 -72
  48. package/docs/confirm-popover.md +5 -0
  49. package/docs/generated/component-maturity.md +7 -12
  50. package/docs/generated/renderer-evidence.json +165 -6
  51. package/docs/generated/renderer-evidence.md +5 -2
  52. package/docs/generated/showcase-manifest.json +44 -127
  53. package/docs/optional-adapters.md +20 -0
  54. package/docs/rating.md +5 -0
  55. package/docs/stable-core.md +21 -1
  56. package/docs/time-picker.md +5 -0
  57. package/docs/toast.md +20 -2
  58. package/docs/tree-select.md +5 -0
  59. package/docs/watermark.md +26 -58
  60. package/package.json +25 -1
@@ -0,0 +1,15 @@
1
+ export declare const watermarkRecipe: {
2
+ readonly tileWidth: 240;
3
+ readonly tileHeight: 160;
4
+ readonly rotate: -22;
5
+ readonly opacity: 0.12;
6
+ readonly foreground: "textSub";
7
+ };
8
+ export declare function resolveWatermark(text: string | readonly string[], width?: number, height?: number, rotate?: number, opacity?: number): {
9
+ lines: string[];
10
+ width: number;
11
+ height: number;
12
+ rotate: number;
13
+ opacity: number;
14
+ };
15
+ //# sourceMappingURL=watermark.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watermark.d.ts","sourceRoot":"","sources":["../src/watermark.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe;;;;;;CAAkG,CAAC;AAC/H,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAAE,KAAK,GAAgC,MAAM,EAAE,MAAM,GAAiC,MAAM,EAAE,MAAM,GAA6B,MAAM,EAAE,OAAO,GAA8B,MAAM;;;;;;EAMpP"}
@@ -0,0 +1,11 @@
1
+ export const watermarkRecipe = { tileWidth: 240, tileHeight: 160, rotate: -22, opacity: 0.12, foreground: "textSub" };
2
+ export function resolveWatermark(text, width = watermarkRecipe.tileWidth, height = watermarkRecipe.tileHeight, rotate = watermarkRecipe.rotate, opacity = watermarkRecipe.opacity) {
3
+ const lines = typeof text === "string" ? [text] : [...text];
4
+ // Bounded tiles prevent accidental invisible/huge overlays; this is decorative labeling, not tamper protection.
5
+ if (!lines.length || lines.length > 3 || lines.some(line => !line.trim() || line.length > 120))
6
+ throw new TypeError("Watermark needs one to three nonempty lines of at most 120 characters");
7
+ if (![width, height, rotate, opacity].every(Number.isFinite) || width < 80 || height < 60 || Math.abs(rotate) > 90 || opacity < 0 || opacity > 1)
8
+ throw new TypeError("Invalid Watermark geometry");
9
+ return { lines, width, height, rotate, opacity };
10
+ }
11
+ //# sourceMappingURL=watermark.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watermark.js","sourceRoot":"","sources":["../src/watermark.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAW,CAAC;AAC/H,MAAM,UAAU,gBAAgB,CAAC,IAAgC,EAAE,QAAQ,eAAe,CAAC,SAAmB,EAAE,SAAS,eAAe,CAAC,UAAoB,EAAE,SAAS,eAAe,CAAC,MAAgB,EAAE,UAAU,eAAe,CAAC,OAAiB;IACnP,MAAM,KAAK,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC5D,gHAAgH;IAChH,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,uEAAuE,CAAC,CAAC;IAC7L,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,KAAK,GAAG,EAAE,IAAI,MAAM,GAAG,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,OAAO,GAAG,CAAC,IAAI,OAAO,GAAG,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,4BAA4B,CAAC,CAAC;IACpM,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;AACnD,CAAC","sourcesContent":["export const watermarkRecipe = { tileWidth: 240, tileHeight: 160, rotate: -22, opacity: 0.12, foreground: \"textSub\" } as const;\nexport function resolveWatermark(text: string | readonly string[], width = watermarkRecipe.tileWidth as number, height = watermarkRecipe.tileHeight as number, rotate = watermarkRecipe.rotate as number, opacity = watermarkRecipe.opacity as number) {\n const lines = typeof text === \"string\" ? [text] : [...text];\n // Bounded tiles prevent accidental invisible/huge overlays; this is decorative labeling, not tamper protection.\n if (!lines.length || lines.length > 3 || lines.some(line => !line.trim() || line.length > 120)) throw new TypeError(\"Watermark needs one to three nonempty lines of at most 120 characters\");\n if (![width, height, rotate, opacity].every(Number.isFinite) || width < 80 || height < 60 || Math.abs(rotate) > 90 || opacity < 0 || opacity > 1) throw new TypeError(\"Invalid Watermark geometry\");\n return { lines, width, height, rotate, opacity };\n}\n"]}
package/docs/affix.md CHANGED
@@ -1,63 +1,27 @@
1
- # Affix — 계약을 만들지 않는다
2
-
3
- ## 문제로 제기된 것
4
-
5
- Ant Design `Affix`는 스크롤이 특정 지점을 지나면 임의 요소를 화면에 고정합니다. 검토
6
- 관찰은 두 가지였습니다 — `TopBar`가 이미 있고(`src/layout.ts:9`, RN `native`, beta), Web
7
- CSS `position: sticky`가 대부분을 푼다면, 이것이 컴포넌트인지 아니면 레이아웃 기법인지
8
- 확인하라는 것과, `docs/expansion-roadmap.md`의 `Stack` 판정("반복되는 내부 flex를 감싸는
9
- 것만으로 제품 의미나 접근성 계약이 생기지 않는다")이 그대로 적용될 가능성이 높다는
10
- 것이었습니다.
11
-
12
- ## 판정: 만들지 않는다
13
-
14
- ### 1. 관찰된 모든 실사용은 이미 다른 컴포넌트나 CSS 한 줄이 소유한다
15
-
16
- - **BurnTok** 웹 헤더(`apps/web/src/app/page.tsx:118`, `messages/page.tsx:27`,
17
- `u/[id]/ProfileClient.tsx:80`, `c/[id]/StoryClient.tsx:287`,
18
- `notifications/page.tsx:44`)는 모두 `<header className="sticky top-0 ...">`로 상단바를
19
- 고정합니다. 이는 정확히 `TopBar`의 자리이고, `position: sticky` 한 줄로 끝나는
20
- 문제입니다 — 스크롤 진행률 계산, 고정 해제 조건 분기, 별도 controlled 상태 없이 CSS가
21
- 전부 해결합니다.
22
- - **Yajalal RN**에도 이미 `TopBar` 구현(`modules/app-rn/src/components/ui/AppTopBar.tsx`)이
23
- 있습니다. Native는애초에 헤더가 화면 최상단에 고정 마운트되는 것이 기본값이라
24
- "스크롤 임계값을 지나면 고정"이라는 Affix 특유의 조건 자체가 성립하지 않습니다.
25
- - 그 외 "고정" 관련 코드는 각 컴포넌트가 이미 소유한 문제입니다: `StatTable`의 첫 열
26
- 고정(`stickyFirstColumn`, `stat-table-contract.ts:23`)은 표 내부 anatomy이고,
27
- `BottomCTA`의 하단 고정 영역(`AppBottomCTA.tsx:39` "Safe-area-aware sticky action
28
- area")은 이미 별도 컴포넌트로 계약돼 있습니다. 둘 다 "임의 요소를 고정하는 범용
29
- wrapper"를 필요로 하지 않습니다.
30
-
31
- ### 2. 임의 콘텐츠를 고정하는 범용 요구 자체가 관측되지 않는다
32
-
33
- antd `Affix`의 핵심 사용법 — 헤더도 테이블도 아닌 **임의 콘텐츠**(플로팅 목차, 사이드
34
- 액션 카드 등)를 스크롤 중간에 고정 — 는 두 제품 어디에도 없습니다. 있는 모든 "고정"
35
- 사례는 위처럼 헤더(TopBar) 아니면 특정 컴포넌트 내부 anatomy(StatTable, BottomCTA)로
36
- 이미 이름이 있습니다.
37
-
38
- ### 3. 계약할 상태 축이 없다
39
-
40
- Affix가 컴포넌트가 되려면 "Web과 Native가 각자 다른 기법으로 같은 결과를 낸다"는 공유
41
- semantic이 있어야 합니다(`docs/architecture.md`의 `adaptive` 정의). 그런데 Web은 CSS
42
- `position: sticky`(레이아웃 엔진이 처리, JS 상태 없음), Native는 애초에 스크롤에 따라
43
- 고정/해제되는 개념 자체가 드뭅니다(헤더는 기본 고정, 그 외에는 `stickyHeaderIndices`처럼
44
- 리스트 컴포넌트가 소유). 이는 `docs/virtual-list.md`가 항목 높이 추정을 판정한 것과 같은
45
- 모양입니다 — 이름도 단위도 의미도 대응하지 않는 렌더러 힌트일 뿐, 공유할 사용자 의미가
46
- 없습니다.
47
-
48
- `Stack`의 판정과 같은 자리입니다: 반복되는 CSS 속성 하나를 감싸는 것만으로 제품 의미나
49
- 접근성 계약이 생기지 않습니다.
50
-
51
- ## 만들지 않은 것
52
-
53
- `src/affix.ts`, `test/affix.test.ts`는 없습니다. `componentCatalog`의
54
- `{ name: "Affix", category: "utility", platform: "web", status: "planned" }` 행과
55
- crosswalk의 `Affix → Affix` direct 관계(`src/component-references.ts:125`)는 건드리지
56
- 않습니다.
57
-
58
- ## 뒤집힐 조건
59
-
60
- 1. 헤더도 테이블도 CTA도 아닌 **임의 콘텐츠**를 스크롤 중간 지점부터 고정해야 하는 실제
61
- 화면(예: 긴 폼 옆의 플로팅 요약 카드)이 두 제품 중 하나에 나타난다.
62
- 2. 그 화면에서 CSS `position: sticky`만으로 부족한 요구(예: 여러 sticky 요소의 겹침
63
- 순서 조정, 고정 해제 시점의 접근성 발표)가 측정된다.
1
+ # Affix — Web 상단 고정
2
+
3
+ 2026-09-30: 사용자의 구현 요청으로 기존 수요 대기 결정을 대체한다. `position: fixed`로 복제/이동하면
4
+ 초점과 폼 상태를 잃거나 폭을 다시 측정해야 하므로 CSS sticky로 원래 DOM과 문서 흐름을 유지한다.
5
+ JS는 고정 상태 관측과 화면보다 큰 콘텐츠의 해제에만 사용한다.
6
+
7
+ `@hjmds/react/affix`의 `Affix`는 `children`, `offset`(기본 0, 유한한 비음수 CSS px),
8
+ `disabled`, `onChange(affixed)`를 받는다. 가장 가까운 스크롤 조상의 상단에서 offset만큼 떨어져
9
+ 고정된다. 부모의 끝에서 해제된다. 부모에 스크롤할 공간이 있어야 하며, 부모의 overflow 설정은
10
+ CSS sticky의 기준을 바꾼다. 부모 자체가 콘텐츠와 같은 높이면 고정 구간이 없다.
11
+
12
+ - `onChange`는 최초 상태와 이후 상태 전환에 호출한다. 렌더링마다 호출하지 않는다.
13
+ - 스크롤·리사이즈는 animation frame으로 모아 측정하고 unmount 시 observer/listener/frame을 해제한다.
14
+ - 콘텐츠가 스크롤 영역 높이에서 offset을 뺀 값보다 크면 일반 흐름으로 돌려 가려진 영역이 없게 한다.
15
+ - disabled/offset 변경에도 자식을 재마운트하지 않아 입력값·초점이 유지된다.
16
+ - 상단 고정만 지원하며 bottom 고정·portal·다중 sticky 영역 자동 충돌 조정은 제공하지 않는다.
17
+ - 별도 role이나 live announcement를 강제하지 않는다. 자식 의미·접근성 이름은 제품이 소유한다.
18
+
19
+ ```tsx
20
+ <Affix offset={16} onChange={setPinned}>
21
+ <Button onClick={save}>{t('form.save')}</Button>
22
+ </Affix>
23
+ ```
24
+
25
+ 계약: [affix.ts](../src/affix.ts). UI: [renderer](../../react/src/affix.tsx).
26
+ 사용 예제: [Web additions](../../../showcase/web/src/patterns/WebAdditions.stories.tsx).
27
+ 검증: [브라우저 스크롤·초점 회귀](../../react/test/web-additions.browser.test.tsx).
@@ -67,11 +67,9 @@ catalog 행 처리도 여기서 갈립니다. **흡수됨**은 행을 지우고
67
67
  남깁니다 — 남겨 두면 "아직 만들 계획"으로 잘못 읽힙니다. **검증할 화면이 없음**은 행을
68
68
  그대로 둡니다. 계약은 유효하고 언젠가 채워질 자리이기 때문입니다. **거절됨**은 행을 지울 수 없습니다 — crosswalk의 `targets`가 가리키고 있고 흡수할 다른 이름이 없기 때문입니다. 대신 `ComponentCatalogEntry.declinedReason`에 사유를 적습니다.
69
69
 
70
- **흡수됨은 다시 두 갈래입니다.** 흡수한 대상이 **정확히 하나**면(`Notification`→Toast,
71
- `Dropdown`→Menu, `Flex`·`Space`→Stack) 행을 지우고 그 하나에 `aliases`로 이름을 남깁니다.
72
- 대상이 **둘 이상의 조합**이면(`TimePicker`→Select 둘, `Rating`→Slider/Statistic,
73
- `ConfirmPopover`→Popover/AlertDialog) alias를 걸 단일 이름이 없으므로 **행을 그대로 둡니다.**
74
- 이 저장소는 이 답을 세 번 반복해서 냈고, 우연이 아니라 규칙입니다.
70
+ 2026-09-30 사용자의 조합·중복 제외 요청에 따라, 구현된 조합도 독립 catalog 행에서 제거합니다.
71
+ 단일 대상 alias나 여러 primitive를 가리키는 crosswalk로 범위를 추적하고 작동 예제는 유지합니다.
72
+ 조합의 이름을 Planned로 예약하는 대안은 이미 해결된 기능을 미구현 수에 포함하므로 폐기합니다.
75
73
 
76
74
  **흡수 대기**는 행과 crosswalk를 **둘 다 건드리지 않습니다.** 흡수를 지금 적용하면 아직 만들지
77
75
  않은 해결책을 완료로 표시하게 됩니다 — `planned`이 거짓말하던 문제와 방향만 반대인 같은 문제입니다.
@@ -88,18 +86,16 @@ evidence registry로 판단합니다.
88
86
  - partial maturity: decomposed target 중 일부만 stable 또는 beta
89
87
  - planned only: 모든 target이 planned
90
88
 
91
- 2026-09-18 SidePanel·Splitter·Tour·Tree·TransferList·Mentions·CommandPalette·DataTable renderer까지 추가한 뒤 status 기반 분포는
92
- **fully mature 62 / partial maturity 0 /
93
- planned only 8**입니다. decomposed Drawer의 두 갈래(Sheet·SidePanel)가 모두 구현되면서
94
- partial maturity가 비었습니다 — 0은 "부분 구현이 없다"는 뜻이고 planned only 20은 그대로입니다. 따라서 73/73 tracking은 73개 구현 완료를 의미하지 않습니다.
95
- 홈과 Component Explorer는 이 수치를 분리해 표시합니다. 이 숫자는 source inventory 수가
96
- 아니라 HJM target의 maturity에서 계산하므로 catalog status가 바뀌면 함께 갱신합니다.
89
+ 2026-09-30 조합 항목을 실제 primitive로 연결한 status 기반 분포는
90
+ **fully mature 70 / partial maturity 0 /
91
+ planned only 0**입니다. 이는 참조표 대상의 성숙도이며 원본 라이브러리 API 전체의 동등성을
92
+ 보증하지 않습니다. ColorPicker·Watermark·Affix에도 Web renderer가 추가되어 Planned가 없습니다.
97
93
 
98
94
  ## lifecycle
99
95
 
100
96
  Ant Design 6.6.1은 기존 `List`를 deprecated로 표시하고 `Listy`를 successor로 추가했습니다.
101
97
  reference inventory는 `List.lifecycle = deprecated`, `Listy.lifecycle = new`로 보존하지만 HJM
102
- `List`를 자동으로 deprecated 처리하지 않습니다. HJM은 기존 비가상 `List`와 planned
98
+ `List`를 자동으로 deprecated 처리하지 않습니다. HJM은 기존 비가상 `List`와 구현된
103
99
  `VirtualList`를 서로 다른 사용 문제로 유지합니다.
104
100
 
105
101
  ## 업데이트 규칙
package/docs/cascader.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Cascader — 별도 컴포넌트를 만들지 않는다
2
2
 
3
+ > 2026-09-30 정리: 구현된 조합을 미구현으로 집계하지 않도록 독립 Planned 행과 catalog ID를 제거했다.
4
+ > 기존 조합 예제와 공개 helper는 유지하며, 참조표는 실제 구성 컴포넌트로 연결한다.
5
+ > 아래 날짜별 설계 기록의 행 유지 지시는 당시 판단이며 이 결정으로 대체한다.
6
+
7
+
3
8
  ## 문제로 제기된 것
4
9
 
5
10
  계층 데이터에서 경로 하나를 고른다(시/도 → 구 → 동). Ant Design `Cascader`와 `direct`
@@ -1,5 +1,10 @@
1
1
  # Catalog이 "만들지 않기로 확정함"을 표현하지 못하는 문제 — 설계 제안
2
2
 
3
+ > 2026-09-30 후속 결정: 구현된 조합 5개는 독립 Planned 행에서 제거하고 참조표를
4
+ > 실제 primitive로 연결했다. 아래의 다중 대상 조합 행 유지 판단은 과거 기록이다.
5
+ > 현재 원칙과 집계는 [참조 범위](ant-design-coverage.md)를 따른다.
6
+
7
+
3
8
  ## 문제
4
9
 
5
10
  `ComponentStatus`는 `"stable" | "beta" | "planned" | "deprecated"` 넷뿐이다(`src/catalog.ts`).
@@ -1,72 +1,30 @@
1
- # ColorPicker — 지금은 만들지 않는다
2
-
3
- ## 문제로 제기된 것
4
-
5
- 임의의 색을 고른다(hex/RGB/HSB 입력, 팔레트, 최근 사용 색). Ant Design `ColorPicker`와
6
- `direct` crosswalk를 따른다. catalog에는 이미 `{ name: "ColorPicker", category: "input",
7
- platform: "web", status: "planned" }` 자리가 예약돼 있다.
8
-
9
- ## 판정: 실사용처 근거 없이 만들지 않는다
10
-
11
- 먼저 가정하지 말라는 지시대로 두 제품에 색을 고르는 화면이 있는지 확인했다.
12
-
13
- - **Yajalal RN**(`modules/app-rn/src`): `grep -rli "colorpicker\|color picker\|color-picker"`가
14
- 0건이다. 코드베이스 전체에 색을 고르는 화면이 없다.
15
- - **BurnTok Web**: 이 머신에 저장소가 없어 코드로 직접 확인할 수 없었다. 로드맵
16
- (`docs/expansion-roadmap.md`)의 제품 적용 순서·완료 슬라이스 기록 어디에도 색 선택
17
- 화면이 등장하지 않는다 — 언급되는 색 관련 작업은 전부 "제품이 이미 고정한 semantic
18
- color를 렌더러가 어떻게 적용하는가"(Toast tone mark, Statistic trend mark, 구단 색
19
- adapter)이고, "사용자가 임의의 색을 고르는" 문제는 한 번도 나오지 않는다.
20
-
21
- 측정된 vertical slice가 없다는 뜻이다. `docs/expansion-roadmap.md`의 maturity gate는
22
- "실제 제품 vertical slice 없이 승격하지 않는다"고 정하고 있고, 이 원칙은 계약을 쓸지
23
- 말지에도 그대로 적용된다 — `docs/notification.md`·`docs/dropdown.md`·
24
- `docs/virtual-list.md`가 이미 "측정된 요구가 없으면 만들지 않는다"로 판정한 것과 같은
25
- 근거다.
26
-
27
- ## 이 판정을 더 무겁게 만드는 이유
28
-
29
- ColorPicker는 만들면 가벼운 계약이 아니다. 로드맵의 상태 축 표 어디에도 없는 완전히
30
- 새로운 세 갈래 문제를 한 번에 열어야 한다.
31
-
32
- 1. **색 공간 표현.** hex/RGB/HSB/alpha 중 무엇을 공개 API로 삼을지, 변환 규칙을 어디
33
- 소유할지부터 새로 정해야 한다 — 이 저장소의 어떤 기존 컴포넌트도 색 값 자체를
34
- 입력 데이터로 다루지 않는다(`semanticColors`/`ColorReference`는 전부 제품이 아니라
35
- 시스템이 미리 고정한 토큰이다).
36
- 2. **대비·접근성.** 사용자가 고른 색이 텍스트/배경 대비 4.5:1을 만족하는지, 선택 UI
37
- 자체의 포커스 인디케이터가 색상 스와치 배경과 충분한 대비를 유지하는지를 계약이
38
- 보장해야 한다 — Statistic의 trend, UploadItem의 상태처럼 이 저장소는 "색으로만
39
- 말하지 않는다"를 지켜왔는데, ColorPicker는 그 규칙과 정반대로 색 자체가 선택 결과인
40
- 컴포넌트라 이 원칙을 어떻게 지킬지부터 새로 설계해야 한다(스와치에 값 텍스트를
41
- 병기하는 것 정도는 쉽지만, 색맹 사용자를 위한 팔레트 순서·명도 대비 규칙은 가볍지
42
- 않다).
43
- 3. **입력 방식.** 팔레트 탭, 텍스트 hex 입력, 슬라이더(hue/saturation/brightness) 최소
44
- 셋을 하나의 컴포넌트가 조율해야 하고, 셋 사이의 값 동기화(hex 입력 중 오타 상태를
45
- 팔레트에 어떻게 반영하는지)는 NumberField/Slider보다 훨씬 큰 상태 기계다.
46
-
47
- 수요 없이 이 세 갈래를 먼저 짜면 다음 사람이 "실제로 무엇을 위해 이렇게 무거운가"를
48
- 또 물어야 하는 계약이 된다. 반대로 실사용처가 나오면, 그 화면이 실제로 필요한 것은
49
- 이 셋 중 일부뿐일 가능성이 높다(예: 구단 색 하나를 브랜드 팔레트에서만 고르는 화면이면
50
- 색 공간 변환도 hex 입력도 필요 없다) — 지금 전체를 설계하면 그 실제 요구보다 큰 계약이
51
- 된다.
52
-
53
- ## 만들지 않은 것
54
-
55
- `src/color-picker.ts`, `test/color-picker.test.ts`는 없다. catalog의
56
- `{ name: "ColorPicker", category: "input", platform: "web", status: "planned" }` row와
57
- `antDesignReferenceComponents`의 `ColorPicker → ColorPicker` crosswalk(`relationship:
58
- "direct"`)는 건드리지 않는다 — 이름 자리를 지우는 것이 아니라, 지금 채울 계약이 없다는
59
- 것이다.
60
-
61
- ## 뒤집힐 조건
62
-
63
- 다음 중 하나가 실제로 측정되면 이 판정을 다시 연다.
64
-
65
- 1. BurnTok 또는 Yajalal에 사용자가 임의의 색을 고르는 실제 화면 요구가 나온다(예: 커스텀
66
- 테마, 사용자 지정 하이라이트 색).
67
- 2. 요구가 팔레트에서 미리 정의된 색 중 하나만 고르는 좁은 범위로 확인되면, ColorPicker
68
- 전체가 아니라 `Chip` 또는 `RadioGroup`의 시각 변형으로 더 가볍게 흡수될 수 있는지부터
69
- 먼저 검토한다 — hex 입력·색 공간 변환이 필요 없는 한 별도 컴포넌트를 열 이유가
70
- 약해진다.
71
- 3. 요구가 hex/RGB 자유 입력까지 포함하면, 이 문서의 세 갈래(색 공간/대비/입력 방식)를
72
- 실제 화면의 좁은 요구에 맞춰 하나씩만 계약하고 나머지는 열지 않는다.
1
+ # ColorPicker — Web sRGB 색상 입력
2
+
3
+ 2026-09-30: 사용자가 남은 세 후보의 개발을 명시적으로 요청하여 기존 수요 대기 결정을 대체했다.
4
+ React Native와 Flutter 구현은 포함하지 않는다. 계약은 HEX sRGB를 저장값으로 선택했다.
5
+ HSV/RGB 입력기를 동시에 만드는 대신 브라우저 색상 선택기와 HEX 입력을 연결해 값 표현을 하나로 유지한다.
6
+
7
+ `@hjmds/react/color-picker`의 `ColorPicker`는 controlled `value`와 `onValueChange`를 받는다.
8
+ `label`과 `labels.color`, `labels.hex`, `labels.opacity`, `labels.invalid`는 제품에서 번역하여 전달한다.
9
+ `alpha`가 false면 `#rrggbb`, true면 `#rrggbbaa`를 내보낸다. 3·4자리 축약 HEX도 입력할 수 있다. alpha가 켜진 상태에서 3·6자리 HEX를 입력하면 색만 바꾸고 현재 투명도를 유지한다(네이티브 색상 입력과 같다).
10
+ alpha를 끈 상태에서 불투명하지 않은 값을 전달하면 투명도를 조용히 버리지 않고 거부한다.
11
+
12
+ - HEX는 Enter/blur에 확정한다. 잘못된 입력은 오류를 보여주고 외부 값은 유지한다.
13
+ Escape는 마지막 controlled 값으로 복구하며 Enter가 상위 폼을 제출하지 않게 한다.
14
+ - 브라우저 색상 선택기는 RGB를 선택하고 기존 alpha를 보존한다. 정확한 선택기 모양은 OS 소유다.
15
+ - 불투명도 range는 0–100%이며 키보드 화살표/Home/End를 지원한다. 8비트 alpha로 반올림한다.
16
+ - `presets`는 HEX 배열이며 색상값을 읽을 수 있는 버튼이다. 중복값은 정규화 후 제거한다.
17
+ - `disabled`는 fieldset을 통해 모든 입력과 팔레트를 비활성화한다.
18
+ - 제어부는 HJM canvas·border·focus 토큰을 사용한다. 견본의 사용자 색상은 콘텐츠 데이터다.
19
+ 선택색을 제품의 본문/배경으로 사용할 때의 대비는 제품이 확인한다.
20
+
21
+ ```tsx
22
+ <ColorPicker label={t('color.title')} labels={{
23
+ color: t('color.choose'), hex: t('color.hex'), opacity: t('color.opacity'), invalid: t('color.invalid'),
24
+ }} value={color} onValueChange={setColor} alpha presets={['#b94627ff', '#338844ff']} />
25
+ ```
26
+
27
+ 계약: [color-picker.ts](../src/color-picker.ts). UI: [renderer](../../react/src/color-picker.tsx).
28
+ 사용 예제: [Web additions](../../../showcase/web/src/patterns/WebAdditions.stories.tsx).
29
+ 검증: [계약 회귀](../test/web-additions.test.ts), [브라우저 동작](../../react/test/web-additions.browser.test.tsx),
30
+ [환경 행렬](../../react/test/scenario-matrix.browser.test.tsx). 브라우저/OS 색상 팝업 자체의 내부 UI는 HJM 검증 대상이 아니다.
@@ -1,5 +1,10 @@
1
1
  # ConfirmPopover — Popover 위의 확인 조합
2
2
 
3
+ > 2026-09-30 정리: 구현된 조합을 미구현으로 집계하지 않도록 독립 Planned 행과 catalog ID를 제거했다.
4
+ > 기존 조합 예제와 공개 helper는 유지하며, 참조표는 실제 구성 컴포넌트로 연결한다.
5
+ > 아래 날짜별 설계 기록의 행 유지 지시는 당시 판단이며 이 결정으로 대체한다.
6
+
7
+
3
8
  ## 문제로 제기된 것
4
9
 
5
10
  Ant Design `Popconfirm`은 "삭제하시겠습니까?" 같은 확인을 Modal보다 가벼운
@@ -1,12 +1,12 @@
1
1
  # Component maturity and evidence manifest
2
2
 
3
- > Generated from `@hjmds/design-contracts` 1.8.0. Do not edit this file directly; run `pnpm contracts:sync`.
3
+ > Generated from `@hjmds/design-contracts` 1.10.0. Do not edit this file directly; run `pnpm contracts:sync`.
4
4
 
5
5
  This document is the human-readable projection of the same catalog and Showcase manifest consumed by product CI.
6
6
 
7
- - Contract: stable 100 · planned 8
8
- - Web renderer: stable 100 · planned 8
9
- - Native renderer: stable 83 · planned 3 · unsupported 22
7
+ - Contract: stable 103
8
+ - Web renderer: stable 103
9
+ - Native renderer: stable 83 · unsupported 20
10
10
 
11
11
  | Component | Canonical story ID | Contract | Web | Native | Documentation | Required evidence |
12
12
  | --- | --- | --- | --- | --- | --- | --- |
@@ -52,16 +52,12 @@ This document is the human-readable projection of the same catalog and Showcase
52
52
  | Combobox | `input/combobox` | stable | stable | stable | generated only | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions |
53
53
  | DatePicker | `input/date-picker` | stable | stable | stable | [authored](../date-picker.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions |
54
54
  | DateRangePicker | `input/date-range-picker` | stable | stable | stable | generated only | contract: contract; web: default, dark, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, large-text, rtl, reduced-motion, accessibility, native-actions |
55
- | TimePicker | `input/time-picker` | planned | planned | planned | [authored](../time-picker.md) | contract: contract |
56
- | ColorPicker | `input/color-picker` | planned | planned | unsupported | [authored](../color-picker.md) | contract: contract |
55
+ | ColorPicker | `input/color-picker` | stable | stable | unsupported | [authored](../color-picker.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard |
57
56
  | FilePicker | `input/file-picker` | stable | stable | stable | [authored](../file-picker.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions |
58
- | Cascader | `input/cascader` | planned | planned | planned | [authored](../cascader.md) | contract: contract |
59
57
  | Form | `input/form` | stable | stable | stable | [authored](../form.md) | contract: contract; web: default, dark, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, large-text, rtl, reduced-motion, accessibility, native-actions |
60
58
  | Agreement | `input/agreement` | stable | stable | stable | [authored](../agreement.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions |
61
59
  | Mentions | `input/mentions` | stable | stable | stable | [authored](../mentions.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions |
62
- | Rating | `input/rating` | planned | planned | planned | [authored](../rating.md) | contract: contract |
63
60
  | TransferList | `input/transfer-list` | stable | stable | stable | [authored](../transfer-list.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions |
64
- | TreeSelect | `input/tree-select` | planned | planned | unsupported | [authored](../tree-select.md) | contract: contract |
65
61
  | UploadItem | `data-display/upload-item` | stable | stable | stable | [authored](../upload-item.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions |
66
62
  | Tabs | `navigation/tabs` | stable | stable | stable | generated only | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions |
67
63
  | TopBar | `navigation/top-bar` | stable | stable | stable | generated only | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility |
@@ -105,16 +101,15 @@ This document is the human-readable projection of the same catalog and Showcase
105
101
  | Result | `feedback/result` | stable | stable | stable | [authored](../result.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility |
106
102
  | BottomInfo | `feedback/bottom-info` | stable | stable | stable | [authored](../bottom-info.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility |
107
103
  | Toast | `feedback/toast` | stable | stable | stable | [authored](../toast.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions |
108
- | Watermark | `feedback/watermark` | planned | planned | unsupported | [authored](../watermark.md) | contract: contract |
104
+ | Watermark | `feedback/watermark` | stable | stable | unsupported | [authored](../watermark.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility |
109
105
  | Dialog | `overlay/dialog` | stable | stable | stable | [authored](../dialog.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions |
110
106
  | AlertDialog | `overlay/alert-dialog` | stable | stable | stable | generated only | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions |
111
107
  | Sheet | `overlay/sheet` | stable | stable | stable | [authored](../sheet.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, native-actions |
112
108
  | SidePanel | `overlay/side-panel` | stable | stable | unsupported | [authored](../side-panel.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard |
113
109
  | Popover | `overlay/popover` | stable | stable | unsupported | [authored](../popover.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard |
114
- | ConfirmPopover | `overlay/confirm-popover` | planned | planned | unsupported | [authored](../confirm-popover.md) | contract: contract |
115
110
  | Tooltip | `overlay/tooltip` | stable | stable | unsupported | [authored](../tooltip.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard |
116
111
  | CommandPalette | `overlay/command-palette` | stable | stable | unsupported | [authored](../command-palette.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard |
117
- | Affix | `utility/affix` | planned | planned | unsupported | [authored](../affix.md) | contract: contract |
112
+ | Affix | `utility/affix` | stable | stable | unsupported | [authored](../affix.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility |
118
113
  | DesignSystemProvider | `provider/design-system-provider` | stable | stable | stable | [authored](../design-system-provider.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility; native: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility |
119
114
  | SkipNav | `utility/skip-nav` | stable | stable | unsupported | [authored](../skip-nav.md) | contract: contract; web: default, dark, long-copy, large-text, rtl, reduced-motion, accessibility, keyboard |
120
115
  | VisuallyHidden | `utility/visually-hidden` | stable | stable | unsupported | [authored](../visually-hidden.md) | contract: contract; web: default, dark, large-text, rtl, reduced-motion, accessibility |
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "designSystemVersion": "1.8.0",
3
+ "designSystemVersion": "1.10.0",
4
4
  "source": "packages/design-contracts/docs/generated/showcase-manifest.json",
5
5
  "surfaces": {
6
6
  "web": {
7
7
  "packageName": "@hjmds/react",
8
- "packageVersion": "1.8.0",
9
- "activeComponents": 100,
10
- "claimedComponents": 100,
11
- "completeScenarioComponents": 100,
8
+ "packageVersion": "1.10.0",
9
+ "activeComponents": 103,
10
+ "claimedComponents": 103,
11
+ "completeScenarioComponents": 103,
12
12
  "components": [
13
13
  {
14
14
  "componentId": "text",
@@ -2306,6 +2306,65 @@
2306
2306
  ],
2307
2307
  "missingScenarios": []
2308
2308
  },
2309
+ {
2310
+ "componentId": "color-picker",
2311
+ "name": "ColorPicker",
2312
+ "status": "stable",
2313
+ "claimed": true,
2314
+ "subpath": "./color-picker",
2315
+ "exportNames": [
2316
+ "ColorPicker"
2317
+ ],
2318
+ "proofs": [
2319
+ {
2320
+ "scenarios": [
2321
+ "default"
2322
+ ],
2323
+ "file": "test/default-render.ssr.test.tsx",
2324
+ "caseId": "color-picker"
2325
+ },
2326
+ {
2327
+ "scenarios": [
2328
+ "accessibility",
2329
+ "dark",
2330
+ "large-text",
2331
+ "rtl",
2332
+ "reduced-motion",
2333
+ "long-copy"
2334
+ ],
2335
+ "file": "test/scenario-matrix.browser.test.tsx",
2336
+ "caseId": "color-picker"
2337
+ },
2338
+ {
2339
+ "scenarios": [
2340
+ "keyboard"
2341
+ ],
2342
+ "file": "test/web-additions.browser.test.tsx",
2343
+ "caseId": "color-picker"
2344
+ }
2345
+ ],
2346
+ "requiredScenarios": [
2347
+ "default",
2348
+ "dark",
2349
+ "long-copy",
2350
+ "large-text",
2351
+ "rtl",
2352
+ "reduced-motion",
2353
+ "accessibility",
2354
+ "keyboard"
2355
+ ],
2356
+ "claimedScenarios": [
2357
+ "default",
2358
+ "accessibility",
2359
+ "dark",
2360
+ "large-text",
2361
+ "rtl",
2362
+ "reduced-motion",
2363
+ "long-copy",
2364
+ "keyboard"
2365
+ ],
2366
+ "missingScenarios": []
2367
+ },
2309
2368
  {
2310
2369
  "componentId": "file-picker",
2311
2370
  "name": "FilePicker",
@@ -4909,6 +4968,56 @@
4909
4968
  ],
4910
4969
  "missingScenarios": []
4911
4970
  },
4971
+ {
4972
+ "componentId": "watermark",
4973
+ "name": "Watermark",
4974
+ "status": "stable",
4975
+ "claimed": true,
4976
+ "subpath": "./watermark",
4977
+ "exportNames": [
4978
+ "Watermark"
4979
+ ],
4980
+ "proofs": [
4981
+ {
4982
+ "scenarios": [
4983
+ "default"
4984
+ ],
4985
+ "file": "test/default-render.ssr.test.tsx",
4986
+ "caseId": "watermark"
4987
+ },
4988
+ {
4989
+ "scenarios": [
4990
+ "accessibility",
4991
+ "dark",
4992
+ "large-text",
4993
+ "rtl",
4994
+ "reduced-motion",
4995
+ "long-copy"
4996
+ ],
4997
+ "file": "test/scenario-matrix.browser.test.tsx",
4998
+ "caseId": "watermark"
4999
+ }
5000
+ ],
5001
+ "requiredScenarios": [
5002
+ "default",
5003
+ "dark",
5004
+ "long-copy",
5005
+ "large-text",
5006
+ "rtl",
5007
+ "reduced-motion",
5008
+ "accessibility"
5009
+ ],
5010
+ "claimedScenarios": [
5011
+ "default",
5012
+ "accessibility",
5013
+ "dark",
5014
+ "large-text",
5015
+ "rtl",
5016
+ "reduced-motion",
5017
+ "long-copy"
5018
+ ],
5019
+ "missingScenarios": []
5020
+ },
4912
5021
  {
4913
5022
  "componentId": "dialog",
4914
5023
  "name": "Dialog",
@@ -5322,6 +5431,56 @@
5322
5431
  ],
5323
5432
  "missingScenarios": []
5324
5433
  },
5434
+ {
5435
+ "componentId": "affix",
5436
+ "name": "Affix",
5437
+ "status": "stable",
5438
+ "claimed": true,
5439
+ "subpath": "./affix",
5440
+ "exportNames": [
5441
+ "Affix"
5442
+ ],
5443
+ "proofs": [
5444
+ {
5445
+ "scenarios": [
5446
+ "default"
5447
+ ],
5448
+ "file": "test/default-render.ssr.test.tsx",
5449
+ "caseId": "affix"
5450
+ },
5451
+ {
5452
+ "scenarios": [
5453
+ "accessibility",
5454
+ "dark",
5455
+ "large-text",
5456
+ "rtl",
5457
+ "reduced-motion",
5458
+ "long-copy"
5459
+ ],
5460
+ "file": "test/scenario-matrix.browser.test.tsx",
5461
+ "caseId": "affix"
5462
+ }
5463
+ ],
5464
+ "requiredScenarios": [
5465
+ "default",
5466
+ "dark",
5467
+ "long-copy",
5468
+ "large-text",
5469
+ "rtl",
5470
+ "reduced-motion",
5471
+ "accessibility"
5472
+ ],
5473
+ "claimedScenarios": [
5474
+ "default",
5475
+ "accessibility",
5476
+ "dark",
5477
+ "large-text",
5478
+ "rtl",
5479
+ "reduced-motion",
5480
+ "long-copy"
5481
+ ],
5482
+ "missingScenarios": []
5483
+ },
5325
5484
  {
5326
5485
  "componentId": "design-system-provider",
5327
5486
  "name": "DesignSystemProvider",
@@ -5483,7 +5642,7 @@
5483
5642
  },
5484
5643
  "native": {
5485
5644
  "packageName": "@hjmds/react-native",
5486
- "packageVersion": "1.8.0",
5645
+ "packageVersion": "1.10.0",
5487
5646
  "activeComponents": 83,
5488
5647
  "claimedComponents": 83,
5489
5648
  "completeScenarioComponents": 83,