@spectrabrain/eds 0.0.4 → 0.1.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.
Files changed (62) hide show
  1. package/AI.md +13 -40
  2. package/AI_CONTEXT.json +90 -0
  3. package/CHANGELOG.md +120 -0
  4. package/CONSUMER_AGENT.md +152 -0
  5. package/FRONTEND_CONVENTIONS.md +252 -0
  6. package/QUICKSTART.md +42 -0
  7. package/README.md +38 -69
  8. package/TAILWIND_V4.md +57 -243
  9. package/TOKEN_CONTEXT.md +98 -0
  10. package/consumer/align/templates/components.json +21 -0
  11. package/consumer/align/templates/env.mock +1 -0
  12. package/consumer/align/templates/eslint.config.js +35 -0
  13. package/consumer/align/templates/package.scripts.json +10 -0
  14. package/consumer/align/templates/prettierignore +2 -0
  15. package/consumer/align/templates/prettierrc.json +7 -0
  16. package/consumer/align/templates/src/app/App.tsx +16 -0
  17. package/consumer/align/templates/src/app/main.tsx +27 -0
  18. package/consumer/align/templates/src/app/providers/ErrorBoundary.tsx +58 -0
  19. package/consumer/align/templates/src/app/providers/ErrorHandler.tsx +29 -0
  20. package/consumer/align/templates/src/app/router/index.tsx +23 -0
  21. package/consumer/align/templates/src/app/styles/index.css +2 -0
  22. package/consumer/align/templates/src/mocks/browser.ts +5 -0
  23. package/consumer/align/templates/src/mocks/handlers.ts +4 -0
  24. package/consumer/align/templates/src/shared/api/index.ts +23 -0
  25. package/consumer/align/templates/src/shared/api/queryClient.ts +24 -0
  26. package/consumer/align/templates/src/shared/lib/eventBus.ts +44 -0
  27. package/consumer/align/templates/src/shared/lib/utils/index.ts +6 -0
  28. package/consumer/align/templates/src/vite-env.d.ts +2 -0
  29. package/consumer/align/templates/tsconfig.json +22 -0
  30. package/consumer/align/templates/vite.config.babel.ts +36 -0
  31. package/consumer/align/templates/vite.config.rolldown.ts +35 -0
  32. package/consumer/align-analysis.mjs +1159 -0
  33. package/consumer/align-plan.mjs +377 -0
  34. package/consumer/align-rules.mjs +473 -0
  35. package/consumer/align.mjs +401 -0
  36. package/consumer/cli.mjs +157 -0
  37. package/consumer/css-root.mjs +259 -0
  38. package/consumer/doctor.mjs +204 -0
  39. package/consumer/generate-tailwind.mjs +83 -0
  40. package/consumer/init.mjs +186 -0
  41. package/consumer/managed-block.mjs +126 -0
  42. package/consumer/paths.mjs +266 -0
  43. package/consumer/register-scripts.mjs +129 -0
  44. package/consumer/scan-classes.mjs +71 -0
  45. package/consumer/sync.mjs +155 -0
  46. package/consumer/update-agent-context.mjs +143 -0
  47. package/consumer-postinstall.mjs +60 -0
  48. package/dist/base.css +4 -0
  49. package/dist/components/Button/README.md +70 -80
  50. package/dist/components/Button/spec.json +71 -133
  51. package/dist/components.css +102 -368
  52. package/dist/index.d.mts +40 -1
  53. package/dist/index.d.ts +40 -1
  54. package/dist/index.js +34 -6250
  55. package/dist/index.mjs +36 -6252
  56. package/dist/styles.css +1 -0
  57. package/dist/tokens.d.mts +5 -2
  58. package/dist/tokens.d.ts +5 -2
  59. package/dist/tokens.js +6 -7
  60. package/dist/tokens.mjs +6 -7
  61. package/llms.txt +22 -9
  62. package/package.json +33 -2
@@ -1,17 +1,48 @@
1
- # Button — 개요, 사용 맥락, 공개 API
1
+ # Button
2
2
 
3
- 이 문서는 `Button` 디렉토리의 기준 문서입니다. `Button`의 개요·사용 맥락·공개 API·접근성 계약을 기록합니다. Button을 사용하기 전에는 이 문서를 먼저 확인합니다.
4
-
5
- 패키지 설치와 공개 경로는 루트 [README.md](../../../README.md)를, 소비처 AI의 공통 규칙은 루트 [AI.md](../../../AI.md)를 참고합니다. 루트 `AI.md`에는 컴포넌트별 정책을 중복해서 기록하지 않습니다.
3
+ `Button`의 사용 맥락·공개 API·접근성 계약입니다. 소비처 AI의 필수 규칙은 루트 [QUICKSTART.md](../../../QUICKSTART.md)에 있습니다.
6
4
 
7
5
  ## 개요
8
6
 
9
7
  `Button`은 사용자가 즉시 실행할 수 있는 행동을 표현하는 인터랙티브 요소입니다. `Size`, `Layout`, `Role`, `Variant`, `State`로 버튼의 크기·구조·의미·위계·상태를 정의합니다.
10
8
 
11
- 탐색 목적의 링크는 `neutral/link` 조합으로 표현하고, 상태 변경·폼 제출·명령 실행은 native `<button>`으로 표현합니다. `Button`은 현재 React 웹과 light theme만 지원합니다.
9
+ 탐색 목적의 링크는 `neutral/link` 조합으로 표현하고, 상태 변경·폼 제출·명령 실행은 native `<button>`으로 표현합니다.
10
+
11
+ ```tsx
12
+ <Button size="sm" role="brand" variant="solid" type="submit" loading={isSaving}>저장</Button>
13
+ <Button size="sm" role="neutral" variant="outline" type="button" onClick={onCancel}>취소</Button>
14
+ <Button size="sm" role="neutral" variant="link" href="/docs" endIconName="ArrowRight">자세히 보기</Button>
15
+ <Button size="xs" role="neutral" variant="ghost" type="button" iconName="Trash" aria-label="행 삭제" />
16
+ ```
17
+
18
+ loading 중에도 라벨은 그대로 둡니다. icon-only는 `aria-label`을 타입으로 강제하지 않으므로 마지막 예시처럼 직접 지정합니다.
12
19
 
13
20
  ## 사용 맥락
14
21
 
22
+ ### Role과 Variant
23
+
24
+ `role`은 색상 취향이 아니라 버튼이 수행하는 행동의 의미입니다. `brand`는 브랜드와 관련되거나 화면에서 가장 중요한 행동, `neutral`은 `brand`보다 낮은 주목도가 필요한 일반적인 액션과 탐색, `critical`은 삭제·초기화처럼 되돌리기 어려운 위험한 행동입니다. `role`은 시각적 prop이며 DOM의 ARIA `role` 속성으로 전달하지 않고, ARIA role을 따로 지정하는 방법은 없습니다. `variant`는 같은 role 안의 시각적 위계입니다.
25
+
26
+ | 행동 | 조합 | 설명 |
27
+ | --- | --- | --- |
28
+ | 생성·저장·관리·확인처럼 화면의 핵심 행동 | `brand/solid` | 사용자가 완료해야 할 대표 행동을 강조합니다. 한 화면에 하나를 권장합니다. |
29
+ | `brand/solid`보다 낮은 위계의 브랜드 보조 행동 | `brand/outline` | 브랜드 맥락의 보조 행동입니다. |
30
+ | 검색·조회·일반적인 주요 실행 | `neutral/solid` | `brand/solid` 다음 위계의 주요 CTA입니다. 한 화면에 하나를 권장합니다. |
31
+ | 취소·닫기·수정 같은 보조 작업 | `neutral/outline` | 주 행동과 시각적 위계를 분리합니다. |
32
+ | 초기화·갱신처럼 부담이 낮은 반복 행동 | `neutral/weak` | CTA를 보조하면서 반복 가능한 행동을 표현합니다. 되돌리기 어려운 초기화는 `critical/weak`입니다. |
33
+ | 툴바·헤더·옵션 메뉴처럼 주변 UI와의 간섭을 줄여야 하는 보조 행동 | `neutral/ghost` | 배경 없이 보조 행동을 표현합니다. |
34
+ | 다른 페이지·콘텐츠로 이동하거나 관련 정보를 확인 | `neutral/link` | 탐색에만 사용하며 상태 변경이나 일반 CTA에는 쓰지 않습니다. `href`가 필요하며 `<a>`로 렌더링합니다. |
35
+ | 되돌리기 어려운 삭제·초기화 | `critical/weak` | 위험을 분명히 전달합니다. 가능하면 소비처의 확인 대화상자(WAI-ARIA `alertdialog` 패턴)로 한 번 더 확인받습니다. |
36
+
37
+ 새 코드는 기존 화면의 조합을 따르지 않고 위 표의 행동 기준으로 조합을 고릅니다. 비동기 처리 중에는 선택한 조합에 `loading`을 지정합니다. 툴바·헤더 안의 보조 행동이 `neutral/outline`과 `neutral/ghost`에 모두 해당할 때의 우선순위는 `미결`입니다.
38
+
39
+ 같은 액션 그룹(함께 놓이는 버튼 묶음)의 조합은 다음을 따릅니다.
40
+
41
+ - `brand/outline`은 같은 액션 그룹에서 `brand/solid`와 함께 쓰지 않는 것을 권장합니다.
42
+ - `neutral/outline`은 `neutral/solid`와 같은 액션 그룹에 두지 않습니다.
43
+ - `brand/outline`과 `neutral/outline`은 함께 둘 수 있습니다.
44
+ - `neutral/solid` 옆에 두는 취소 버튼의 조합은 `미결`입니다.
45
+
15
46
  ### Size
16
47
 
17
48
  사용 가능한 크기는 `xs`, `sm`, `md`입니다. 사용할 공간과 행동의 중요도를 함께 고려합니다.
@@ -22,67 +53,36 @@
22
53
  | `sm` | 일반적인 화면 액션에 사용합니다. |
23
54
  | `md` | 공간이 충분하거나 중요한 행동을 명확히 강조해야 할 때 사용합니다. |
24
55
 
25
- ### Layout
56
+ ### Layout과 아이콘
26
57
 
27
- Layout은 라벨과 아이콘의 조합으로 결정합니다. 아이콘은 행동의 목적이나 방향을 보조할 때만 사용합니다.
58
+ Layout은 prop으로 지정하지 않고 슬롯 조합에서 추론합니다. 아이콘은 생성·수정·삭제·외부 이동·다운로드처럼 행동의 목적이나 방향을 보조할 때만 사용하고, 표 행 조작이나 pagination처럼 공간이 좁은 곳에서는 `iconOnly`를 사용할 수 있습니다.
28
59
 
29
- | layout | 구성 | 사용 맥락 |
60
+ | layout | 슬롯 조합 | 사용 맥락 |
30
61
  | --- | --- | --- |
31
- | `textOnly` | 라벨만 제공합니다. | 아이콘 없이도 행동을 명확히 전달할 수 있을 때 사용합니다. |
32
- | `iconFirst` | 라벨 앞에 아이콘을 둡니다. | 아이콘이 행동의 목적을 보조할 때 사용합니다. |
33
- | `iconLast` | 라벨 뒤에 아이콘을 둡니다. | 이동 방향이나 다음 단계를 나타낼 때 사용합니다. |
34
- | `iconBoth` | 라벨 양쪽에 아이콘을 둡니다. | 양쪽 아이콘이 모두 의미를 더하는 명확한 경우에만 사용합니다. |
35
- | `iconOnly` | 아이콘만 제공합니다. | 아이콘만으로 행동의 의미가 충분히 명확할 때 사용하며, 접근성 이름을 제공하려면 `aria-label`을 지정합니다. |
36
-
37
- ### Role
38
-
39
- Role은 색상 취향이 아니라 버튼이 수행하는 행동의 의미를 정의합니다. `role`은 시각적 prop이며 DOM의 ARIA `role` 속성으로 전달하지 않습니다.
40
-
41
- | role | 사용 맥락 |
42
- | --- | --- |
43
- | `brand` | 브랜드와 관련되거나 화면에서 가장 중요한 행동을 강조할 때 사용합니다. |
44
- | `neutral` | 일반적인 액션과 탐색에 사용합니다. `brand`보다 낮은 주목도가 필요할 때 선택합니다. |
45
- | `critical` | 삭제·초기화처럼 오류나 되돌리기 어려운 위험한 행동을 나타낼 때 사용합니다. 가능하면 확인 절차와 함께 사용합니다. |
46
-
47
- ### Variant
62
+ | `textOnly` | `children`만 지정합니다. | |
63
+ | `iconFirst` | `children`과 `startIcon` 또는 `startIconName`을 지정합니다. | 아이콘이 행동의 목적을 보조할 때 사용합니다. |
64
+ | `iconLast` | `children`과 `endIcon` 또는 `endIconName`을 지정합니다. | 이동 방향이나 다음 단계를 나타낼 때 사용합니다. |
65
+ | `iconBoth` | `children`과 앞·뒤 아이콘을 모두 지정합니다. | 양쪽 아이콘이 모두 의미를 더하는 명확한 경우에만 사용합니다. |
66
+ | `iconOnly` | `children` 없이 `icon` 또는 `iconName`과 `aria-label`을 지정합니다. | |
48
67
 
49
- Variant는 같은 Role 안에서 시각적 위계를 정의합니다.
50
-
51
- | role / variant | 사용 맥락 |
52
- | --- | --- |
53
- | `brand/solid` | 서비스의 핵심 행동에 사용합니다. 한 화면에 하나를 권장합니다. |
54
- | `brand/outline` | `brand/solid`보다 낮은 위계의 브랜드 보조 행동에 사용합니다. `neutral/outline`과 조합할 수 있으며 같은 액션 그룹에서 `brand/solid`와 함께 사용하지 않는 것을 권장합니다. |
55
- | `neutral/solid` | `brand/solid` 다음 위계의 일반적인 주요 CTA에 사용합니다. 한 화면에 하나를 권장합니다. |
56
- | `neutral/weak` | CTA를 제외한 일반적인 보조 행동에 사용하며, CTA와 함께 제공할 때 보조 위계를 표현합니다. |
57
- | `neutral/outline` | 시각적 부담을 낮춘 대안이나 보조 행동에 사용합니다. `neutral/solid`과 같은 액션 그룹에서 함께 사용하지 않으며, `brand/outline`과 조합할 수 있습니다. |
58
- | `neutral/ghost` | 툴바·헤더·옵션 메뉴처럼 주변 UI와의 간섭을 줄여야 하는 보조 행동에 사용합니다. |
59
- | `neutral/link` | 다른 페이지·콘텐츠로 이동하거나 관련 정보를 확인하는 탐색성 행동에 사용합니다. 상태 변경이나 일반 CTA에는 사용하지 않습니다. `href`가 필요하며 `<a>`로 렌더링합니다. |
60
- | `critical/weak` | 삭제·초기화처럼 되돌리기 어려운 행동에 사용합니다. 위험을 분명히 전달하며 가능하면 확인 대화상자나 Alert Dialog와 함께 사용합니다. |
61
-
62
- 지원하지 않는 Role·Variant 조합을 `className`, inline style, 임의 CSS로 우회하지 않습니다.
68
+ - 이름 슬롯(`iconName`·`startIconName`·`endIconName`)은 승인된 아이콘 목록의 lucide PascalCase 전체 이름만 받습니다. `ButtonIconName`이 이 목록과 같은 union이라 목록에 없는 이름은 TypeScript 오류이고, 런타임에 전달되면 render 오류입니다. 전체 목록은 설치된 패키지 `spec.json`의 `api.iconNames`에 있습니다.
69
+ - 목록은 `lucide-react`의 canonical 이름만 포함하며 `AlertCircle`처럼 deprecated된 alias는 허용하지 않습니다.
70
+ - 목록 밖 아이콘은 `icon`, `startIcon`, `endIcon`에 `ReactElement`로 전달하며, 같은 슬롯에 이름과 custom element를 함께 지정하지 않습니다. `lucide-react`에서 직접 import할 때는 EDS package root `package.json`의 `dependencies["lucide-react"]`와 같은 exact 버전을 소비처 `package.json`에 선언합니다. pnpm처럼 엄격한 node_modules에서는 선언 없이 resolve되지 않고, 버전이 다르면 아이콘 코드가 두 벌 번들됩니다. custom lucide 아이콘은 lucide 기본 stroke를 그대로 써서 이름으로 지정한 아이콘과 맞춥니다.
71
+ - 목록 밖 아이콘을 `ReactElement`로 썼다면 완료 보고에 아이콘 이름과 사용 파일을 allowlist 추가 후보로 적습니다.
63
72
 
64
73
  ### State
65
74
 
66
- | state | 의미 |
67
- | --- | --- |
68
- | enabled | 사용자가 실행할 수 있는 기본 상태입니다. |
69
- | hover | 포인터가 버튼 위에 있을 때의 상태이며 CSS `:hover`로 처리합니다. |
70
- | loading | 동작을 처리하는 중인 상태입니다. 라벨·아이콘 공간을 유지한 progress circle을 표시하고 상호작용을 차단합니다. |
71
- | disabled | 사용자가 실행할 수 없는 상태입니다. |
72
-
73
- `active`, `pressed`, `visited` 등 문서에 없는 상태는 추가하지 않습니다.
75
+ 상태는 `loading`과 `disabled`만 prop으로 지정합니다. hover와 focus-visible은 컴포넌트가 처리하며, `active`, `pressed`, `visited` 상태는 제공하지 않습니다. `loading`은 라벨·아이콘 공간을 유지한 progress circle을 표시하고 상호작용을 차단합니다.
74
76
 
75
77
  ## 공개 API contract
76
78
 
77
- 다음 props는 필수입니다.
79
+ 모든 Button에 다음 props가 필수입니다.
78
80
 
79
81
  | prop | 허용 값 | 설명 |
80
82
  | --- | --- | --- |
81
83
  | `size` | `xs`, `sm`, `md` | 버튼 크기입니다. |
82
84
  | `role` | `brand`, `neutral`, `critical` | 시각적 역할이며 DOM ARIA `role`로 전달하지 않습니다. |
83
85
  | `variant` | role별 아래 표 | 역할에 종속된 표현입니다. |
84
- | `type` | `button`, `submit`, `reset` | native button branch에서 필수입니다. |
85
- | `href` | `string` | `neutral/link` branch에서 필수이며 `<a>`로 렌더링합니다. |
86
86
 
87
87
  허용하는 role·variant 조합은 다음과 같습니다.
88
88
 
@@ -92,52 +92,42 @@ Variant는 같은 Role 안에서 시각적 위계를 정의합니다.
92
92
  | `neutral` | `solid`, `weak`, `outline`, `ghost`, `link` |
93
93
  | `critical` | `weak` |
94
94
 
95
+ 조합에 따라 branch가 정해집니다.
96
+
97
+ - `role="neutral"`·`variant="link"`이면 link branch입니다. 비어 있지 않은 `href`가 필수이고 `<a>`로 렌더링하며 `type`은 받지 않습니다.
98
+ - 그 밖의 조합은 native button branch입니다. `type`(`button`, `submit`, `reset`)이 필수이고 기본값이 없으며 `href`는 받지 않습니다. 폼 제출은 `submit`, 그 밖의 행동은 `button`입니다.
99
+
95
100
  ### API reference
96
101
 
97
102
  | prop | 타입·규칙 |
98
103
  | --- | --- |
99
- | `children` | `ReactNode` label입니다. 렌더링 가능한 내용이 하나 이상 필요하며 `startIcon`·`endIcon`과 조합할 수 있습니다. |
100
- | `icon` | icon-only용 단일 custom `ReactElement`입니다. children이나 start/end icon과 함께 사용하지 않습니다. |
104
+ | `children` | `ReactNode` label입니다. 렌더링 가능한 내용이 하나 이상 필요하며 한 줄로 표시합니다. `startIcon`·`endIcon`과 조합할 수 있습니다. |
105
+ | `icon` | icon-only용 단일 custom `ReactElement`입니다. children이나 start/end 아이콘과 함께 사용하지 않습니다. |
101
106
  | `iconName` | icon-only용 `ButtonIconName` lucide 이름입니다. |
102
107
  | `startIcon` / `endIcon` | label 앞·뒤의 단일 custom `ReactElement` 슬롯입니다. |
103
108
  | `startIconName` / `endIconName` | label 앞·뒤의 `ButtonIconName` lucide 이름 슬롯입니다. |
104
- | `aria-label` | 선택적인 `string`입니다. icon-only의 accessible name을 제공할 때 지정하며, 필수로 강제하지 않습니다. |
109
+ | `aria-label` | 선택적인 `string`이며, 지정하면 비어 있지 않아야 합니다. icon-only에서는 항상 지정합니다. |
105
110
  | `disabled` | `boolean`입니다. native button은 `disabled`, link는 href 제거·`aria-disabled="true"`·`tabIndex={-1}`를 적용합니다. |
106
- | `loading` | `boolean`입니다. label·아이콘 공간을 유지한 progress circle을 표시하고 상호작용을 차단합니다. `aria-busy="true"`를 적용합니다. |
107
- | `className` | `string`이며 배치·컨테이너 보정에만 허용합니다. |
111
+ | `loading` | `boolean`입니다. label·아이콘 공간을 유지한 progress circle을 표시하고 상호작용을 차단하며 `aria-busy="true"`를 적용합니다. |
112
+ | `className` | `string`입니다. 허용 범위는 `QUICKSTART.md` 규칙 5를 따릅니다. |
113
+ | `style` | 지원하지 않습니다(`never`). 스타일 override는 공개 API가 아닙니다. |
108
114
  | `aria-*` / `data-*` | 각 branch의 native HTML 속성으로 전달할 수 있습니다. |
109
115
  | `ref` | branch에 따라 `HTMLButtonElement` 또는 `HTMLAnchorElement`를 전달합니다. |
110
116
 
111
- `style`과 임의 스타일 override는 공개하지 않습니다. 이름으로 지정한 lucide 아이콘은 `lucide-react` 기본 시각 설정을 사용하며 Button은 아이콘 영역 크기만 조정합니다. 공개 소비처의 role·variant 선택과 사용 맥락도 이 문서를 기준으로 합니다.
112
-
113
- `variant="link"`는 `role="neutral"`에서만 사용할 수 있으며 `href`가 필수입니다. 이 조합은 `<a>`로 렌더링합니다. native button에는 기본값을 제공하지 않습니다.
114
-
115
- `startIcon`, `endIcon`, `icon`은 단일 `ReactElement` 슬롯입니다. lucide 아이콘은 `startIconName`, `endIconName`, `iconName`으로 PascalCase 전체 이름을 지정합니다. 이름 prop은 `ButtonIconName` 자동완성을 제공하며 custom ReactElement와 같은 슬롯에 함께 지정할 수 없습니다.
116
-
117
- 이름으로 지정한 lucide 아이콘은 `lucide-react`의 기본 시각 설정을 사용합니다. Button은 아이콘 영역의 크기만 조정하며, `color`, `size`, `strokeWidth`, `strokeLinecap`, `strokeLinejoin`을 소비처 prop으로 노출하거나 덮어쓰지 않습니다. `aria-hidden`과 `focusable`은 Button 접근성 contract에 따라 내부에서 설정합니다.
118
-
119
- - children만 있으면 `textOnly`입니다.
120
- - children과 `startIcon`이 있으면 `iconFirst`입니다.
121
- - children과 `endIcon`이 있으면 `iconLast`입니다.
122
- - children과 두 아이콘이 있으면 `iconBoth`입니다.
123
- - children 없이 `icon` 또는 `iconName`만 있으면 `iconOnly`입니다. 접근성 이름이 필요하면 비어 있지 않은 `aria-label`을 지정합니다.
124
-
125
- 빈 label·icon, 제공된 빈 `aria-label`과 잘못된 슬롯 조합은 모든 환경에서 오류로 처리합니다. label은 한 줄을 유지합니다.
126
-
127
- `className`, native `aria-*`, `data-*`, `forwardRef`를 지원합니다. `style`과 임의 스타일 override는 공개하지 않습니다.
128
-
129
- `loading`은 선택 prop입니다. 라벨과 아이콘의 공간·접근성 이름을 유지하면서 내부 2색 SVG progress circle을 표시합니다. 라벨과 아이콘은 시각적으로만 숨겨지며, native button은 `disabled`, link는 href 제거·`aria-disabled="true"`·`tabIndex={-1}`로 상호작용을 차단합니다. loading 중에는 `aria-busy="true"`를 적용하며 `disabled`와 함께 지정하면 loading 시각을 우선합니다.
117
+ 다음 입력은 타입을 통과해도 render 중 `Error`를 던지며, error boundary가 없으면 그 화면 트리가 unmount됩니다: 빈 `children`·`href`·지정된 `aria-label`(공백만 있는 문자열 포함), 한 슬롯에 이름과 element를 함께 지정, `icon`과 `children`·start/end 아이콘을 함께 지정, 목록 밖 아이콘 이름, 표에 없는 role·variant 조합. 데이터나 번역에서 받은 값이 비어 있을 수 있으면 `""`로 채우지 말고 Button을 렌더링하지 않거나 prop을 생략합니다.
130
118
 
131
119
  ## 접근성·상호작용
132
120
 
121
+ - 아이콘은 시각 장식으로 `aria-hidden="true"` 처리합니다. 그래서 icon-only Button에는 항상 비어 있지 않은 `aria-label` 또는 `aria-labelledby`를 지정합니다. 타입이 이를 요구하지 않아 빠뜨려도 typecheck가 통과합니다.
133
122
  - native button은 `disabled` 속성을 사용합니다.
134
123
  - disabled link는 `href`를 제거하고 `aria-disabled="true"`, `tabIndex={-1}`를 적용하며 클릭을 막습니다. href가 제거된 앵커는 의도적으로 link role을 노출하지 않습니다.
135
- - hover는 CSS `:hover`로 처리합니다.
136
- - 승인된 시각 상태는 enabled, hover, disabled, loading이며 focus-visible은 별도 focus ring 상태로 제공합니다. active·pressed·visited 상태는 지원하지 않습니다.
137
- - focus-visible ring은 `color-stroke-focusRing` semantic token과 2px outline·offset 구조값을 사용합니다.
138
- - 아이콘은 시각 장식으로 `aria-hidden="true"` 처리하며, icon-only의 accessible name을 제공할 때 Button의 `aria-label`을 사용합니다. `aria-label`은 필수로 강제하지 않습니다.
139
- - loading progress circle은 `800ms linear infinite` 회전하며 `prefers-reduced-motion: reduce`에서는 정지합니다.
124
+ - `loading`은 라벨과 아이콘을 시각적으로만 숨기고 accessible name을 유지합니다. native button은 `disabled`, link는 href 제거·`aria-disabled="true"`·`tabIndex={-1}`로 상호작용을 차단하며 `aria-busy="true"`를 적용합니다. `disabled`와 함께 지정하면 loading 시각을 우선합니다.
125
+ - loading 중 native button은 `disabled`가 되어 탭 순서에서 빠지므로 포커스를 잃을 수 있습니다. loading이 끝나면 포커스를 원래 Button이나 결과를 보여 주는 요소로 옮기고, 완료·실패 결과는 Button 밖의 `role="status"`(실패는 `role="alert"`) 영역으로 알립니다. 진행 중 문구는 live region에 넣지 않습니다.
126
+ - loading progress circle은 회전하며 `prefers-reduced-motion: reduce`에서는 정지합니다.
127
+ - `neutral/link`는 일반 `<a href>`를 렌더링하며 client router와 연결하지 않으므로 이동하면 문서 전체를 다시 불러옵니다. router의 `<Link>`로 감싸지 않습니다(`<a>`가 중첩됩니다). 내부 route 이동도 `href`만 지정하고, 새로고침이 문제가 되는 화면이면 완료 보고에 적습니다. SPA router 연동 방식은 `미결`입니다.
140
128
 
141
129
  ## 문서와 검증
142
130
 
143
- Button의 공개 API와 사용 맥락은 이 문서와 설치된 패키지의 공개 `spec.json`을 함께 기준으로 사용합니다. 공개 Props·variant·상태를 변경할 때는 디자인 시스템 담당자의 검토와 적절한 버전 변경이 필요합니다.
131
+ Button의 사용 기준은 이 문서이며, 같은 폴더의 `spec.json`은 같은 계약을 도구가 읽는 형식으로 담습니다(이름으로 쓰는 아이콘 목록은 `api.iconNames`). 필요한 prop·variant·상태가 없으면 우회하지 말고 멈춰 사용자에게 확인합니다.
132
+
133
+ 기존 코드를 한 번에 교체하지 않고 요청 범위에서 새로 쓰거나 고치는 코드부터 EDS `Button`을 적용합니다. 저장소에 있던 로컬 Button wrapper는 EDS `Button` contract로 표현할 수 없는 기능이 있을 때만 유지합니다.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "specVersion": 1,
3
- "packageVersion": "0.0.4",
3
+ "packageVersion": "0.1.1",
4
4
  "name": "Button",
5
5
  "purpose": "사용자가 즉시 실행할 수 있는 행동과 탐색을 표현하는 인터랙티브 요소입니다.",
6
6
  "usageContexts": [
@@ -8,12 +8,19 @@
8
8
  "다른 페이지나 콘텐츠로 이동하는 탐색에는 neutral/link branch를 사용합니다.",
9
9
  "brand는 화면에서 가장 중요한 행동을 강조할 때 사용합니다.",
10
10
  "neutral은 일반적인 액션과 탐색에 사용합니다.",
11
- "critical은 삭제·초기화처럼 되돌리기 어려운 행동에 사용합니다.",
12
- "icon-only의 접근성 이름을 제공하려면 aria-label을 지정합니다. aria-label은 필수로 강제하지 않습니다."
11
+ "critical은 삭제·초기화처럼 되돌리기 어려운 행동에 critical/weak로 사용하며, 가능하면 확인 절차를 함께 둡니다.",
12
+ "생성·저장·관리·확인처럼 화면의 핵심 행동에는 brand/solid를 사용합니다.",
13
+ "검색·조회·일반적인 주요 실행에는 neutral/solid를 사용합니다.",
14
+ "취소·닫기·수정 같은 보조 작업에는 neutral/outline을 사용합니다.",
15
+ "초기화·갱신처럼 부담이 낮은 반복 행동에는 neutral/weak를 사용합니다.",
16
+ "비동기 저장·확인에는 loading을 사용해 중복 실행을 막고 라벨 공간을 유지합니다.",
17
+ "표 행 조작·pagination처럼 공간이 좁은 곳에는 icon-only를 쓸 수 있습니다(accessible name은 accessibility.iconOnlyName).",
18
+ "icon은 생성·수정·삭제·외부 이동·다운로드처럼 행동의 목적이나 방향을 보조할 때 사용합니다."
13
19
  ],
14
20
  "unsupportedContexts": [
15
21
  "문서에 없는 role·variant·상태를 className이나 inline style로 우회하지 않습니다.",
16
- "상태 변경이나 일반 CTA를 neutral/link로 표현하지 않습니다."
22
+ "상태 변경이나 일반 CTA를 neutral/link로 표현하지 않습니다.",
23
+ "neutral/outline을 neutral/solid와 같은 액션 그룹에 두지 않습니다(neutral/solid 옆 취소 버튼의 조합은 미결)."
17
24
  ],
18
25
  "api": {
19
26
  "props": [
@@ -27,7 +34,7 @@
27
34
  "name": "role",
28
35
  "type": "ButtonRole",
29
36
  "required": true,
30
- "description": "시각적 역할입니다."
37
+ "description": "행동의 의미(brand·neutral·critical)를 나타내는 시각적 역할입니다. DOM의 ARIA role로 전달되지 않으며 ARIA role은 지정할 수 없습니다."
31
38
  },
32
39
  {
33
40
  "name": "variant",
@@ -40,20 +47,20 @@
40
47
  "type": "NativeButtonType",
41
48
  "required": true,
42
49
  "branch": "native button",
43
- "description": "native button의 동작 유형입니다."
50
+ "description": "native button의 동작 유형입니다. 폼 제출은 submit, 그 밖의 행동은 button을 지정하며 기본값은 없습니다."
44
51
  },
45
52
  {
46
53
  "name": "href",
47
54
  "type": "string",
48
55
  "required": true,
49
56
  "branch": "neutral/link",
50
- "description": "탐색 대상 주소입니다."
57
+ "description": "탐색 대상 주소입니다. 빈 문자열이나 공백만 있는 값은 render 오류입니다."
51
58
  },
52
59
  {
53
60
  "name": "children",
54
61
  "type": "ReactNode",
55
62
  "required": false,
56
- "description": "text label입니다."
63
+ "description": "text label입니다. iconOnly를 뺀 모든 slot에서 필수이며, 렌더링할 내용이 없으면(빈 문자열·공백·null) render 오류입니다. slot 조합은 slots를 따릅니다."
57
64
  },
58
65
  {
59
66
  "name": "icon",
@@ -95,7 +102,7 @@
95
102
  "name": "aria-label",
96
103
  "type": "string",
97
104
  "required": false,
98
- "description": "accessible name입니다. icon-only의 접근성 이름을 제공할 때 사용합니다."
105
+ "description": "accessible name입니다. 지정하면 비어 있지 않아야 하며, 빈 문자열이나 공백은 render 오류입니다. icon-only에서는 accessibility.iconOnlyName을 따릅니다."
99
106
  },
100
107
  {
101
108
  "name": "disabled",
@@ -107,13 +114,13 @@
107
114
  "name": "loading",
108
115
  "type": "boolean",
109
116
  "required": false,
110
- "description": "처리 중 상태입니다."
117
+ "description": "비동기 처리 중 상태입니다. aria-busy를 적용하고 상호작용을 막으며 label·아이콘 공간과 accessible name을 유지합니다."
111
118
  },
112
119
  {
113
120
  "name": "className",
114
121
  "type": "string",
115
122
  "required": false,
116
- "description": "배치·컨테이너 보정용 class입니다."
123
+ "description": "바깥 배치(margin·정렬·grid 위치)용 class입니다."
117
124
  },
118
125
  {
119
126
  "name": "ref",
@@ -155,8 +162,57 @@
155
162
  "iconFirst": "children과 startIcon 또는 startIconName을 제공합니다.",
156
163
  "iconLast": "children과 endIcon 또는 endIconName을 제공합니다.",
157
164
  "iconBoth": "children과 앞·뒤 아이콘을 모두 제공합니다.",
158
- "iconOnly": "children 없이 icon 또는 iconName을 제공합니다. 접근성 이름이 필요하면 aria-label을 지정합니다."
159
- },
165
+ "iconOnly": "children 없이 icon 또는 iconName을 제공합니다. 이름은 accessibility.iconOnlyName을 따릅니다."
166
+ },
167
+ "iconNames": [
168
+ "ArrowLeft",
169
+ "ArrowRight",
170
+ "Check",
171
+ "ChevronDown",
172
+ "ChevronLeft",
173
+ "ChevronRight",
174
+ "ChevronUp",
175
+ "ChevronsUpDown",
176
+ "Circle",
177
+ "CircleAlert",
178
+ "CircleCheckBig",
179
+ "CirclePause",
180
+ "CirclePlay",
181
+ "CircleQuestionMark",
182
+ "ClockAlert",
183
+ "Copy",
184
+ "Eye",
185
+ "EyeOff",
186
+ "FaceNeutral",
187
+ "FileDown",
188
+ "FilePenLine",
189
+ "FileX",
190
+ "FolderClock",
191
+ "Fullscreen",
192
+ "ImageOff",
193
+ "ImagePlus",
194
+ "Info",
195
+ "Key",
196
+ "Languages",
197
+ "Layers2",
198
+ "List",
199
+ "LoaderCircle",
200
+ "LockOpen",
201
+ "Mail",
202
+ "Megaphone",
203
+ "Minimize",
204
+ "Minus",
205
+ "Moon",
206
+ "Paperclip",
207
+ "Plus",
208
+ "RotateCw",
209
+ "Search",
210
+ "Sun",
211
+ "ThumbsUp",
212
+ "Trash",
213
+ "VectorSquare",
214
+ "X"
215
+ ],
160
216
  "typeExports": [
161
217
  "ButtonProps",
162
218
  "ButtonComponent",
@@ -169,127 +225,9 @@
169
225
  ]
170
226
  },
171
227
  "accessibility": {
172
- "nativeButtonType": "native button branch에서는 type을 필수로 지정합니다.",
173
- "linkHref": "neutral/link branch에서는 href를 필수로 지정합니다.",
174
- "iconOnlyName": "icon-only layout에서는 접근성 이름을 제공할 때 비어 있지 않은 aria-label을 지정합니다. aria-label은 필수가 아닙니다.",
175
- "disabledLink": "비활성 link는 href를 제거하고 aria-disabled와 tabIndex를 적용합니다.",
176
- "loading": "loading 중에는 aria-busy를 적용하고 상호작용을 차단합니다.",
177
- "focusVisible": "keyboard focus에는 승인된 focus ring을 표시합니다."
228
+ "iconOnlyName": "icon-only의 아이콘은 aria-hidden이므로 항상 비어 있지 않은 aria-label 또는 aria-labelledby로 이름을 지정합니다. 타입이 이를 요구하지 않아 빠뜨려도 typecheck가 통과하며, 빈 aria-label은 render 오류입니다.",
229
+ "loading": "loading 중에도 label·아이콘 공간과 accessible name을 유지합니다. native button은 disabled가 되어 포커스를 잃을 수 있으므로, 끝나면 포커스를 원래 Button이나 결과 요소로 옮기고 결과는 Button 밖의 status·alert 영역으로 알립니다."
178
230
  },
179
- "semanticTokens": [
180
- {
181
- "name": "color-bg-brand-primary",
182
- "usage": "brand solid의 기본 배경입니다."
183
- },
184
- {
185
- "name": "color-bg-brand-primary-hover",
186
- "usage": "brand solid의 hover 배경입니다."
187
- },
188
- {
189
- "name": "color-bg-critical-weak",
190
- "usage": "critical weak의 기본 배경입니다."
191
- },
192
- {
193
- "name": "color-bg-critical-weak-hover",
194
- "usage": "critical weak의 hover 배경입니다."
195
- },
196
- {
197
- "name": "color-bg-disabled",
198
- "usage": "비활성 배경입니다."
199
- },
200
- {
201
- "name": "color-bg-neutral-inverted",
202
- "usage": "neutral solid의 기본 배경입니다."
203
- },
204
- {
205
- "name": "color-bg-neutral-inverted-hover",
206
- "usage": "neutral solid의 hover 배경입니다."
207
- },
208
- {
209
- "name": "color-bg-neutral-weak",
210
- "usage": "neutral weak의 기본 배경입니다."
211
- },
212
- {
213
- "name": "color-bg-neutral-weak-hover",
214
- "usage": "neutral weak의 hover 배경입니다."
215
- },
216
- {
217
- "name": "color-bg-transparent",
218
- "usage": "outline·ghost·link의 투명 배경입니다."
219
- },
220
- {
221
- "name": "color-bg-transparent-hover",
222
- "usage": "outline·ghost·link의 hover 배경입니다."
223
- },
224
- {
225
- "name": "color-fg-brand",
226
- "usage": "brand outline의 전경입니다."
227
- },
228
- {
229
- "name": "color-fg-critical",
230
- "usage": "critical weak의 전경입니다."
231
- },
232
- {
233
- "name": "color-fg-disabled",
234
- "usage": "비활성 전경입니다."
235
- },
236
- {
237
- "name": "color-fg-neutral",
238
- "usage": "neutral outline·ghost·link의 전경입니다."
239
- },
240
- {
241
- "name": "color-fg-neutral-inverted",
242
- "usage": "solid variant의 대비 전경입니다."
243
- },
244
- {
245
- "name": "color-stroke-focusRing",
246
- "usage": "focus-visible ring의 전경입니다."
247
- },
248
- {
249
- "name": "color-stroke-neutral-secondary",
250
- "usage": "neutral outline의 경계선입니다."
251
- },
252
- {
253
- "name": "typography-body3-fontFamily",
254
- "usage": "xs label의 글꼴입니다."
255
- },
256
- {
257
- "name": "typography-body3-fontSize",
258
- "usage": "xs label의 글꼴 크기입니다."
259
- },
260
- {
261
- "name": "typography-body3-fontWeight",
262
- "usage": "xs label의 글꼴 두께입니다."
263
- },
264
- {
265
- "name": "typography-body3-letterSpacing",
266
- "usage": "xs label의 자간입니다."
267
- },
268
- {
269
- "name": "typography-body3-lineHeight",
270
- "usage": "xs label의 행간입니다."
271
- },
272
- {
273
- "name": "typography-title3-fontFamily",
274
- "usage": "sm·md label의 글꼴입니다."
275
- },
276
- {
277
- "name": "typography-title3-fontSize",
278
- "usage": "sm·md label의 글꼴 크기입니다."
279
- },
280
- {
281
- "name": "typography-title3-fontWeight",
282
- "usage": "sm·md label의 글꼴 두께입니다."
283
- },
284
- {
285
- "name": "typography-title3-letterSpacing",
286
- "usage": "sm·md label의 자간입니다."
287
- },
288
- {
289
- "name": "typography-title3-lineHeight",
290
- "usage": "sm·md label의 행간입니다."
291
- }
292
- ],
293
231
  "unsupportedProps": [
294
232
  {
295
233
  "name": "style",