@spectrabrain/eds 0.1.0 → 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.
- package/AI.md +13 -63
- package/AI_CONTEXT.json +26 -95
- package/CHANGELOG.md +66 -4
- package/CONSUMER_AGENT.md +86 -164
- package/FRONTEND_CONVENTIONS.md +252 -0
- package/QUICKSTART.md +24 -29
- package/README.md +24 -73
- package/TAILWIND_V4.md +49 -184
- package/TOKEN_CONTEXT.md +63 -36
- package/consumer/align/templates/components.json +21 -0
- package/consumer/align/templates/env.mock +1 -0
- package/consumer/align/templates/eslint.config.js +35 -0
- package/consumer/align/templates/package.scripts.json +10 -0
- package/consumer/align/templates/prettierignore +2 -0
- package/consumer/align/templates/prettierrc.json +7 -0
- package/consumer/align/templates/src/app/App.tsx +16 -0
- package/consumer/align/templates/src/app/main.tsx +27 -0
- package/consumer/align/templates/src/app/providers/ErrorBoundary.tsx +58 -0
- package/consumer/align/templates/src/app/providers/ErrorHandler.tsx +29 -0
- package/consumer/align/templates/src/app/router/index.tsx +23 -0
- package/consumer/align/templates/src/app/styles/index.css +2 -0
- package/consumer/align/templates/src/mocks/browser.ts +5 -0
- package/consumer/align/templates/src/mocks/handlers.ts +4 -0
- package/consumer/align/templates/src/shared/api/index.ts +23 -0
- package/consumer/align/templates/src/shared/api/queryClient.ts +24 -0
- package/consumer/align/templates/src/shared/lib/eventBus.ts +44 -0
- package/consumer/align/templates/src/shared/lib/utils/index.ts +6 -0
- package/consumer/align/templates/src/vite-env.d.ts +2 -0
- package/consumer/align/templates/tsconfig.json +22 -0
- package/consumer/align/templates/vite.config.babel.ts +36 -0
- package/consumer/align/templates/vite.config.rolldown.ts +35 -0
- package/consumer/align-analysis.mjs +1159 -0
- package/consumer/align-plan.mjs +377 -0
- package/consumer/align-rules.mjs +473 -0
- package/consumer/align.mjs +401 -0
- package/consumer/cli.mjs +90 -40
- package/consumer/css-root.mjs +24 -4
- package/consumer/doctor.mjs +47 -21
- package/consumer/init.mjs +39 -25
- package/consumer/managed-block.mjs +11 -5
- package/consumer/paths.mjs +69 -14
- package/consumer/register-scripts.mjs +17 -12
- package/consumer/scan-classes.mjs +4 -3
- package/consumer/sync.mjs +35 -13
- package/consumer/update-agent-context.mjs +63 -7
- package/consumer-postinstall.mjs +8 -7
- package/dist/components/Button/README.md +66 -95
- package/dist/components/Button/spec.json +19 -138
- package/dist/components.css +102 -366
- package/dist/index.d.mts +20 -18
- package/dist/index.d.ts +20 -18
- package/dist/tokens.d.mts +3 -0
- package/dist/tokens.d.ts +3 -0
- package/llms.txt +20 -14
- package/package.json +3 -1
|
@@ -1,17 +1,21 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { applyBlock, locateBlock, maskFencedBlocks, readBlockVersion, readTemplateBlock, renderBlock } from "./managed-block.mjs";
|
|
3
|
-
import { detectEol, readTextIfExists, realpathOrNull, relativeTo, withEol } from "./paths.mjs";
|
|
4
|
+
import { detectEol, findGitRoot, readTextIfExists, realpathOrNull, relativeTo, toPosix, withEol } from "./paths.mjs";
|
|
4
5
|
|
|
5
6
|
export const claudeBeginMarker = "<!-- EDS:CLAUDE:BEGIN -->";
|
|
6
7
|
export const claudeEndMarker = "<!-- EDS:CLAUDE:END -->";
|
|
7
8
|
export const claudeImportLine = "@AGENTS.md";
|
|
9
|
+
export const claudeNestedImportLine = "@../AGENTS.md";
|
|
8
10
|
export const geminiBeginMarker = "<!-- EDS:GEMINI:BEGIN -->";
|
|
9
11
|
export const geminiEndMarker = "<!-- EDS:GEMINI:END -->";
|
|
10
12
|
// Gemini CLI documents its import syntax with an explicit relative path.
|
|
11
13
|
export const geminiImportLine = "@./AGENTS.md";
|
|
12
14
|
|
|
13
|
-
// Claude Code and Gemini CLI accept `@AGENTS.md
|
|
14
|
-
|
|
15
|
+
// Claude Code and Gemini CLI accept `@AGENTS.md`, `@./AGENTS.md` and (from .claude/) `@../AGENTS.md` and ignore fenced
|
|
16
|
+
// code blocks.
|
|
17
|
+
const importPattern = /^[ \t]*@(?:\.\.?\/)?AGENTS\.md[ \t]*$/m;
|
|
18
|
+
const endMarkerText = "<!-- EDS:END -->";
|
|
15
19
|
|
|
16
20
|
export function planAgentContext({ consumerRoot, pkg, packageManager = null }) {
|
|
17
21
|
const filePath = path.join(consumerRoot, "AGENTS.md");
|
|
@@ -59,18 +63,20 @@ function planImportFile({ consumerRoot, fileName, label, beginMarker, endMarker,
|
|
|
59
63
|
const after = hasImport(before, beginMarker, endMarker)
|
|
60
64
|
? before
|
|
61
65
|
: (trimmed ? trimmed + eol + eol + block + eol : block + eol);
|
|
62
|
-
return { label, path: filePath, relativePath: relativeTo(consumerRoot, filePath), before, after };
|
|
66
|
+
return { label, path: filePath, relativePath: relativeTo(consumerRoot, filePath), importLine, before, after };
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
// Claude Code reads CLAUDE.md
|
|
69
|
+
// Claude Code reads CLAUDE.md or .claude/CLAUDE.md instead of AGENTS.md when either exists, so the import goes into
|
|
70
|
+
// whichever the consumer has (CLAUDE.md first). Imports resolve relative to the importing file.
|
|
66
71
|
export function planClaudeImport({ consumerRoot }) {
|
|
72
|
+
const nested = !fs.existsSync(path.join(consumerRoot, "CLAUDE.md")) && fs.existsSync(path.join(consumerRoot, ".claude", "CLAUDE.md"));
|
|
67
73
|
return planImportFile({
|
|
68
74
|
consumerRoot,
|
|
69
|
-
fileName: "CLAUDE.md",
|
|
75
|
+
fileName: nested ? path.join(".claude", "CLAUDE.md") : "CLAUDE.md",
|
|
70
76
|
label: "claude",
|
|
71
77
|
beginMarker: claudeBeginMarker,
|
|
72
78
|
endMarker: claudeEndMarker,
|
|
73
|
-
importLine: claudeImportLine,
|
|
79
|
+
importLine: nested ? claudeNestedImportLine : claudeImportLine,
|
|
74
80
|
});
|
|
75
81
|
}
|
|
76
82
|
|
|
@@ -85,3 +91,53 @@ export function planGeminiImport({ consumerRoot }) {
|
|
|
85
91
|
importLine: geminiImportLine,
|
|
86
92
|
});
|
|
87
93
|
}
|
|
94
|
+
|
|
95
|
+
const codexBudgetBytes = 32 * 1024;
|
|
96
|
+
|
|
97
|
+
// Where the block may not reach an agent even though AGENTS.md is synced. Notes never change an exit code.
|
|
98
|
+
export function planDeliveryNotes({ consumerRoot, agents, packageManager = null }) {
|
|
99
|
+
const notes = [];
|
|
100
|
+
if (packageManager?.pnp) {
|
|
101
|
+
notes.push(
|
|
102
|
+
"Yarn Plug'n'Play detected: the EDS docs the AGENTS.md block points to are not in node_modules/@spectrabrain/eds, " +
|
|
103
|
+
"and EDS support for Plug'n'Play is undecided. Ask the repository owner before changing nodeLinker.",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const exists = (relativePath) => fs.existsSync(path.join(consumerRoot, relativePath));
|
|
107
|
+
const hasClaude = exists("CLAUDE.md") || exists(path.join(".claude", "CLAUDE.md"));
|
|
108
|
+
if (!hasClaude && exists("CLAUDE.local.md")) {
|
|
109
|
+
notes.push("CLAUDE.local.md exists but CLAUDE.md does not, so Claude Code reads CLAUDE.local.md instead of AGENTS.md; add a CLAUDE.md and run sync again (sync adds the import).");
|
|
110
|
+
} else {
|
|
111
|
+
// Claude Code is named whenever it has no file. Gemini CLI only rides along then: a repository that set up
|
|
112
|
+
// CLAUDE.md and no GEMINI.md most likely does not use Gemini, and a note on every run would be noise.
|
|
113
|
+
const missing = hasClaude ? [] : ["Claude Code (CLAUDE.md)", ...(exists("GEMINI.md") ? [] : ["Gemini CLI (GEMINI.md)"])];
|
|
114
|
+
if (missing.length > 0) {
|
|
115
|
+
notes.push(
|
|
116
|
+
"no instruction file for " + missing.join(" or ") + " here, so " + (missing.length > 1 ? "they" : "it") +
|
|
117
|
+
" may not load the EDS block in AGENTS.md (Codex reads AGENTS.md). If the team uses " + (missing.length > 1 ? "them" : "it") +
|
|
118
|
+
", create that file and run sync again; sync adds the import.",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (exists("AGENTS.override.md")) {
|
|
123
|
+
notes.push("AGENTS.override.md exists; Codex reads it instead of AGENTS.md, so copy or import the EDS block there.");
|
|
124
|
+
}
|
|
125
|
+
const endIndex = agents?.after?.indexOf(endMarkerText) ?? -1;
|
|
126
|
+
if (endIndex >= 0 && Buffer.byteLength(agents.after.slice(0, endIndex), "utf8") > codexBudgetBytes) {
|
|
127
|
+
notes.push("the EDS block in AGENTS.md ends after the first 32 KiB, where Codex truncates by default; move the block toward the top (sync keeps it where it is).");
|
|
128
|
+
}
|
|
129
|
+
const gitRoot = findGitRoot(consumerRoot);
|
|
130
|
+
const packageRelative = gitRoot === null ? "" : toPosix(path.relative(gitRoot, consumerRoot));
|
|
131
|
+
if (packageRelative !== "") {
|
|
132
|
+
const pointer = packageRelative + "/AGENTS.md";
|
|
133
|
+
const rootMentions = ["AGENTS.md", "CLAUDE.md"].some((fileName) => (readTextIfExists(path.join(gitRoot, fileName)) ?? "").includes(pointer));
|
|
134
|
+
if (!rootMentions) {
|
|
135
|
+
notes.push(
|
|
136
|
+
"this package is " + packageRelative + " inside the repository; agents started at the repository root do not load " + pointer +
|
|
137
|
+
" by default. Add to the root AGENTS.md or CLAUDE.md: \"Before working in " + packageRelative + ", read " + pointer +
|
|
138
|
+
" and run its commands from " + packageRelative + ".\"",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return notes;
|
|
143
|
+
}
|
package/consumer-postinstall.mjs
CHANGED
|
@@ -12,7 +12,7 @@ if (!consumerRoot || path.resolve(consumerRoot) === packageRoot) {
|
|
|
12
12
|
|
|
13
13
|
const consumerPackagePath = path.join(consumerRoot, "package.json");
|
|
14
14
|
if (!fs.existsSync(consumerPackagePath)) {
|
|
15
|
-
console.log("[
|
|
15
|
+
console.log("[EDS] Consumer package.json was not found; run npx @spectrabrain/eds init in the package that depends on @spectrabrain/eds.");
|
|
16
16
|
process.exit(0);
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -26,13 +26,13 @@ function realpathOrNull(filePath) {
|
|
|
26
26
|
|
|
27
27
|
const installedPath = realpathOrNull(path.join(consumerRoot, "node_modules", "@spectrabrain", "eds"));
|
|
28
28
|
if (installedPath === null || installedPath !== realpathOrNull(packageRoot)) {
|
|
29
|
-
console.log("[
|
|
29
|
+
console.log("[EDS] Skipping consumer setup: this install is not the consumer's node_modules/@spectrabrain/eds. Run npx @spectrabrain/eds init in the package that depends on @spectrabrain/eds.");
|
|
30
30
|
process.exit(0);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// pnpm rewrites the consumer package.json after dependency lifecycle scripts, so edits made here are lost.
|
|
34
34
|
if (String(process.env.npm_config_user_agent ?? "").startsWith("pnpm/")) {
|
|
35
|
-
console.log("[
|
|
35
|
+
console.log("[EDS] pnpm discards package.json edits made during install; run pnpm exec eds init to register the EDS scripts.");
|
|
36
36
|
process.exit(0);
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -40,20 +40,21 @@ let consumerPackage;
|
|
|
40
40
|
try {
|
|
41
41
|
consumerPackage = JSON.parse(fs.readFileSync(consumerPackagePath, "utf8"));
|
|
42
42
|
} catch (error) {
|
|
43
|
-
console.log("[
|
|
43
|
+
console.log("[EDS] Consumer package.json could not be parsed (" + error.message + "); run npx @spectrabrain/eds init after fixing it.");
|
|
44
44
|
process.exit(0);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// A workspaces root hoists the package but is not the app that uses it: registering the scripts and hooks
|
|
48
48
|
// there would make the root build look for src/styles/eds-tokens.css and AGENTS.md in the wrong place.
|
|
49
49
|
if (consumerPackage.workspaces !== undefined) {
|
|
50
|
-
console.log("[
|
|
50
|
+
console.log("[EDS] Skipping consumer setup: package.json declares workspaces. Run npx @spectrabrain/eds init inside the app package that depends on @spectrabrain/eds.");
|
|
51
51
|
process.exit(0);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// The install must never fail because of this setup step; anything unexpected is reported and left to eds doctor.
|
|
55
55
|
try {
|
|
56
|
-
registerConsumerScripts(consumerRoot);
|
|
56
|
+
const result = registerConsumerScripts(consumerRoot);
|
|
57
|
+
console.log("[EDS] Run " + result.syncRunCommand + " to finish setup.");
|
|
57
58
|
} catch (error) {
|
|
58
|
-
console.log("[
|
|
59
|
+
console.log("[EDS] Consumer setup was skipped (" + (error instanceof Error ? error.message : String(error)) + "). Run npx @spectrabrain/eds init to register the EDS scripts.");
|
|
59
60
|
}
|
|
@@ -1,31 +1,47 @@
|
|
|
1
|
-
# Button
|
|
1
|
+
# Button
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
패키지 설치와 공개 경로는 루트 [README.md](../../../README.md)를, 소비처 AI의 필수 규칙은 루트 [QUICKSTART.md](../../../QUICKSTART.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>`으로 표현합니다.
|
|
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`을 타입으로 강제하지 않으므로 마지막 예시처럼 직접 지정합니다.
|
|
19
|
+
|
|
20
|
+
## 사용 맥락
|
|
21
|
+
|
|
22
|
+
### Role과 Variant
|
|
12
23
|
|
|
13
|
-
화면에서
|
|
24
|
+
`role`은 색상 취향이 아니라 버튼이 수행하는 행동의 의미입니다. `brand`는 브랜드와 관련되거나 화면에서 가장 중요한 행동, `neutral`은 `brand`보다 낮은 주목도가 필요한 일반적인 액션과 탐색, `critical`은 삭제·초기화처럼 되돌리기 어려운 위험한 행동입니다. `role`은 시각적 prop이며 DOM의 ARIA `role` 속성으로 전달하지 않고, ARIA role을 따로 지정하는 방법은 없습니다. `variant`는 같은 role 안의 시각적 위계입니다.
|
|
14
25
|
|
|
15
|
-
| 행동
|
|
26
|
+
| 행동 | 조합 | 설명 |
|
|
16
27
|
| --- | --- | --- |
|
|
17
28
|
| 생성·저장·관리·확인처럼 화면의 핵심 행동 | `brand/solid` | 사용자가 완료해야 할 대표 행동을 강조합니다. 한 화면에 하나를 권장합니다. |
|
|
18
|
-
|
|
|
29
|
+
| `brand/solid`보다 낮은 위계의 브랜드 보조 행동 | `brand/outline` | 브랜드 맥락의 보조 행동입니다. |
|
|
30
|
+
| 검색·조회·일반적인 주요 실행 | `neutral/solid` | `brand/solid` 다음 위계의 주요 CTA입니다. 한 화면에 하나를 권장합니다. |
|
|
19
31
|
| 취소·닫기·수정 같은 보조 작업 | `neutral/outline` | 주 행동과 시각적 위계를 분리합니다. |
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
32
|
+
| 초기화·갱신처럼 부담이 낮은 반복 행동 | `neutral/weak` | CTA를 보조하면서 반복 가능한 행동을 표현합니다. 되돌리기 어려운 초기화는 `critical/weak`입니다. |
|
|
33
|
+
| 툴바·헤더·옵션 메뉴처럼 주변 UI와의 간섭을 줄여야 하는 보조 행동 | `neutral/ghost` | 배경 없이 보조 행동을 표현합니다. |
|
|
34
|
+
| 다른 페이지·콘텐츠로 이동하거나 관련 정보를 확인 | `neutral/link` | 탐색에만 사용하며 상태 변경이나 일반 CTA에는 쓰지 않습니다. `href`가 필요하며 `<a>`로 렌더링합니다. |
|
|
35
|
+
| 되돌리기 어려운 삭제·초기화 | `critical/weak` | 위험을 분명히 전달합니다. 가능하면 소비처의 확인 대화상자(WAI-ARIA `alertdialog` 패턴)로 한 번 더 확인받습니다. |
|
|
23
36
|
|
|
24
|
-
기존
|
|
37
|
+
새 코드는 기존 화면의 조합을 따르지 않고 위 표의 행동 기준으로 조합을 고릅니다. 비동기 처리 중에는 선택한 조합에 `loading`을 지정합니다. 툴바·헤더 안의 보조 행동이 `neutral/outline`과 `neutral/ghost`에 모두 해당할 때의 우선순위는 `미결`입니다.
|
|
25
38
|
|
|
26
|
-
|
|
39
|
+
같은 액션 그룹(함께 놓이는 버튼 묶음)의 조합은 다음을 따릅니다.
|
|
27
40
|
|
|
28
|
-
|
|
41
|
+
- `brand/outline`은 같은 액션 그룹에서 `brand/solid`와 함께 쓰지 않는 것을 권장합니다.
|
|
42
|
+
- `neutral/outline`은 `neutral/solid`와 같은 액션 그룹에 두지 않습니다.
|
|
43
|
+
- `brand/outline`과 `neutral/outline`은 함께 둘 수 있습니다.
|
|
44
|
+
- `neutral/solid` 옆에 두는 취소 버튼의 조합은 `미결`입니다.
|
|
29
45
|
|
|
30
46
|
### Size
|
|
31
47
|
|
|
@@ -37,67 +53,36 @@
|
|
|
37
53
|
| `sm` | 일반적인 화면 액션에 사용합니다. |
|
|
38
54
|
| `md` | 공간이 충분하거나 중요한 행동을 명확히 강조해야 할 때 사용합니다. |
|
|
39
55
|
|
|
40
|
-
### Layout
|
|
56
|
+
### Layout과 아이콘
|
|
41
57
|
|
|
42
|
-
Layout은
|
|
58
|
+
Layout은 prop으로 지정하지 않고 슬롯 조합에서 추론합니다. 아이콘은 생성·수정·삭제·외부 이동·다운로드처럼 행동의 목적이나 방향을 보조할 때만 사용하고, 표 행 조작이나 pagination처럼 공간이 좁은 곳에서는 `iconOnly`를 사용할 수 있습니다.
|
|
43
59
|
|
|
44
|
-
| layout |
|
|
60
|
+
| layout | 슬롯 조합 | 사용 맥락 |
|
|
45
61
|
| --- | --- | --- |
|
|
46
|
-
| `textOnly` |
|
|
47
|
-
| `iconFirst` |
|
|
48
|
-
| `iconLast` |
|
|
49
|
-
| `iconBoth` |
|
|
50
|
-
| `iconOnly` |
|
|
51
|
-
|
|
52
|
-
### Role
|
|
53
|
-
|
|
54
|
-
Role은 색상 취향이 아니라 버튼이 수행하는 행동의 의미를 정의합니다. `role`은 시각적 prop이며 DOM의 ARIA `role` 속성으로 전달하지 않습니다.
|
|
55
|
-
|
|
56
|
-
| role | 사용 맥락 |
|
|
57
|
-
| --- | --- |
|
|
58
|
-
| `brand` | 브랜드와 관련되거나 화면에서 가장 중요한 행동을 강조할 때 사용합니다. |
|
|
59
|
-
| `neutral` | 일반적인 액션과 탐색에 사용합니다. `brand`보다 낮은 주목도가 필요할 때 선택합니다. |
|
|
60
|
-
| `critical` | 삭제·초기화처럼 오류나 되돌리기 어려운 위험한 행동을 나타낼 때 사용합니다. 가능하면 확인 절차와 함께 사용합니다. |
|
|
61
|
-
|
|
62
|
-
### Variant
|
|
63
|
-
|
|
64
|
-
Variant는 같은 Role 안에서 시각적 위계를 정의합니다.
|
|
65
|
-
|
|
66
|
-
| role / variant | 사용 맥락 |
|
|
67
|
-
| --- | --- |
|
|
68
|
-
| `brand/solid` | 서비스의 핵심 행동에 사용합니다. 한 화면에 하나를 권장합니다. |
|
|
69
|
-
| `brand/outline` | `brand/solid`보다 낮은 위계의 브랜드 보조 행동에 사용합니다. `neutral/outline`과 조합할 수 있으며 같은 액션 그룹에서 `brand/solid`와 함께 사용하지 않는 것을 권장합니다. |
|
|
70
|
-
| `neutral/solid` | `brand/solid` 다음 위계의 일반적인 주요 CTA에 사용합니다. 한 화면에 하나를 권장합니다. |
|
|
71
|
-
| `neutral/weak` | 초기화·갱신·되돌리기처럼 부담이 낮은 반복 행동에 사용합니다. 취소·닫기·수정은 `neutral/outline`을 사용합니다. |
|
|
72
|
-
| `neutral/outline` | 취소·닫기·수정처럼 주 행동과 위계를 분리하는 보조 행동에 사용합니다. `neutral/solid`과 같은 액션 그룹에서 함께 사용하지 않으며, `brand/outline`과 조합할 수 있습니다. |
|
|
73
|
-
| `neutral/ghost` | 툴바·헤더·옵션 메뉴처럼 주변 UI와의 간섭을 줄여야 하는 보조 행동에 사용합니다. |
|
|
74
|
-
| `neutral/link` | 다른 페이지·콘텐츠로 이동하거나 관련 정보를 확인하는 탐색성 행동에 사용합니다. 상태 변경이나 일반 CTA에는 사용하지 않습니다. `href`가 필요하며 `<a>`로 렌더링합니다. |
|
|
75
|
-
| `critical/weak` | 삭제·초기화처럼 되돌리기 어려운 행동에 사용합니다. 위험을 분명히 전달하며 가능하면 확인 대화상자나 Alert Dialog와 함께 사용합니다. |
|
|
62
|
+
| `textOnly` | `children`만 지정합니다. | |
|
|
63
|
+
| `iconFirst` | `children`과 `startIcon` 또는 `startIconName`을 지정합니다. | 아이콘이 행동의 목적을 보조할 때 사용합니다. |
|
|
64
|
+
| `iconLast` | `children`과 `endIcon` 또는 `endIconName`을 지정합니다. | 이동 방향이나 다음 단계를 나타낼 때 사용합니다. |
|
|
65
|
+
| `iconBoth` | `children`과 앞·뒤 아이콘을 모두 지정합니다. | 양쪽 아이콘이 모두 의미를 더하는 명확한 경우에만 사용합니다. |
|
|
66
|
+
| `iconOnly` | `children` 없이 `icon` 또는 `iconName`과 `aria-label`을 지정합니다. | |
|
|
76
67
|
|
|
77
|
-
|
|
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 추가 후보로 적습니다.
|
|
78
72
|
|
|
79
73
|
### State
|
|
80
74
|
|
|
81
|
-
|
|
82
|
-
| --- | --- |
|
|
83
|
-
| enabled | 사용자가 실행할 수 있는 기본 상태입니다. |
|
|
84
|
-
| hover | 포인터가 버튼 위에 있을 때의 상태이며 CSS `:hover`로 처리합니다. |
|
|
85
|
-
| loading | 동작을 처리하는 중인 상태입니다. 라벨·아이콘 공간을 유지한 progress circle을 표시하고 상호작용을 차단합니다. |
|
|
86
|
-
| disabled | 사용자가 실행할 수 없는 상태입니다. |
|
|
87
|
-
|
|
88
|
-
`active`, `pressed`, `visited` 등 문서에 없는 상태는 추가하지 않습니다.
|
|
75
|
+
상태는 `loading`과 `disabled`만 prop으로 지정합니다. hover와 focus-visible은 컴포넌트가 처리하며, `active`, `pressed`, `visited` 상태는 제공하지 않습니다. `loading`은 라벨·아이콘 공간을 유지한 progress circle을 표시하고 상호작용을 차단합니다.
|
|
89
76
|
|
|
90
77
|
## 공개 API contract
|
|
91
78
|
|
|
92
|
-
다음 props
|
|
79
|
+
모든 Button에 다음 props가 필수입니다.
|
|
93
80
|
|
|
94
81
|
| prop | 허용 값 | 설명 |
|
|
95
82
|
| --- | --- | --- |
|
|
96
83
|
| `size` | `xs`, `sm`, `md` | 버튼 크기입니다. |
|
|
97
84
|
| `role` | `brand`, `neutral`, `critical` | 시각적 역할이며 DOM ARIA `role`로 전달하지 않습니다. |
|
|
98
85
|
| `variant` | role별 아래 표 | 역할에 종속된 표현입니다. |
|
|
99
|
-
| `type` | `button`, `submit`, `reset` | native button branch에서 필수입니다. |
|
|
100
|
-
| `href` | `string` | `neutral/link` branch에서 필수이며 `<a>`로 렌더링합니다. |
|
|
101
86
|
|
|
102
87
|
허용하는 role·variant 조합은 다음과 같습니다.
|
|
103
88
|
|
|
@@ -107,56 +92,42 @@ Variant는 같은 Role 안에서 시각적 위계를 정의합니다.
|
|
|
107
92
|
| `neutral` | `solid`, `weak`, `outline`, `ghost`, `link` |
|
|
108
93
|
| `critical` | `weak` |
|
|
109
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
|
+
|
|
110
100
|
### API reference
|
|
111
101
|
|
|
112
102
|
| prop | 타입·규칙 |
|
|
113
103
|
| --- | --- |
|
|
114
|
-
| `children` | `ReactNode` label입니다. 렌더링 가능한 내용이 하나 이상 필요하며 `startIcon`·`endIcon`과 조합할 수 있습니다. |
|
|
115
|
-
| `icon` | icon-only용 단일 custom `ReactElement`입니다. children이나 start/end
|
|
116
|
-
| `iconName` | icon-only용 `ButtonIconName` lucide 이름입니다.
|
|
104
|
+
| `children` | `ReactNode` label입니다. 렌더링 가능한 내용이 하나 이상 필요하며 한 줄로 표시합니다. `startIcon`·`endIcon`과 조합할 수 있습니다. |
|
|
105
|
+
| `icon` | icon-only용 단일 custom `ReactElement`입니다. children이나 start/end 아이콘과 함께 사용하지 않습니다. |
|
|
106
|
+
| `iconName` | icon-only용 `ButtonIconName` lucide 이름입니다. |
|
|
117
107
|
| `startIcon` / `endIcon` | label 앞·뒤의 단일 custom `ReactElement` 슬롯입니다. |
|
|
118
|
-
| `startIconName` / `endIconName` | label 앞·뒤의 `ButtonIconName` lucide 이름 슬롯입니다.
|
|
119
|
-
| `aria-label` | 선택적인 `string
|
|
108
|
+
| `startIconName` / `endIconName` | label 앞·뒤의 `ButtonIconName` lucide 이름 슬롯입니다. |
|
|
109
|
+
| `aria-label` | 선택적인 `string`이며, 지정하면 비어 있지 않아야 합니다. icon-only에서는 항상 지정합니다. |
|
|
120
110
|
| `disabled` | `boolean`입니다. native button은 `disabled`, link는 href 제거·`aria-disabled="true"`·`tabIndex={-1}`를 적용합니다. |
|
|
121
|
-
| `loading` | `boolean`입니다. label·아이콘 공간을 유지한 progress circle을 표시하고 상호작용을
|
|
122
|
-
| `className` | `string
|
|
111
|
+
| `loading` | `boolean`입니다. label·아이콘 공간을 유지한 progress circle을 표시하고 상호작용을 차단하며 `aria-busy="true"`를 적용합니다. |
|
|
112
|
+
| `className` | `string`입니다. 허용 범위는 `QUICKSTART.md` 규칙 5를 따릅니다. |
|
|
113
|
+
| `style` | 지원하지 않습니다(`never`). 스타일 override는 공개 API가 아닙니다. |
|
|
123
114
|
| `aria-*` / `data-*` | 각 branch의 native HTML 속성으로 전달할 수 있습니다. |
|
|
124
115
|
| `ref` | branch에 따라 `HTMLButtonElement` 또는 `HTMLAnchorElement`를 전달합니다. |
|
|
125
116
|
|
|
126
|
-
`
|
|
127
|
-
|
|
128
|
-
`variant="link"`는 `role="neutral"`에서만 사용할 수 있으며 `href`가 필수입니다. 이 조합은 `<a>`로 렌더링합니다. native button에는 기본값을 제공하지 않습니다.
|
|
129
|
-
|
|
130
|
-
`startIcon`, `endIcon`, `icon`은 단일 `ReactElement` 슬롯입니다. lucide 아이콘은 `startIconName`, `endIconName`, `iconName`으로 PascalCase 전체 이름을 지정합니다. 이름 prop은 `ButtonIconName` 자동완성을 제공하며 custom ReactElement와 같은 슬롯에 함께 지정할 수 없습니다.
|
|
131
|
-
|
|
132
|
-
`ButtonIconName`은 EVA 화면에서 실제로 사용하는 lucide 아이콘을 모은 승인된 아이콘 목록입니다. 목록은 `lucide-react`의 canonical 이름만 포함하며 `AlertCircle`처럼 deprecated된 alias는 허용하지 않습니다. 승인된 이름 전체는 설치된 패키지의 공개 `spec.json`의 `api.iconNames`와 내부 Storybook 아이콘 카탈로그에서 확인합니다. 이름으로 지정한 아이콘은 동기적으로 렌더링하며 SSR에서도 SVG를 바로 출력합니다. 목록에 없는 이름은 TypeScript 오류로 막고, 런타임에 전달되면 render 오류로 처리합니다.
|
|
133
|
-
|
|
134
|
-
승인된 아이콘 목록 밖의 아이콘이 필요하면 `icon`, `startIcon`, `endIcon`에 `ReactElement`로 전달합니다. 같은 아이콘이 두 화면 이상에서 반복되면 승인된 아이콘 목록에 추가하는 PR을 엽니다. 슬롯 아이콘을 `lucide-react`에서 직접 import할 때는 소비처 `package.json`에 `lucide-react`를 EDS와 같은 `1.45.0`으로 선언합니다. pnpm처럼 엄격한 node_modules에서는 선언 없이 resolve되지 않고, 버전이 다르면 아이콘 코드가 두 벌 번들됩니다. 슬롯 아이콘의 크기는 Button이 맞추므로 stroke 두께만 lucide 기본값 `2`로 두면 이름으로 지정한 아이콘과 같아 보입니다.
|
|
135
|
-
|
|
136
|
-
이름으로 지정한 lucide 아이콘은 `lucide-react`의 기본 시각 설정을 사용합니다. Button은 아이콘 영역의 크기만 조정하며, `color`, `size`, `strokeWidth`, `strokeLinecap`, `strokeLinejoin`을 소비처 prop으로 노출하거나 덮어쓰지 않습니다. `aria-hidden`과 `focusable`은 Button 접근성 contract에 따라 내부에서 설정합니다.
|
|
137
|
-
|
|
138
|
-
- children만 있으면 `textOnly`입니다.
|
|
139
|
-
- children과 `startIcon`이 있으면 `iconFirst`입니다.
|
|
140
|
-
- children과 `endIcon`이 있으면 `iconLast`입니다.
|
|
141
|
-
- children과 두 아이콘이 있으면 `iconBoth`입니다.
|
|
142
|
-
- children 없이 `icon` 또는 `iconName`만 있으면 `iconOnly`입니다. 접근성 이름이 필요하면 비어 있지 않은 `aria-label`을 지정합니다.
|
|
143
|
-
|
|
144
|
-
빈 label·icon, 제공된 빈 `aria-label`과 잘못된 슬롯 조합은 모든 환경에서 오류로 처리합니다. label은 한 줄을 유지합니다.
|
|
145
|
-
|
|
146
|
-
`className`, native `aria-*`, `data-*`, `forwardRef`를 지원합니다. `style`과 임의 스타일 override는 공개하지 않습니다.
|
|
147
|
-
|
|
148
|
-
`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을 생략합니다.
|
|
149
118
|
|
|
150
119
|
## 접근성·상호작용
|
|
151
120
|
|
|
121
|
+
- 아이콘은 시각 장식으로 `aria-hidden="true"` 처리합니다. 그래서 icon-only Button에는 항상 비어 있지 않은 `aria-label` 또는 `aria-labelledby`를 지정합니다. 타입이 이를 요구하지 않아 빠뜨려도 typecheck가 통과합니다.
|
|
152
122
|
- native button은 `disabled` 속성을 사용합니다.
|
|
153
123
|
- disabled link는 `href`를 제거하고 `aria-disabled="true"`, `tabIndex={-1}`를 적용하며 클릭을 막습니다. href가 제거된 앵커는 의도적으로 link role을 노출하지 않습니다.
|
|
154
|
-
-
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
158
|
-
- 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 연동 방식은 `미결`입니다.
|
|
159
128
|
|
|
160
129
|
## 문서와 검증
|
|
161
130
|
|
|
162
|
-
Button의
|
|
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.1.
|
|
3
|
+
"packageVersion": "0.1.1",
|
|
4
4
|
"name": "Button",
|
|
5
5
|
"purpose": "사용자가 즉시 실행할 수 있는 행동과 탐색을 표현하는 인터랙티브 요소입니다.",
|
|
6
6
|
"usageContexts": [
|
|
@@ -8,20 +8,19 @@
|
|
|
8
8
|
"다른 페이지나 콘텐츠로 이동하는 탐색에는 neutral/link branch를 사용합니다.",
|
|
9
9
|
"brand는 화면에서 가장 중요한 행동을 강조할 때 사용합니다.",
|
|
10
10
|
"neutral은 일반적인 액션과 탐색에 사용합니다.",
|
|
11
|
-
"critical은 삭제·초기화처럼 되돌리기 어려운 행동에
|
|
12
|
-
"생성·저장·관리·확인처럼 화면의 핵심 행동에는 brand/solid를
|
|
13
|
-
"검색·조회·일반적인 주요 실행에는 neutral/solid를
|
|
11
|
+
"critical은 삭제·초기화처럼 되돌리기 어려운 행동에 critical/weak로 사용하며, 가능하면 확인 절차를 함께 둡니다.",
|
|
12
|
+
"생성·저장·관리·확인처럼 화면의 핵심 행동에는 brand/solid를 사용합니다.",
|
|
13
|
+
"검색·조회·일반적인 주요 실행에는 neutral/solid를 사용합니다.",
|
|
14
14
|
"취소·닫기·수정 같은 보조 작업에는 neutral/outline을 사용합니다.",
|
|
15
|
-
"
|
|
16
|
-
"되돌리기 어려운 삭제·초기화는 critical/weak와 확인 절차를 먼저 검토합니다.",
|
|
15
|
+
"초기화·갱신처럼 부담이 낮은 반복 행동에는 neutral/weak를 사용합니다.",
|
|
17
16
|
"비동기 저장·확인에는 loading을 사용해 중복 실행을 막고 라벨 공간을 유지합니다.",
|
|
18
|
-
"표 행 조작·pagination처럼 공간이 좁은
|
|
19
|
-
"icon은 생성·수정·삭제·외부 이동·다운로드처럼 행동의 목적이나 방향을 보조할 때 사용합니다."
|
|
20
|
-
"icon-only의 접근성 이름을 제공하려면 aria-label을 지정합니다. aria-label은 필수로 강제하지 않습니다."
|
|
17
|
+
"표 행 조작·pagination처럼 공간이 좁은 곳에는 icon-only를 쓸 수 있습니다(accessible name은 accessibility.iconOnlyName).",
|
|
18
|
+
"icon은 생성·수정·삭제·외부 이동·다운로드처럼 행동의 목적이나 방향을 보조할 때 사용합니다."
|
|
21
19
|
],
|
|
22
20
|
"unsupportedContexts": [
|
|
23
21
|
"문서에 없는 role·variant·상태를 className이나 inline style로 우회하지 않습니다.",
|
|
24
|
-
"상태 변경이나 일반 CTA를 neutral/link로 표현하지 않습니다."
|
|
22
|
+
"상태 변경이나 일반 CTA를 neutral/link로 표현하지 않습니다.",
|
|
23
|
+
"neutral/outline을 neutral/solid와 같은 액션 그룹에 두지 않습니다(neutral/solid 옆 취소 버튼의 조합은 미결)."
|
|
25
24
|
],
|
|
26
25
|
"api": {
|
|
27
26
|
"props": [
|
|
@@ -35,7 +34,7 @@
|
|
|
35
34
|
"name": "role",
|
|
36
35
|
"type": "ButtonRole",
|
|
37
36
|
"required": true,
|
|
38
|
-
"description": "시각적 역할입니다."
|
|
37
|
+
"description": "행동의 의미(brand·neutral·critical)를 나타내는 시각적 역할입니다. DOM의 ARIA role로 전달되지 않으며 ARIA role은 지정할 수 없습니다."
|
|
39
38
|
},
|
|
40
39
|
{
|
|
41
40
|
"name": "variant",
|
|
@@ -48,20 +47,20 @@
|
|
|
48
47
|
"type": "NativeButtonType",
|
|
49
48
|
"required": true,
|
|
50
49
|
"branch": "native button",
|
|
51
|
-
"description": "native button의 동작 유형입니다."
|
|
50
|
+
"description": "native button의 동작 유형입니다. 폼 제출은 submit, 그 밖의 행동은 button을 지정하며 기본값은 없습니다."
|
|
52
51
|
},
|
|
53
52
|
{
|
|
54
53
|
"name": "href",
|
|
55
54
|
"type": "string",
|
|
56
55
|
"required": true,
|
|
57
56
|
"branch": "neutral/link",
|
|
58
|
-
"description": "탐색 대상 주소입니다."
|
|
57
|
+
"description": "탐색 대상 주소입니다. 빈 문자열이나 공백만 있는 값은 render 오류입니다."
|
|
59
58
|
},
|
|
60
59
|
{
|
|
61
60
|
"name": "children",
|
|
62
61
|
"type": "ReactNode",
|
|
63
62
|
"required": false,
|
|
64
|
-
"description": "text label입니다."
|
|
63
|
+
"description": "text label입니다. iconOnly를 뺀 모든 slot에서 필수이며, 렌더링할 내용이 없으면(빈 문자열·공백·null) render 오류입니다. slot 조합은 slots를 따릅니다."
|
|
65
64
|
},
|
|
66
65
|
{
|
|
67
66
|
"name": "icon",
|
|
@@ -103,7 +102,7 @@
|
|
|
103
102
|
"name": "aria-label",
|
|
104
103
|
"type": "string",
|
|
105
104
|
"required": false,
|
|
106
|
-
"description": "accessible name입니다. icon-only
|
|
105
|
+
"description": "accessible name입니다. 지정하면 비어 있지 않아야 하며, 빈 문자열이나 공백은 render 오류입니다. icon-only에서는 accessibility.iconOnlyName을 따릅니다."
|
|
107
106
|
},
|
|
108
107
|
{
|
|
109
108
|
"name": "disabled",
|
|
@@ -115,13 +114,13 @@
|
|
|
115
114
|
"name": "loading",
|
|
116
115
|
"type": "boolean",
|
|
117
116
|
"required": false,
|
|
118
|
-
"description": "처리 중 상태입니다."
|
|
117
|
+
"description": "비동기 처리 중 상태입니다. aria-busy를 적용하고 상호작용을 막으며 label·아이콘 공간과 accessible name을 유지합니다."
|
|
119
118
|
},
|
|
120
119
|
{
|
|
121
120
|
"name": "className",
|
|
122
121
|
"type": "string",
|
|
123
122
|
"required": false,
|
|
124
|
-
"description": "
|
|
123
|
+
"description": "바깥 배치(margin·정렬·grid 위치)용 class입니다."
|
|
125
124
|
},
|
|
126
125
|
{
|
|
127
126
|
"name": "ref",
|
|
@@ -163,7 +162,7 @@
|
|
|
163
162
|
"iconFirst": "children과 startIcon 또는 startIconName을 제공합니다.",
|
|
164
163
|
"iconLast": "children과 endIcon 또는 endIconName을 제공합니다.",
|
|
165
164
|
"iconBoth": "children과 앞·뒤 아이콘을 모두 제공합니다.",
|
|
166
|
-
"iconOnly": "children 없이 icon 또는 iconName을 제공합니다.
|
|
165
|
+
"iconOnly": "children 없이 icon 또는 iconName을 제공합니다. 이름은 accessibility.iconOnlyName을 따릅니다."
|
|
167
166
|
},
|
|
168
167
|
"iconNames": [
|
|
169
168
|
"ArrowLeft",
|
|
@@ -226,127 +225,9 @@
|
|
|
226
225
|
]
|
|
227
226
|
},
|
|
228
227
|
"accessibility": {
|
|
229
|
-
"
|
|
230
|
-
"
|
|
231
|
-
"iconOnlyName": "icon-only layout에서는 접근성 이름을 제공할 때 비어 있지 않은 aria-label을 지정합니다. aria-label은 필수가 아닙니다.",
|
|
232
|
-
"disabledLink": "비활성 link는 href를 제거하고 aria-disabled와 tabIndex를 적용합니다.",
|
|
233
|
-
"loading": "loading 중에는 aria-busy를 적용하고 상호작용을 차단합니다.",
|
|
234
|
-
"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 영역으로 알립니다."
|
|
235
230
|
},
|
|
236
|
-
"semanticTokens": [
|
|
237
|
-
{
|
|
238
|
-
"name": "color-bg-brand-primary",
|
|
239
|
-
"usage": "brand solid의 기본 배경입니다."
|
|
240
|
-
},
|
|
241
|
-
{
|
|
242
|
-
"name": "color-bg-brand-primary-hover",
|
|
243
|
-
"usage": "brand solid의 hover 배경입니다."
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
"name": "color-bg-critical-weak",
|
|
247
|
-
"usage": "critical weak의 기본 배경입니다."
|
|
248
|
-
},
|
|
249
|
-
{
|
|
250
|
-
"name": "color-bg-critical-weak-hover",
|
|
251
|
-
"usage": "critical weak의 hover 배경입니다."
|
|
252
|
-
},
|
|
253
|
-
{
|
|
254
|
-
"name": "color-bg-disabled",
|
|
255
|
-
"usage": "비활성 배경입니다."
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
"name": "color-bg-neutral-inverted",
|
|
259
|
-
"usage": "neutral solid의 기본 배경입니다."
|
|
260
|
-
},
|
|
261
|
-
{
|
|
262
|
-
"name": "color-bg-neutral-inverted-hover",
|
|
263
|
-
"usage": "neutral solid의 hover 배경입니다."
|
|
264
|
-
},
|
|
265
|
-
{
|
|
266
|
-
"name": "color-bg-neutral-weak",
|
|
267
|
-
"usage": "neutral weak의 기본 배경입니다."
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
"name": "color-bg-neutral-weak-hover",
|
|
271
|
-
"usage": "neutral weak의 hover 배경입니다."
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
"name": "color-bg-transparent",
|
|
275
|
-
"usage": "outline·ghost·link의 투명 배경입니다."
|
|
276
|
-
},
|
|
277
|
-
{
|
|
278
|
-
"name": "color-bg-transparent-hover",
|
|
279
|
-
"usage": "outline·ghost·link의 hover 배경입니다."
|
|
280
|
-
},
|
|
281
|
-
{
|
|
282
|
-
"name": "color-fg-brand",
|
|
283
|
-
"usage": "brand outline의 전경입니다."
|
|
284
|
-
},
|
|
285
|
-
{
|
|
286
|
-
"name": "color-fg-critical",
|
|
287
|
-
"usage": "critical weak의 전경입니다."
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
"name": "color-fg-disabled",
|
|
291
|
-
"usage": "비활성 전경입니다."
|
|
292
|
-
},
|
|
293
|
-
{
|
|
294
|
-
"name": "color-fg-neutral",
|
|
295
|
-
"usage": "neutral outline·ghost·link의 전경입니다."
|
|
296
|
-
},
|
|
297
|
-
{
|
|
298
|
-
"name": "color-fg-neutral-inverted",
|
|
299
|
-
"usage": "solid variant의 대비 전경입니다."
|
|
300
|
-
},
|
|
301
|
-
{
|
|
302
|
-
"name": "color-stroke-focusRing",
|
|
303
|
-
"usage": "focus-visible ring의 전경입니다."
|
|
304
|
-
},
|
|
305
|
-
{
|
|
306
|
-
"name": "color-stroke-neutral-secondary",
|
|
307
|
-
"usage": "neutral outline의 경계선입니다."
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
"name": "typography-body3-fontFamily",
|
|
311
|
-
"usage": "xs label의 글꼴입니다."
|
|
312
|
-
},
|
|
313
|
-
{
|
|
314
|
-
"name": "typography-body3-fontSize",
|
|
315
|
-
"usage": "xs label의 글꼴 크기입니다."
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
"name": "typography-body3-fontWeight",
|
|
319
|
-
"usage": "xs label의 글꼴 두께입니다."
|
|
320
|
-
},
|
|
321
|
-
{
|
|
322
|
-
"name": "typography-body3-letterSpacing",
|
|
323
|
-
"usage": "xs label의 자간입니다."
|
|
324
|
-
},
|
|
325
|
-
{
|
|
326
|
-
"name": "typography-body3-lineHeight",
|
|
327
|
-
"usage": "xs label의 행간입니다."
|
|
328
|
-
},
|
|
329
|
-
{
|
|
330
|
-
"name": "typography-title3-fontFamily",
|
|
331
|
-
"usage": "sm·md label의 글꼴입니다."
|
|
332
|
-
},
|
|
333
|
-
{
|
|
334
|
-
"name": "typography-title3-fontSize",
|
|
335
|
-
"usage": "sm·md label의 글꼴 크기입니다."
|
|
336
|
-
},
|
|
337
|
-
{
|
|
338
|
-
"name": "typography-title3-fontWeight",
|
|
339
|
-
"usage": "sm·md label의 글꼴 두께입니다."
|
|
340
|
-
},
|
|
341
|
-
{
|
|
342
|
-
"name": "typography-title3-letterSpacing",
|
|
343
|
-
"usage": "sm·md label의 자간입니다."
|
|
344
|
-
},
|
|
345
|
-
{
|
|
346
|
-
"name": "typography-title3-lineHeight",
|
|
347
|
-
"usage": "sm·md label의 행간입니다."
|
|
348
|
-
}
|
|
349
|
-
],
|
|
350
231
|
"unsupportedProps": [
|
|
351
232
|
{
|
|
352
233
|
"name": "style",
|